Compare commits

...
226 Commits
Author SHA1 Message Date
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 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 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
176 changed files with 10921 additions and 2260 deletions
+10
View File
@@ -0,0 +1,10 @@
name: Build
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v2
- name: Build the app
run: ./gradlew build
-15
View File
@@ -1,15 +0,0 @@
language: android
env:
global:
- ANDROID_HOME=/usr/local/android-sdk
android:
components:
- android-29
- build-tools-29.0.2
before_install:
- mkdir -p $ANDROID_HOME/licenses
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
script:
- ./gradlew build test
+29 -13
View File
@@ -1,25 +1,40 @@
# Contributing
There are a couple of ways to contribute to Aegis. This document contains some
general guidelines for each type of contribution.
Looking to contribute to Aegis? That's great! There are a couple of ways to help
out. This document contains some general guidelines for each type of
contribution.
## Translations
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
translations of Aegis for lots of different languages. __Pull requests that
add/update a translation are not 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``._
+40 -42
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://api.travis-ci.org/beemdevelopment/Aegis.svg?branch=master)](https://travis-ci.org/github/beemdevelopment/aegis) [![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: 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
@@ -106,24 +107,21 @@ Certificate fingerprints:
- [aegis-icons](https://github.com/krisu5/aegis-icons)
While we're still working on better icon support in Aegis, krisu5 has started
a third-party repository for icons that you may want to check out.
While we're still [working on better icon
support](https://github.com/beemdevelopment/Aegis/issues/509) in Aegis, krisu5
has started a third-party repository for icons that you may want to check out.
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
## Libraries
## Contributing
- [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
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
+81 -33
View File
@@ -13,18 +13,25 @@ def getCmdOutput = { cmd ->
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
def packageName = "com.beemdevelopment.aegis"
def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
applicationId "com.beemdevelopment.aegis"
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 29
versionCode 31
versionName "1.2-beta2"
targetSdkVersion 30
versionCode 45
versionName "1.4.2"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
@@ -33,6 +40,8 @@ android {
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.all {
useJUnitPlatform()
@@ -46,7 +55,12 @@ android {
buildTypes {
debug {
applicationIdSuffix ".debug"
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
manifestPlaceholders = [
title: "AegisDev",
iconName: "ic_launcher_debug",
fileProviderAuthority: "${fileProviderAuthorityDebug}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
resValue "bool", "pref_secure_screen_default", "false"
postprocessing {
removeUnusedCode true
@@ -57,7 +71,12 @@ android {
}
}
release {
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
manifestPlaceholders = [
title: "Aegis",
iconName: "ic_launcher",
fileProviderAuthority: "${fileProviderAuthority}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
resValue "bool", "pref_secure_screen_default", "true"
postprocessing {
removeUnusedCode true
@@ -72,6 +91,7 @@ android {
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
}
@@ -91,37 +111,65 @@ protobuf {
}
dependencies {
def libsuVersion = '2.5.1'
def cameraxVersion = '1.0.0-rc01'
def glideVersion = '4.11.0'
def guavaVersion = '30.1'
def junitVersion = '5.7.0'
def libsuVersion = '3.0.2'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
implementation 'net.lingala.zip4j:zip4j:2.5.2'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:1.0.0-alpha20"
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.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.2.0"
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.guava:guava:28.2-android'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:6.0.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation 'com.github.bumptech.glide:annotations:4.10.0'
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
implementation 'com.github.avito-tech:krop:0.51'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
transitive = false
}
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation 'com.google.guava:guava:28.2-jre"'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.protobuf:protobuf-javalite:3.14.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.3.1'
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:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
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:4.13.1'
androidTestUtil 'androidx.test:orchestrator:1.3.0'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
}
+2
View File
@@ -16,6 +16,8 @@
# public *;
#}
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
@@ -0,0 +1,108 @@
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() {
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);
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 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,190 @@
package com.beemdevelopment.aegis;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
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.onData;
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;
import static org.hamcrest.Matchers.anything;
@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++) {
assertTrue(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.spinner_group)).perform(click());
onData(anything()).atPosition(1).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_encryption_title)), 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.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), 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_expand_menu_button)).perform(click());
onView(withId(R.id.fab_enter)).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) {
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
try {
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
} catch (AmbiguousViewMatcherException e) {
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
}
if (entry.getInfo() instanceof HotpInfo) {
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
}
if (entry.getInfo() instanceof SteamInfo) {
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
}
}
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());
}
}
+55 -13
View File
@@ -6,8 +6,12 @@
<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"
@@ -18,16 +22,17 @@
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
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:label="@string/title_activity_select_entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}"
android:launchMode="singleTask">
android:label="${title}">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -45,24 +50,61 @@
<activity
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="Scan a QR code"
android:label="@string/title_activity_scan_qr"
android:screenOrientation="portrait" />
<activity
android:name=".ui.EditEntryActivity"
android:label="Edit entry" />
android:label="@string/title_activity_edit_entry" />
<activity
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait" />
<activity
android:name=".ui.AuthActivity"
android:theme="@style/AppTheme.NoActionBar" />
<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>
</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>
+75
View File
@@ -31,6 +31,74 @@
</head>
<body>
<div></div>
<h3>Version 1.4.2</h3>
<h4>Fixes</h4>
<ul>
<li>The app would crash if DocumentsUI is not present on the device</li>
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
<li>Importing from Authy was flaky for entries that have an icon</li>
<li>The dark theme was not properly applied to the QR code scanner view</li>
<li>The app would crash on plain text export on some devices</li>
<li>Importing from Authenticator Plus stopped working</li>
</ul>
<h3>Version 1.4.1</h3>
<h4>Fixes</h4>
<ul>
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
</ul>
<h3>Version 1.4</h3>
<h4>New</h4>
<ul>
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
<li>More customizable auto-lock</li>
<li>More flexible export options
<ul>
<li>Share mechanism</li>
<li>Offer to encrypt even if this feature is disabled in the app</li>
<li>Export to a Google Authenticator URI file</li>
</ul>
</li>
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
<li>Color improvements to the dark theme (slightly darker)</li>
<li>Offer more locations to select an image/icon from</li>
<li>Display some helpful information when importing from a different app</li>
<li>Minimum tap to reveal timeout changed to 1 second</li>
<li>After an entry is added, scroll to it and highlight it</li>
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Scanning large images for QR codes would fail</li>
<li>The FAB would remain hidden under certain circumstances</li>
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
<li>The PIN keyboard would show even if a new non-digit password was set</li>
<li>The password reminder popup would be occluded by the autofill popup</li>
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
</ul>
<h3>Version 1.3</h3>
<h4>New</h4>
<ul>
<li>Completely rewritten intro/onboarding</li>
<li>Option to show a PIN keyboard when unlocking Aegis</li>
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
<li>RTL support</li>
<li>Arabic and Portuguese translations</li>
<li>Updates to existing translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Better lifecycle handling of the biometric authentication prompt</li>
<li>The filename of exported vaults had a double .json extension</li>
<li>The navigation bar color was incorrect on devices pre API 27</li>
<li>QR code scanner performance and stability improvements</li>
<li>Various other small usability and stability improvements</li>
</ul>
<h3>Version 1.2.1</h3>
<h4>Fixes</h4>
<ul>
<li>Fix a rare issue where the intro could end up in a bad state</li>
</ul>
<h3>Version 1.2</h3>
<h4>New</h4>
<ul>
@@ -41,6 +109,7 @@
<li>Add support for importing a plain text Google Authenticator URI file</li>
<li>Add support for importing from the new Google Authenticator export QR codes</li>
<li>Add support for otpauth://steam URI's</li>
<li>Add an option to copy tokens on tap (and disable it by default)</li>
<li>Improve method to notify users on copy</li>
<li>Add support for backups</li>
<li>Improve multiselect flow</li>
@@ -49,17 +118,23 @@
<li>Use most frequent period to show progress</li>
<li>Append a timestamp to the filename of exported vaults</li>
<li>Add Hungarian translation</li>
<li>Add Turkish translation</li>
<li>Display a warning if automatic time sync is not enabled</li>
<li>Minor card entry layout overhaul</li>
<li>Ability to transfer tokens with qr codes</li>
<li>Lockscreen overhaul</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Improve overall exception handling and error feedback to the user</li>
<li>Improve icon editing flow</li>
<li>Protect writes of the vault file against corruption with AtomicFile</li>
<li>Make the parsing logic of the QR code URI more robust</li>
<li>Importing from Authy now asks for password if needed</li>
<li>Update Russian localization</li>
<li>Increase password reminder period to 30 days</li>
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
<li>Respect the global animator duration scale setting</li>
</ul>
<p>Various other minor improvements</p>
<h3>Version 1.1.4</h3>
@@ -12,10 +12,16 @@ 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.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
@@ -23,6 +29,7 @@ import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import java.util.Collections;
@@ -33,10 +40,16 @@ public class AegisApplication extends Application {
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private boolean _blockAutoLock;
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.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
}
@Override
public void onCreate() {
super.onCreate();
@@ -53,6 +66,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();
}
@@ -111,8 +130,8 @@ public class AegisApplication extends Application {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isEncryptionEnabled() && !isVaultLocked();
public boolean isAutoLockEnabled(int autoLockType) {
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
}
public void registerLockListener(LockListener listener) {
@@ -123,10 +142,25 @@ public class AegisApplication extends Application {
_lockListeners.remove(listener);
}
public void lock() {
/**
* Sets whether to block automatic lock on minimization. This should only be called
* by activities before invoking an intent that shows a DocumentsUI, because that
* action leads AppLifecycleObserver to believe that the app has been minimized.
*/
public void setBlockAutoLock(boolean block) {
_blockAutoLock = block;
}
/**
* Locks the vault and the app.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
public void lock(boolean userInitiated) {
_manager.destroy();
_manager = null;
for (LockListener listener : _lockListeners) {
listener.onLocked();
listener.onLocked(userInitiated);
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
@@ -141,6 +175,7 @@ public class AegisApplication extends Application {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
@@ -167,16 +202,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);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -12,6 +12,17 @@ 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) {
@@ -34,6 +45,10 @@ public class Preferences {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
@@ -73,8 +88,25 @@ public class Preferences {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public boolean isAutoLockEnabled() {
return _prefs.getBoolean("pref_auto_lock", true);
return getAutoLockMask() != AUTO_LOCK_OFF;
}
public boolean isAutoLockTypeEnabled(int autoLockType) {
return (getAutoLockMask() & autoLockType) == autoLockType;
}
public void setAutoLockMask(int autoLock) {
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
}
public void setIntroDone(boolean done) {
@@ -128,7 +160,12 @@ 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 isBackupsEnabled() {
@@ -160,6 +197,18 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
}
public boolean isPinKeyboardEnabled() {
return _prefs.getBoolean("pref_pin_keyboard", false);
}
public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true);
}
@@ -167,4 +216,8 @@ public class Preferences {
public void setIsTimeSyncWarningEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
}
}
@@ -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.AppTheme,
Theme.DARK, R.style.AppTheme_Dark,
Theme.AMOLED, R.style.AppTheme_TrueBlack
);
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme_Light_NoActionBar,
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme_Fullscreen,
Theme.DARK, R.style.AppTheme_Fullscreen_Dark,
Theme.AMOLED, R.style.AppTheme_Fullscreen_TrueBlack
);
}
@@ -63,9 +63,9 @@ public class KeyStoreHandle {
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause instanceof KeyStoreException) {
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
public GuardianProjectFDroidRSA2048() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
public GuardianProjectFDroidRSA4096() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -3,21 +3,19 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
public class Base32 {
private static final BaseEncoding _encoding = BaseEncoding.base32().omitPadding();
private Base32() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return _encoding.decode(s.toUpperCase());
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return _encoding.encode(data);
return BaseEncoding.base32().omitPadding().encode(data);
}
}
@@ -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();
}
}
@@ -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;
}
@@ -18,6 +18,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 +52,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,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,68 @@
package com.beemdevelopment.aegis.helpers;
import android.util.Log;
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();
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;
}
ByteBuffer buf = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
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) {
_listener.onQrCodeDetected(result);
}
} catch (ChecksumException | FormatException | NotFoundException ignored) {
} finally {
image.close();
}
}
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,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);
}
}
@@ -4,17 +4,20 @@ import android.content.Context;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
public class AegisImporter extends DatabaseImporter {
@@ -23,19 +26,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);
@@ -3,26 +3,32 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.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 decryptData(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,51 @@ public class AndOtpImporter extends DatabaseImporter {
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
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;
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");
DecryptedState state = decryptData(key, 0);
listener.onStateDecrypted(state);
} else {
int offset = INT_SIZE + SALT_SIZE;
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, offset);
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
try {
DecryptedState state = decryptData(key1, offset);
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,8 +169,7 @@ 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);
}
@@ -221,12 +231,17 @@ public class AndOtpImporter extends DatabaseImporter {
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
name = parts[0];
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
@@ -235,4 +250,64 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public static class Params {
private char[] _password;
private byte[] _salt;
private int _iterations;
public Params(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;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
}
@@ -3,6 +3,8 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
@@ -11,6 +13,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
@@ -20,19 +23,14 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(reader.readAll());
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
@@ -55,9 +53,8 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
FileReader reader = new FileReader(zipStream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
GoogleAuthUriImporter.State state = importer.read(reader);
DatabaseImporter.State state = importer.read(zipStream);
listener.onStateDecrypted(state);
return;
}
@@ -1,18 +1,23 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.json.JSONArray;
import org.json.JSONException;
@@ -21,6 +26,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -38,8 +44,10 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _subPath = "shared_prefs";
private static final String _pkgName = "com.authy.authy";
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
private static final String _authyFilename = "com.authy.storage.tokens.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
@@ -53,42 +61,91 @@ public class AuthyImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
JSONArray array;
JSONArray authyArray;
try {
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
try {
for (int i = 0; i < authyArray.length(); i++) {
array.put(authyArray.getJSONObject(i));
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return read(array);
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.setInput(stream, null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
if (entry.Name.equals(String.format("%s.key", _authFilename))
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
array = new JSONArray(entry.Value);
break;
}
}
for (int i = 0; i < array.length(); i++) {
if (!array.getJSONObject(i).has("decryptedSecret")) {
return new EncryptedState(array);
}
}
return new DecryptedState(array);
return read(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
private State read(JSONArray array) throws DatabaseImporterException {
try {
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
return new EncryptedState(array);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(array);
}
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(inStream, null);
parser.nextTag();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(key)) {
return new JSONArray(entry.Value);
}
}
} catch (JSONException ignored) {
}
return new JSONArray();
}
public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _array;
@@ -103,7 +160,7 @@ public class AuthyImporter extends DatabaseImporter {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = obj.optString("encryptedSecret", null);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
@@ -173,40 +230,50 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.optString("originalName", null);
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
boolean isAuthy = entry.has("secretSeed");
sanitizeEntryInfo(authyEntryInfo, isAuthy);
byte[] secret;
if (isAuthy) {
secret = Hex.decode(entry.getString("secretSeed"));
} else {
secret = Base32.decode(entry.getString("decryptedSecret"));
}
int digits = entry.getInt("digits");
byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
OtpInfo info = new TotpInfo(secret, "SHA1", digits, isAuthy ? 10 : 30);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String separator = "";
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
if (!isAuthy) {
String separator = "";
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + separator, "");
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
info.Issuer = info.Name;
info.Name = "";
}
info.Name = info.Name.replace(info.Issuer + separator, "");
}
}
@@ -3,50 +3,42 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.util.IOUtils;
import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public abstract class DatabaseImporter {
private Context _context;
private static Map<String, Class<? extends DatabaseImporter>> _importers;
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
private static List<Definition> _importers;
static {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.class);
_importers.put("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_importers.put("Plain text", GoogleAuthUriImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers = new ArrayList<>();
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, 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("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
public DatabaseImporter(Context context) {
@@ -57,20 +49,27 @@ public abstract class DatabaseImporter {
return _context;
}
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(getAppPkgName(), getAppSubPath());
}
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
protected abstract String getAppPkgName();
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
public State read(InputStream stream) throws DatabaseImporterException {
return read(stream, false);
}
public abstract State read(FileReader reader) throws DatabaseImporterException;
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile file = getAppPath();
try (SuFileInputStream stream = new SuFileInputStream(file)) {
return read(stream, true);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
try {
@@ -81,12 +80,42 @@ public abstract class DatabaseImporter {
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return Collections.unmodifiableMap(_importers);
public static List<Definition> getImporters(boolean isDirect) {
if (isDirect) {
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
}
return Collections.unmodifiableList(_importers);
}
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
return Collections.unmodifiableMap(_appImporters);
public static class Definition {
private final String _name;
private final Class<? extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
_help = help;
_supportsDirect = supportsDirect;
}
public String getName() {
return _name;
}
public Class<? extends DatabaseImporter> getType() {
return _type;
}
public @StringRes int getHelp() {
return _help;
}
public boolean supportsDirect() {
return _supportsDirect;
}
}
public static abstract class State {
@@ -138,36 +167,6 @@ public abstract class DatabaseImporter {
}
}
public static class FileReader {
private InputStream _stream;
private boolean _internal;
public FileReader(InputStream stream) {
this(stream, false);
}
public FileReader(InputStream stream, boolean internal) {
_stream = stream;
_internal = internal;
}
public byte[] readAll() throws IOException {
return IOUtils.readAll(_stream);
}
public InputStream getStream() {
return _stream;
}
/**
* Reports whether this reader reads the internal state of an app.
* @return true if reading from internal file, false if reading from external file
*/
public boolean isInternal() {
return _internal;
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
@@ -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<>();
@@ -1,12 +1,17 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -20,24 +25,19 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
State state;
if (reader.isInternal()) {
state = new FreeOtpImporter(getContext()).read(reader);
if (isInternal) {
state = new FreeOtpImporter(getContext()).read(stream);
} else {
try {
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
@@ -1,16 +1,19 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class GoogleAuthImporter extends DatabaseImporter {
@@ -25,19 +28,22 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
@@ -67,7 +73,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret());
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
@@ -5,9 +5,11 @@ import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
@@ -17,20 +19,15 @@ public class GoogleAuthUriImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return null;
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public GoogleAuthUriImporter.State read(DatabaseImporter.FileReader reader) throws DatabaseImporterException {
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(reader.getStream());
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
@@ -10,7 +11,9 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
@@ -25,19 +28,22 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
@@ -5,7 +5,10 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.topjohnwu.superuser.ShellUtils;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.common.io.Files;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
@@ -24,19 +27,75 @@ public class SqlImporterHelper {
_context = context;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file;
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
File dir = Files.createTempDir();
File mainFile = new File(dir, path.getName());
List<File> fileCopies = new ArrayList<>();
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
// create temporary copies of the database files so that SQLiteDatabase can open them
File fileCopy = null;
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
}
fileCopies.add(fileCopy);
} catch (IOException e) {
if (fileCopy != null) {
fileCopy.delete();
}
for (File fileCopy2 : fileCopies) {
fileCopy2.delete();
}
throw new DatabaseImporterException(e);
}
}
try {
return read(type, mainFile, table);
} finally {
for (File fileCopy : fileCopies) {
fileCopy.delete();
}
}
}
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
}
return files;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file = null;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("db-import-", "", _context.getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(inStream, out);
IOUtils.copy(inStream, out);
}
} catch (IOException e) {
if (file != null) {
file.delete();
}
throw new DatabaseImporterException(e);
}
try {
return read(type, file, table);
} finally {
// always delete the temporary file
file.delete();
}
}
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();
@@ -55,9 +114,6 @@ public class SqlImporterHelper {
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@@ -7,6 +7,7 @@ import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
@@ -14,6 +15,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class SteamImporter extends DatabaseImporter {
@@ -25,27 +27,22 @@ public class SteamImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
// NOTE: this assumes that a global root shell has already been obtained by the caller
SuFile path = getAppPath(getAppPkgName(), _subDir);
SuFile path = getAppPath(_pkgName, _subDir);
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
}
// TODO: handle multiple files (can this even occur?)
return new SuFile(_subDir, files[0].getName()).getPath();
return files[0];
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
@@ -14,8 +15,10 @@ import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
@@ -24,6 +27,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -59,22 +63,17 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (reader.isInternal()) {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.setInput(stream, null);
parser.nextTag();
String data = null;
@@ -91,7 +90,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = reader.readAll();
byte[] base64 = IOUtils.readAll(stream);
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
@@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
@@ -10,32 +13,27 @@ public class WinAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return null;
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
GoogleAuthUriImporter.State state = importer.read(reader);
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private GoogleAuthUriImporter.State _state;
private DatabaseImporter.State _state;
private State(GoogleAuthUriImporter.State state) {
private State(DatabaseImporter.State state) {
super(false);
_state = state;
}
@Override
public Result convert() {
public Result convert() throws DatabaseImporterException {
Result result = _state.convert();
for (VaultEntry entry : result.getEntries()) {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.ui.glide;
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class ProtobufLicense extends License {
@Override
public String getName() {
return "Protocol Buffers License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
}
}
@@ -8,10 +8,11 @@ import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class GoogleAuthInfo {
public class GoogleAuthInfo implements Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
@@ -49,7 +50,7 @@ public class GoogleAuthInfo {
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
if (_issuer != null && !_issuer.equals("")) {
builder.path(String.format("%s:%s", _issuer, _accountName));
@@ -64,7 +65,7 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
@@ -72,29 +73,27 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
}
// decode secret
byte[] secret;
try {
secret = Base32.decode(encodedSecret);
secret = parseSecret(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException("Bad secret", e);
throw new GoogleAuthInfoException(uri, "Bad secret", e);
}
// check the otp type
OtpInfo info;
try {
String type = uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
@@ -118,16 +117,16 @@ public class GoogleAuthInfo {
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
// provider info used to disambiguate accounts
@@ -167,16 +166,24 @@ public class GoogleAuthInfo {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
return new GoogleAuthInfo(info, accountName, issuer);
}
/**
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
*/
public static byte[] parseSecret(String s) throws EncodingException {
s = s.trim().replace("-", "").replace(" ", "");
return Base32.decode(s);
}
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("Bad URI format");
throw new GoogleAuthInfoException(uri, "Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
@@ -184,17 +191,17 @@ public class GoogleAuthInfo {
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException("Unsupported host");
throw new GoogleAuthInfoException(uri, "Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException("Parameter 'data' is not set");
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
@@ -202,26 +209,59 @@ public class GoogleAuthInfo {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
List<GoogleAuthInfo> infos = new ArrayList<>();
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_HOTP:
otp = new HotpInfo(secret, params.getCounter());
int digits;
switch (params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = 6;
break;
case OTP_TOTP:
otp = new TotpInfo(secret);
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
}
String algo;
switch (params.getAlgorithm()) {
case ALGORITHM_UNSPECIFIED:
// intentional fallthrough
case ALGORITHM_SHA1:
algo = "SHA1";
break;
case ALGORITHM_SHA256:
algo = "SHA256";
break;
case ALGORITHM_SHA512:
algo = "SHA512";
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
}
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
case OTP_TYPE_TOTP:
otp = new TotpInfo(secret, algo, digits, 30);
break;
case OTP_TYPE_HOTP:
otp = new HotpInfo(secret, algo, digits, params.getCounter());
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e){
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
String name = params.getName();
@@ -1,16 +1,30 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public class GoogleAuthInfoException extends Exception {
public GoogleAuthInfoException(Throwable cause) {
private final Uri _uri;
public GoogleAuthInfoException(Uri uri, Throwable cause) {
super(cause);
_uri = uri;
}
public GoogleAuthInfoException(String message) {
public GoogleAuthInfoException(Uri uri, String message) {
super(message);
_uri = uri;
}
public GoogleAuthInfoException(String message, Throwable cause) {
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
super(message, cause);
_uri = uri;
}
/**
* Reports whether the scheme of the URI is phonefactor://.
*/
public boolean isPhoneFactor() {
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
}
@Override
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}
@@ -6,13 +6,12 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
@@ -20,7 +19,8 @@ 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.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
@@ -43,6 +43,11 @@ public class AboutActivity extends AegisActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
@@ -74,7 +79,7 @@ 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(), "CHANGELOG_DIALOG");
});
}
@@ -112,7 +117,7 @@ public class AboutActivity extends AegisActivity {
private void showLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
String textColor = getThemeColorAsHex(R.attr.primaryText);
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
@@ -121,6 +126,8 @@ public class AboutActivity extends AegisActivity {
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
LicenseResolver.registerLicense(new GlideLicense());
LicenseResolver.registerLicense(new ProtobufLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
@@ -133,4 +140,17 @@ public class AboutActivity extends AegisActivity {
private String getThemeColorAsHex(@AttrRes int attributeId) {
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,28 +1,28 @@
package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
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 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.vault.VaultManagerException;
import java.util.Locale;
import java.util.Map;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -30,15 +30,16 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
// set the theme and locale before creating the activity
Preferences prefs = getPreferences();
setPreferredTheme(prefs.getCurrentTheme());
onSetTheme();
setLocale(prefs.getLocale());
super.onCreate(savedInstanceState);
// if the app was killed, relaunch MainActivity and close everything else
if (savedInstanceState != null && isOrphan()) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
@@ -47,9 +48,6 @@ 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);
}
@@ -60,25 +58,36 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
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 startActivityForResult(Intent intent, int requestCode, Bundle bundle) {
if (isAutoLockBypassedForAction(intent.getAction())) {
_app.setBlockAutoLock(true);
}
try {
super.startActivityForResult(intent, requestCode, bundle);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(this, R.string.documentsui_error, e);
} else {
throw e;
}
}
}
@Override
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
finishAndRemoveTask();
}
protected AegisApplication getApp() {
@@ -89,9 +98,26 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
return _app.getPreferences();
}
protected void setPreferredTheme(Theme theme) {
/**
* Called when the activity is expected to set its theme.
*/
protected void onSetTheme() {
setTheme(ThemeMap.DEFAULT);
}
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
protected void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
setTheme(theme);
}
protected Theme getConfiguredTheme() {
Theme theme = getPreferences().getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
// set the theme based on the system theme
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
@@ -103,19 +129,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
_currentTheme = theme;
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack);
break;
}
return theme;
}
protected void setLocale(Locale locale) {
@@ -127,9 +141,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
}
protected boolean saveVault() {
protected boolean saveVault(boolean backup) {
try {
getApp().getVaultManager().save();
getApp().getVaultManager().save(backup);
return true;
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
@@ -137,33 +151,24 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
/**
* Reports whether this Activity has been resumed. (i.e. onResume was called)
*/
protected boolean isOpen() {
return _resumed;
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was locked by an external trigger while the Activity was still open.
*/
private boolean isOrphan() {
return !(this instanceof MainActivity) && _app.isVaultLocked();
protected boolean isOrphan() {
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
private void setGlobalAnimationDurationScale() {
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
if (durationScale != 1) {
try {
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
} catch (Throwable t) {
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
}
}
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));
}
protected Theme getCurrentTheme() {
return _currentTheme;
private static boolean isAutoLockBypassedForAction(@Nullable String action) {
return isDocsAction(action) || (action != null && (action.equals(Intent.ACTION_PICK)
|| action.equals(Intent.ACTION_SEND)
|| action.equals(Intent.ACTION_CHOOSER)));
}
}
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +15,7 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -21,19 +23,19 @@ import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -50,13 +52,17 @@ import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
private int _failedUnlockAttempts;
// the first time this activity is resumed after creation, it's possible to inhibit showing the
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
private boolean _inhibitBioPrompt;
private Preferences _prefs;
private boolean _stateless;
@@ -68,7 +74,7 @@ public class AuthActivity extends AegisActivity {
_textPassword = findViewById(R.id.text_password);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button biometricsButton = findViewById(R.id.button_biometrics);
TextView biometricsButton = findViewById(R.id.button_biometrics);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
@@ -77,8 +83,16 @@ public class AuthActivity extends AegisActivity {
return false;
});
if (_prefs.isPinKeyboardEnabled()) {
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
Intent intent = getIntent();
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
if (savedInstanceState == null) {
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
} else {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
_slots = (SlotList) intent.getSerializableExtra("slots");
_stateless = _slots != null;
if (!_stateless) {
@@ -126,6 +140,7 @@ public class AuthActivity extends AegisActivity {
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxBiometricInfo.setVisibility(View.VISIBLE);
biometricsButton.setVisibility(View.GONE);
}
}
@@ -136,7 +151,8 @@ public class AuthActivity extends AegisActivity {
char[] password = EditTextHelper.getEditTextChars(_textPassword);
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
});
biometricsButton.setOnClickListener(v -> {
@@ -144,6 +160,17 @@ public class AuthActivity extends AegisActivity {
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
private void selectPassword() {
_textPassword.selectAll();
@@ -153,11 +180,10 @@ public class AuthActivity extends AegisActivity {
@Override
public void onBackPressed() {
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
finish();
if (_stateless) {
super.onBackPressed();
} else {
finishAffinity();
}
}
@@ -165,24 +191,40 @@ public class AuthActivity extends AegisActivity {
public void onResume() {
super.onResume();
if (_bioKey != null) {
if (_prefs.isPasswordReminderNeeded()) {
focusPasswordField();
} else {
showBiometricPrompt();
}
} else {
boolean remindPassword = _prefs.isPasswordReminderNeeded();
if (_bioKey == null || remindPassword) {
focusPasswordField();
}
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
_bioPrompt = showBiometricPrompt();
}
_inhibitBioPrompt = false;
}
@Override
public void onPause() {
if (!isChangingConfigurations() && _bioPrompt != null) {
_bioPrompt.cancelAuthentication();
_bioPrompt = null;
}
super.onPause();
}
@Override
public void onAttachedToWindow() {
if (_bioPrompt != null && _prefs.isPasswordReminderNeeded()) {
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
showPasswordReminder();
}
}
@Override
protected boolean isOrphan() {
return _stateless && super.isOrphan();
}
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@@ -190,44 +232,48 @@ public class AuthActivity extends AegisActivity {
private void showPasswordReminder() {
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
popupLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setFocusable(false);
popup.setOutsideTouchable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
popup.setElevation(5.0f);
}
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
_textPassword.post(() -> {
// calculating the actual height of the popup window does not seem possible
// adding 25dp seems to look good enough
int yoff = _textPassword.getHeight()
+ popupLayout.getMeasuredHeight()
+ MetricsHelper.convertDpToPixels(this, 25);
popup.showAsDropDown(_textPassword, 0, -yoff);
});
_textPassword.postDelayed(popup::dismiss, 5000);
}
public void showBiometricPrompt() {
public BiometricPrompt showBiometricPrompt() {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0);
Cipher cipher;
try {
cipher = _bioSlot.createDecryptCipher(_bioKey);
} catch (SlotException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
return;
return null;
}
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt prompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(false)
.build();
_bioPrompt.authenticate(info, cryptoObj);
}
@Override
public void onPause() {
super.onPause();
if (_bioPrompt != null) {
_bioPrompt.cancelAuthentication();
}
prompt.authenticate(info, cryptoObj);
return prompt;
}
private void finish(MasterKey key, boolean isSlotRepaired) {
@@ -241,10 +287,9 @@ public class AuthActivity extends AegisActivity {
} else {
try {
AegisApplication app = getApp();
VaultManager vault = app.initVaultManager(app.loadVaultFile(), creds);
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
vault.setCredentials(creds);
saveVault();
saveVault(true);
}
} catch (VaultManagerException e) {
e.printStackTrace();
@@ -258,6 +303,21 @@ public class AuthActivity extends AegisActivity {
finish();
}
private void onInvalidPassword() {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
_failedUnlockAttempts ++;
if (_failedUnlockAttempts >= 3) {
_textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
}
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
@@ -273,12 +333,7 @@ public class AuthActivity extends AegisActivity {
finish(result.getKey(), result.isSlotRepaired());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
onInvalidPassword();
}
}
}
@@ -287,6 +342,8 @@ public class AuthActivity extends AegisActivity {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
_bioPrompt = null;
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
@@ -295,6 +352,7 @@ public class AuthActivity extends AegisActivity {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_bioPrompt = null;
MasterKey key;
BiometricSlot slot = _slots.find(BiometricSlot.class);
@@ -6,8 +6,8 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.provider.Settings;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
@@ -15,24 +15,34 @@ import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.activity.ComponentActivity;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
@@ -66,7 +76,7 @@ public class Dialogs {
String title, message;
if (totalEntries > 1) {
title = activity.getString(R.string.delete_entries);
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, totalEntries, totalEntries);
} else {
title = activity.getString(R.string.delete_entry);
message = activity.getString(R.string.delete_entry_description);
@@ -89,10 +99,14 @@ public class Dialogs {
.create());
}
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
public static void showSetPasswordDialog(ComponentActivity activity, Dialogs.SlotListener listener) {
Zxcvbn zxcvbn = new Zxcvbn();
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
ProgressBar barPasswordStrength = view.findViewById(R.id.progressBar);
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
@@ -113,6 +127,7 @@ public class Dialogs {
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
@@ -140,19 +155,30 @@ public class Dialogs {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new KeyDerivationTask.Params(slot, password));
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, password);
task.execute(activity.getLifecycle(), params);
});
});
TextWatcher watcher = new TextWatcher() {
@Override
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
Strength strength = zxcvbn.measure(textPassword.getText());
barPasswordStrength.setProgress(strength.getScore());
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
textPasswordWrapper.setError(strength.getFeedback().getWarning());
strength.wipe();
}
@Override
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable c) {
}
};
@@ -162,7 +188,7 @@ public class Dialogs {
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
if (isSecret) {
@@ -177,16 +203,22 @@ public class Dialogs {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
});
if (dismissListener != null) {
builder.setOnDismissListener(dismissListener);
}
if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
@@ -198,13 +230,48 @@ public class Dialogs {
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) {
showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true);
}
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_checkbox, null);
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setText(checkboxMessageId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setNegativeButton(R.string.no, (dialog1, which) ->
listener.onCheckboxInputResult(false))
.setPositiveButton(R.string.yes, (dialog1, which) ->
listener.onCheckboxInputResult(checkBox.isChecked()));
if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setEnabled(false);
buttonOK.set(button);
});
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> buttonOK.get().setEnabled(isChecked));
showSecureDialog(dialog);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
numberPicker.setMinValue(3);
numberPicker.setMinValue(1);
numberPicker.setMaxValue(60);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
numberPicker.setWrapSelectorWheel(true);
@@ -320,6 +387,41 @@ public class Dialogs {
.create());
}
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new);
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
TextView helpText = view.findViewById(R.id.text_importer_help);
setImporterHelpText(helpText, importers.get(0), isDirect);
ListView listView = view.findViewById(R.id.list_importers);
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
listView.setItemChecked(0, true);
listView.setOnItemClickListener((parent, view1, position, id) -> {
setImporterHelpText(helpText, importers.get(position), isDirect);
});
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_application)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition()));
})
.create());
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
} else {
view.setText(definition.getHelp());
}
}
public interface CheckboxInputListener {
void onCheckboxInputResult(boolean checkbox);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
@@ -332,4 +434,8 @@ public class Dialogs {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
}
@@ -6,6 +6,7 @@ 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;
@@ -36,6 +37,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -67,6 +69,7 @@ public class EditEntryActivity extends AegisActivity {
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private boolean _isEditingIcon;
private CircleImageView _iconView;
private ImageView _saveImageButton;
@@ -245,11 +248,7 @@ public class EditEntryActivity extends AegisActivity {
});
_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);
startIconSelectionActivity();
});
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
@@ -326,6 +325,11 @@ public class EditEntryActivity extends AegisActivity {
@Override
public void onBackPressed() {
if (_isEditingIcon) {
stopEditingIcon(false);
return;
}
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<VaultEntry> entry = new AtomicReference<>();
@@ -370,6 +374,9 @@ public class EditEntryActivity extends AegisActivity {
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelectionActivity();
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
@@ -382,6 +389,56 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void startIconSelectionActivity() {
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 });
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
}
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) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
}
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save;
_isEditingIcon = false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit, menu);
@@ -396,7 +453,11 @@ public class EditEntryActivity extends AegisActivity {
}
private void addAndFinish(VaultEntry entry) {
if (_isNew) {
// It's possible that the new entry was already added to the vault, but writing the
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
// again would cause a crash in that case, so the isEntryDuplicate check prevents
// that.
if (_isNew && !_vault.isEntryDuplicate(entry)) {
_vault.addEntry(entry);
} else {
_vault.replaceEntry(entry);
@@ -415,7 +476,7 @@ public class EditEntryActivity extends AegisActivity {
intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete);
if (saveVault()) {
if (saveVault(true)) {
setResult(RESULT_OK, intent);
finish();
}
@@ -424,35 +485,7 @@ public class EditEntryActivity extends AegisActivity {
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Glide.with(this)
.asBitmap()
.load(data.getData())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_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;
}
});
startEditingIcon(data.getData());
}
super.onActivityResult(requestCode, resultCode, data);
@@ -483,7 +516,8 @@ public class EditEntryActivity extends AegisActivity {
byte[] secret;
try {
secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
secret = GoogleAuthInfo.parseSecret(secretString);
if (secret.length == 0) {
throw new ParseException("Secret cannot be empty");
}
@@ -556,6 +590,10 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
if (_isEditingIcon) {
stopEditingIcon(true);
}
VaultEntry entry;
try {
entry = parseEntry();
@@ -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,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
@@ -11,7 +12,6 @@ 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;
@@ -19,6 +19,8 @@ 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) {
@@ -29,21 +31,35 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_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 +70,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());
@@ -1,103 +1,77 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.github.appintro.AppIntro2;
import com.github.appintro.AppIntroFragment;
import com.github.appintro.model.SliderPage;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.json.JSONObject;
public class IntroActivity extends AppIntro2 {
private CustomAuthenticatedSlide _authenticatedSlide;
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private AegisApplication _app;
private Preferences _prefs;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
public class IntroActivity extends IntroBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
setWizardMode(true);
setSkipButtonEnabled(false);
showStatusBar(true);
setSystemBackButtonLocked(true);
setBarColor(getResources().getColor(R.color.colorPrimary));
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_authenticationSlide = new CustomAuthenticationSlide();
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticationSlide);
_authenticatedSlide = new CustomAuthenticatedSlide();
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticatedSlide);
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
addSlide(WelcomeSlide.class);
addSlide(SecurityPickerSlide.class);
addSlide(SecuritySetupSlide.class);
addSlide(DoneSlide.class);
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
int cryptType = getIntent().getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
goToNextSlide(false);
}
}
setSwipeLock(true);
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
@Override
public void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
skipToSlide(DoneSlide.class);
return true;
}
int cryptType = _authenticatedSlide.getCryptType();
VaultFileCredentials creds = _authenticatedSlide.getCredentials();
return false;
}
@Override
protected void onDonePressed() {
Bundle state = getState();
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
if (cryptType == CRYPT_TYPE_INVALID
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
}
Vault vault = new Vault();
VaultFile vaultFile = new VaultFile();
try {
JSONObject obj = vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
vaultFile.setContent(obj, creds);
@@ -110,16 +84,16 @@ public class IntroActivity extends AppIntro2 {
return;
}
_app.initVaultManager(vault, creds);
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();
}
public void goToNextSlide() {
super.goToNextSlide(false);
}
}
@@ -16,16 +16,20 @@ 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.otp.GoogleAuthInfo;
@@ -40,12 +44,12 @@ 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;
@@ -74,6 +78,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private String _selectedGroup;
private boolean _searchSubmitted;
private boolean _isAuthenticating;
private boolean _isDoingIntro;
private List<VaultEntry> _selectedEntries;
private ActionMode _actionMode;
@@ -81,6 +88,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private LinearLayout _btnBackupError;
private FabScrollHelper _fabScrollHelper;
@@ -89,13 +97,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
_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);
@@ -108,6 +119,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_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);
@@ -124,10 +136,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startScanActivity();
});
_btnBackupError = findViewById(R.id.btn_backup_error);
_btnBackupError.setOnClickListener(view -> {
startPreferencesActivity("pref_backups");
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
_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);
@@ -151,20 +175,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return super.dispatchTouchEvent(event);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
doShortcutActions();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
return;
}
_isAuthenticating = false;
_isDoingIntro = false;
if (resultCode != RESULT_OK) {
return;
@@ -215,24 +229,28 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void onPreferencesResult(Intent data) {
// refresh the entire entry list if needed
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
if (_loaded) {
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
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.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
_entryListView.refresh(true);
}
}
}
@@ -254,27 +272,33 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else {
for (VaultEntry entry : entries) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
if (_loaded) {
_entryListView.addEntry(entry);
}
}
saveVault();
saveVault(true);
}
}
private void onAddEntryResult(Intent data) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry);
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry, true);
}
}
private void onEditEntryResult(Intent data) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
@@ -287,6 +311,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
bitmap = BitmapHelper.resize(bitmap, 640, 480);
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
@@ -295,7 +320,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new MultiFormatReader();
Reader reader = new QRCodeReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
@@ -385,8 +410,18 @@ 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 });
startActivityForResult(chooserIntent, CODE_SCAN_IMAGE);
}
private void startPreferencesActivity(String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
}
private void doShortcutActions() {
@@ -411,10 +446,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
getIntent().setData(null);
getIntent().setAction(null);
Uri uri = intent.getData();
if (Intent.ACTION_VIEW.equals(intent.getAction()) && uri != null) {
intent.setData(null);
intent.setAction(null);
GoogleAuthInfo info = null;
try {
@@ -437,12 +472,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (_vault == null) {
// start the intro if the vault file doesn't exist
if (!VaultManager.fileExists(this)) {
if (!_isDoingIntro && !VaultManager.fileExists(this)) {
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
Intent intro = new Intent(this, IntroActivity.class);
startActivityForResult(intro, CODE_DO_INTRO);
_isDoingIntro = true;
return;
}
@@ -461,7 +497,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
if (_app.isVaultLocked()) {
startAuthActivity();
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
@@ -478,6 +514,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
handleDeeplink();
updateLockIcon();
doShortcutActions();
updateBackupErrorBar();
}
@Override
@@ -492,8 +529,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
if (_app.isAutoLockEnabled()) {
_app.lock();
if (_app.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
_app.lock(false);
return;
}
@@ -506,7 +543,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.removeEntry(oldEntry);
}
saveVault();
saveVault(true);
}
@Override
@@ -556,8 +593,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(null);
return true;
}
case R.id.action_about: {
@@ -566,7 +602,7 @@ 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) {
@@ -615,16 +651,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void loadEntries() {
List<VaultEntry> entries = new ArrayList<>(_vault.getEntries());
_entryListView.addEntries(entries);
_entryListView.runEntriesAnimation();
_loaded = true;
if (!_loaded) {
_entryListView.addEntries(_vault.getEntries());
_entryListView.runEntriesAnimation();
_loaded = true;
}
}
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
private void startAuthActivity(boolean inhibitBioPrompt) {
if (!_isAuthenticating) {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
startActivityForResult(intent, CODE_DECRYPT);
_isAuthenticating = true;
}
}
private void updateLockIcon() {
@@ -635,6 +675,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void updateBackupErrorBar() {
String error = null;
if (_app.getPreferences().isBackupsEnabled()) {
error = _app.getPreferences().getBackupsError();
}
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
}
@Override
public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) {
@@ -646,8 +695,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
copyEntryCode(entry);
}
@Override
@@ -684,12 +731,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryDrop(VaultEntry entry) {
saveVault();
saveVault(false);
}
@Override
public void onEntryChange(VaultEntry entry) {
saveVault();
saveVault(true);
}
public void onEntryCopy(VaultEntry entry) {
copyEntryCode(entry);
}
@Override
@@ -698,7 +749,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();
}
@@ -706,11 +760,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.clearEntries();
_loaded = false;
if (isOpen()) {
startAuthActivity();
if (userInitiated) {
startAuthActivity(true);
} else {
super.onLocked(userInitiated);
}
super.onLocked();
}
private void copyEntryCode(VaultEntry entry) {
@@ -745,6 +799,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
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) -> {
deleteEntries(_selectedEntries);
@@ -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,6 +1,8 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@@ -9,6 +11,11 @@ public class PreferencesActivity extends AegisActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (savedInstanceState == null) {
_fragment = new PreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
@@ -18,6 +25,18 @@ public class PreferencesActivity extends AegisActivity {
}
}
@Override
public void onResume() {
super.onResume();
Intent intent = getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
_fragment.scrollToPreference(preference);
intent.removeExtra("pref");
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
// pass permission request results to the fragment
@@ -40,4 +59,17 @@ public class PreferencesActivity extends AegisActivity {
outState.putParcelable("result", _fragment.getResult());
super.onSaveInstanceState(outState);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -7,20 +7,27 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.FileProvider;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
@@ -29,6 +36,7 @@ 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.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
@@ -36,6 +44,8 @@ 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.ui.tasks.ExportTask;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
@@ -49,22 +59,21 @@ 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.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
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;
import static android.text.TextUtils.isDigitsOnly;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
@@ -73,10 +82,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private static final int CODE_GROUPS = 3;
private static final int CODE_SELECT_ENTRIES = 4;
private static final int CODE_EXPORT = 5;
private static final int CODE_EXPORT_ENCRYPT = 6;
private static final int CODE_BACKUPS = 7;
private static final int CODE_EXPORT_PLAIN = 6;
private static final int CODE_EXPORT_GOOGLE_URI = 7;
private static final int CODE_BACKUPS = 8;
private Intent _result;
private AegisApplication _app;
private Preferences _prefs;
private VaultManager _vault;
@@ -92,6 +103,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private Preference _slotsPreference;
private Preference _groupsPreference;
private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _pinKeyboardPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
@@ -101,9 +113,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_prefs = app.getPreferences();
_vault = app.getVaultManager();
_app = (AegisApplication) getActivity().getApplication();
_prefs = _app.getPreferences();
_vault = _app.getVaultManager();
// set the result intent in advance
setResult(new Intent());
@@ -174,7 +186,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startImport();
Dialogs.showImportersDialog(getContext(), false, definition -> {
_importerType = definition.getType();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
});
return true;
}
});
@@ -183,7 +201,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImportApp();
Dialogs.showImportersDialog(getContext(), true, definition -> {
DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType());
importApp(importer);
});
return true;
}
});
@@ -228,6 +249,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return true;
});
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@@ -246,7 +273,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean)newValue) {
if ((boolean) newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
@@ -256,14 +283,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
});
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
tapToRevealTimePreference.setSummary(app.getPreferences().getTapToRevealTime() + " seconds");
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);
_app.getPreferences().setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
_result.putExtra("needsRefresh", true);
}
@@ -366,6 +393,33 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
_pinKeyboardPreference = findPreference("pref_pin_keyboard");
_pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!(boolean) newValue) {
return true;
}
Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
if (isDigitsOnly(new String(password))) {
List<PasswordSlot> slots = _vault.getCredentials().getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener());
task.execute(getLifecycle(), params);
} else {
setPinKeyboardPreference(false);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.pin_keyboard_error_description)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
}
}, dialog -> {
setPinKeyboardPreference(false);
});
return false;
});
_groupsPreference = findPreference("pref_groups");
_groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
@@ -404,6 +458,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
if (_prefs.isBackupsEnabled()) {
try {
_vault.backup();
Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
@@ -413,23 +468,52 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
});
_backupsVersionsPreference = findPreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
number = number * 5 + 5;
_prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
});
return false;
});
_autoLockPreference = findPreference("pref_auto_lock");
_autoLockPreference.setSummary(getAutoLockSummary());
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
final boolean[] checkedItems = new boolean[items.length];
for (int i = 0; i < items.length; i++) {
checkedItems[i] = _prefs.isAutoLockTypeEnabled(items[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_auto_lock_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int autoLock = Preferences.AUTO_LOCK_OFF;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
autoLock |= items[i];
}
}
_prefs.setAutoLockMask(autoLock);
_autoLockPreference.setSummary(getAutoLockSummary());
})
.setNegativeButton(android.R.string.cancel, null);
Dialogs.showSecureDialog(builder.create());
return false;
});
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onResume() {
super.onResume();
updateEncryptionPreferences();
updateBackupPreference();
}
@@ -458,8 +542,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_ENCRYPT:
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
case CODE_EXPORT_PLAIN:
// intentional fallthrough
case CODE_EXPORT_GOOGLE_URI:
onExportResult(requestCode, resultCode, data);
break;
case CODE_BACKUPS:
onSelectBackupsLocationResult(resultCode, data);
break;
@@ -475,42 +562,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
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()
@@ -520,11 +571,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
SuFile file = importer.getAppPath();
try (SuFileInputStream stream = new SuFileInputStream(file)) {
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true);
importDatabase(importer, reader);
}
DatabaseImporter.State state = importer.readFromApp();
processImporterState(state);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
@@ -534,9 +582,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
private void processImporterState(DatabaseImporter.State state) {
try {
DatabaseImporter.State state = importer.read(reader);
if (state.isEncrypted()) {
// temporary special case for encrypted Aegis vaults
if (state instanceof AegisImporter.EncryptedState) {
@@ -544,7 +591,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
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() {
@@ -597,11 +643,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) {
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream);
importDatabase(importer, reader);
DatabaseImporter.State state = importer.read(stream);
processImporterState(state);
} catch (FileNotFoundException e) {
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
} catch (DatabaseImporterException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
}
@@ -626,39 +672,111 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) result.getErrors());
intent.putExtra("vaultContainsEntries", _vault.getEntries().size() > 0);
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())
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null);
TextView warningText = view.findViewById(R.id.text_export_warning);
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
Spinner spinner = view.findViewById(R.id.spinner_export_format);
SpinnerHelper.fillSpinner(getContext(), spinner, R.array.export_formats);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
checkBoxEncrypt.setChecked(position == 0);
checkBoxEncrypt.setEnabled(position == 0);
warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename = checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN;
filename = new VaultBackupManager.FileInfo(filename).toString();
.setView(view)
.setNeutralButton(R.string.share, null)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, filename);
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
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);
}
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setChecked(false);
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
} else {
builder.setMessage(R.string.export_warning);
}
Dialogs.showSecureDialog(builder.create());
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
btnPos.setOnClickListener(v -> {
dialog.dismiss();
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
startActivityForResult(intent, requestCode);
});
btnNeutral.setOnClickListener(v -> {
dialog.dismiss();
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
File file;
try {
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
} catch (IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
startExportVault(requestCode, cb -> {
try (OutputStream stream = new FileOutputStream(file)) {
cb.exportVault(stream);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file);
Intent intent = new Intent(Intent.ACTION_SEND)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_STREAM, uri);
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
startActivity(chooser);
});
});
});
Dialogs.showSecureDialog(dialog);
}
private void onSlotManagerResult(int resultCode, Intent data) {
@@ -684,6 +802,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
entry.setGroup(null);
}
}
saveVault();
}
private void onSelectEntriesResult(int resultCode, Intent data) {
@@ -691,6 +811,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
boolean wipeEntries = data.getBooleanExtra("wipeEntries", false);
if (wipeEntries) {
_vault.wipeEntries();
}
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
@@ -708,27 +833,109 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
String toastMessage = getResources().getString(R.string.imported_entries_count, selectedEntries.size());
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
_result.putExtra("needsRecreate", true);
}
private void onExportResult(int resultCode, Intent data, boolean encrypt) {
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
}
return CODE_EXPORT_GOOGLE_URI;
}
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN;
return new VaultBackupManager.FileInfo(filename);
}
return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt");
}
private static String getExportMimeType(int requestCode) {
return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json";
}
private File getExportCacheDir() throws IOException {
File dir = new File(getContext().getCacheDir(), "export");
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create directory %s", dir));
}
return dir;
}
private void startExportVault(int requestCode, StartExportCallback cb) {
switch (requestCode) {
case CODE_EXPORT:
if (_vault.isEncryptionEnabled()) {
cb.exportVault(stream -> _vault.export(stream));
} else {
Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
} catch (SlotException e) {
onException(e);
return;
}
cb.exportVault(stream -> _vault.export(stream, creds));
}
@Override
public void onException(Exception e) {
}
});
}
break;
case CODE_EXPORT_PLAIN:
cb.exportVault((stream) -> _vault.export(stream, null));
break;
case CODE_EXPORT_GOOGLE_URI:
cb.exportVault((stream) -> _vault.exportGoogleUris(stream));
break;
}
}
private void onExportResult(int requestCode, int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
_vault.export(stream, encrypt);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
startExportVault(requestCode, cb -> {
File file;
OutputStream outStream = null;
try {
file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
outStream = new FileOutputStream(file);
cb.exportVault(outStream);
new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
} catch (VaultManagerException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} finally {
try {
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
@@ -742,13 +949,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
}
private boolean saveVault() {
try {
_vault.save();
_vault.save(true);
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
@@ -765,6 +973,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
_pinKeyboardPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = _vault.getCredentials().getSlots();
@@ -804,10 +1013,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
startActivityForResult(intent, CODE_BACKUPS);
}
private void setPinKeyboardPreference(boolean enable) {
_pinKeyboardPreference.setChecked(enable);
}
private String getAutoLockSummary() {
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < settings.length; i++) {
if (_prefs.isAutoLockTypeEnabled(settings[i])) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(descriptions[i].toLowerCase());
}
}
if (builder.length() == 0) {
return getString(R.string.pref_auto_lock_summary_disabled);
}
return getString(R.string.pref_auto_lock_summary, builder.toString());
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
@@ -820,7 +1054,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
slots.remove(oldSlot);
if (oldSlot != null) {
slots.remove(oldSlot);
}
// add the new master password slot
slots.add(slot);
@@ -831,6 +1067,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_vault.setCredentials(creds);
saveVault();
if (_prefs.isPinKeyboardEnabled()) {
setPinKeyboardPreference(false);
Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show();
}
}
@Override
@@ -891,4 +1132,41 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
if (result != null) {
setPinKeyboardPreference(true);
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.invalid_password)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
setPinKeyboardPreference(false);
}
}
}
private class ExportResultListener implements ExportTask.Callback {
@Override
public void onTaskFinished(Exception e) {
if (e != null) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} else {
Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
}
}
private interface FinishExportCallback {
void exportVault(OutputStream stream) throws IOException, VaultManagerException;
}
private interface StartExportCallback {
void exportVault(FinishExportCallback exportCb);
}
}
@@ -1,133 +1,166 @@
package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.zxing.BarcodeFormat;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import 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 PreviewView _previewView;
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);
_entries = new ArrayList<>();
_scannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
return new SquareFinderView(context);
}
};
_scannerView.setResultHandler(this);
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
_lenses = new ArrayList<>();
_previewView = findViewById(R.id.preview_view);
int camera = getRearCameraId();
if (camera == -1) {
camera = getFrontCameraId();
if (camera == -1) {
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
_cameraProviderFuture.addListener(() -> {
try {
_cameraProvider = _cameraProviderFuture.get();
} catch (ExecutionException | InterruptedException e) {
// if we're to believe the Android documentation, this should never happen
// https://developer.android.com/training/camerax/preview#check-provider
throw new RuntimeException(e);
}
addCamera(CameraSelector.LENS_FACING_BACK);
addCamera(CameraSelector.LENS_FACING_FRONT);
if (_lenses.size() == 0) {
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
finish();
return;
}
_facing = CAMERA_FACING_FRONT;
}
_scannerView.startCamera(camera);
_currentLens = _lenses.get(0);
updateCameraIcon();
setContentView(_scannerView);
bindPreview(_cameraProvider);
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void setPreferredTheme(Theme theme) {
setTheme(R.style.AppTheme_Fullscreen);
protected void 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) {
_cameraProvider.unbindAll();
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
bindPreview(_cameraProvider);
updateCameraIcon();
return true;
}
return super.onOptionsItemSelected(item);
}
private void addCamera(int lens) {
try {
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
if (_cameraProvider.hasCamera(camera)) {
_lenses.add(lens);
}
} catch (CameraInfoUnavailableException e) {
e.printStackTrace();
}
}
@Override
public void onResume() {
super.onResume();
_scannerView.startCamera(getCameraId(_facing));
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = _lenses.size() > 1;
if (dual) {
switch (_currentLens) {
case CameraSelector.LENS_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CameraSelector.LENS_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
}
item.setVisible(dual);
}
}
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(_previewView.getSurfaceProvider());
CameraSelector selector = new CameraSelector.Builder()
.requireLensFacing(_currentLens)
.build();
ImageAnalysis analysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QrCodeAnalyzer(this));
cameraProvider.bindToLifecycle(this, selector, preview, analysis);
}
@Override
public void onPause() {
super.onPause();
_scannerView.stopCamera();
}
@Override
public void handleResult(Result rawResult) {
public void onQrCodeDetected(Result result) {
try {
Uri uri = Uri.parse(rawResult.getText().trim());
Uri uri = Uri.parse(result.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
}
_scannerView.resumeCameraPreview(this);
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
_scannerView.resumeCameraPreview(this);
});
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
_cameraProvider.unbindAll();
}
}
@@ -159,7 +192,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
finish(_entries);
}
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
Toast.makeText(this, getResources().getQuantityString(R.plurals.google_qr_export_scanned, export.getBatchSize(), _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
} else if (_batchIndex != batchIndex) {
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
}
@@ -171,42 +204,4 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
setResult(RESULT_OK, intent);
finish();
}
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
if (dual) {
switch (_facing) {
case CAMERA_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CAMERA_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
}
item.setVisible(dual);
}
}
private static int getCameraId(int facing) {
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
Camera.getCameraInfo(i, info);
if (info.facing == facing) {
return i;
}
}
return -1;
}
private static int getRearCameraId() {
return getCameraId(CAMERA_FACING_BACK);
}
private static int getFrontCameraId() {
return getCameraId(CAMERA_FACING_FRONT);
}
}
@@ -1,11 +1,5 @@
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;
@@ -16,6 +10,12 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
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 com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
@@ -25,10 +25,12 @@ import com.getbase.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class SelectEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private boolean _vaultContainsEntries;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -57,6 +59,7 @@ public class SelectEntriesActivity extends AegisActivity {
Intent intent = getIntent();
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
_vaultContainsEntries = intent.getBooleanExtra("vaultContainsEntries", false);
for (ImportEntry entry : entries) {
_adapter.addEntry(entry);
@@ -67,14 +70,20 @@ public class SelectEntriesActivity extends AegisActivity {
}
FloatingActionButton fabMenu = findViewById(R.id.fab);
fabMenu.setOnClickListener(v -> returnSelectedEntries());
fabMenu.setOnClickListener(v -> {
if (_vaultContainsEntries) {
showWipeEntriesDialog();
} else {
returnSelectedEntries(false);
}
});
_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()))
.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());
@@ -100,10 +109,19 @@ public class SelectEntriesActivity extends AegisActivity {
.create());
}
private void returnSelectedEntries() {
List<ImportEntry> entries = _adapter.getCheckedEntries();
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::returnSelectedEntries
);
}
private void returnSelectedEntries(boolean wipeEntries) {
Intent intent = new Intent();
List<ImportEntry> entries = _adapter.getCheckedEntries();
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("wipeEntries", wipeEntries);
setResult(RESULT_OK, intent);
finish();
}
@@ -0,0 +1,132 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
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);
_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 = null;
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;
}
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 : Color.WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
_qrImage.setImageBitmap(bitmap);
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.ui.intro;
import android.os.Bundle;
import androidx.annotation.NonNull;
public interface IntroActivityInterface {
/**
* Navigate to the next slide.
*/
void goToNextSlide();
/**
* Navigate to the previous slide.
*/
void goToPreviousSlide();
/**
* Navigate to the slide of the given type.
*/
void skipToSlide(Class<? extends SlideFragment> type);
/**
* Retrieves the state of the intro. The state is shared among all slides and is
* properly restored after a configuration change. This method may only be called
* after onAttach has been called.
*/
@NonNull
Bundle getState();
}
@@ -0,0 +1,215 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ui.AegisActivity;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public abstract class IntroBaseActivity extends AegisActivity implements IntroActivityInterface {
private Bundle _state;
private ViewPager2 _pager;
private ScreenSlidePagerAdapter _adapter;
private List<Class<? extends SlideFragment>> _slides;
private WeakReference<SlideFragment> _currentSlide;
private ImageButton _btnPrevious;
private ImageButton _btnNext;
private SlideIndicator _slideIndicator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intro);
_slides = new ArrayList<>();
_state = new Bundle();
_btnPrevious = findViewById(R.id.btnPrevious);
_btnPrevious.setOnClickListener(v -> goToPreviousSlide());
_btnNext = findViewById(R.id.btnNext);
_btnNext.setOnClickListener(v -> goToNextSlide());
_slideIndicator = findViewById(R.id.slideIndicator);
_adapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
_pager = findViewById(R.id.pager);
_pager.setAdapter(_adapter);
_pager.setUserInputEnabled(false);
_pager.registerOnPageChangeCallback(new SlideSkipBlocker());
View pagerChild = _pager.getChildAt(0);
if (pagerChild instanceof RecyclerView) {
pagerChild.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
}
@Override
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
_state = savedInstanceState.getBundle("introState");
updatePagerControls();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBundle("introState", _state);
}
void setCurrentSlide(SlideFragment slide) {
_currentSlide = new WeakReference<>(slide);
}
@Override
public void goToNextSlide() {
int pos = _pager.getCurrentItem();
if (pos != _slides.size() - 1) {
SlideFragment currentSlide = _currentSlide.get();
if (currentSlide.isFinished()) {
currentSlide.onSaveIntroState(_state);
setPagerPosition(pos, 1);
} else {
currentSlide.onNotFinishedError();
}
} else {
onDonePressed();
}
}
@Override
public void goToPreviousSlide() {
int pos = _pager.getCurrentItem();
if (pos != 0 && pos != _slides.size() - 1) {
setPagerPosition(pos, -1);
}
}
@Override
public void skipToSlide(Class<? extends SlideFragment> type) {
int i = _slides.indexOf(type);
if (i == -1) {
throw new IllegalStateException(String.format("Cannot skip to slide of type %s because it is not in the slide list", type.getName()));
}
setPagerPosition(i);
}
/**
* Called before a slide change is made. Overriding gives implementers the
* opportunity to block a slide change. onSaveIntroState is guaranteed to have been
* called on oldSlide before onBeforeSlideChanged is called.
* @param oldSlide the slide that is currently shown.
* @param newSlide the next slide that will be shown.
* @return whether to block the transition.
*/
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
return false;
}
/**
* Called after a slide change was made.
* @param oldSlide the slide that was previously shown.
* @param newSlide the slide that is now shown.
*/
protected void onAfterSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
}
private void setPagerPosition(int pos) {
Class<? extends SlideFragment> oldSlide = _currentSlide.get().getClass();
Class<? extends SlideFragment> newSlide = _slides.get(pos);
if (!onBeforeSlideChanged(oldSlide, newSlide)) {
_pager.setCurrentItem(pos);
}
onAfterSlideChanged(oldSlide, newSlide);
updatePagerControls();
}
private void setPagerPosition(int pos, int delta) {
pos += delta;
setPagerPosition(pos);
}
private void updatePagerControls() {
int pos = _pager.getCurrentItem();
_btnPrevious.setVisibility(
pos != 0 && pos != _slides.size() - 1
? View.VISIBLE
: View.INVISIBLE);
if (pos == _slides.size() - 1) {
_btnNext.setImageResource(R.drawable.circular_button_done);
}
_slideIndicator.setSlideCount(_slides.size());
_slideIndicator.setCurrentSlide(pos);
}
@NonNull
public Bundle getState() {
return _state;
}
@Override
public void onBackPressed() {
goToPreviousSlide();
}
protected abstract void onDonePressed();
public void addSlide(Class<? extends SlideFragment> type) {
if (_slides.contains(type)) {
throw new IllegalStateException(String.format("Only one slide of type %s may be added to the intro", type.getName()));
}
_slides.add(type);
_slideIndicator.setSlideCount(_slides.size());
}
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, getLifecycle());
}
@NonNull
@Override
public Fragment createFragment(int position) {
Class<? extends SlideFragment> type = _slides.get(position);
try {
return type.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
@Override
public int getItemCount() {
return _slides.size();
}
}
private class SlideSkipBlocker extends ViewPager2.OnPageChangeCallback {
@Override
public void onPageScrollStateChanged(@ViewPager2.ScrollState int state) {
// disable the buttons while scrolling to prevent disallowed skipping of slides
boolean enabled = state == ViewPager2.SCROLL_STATE_IDLE;
_btnNext.setEnabled(enabled);
_btnPrevious.setEnabled(enabled);
}
}
}
@@ -0,0 +1,87 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import java.lang.ref.WeakReference;
public abstract class SlideFragment extends Fragment implements IntroActivityInterface {
private WeakReference<IntroBaseActivity> _parent;
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (!(context instanceof IntroBaseActivity)) {
throw new ClassCastException("Parent context is expected to be of type IntroBaseActivity");
}
_parent = new WeakReference<>((IntroBaseActivity) context);
}
@CallSuper
@Override
public void onResume() {
super.onResume();
getParent().setCurrentSlide(this);
}
/**
* Reports whether or not all required user actions are finished on this slide,
* indicating that we're ready to move to the next slide.
*/
public boolean isFinished() {
return true;
}
/**
* Called if the user tried to move to the next slide, but isFinished returned false.
*/
protected void onNotFinishedError() {
}
/**
* Called when the SlideFragment is expected to write its state to the given shared
* introState. This is only called if the user navigates to the next slide, not
* when a previous slide is next to be shown.
*/
protected void onSaveIntroState(@NonNull Bundle introState) {
}
@Override
public void goToNextSlide() {
getParent().goToNextSlide();
}
@Override
public void goToPreviousSlide() {
getParent().goToPreviousSlide();
}
@Override
public void skipToSlide(Class<? extends SlideFragment> type) {
getParent().skipToSlide(type);
}
@NonNull
@Override
public Bundle getState() {
return getParent().getState();
}
@NonNull
private IntroBaseActivity getParent() {
if (_parent == null || _parent.get() == null) {
throw new IllegalStateException("This method must not be called before onAttach()");
}
return _parent.get();
}
}
@@ -0,0 +1,98 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.R;
public class SlideIndicator extends View {
private Paint _paint;
private int _slideCount;
private int _slideIndex;
private float _dotRadius;
private float _dotSeparator;
private int _dotColor;
private int _dotColorSelected;
public SlideIndicator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
_paint = new Paint();
_paint.setAntiAlias(true);
_paint.setStyle(Paint.Style.FILL);
TypedArray array = null;
try {
array = context.obtainStyledAttributes(attrs, R.styleable.SlideIndicator);
_dotRadius = array.getDimension(R.styleable.SlideIndicator_dot_radius, 5f);
_dotSeparator = array.getDimension(R.styleable.SlideIndicator_dot_separation, 5f);
_dotColor = array.getColor(R.styleable.SlideIndicator_dot_color, Color.GRAY);
_dotColorSelected = array.getColor(R.styleable.SlideIndicator_dot_color_selected, Color.BLACK);
} finally {
if (array != null) {
array.recycle();
}
}
}
public void setSlideCount(int slideCount) {
if (slideCount < 0) {
throw new IllegalArgumentException("Slide count cannot be negative");
}
_slideCount = slideCount;
invalidate();
}
public void setCurrentSlide(int index) {
if (index < 0) {
throw new IllegalArgumentException("Slide index cannot be negative");
}
if (index + 1 > _slideCount) {
throw new IllegalStateException(String.format("Slide index out of range, slides: %d, index: %d", _slideCount, index));
}
_slideIndex = index;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
if (_slideCount <= 0) {
return;
}
float density = getResources().getDisplayMetrics().density;
float dotDp = density * _dotRadius * 2;
float spaceDp = density * _dotSeparator;
float offset;
if (_slideCount % 2 == 0) {
offset = (spaceDp / 2) + (dotDp / 2) + dotDp * (_slideCount / 2f - 1) + spaceDp * (_slideCount / 2f - 1);
} else {
int spaces = _slideCount > 1 ? _slideCount - 2 : 0;
offset = (_slideCount - 1) * (dotDp / 2) + spaces * spaceDp;
}
canvas.translate((getWidth() / 2f) - offset,getHeight() / 2f);
for (int i = 0; i < _slideCount; i++) {
int slideIndex = isRtl() ? (_slideCount - 1) - _slideIndex : _slideIndex;
_paint.setColor(i == slideIndex ? _dotColorSelected : _dotColor);
canvas.drawCircle(0,0, dotDp / 2, _paint);
canvas.translate(dotDp + spaceDp,0);
}
}
private boolean isRtl() {
return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
}
@@ -1,87 +0,0 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.appintro.SlidePolicy;
import com.google.android.material.snackbar.Snackbar;
public class CustomAuthenticationSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authentication_slide, container, false);
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
_buttonGroup.setOnCheckedChangeListener(this);
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
if (BiometricsHelper.isAvailable(getContext())) {
RadioButton button = view.findViewById(R.id.rb_biometrics);
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
_buttonGroup.check(R.id.rb_biometrics);
}
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
public void setBgColor(int color) {
_bgColor = color;
}
@Override
public boolean isPolicyRespected() {
return _buttonGroup.getCheckedRadioButtonId() != -1;
}
@Override
public void onUserIllegallyRequestedNextPage() {
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.snackbar_authentication_method), Snackbar.LENGTH_LONG);
snackbar.show();
}
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if (i == -1) {
return;
}
int id;
switch (i) {
case R.id.rb_none:
id = CRYPT_TYPE_NONE;
break;
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
break;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
}
Intent intent = getActivity().getIntent();
intent.putExtra("cryptType", id);
}
}
@@ -0,0 +1,16 @@
package com.beemdevelopment.aegis.ui.slides;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class DoneSlide extends SlideFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_done_slide, container, false);
}
}
@@ -0,0 +1,93 @@
package com.beemdevelopment.aegis.ui.slides;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class SecurityPickerSlide extends SlideFragment {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private RadioButton _bioButton;
private TextView _bioText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
_bioButton = view.findViewById(R.id.rb_biometrics);
_bioText = view.findViewById(R.id.text_rb_biometrics);
updateBiometricsOption(true);
return view;
}
@Override
public void onResume() {
super.onResume();
updateBiometricsOption(false);
}
/**
* Updates the status of the biometrics option. Auto-selects the biometrics option
* if the API version is new enough, permission is granted and a scanner is found.
*/
private void updateBiometricsOption(boolean autoSelect) {
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_bioButton.setEnabled(canUseBio);
_bioText.setEnabled(canUseBio);
if (!canUseBio && _buttonGroup.getCheckedRadioButtonId() == R.id.rb_biometrics) {
_buttonGroup.check(R.id.rb_password);
}
if (canUseBio && autoSelect) {
_buttonGroup.check(R.id.rb_biometrics);
}
}
@Override
public boolean isFinished() {
return _buttonGroup.getCheckedRadioButtonId() != -1;
}
@Override
public void onNotFinishedError() {
Toast.makeText(getContext(), R.string.snackbar_authentication_method, Toast.LENGTH_SHORT).show();
}
@Override
public void onSaveIntroState(@NonNull Bundle introState) {
int buttonId = _buttonGroup.getCheckedRadioButtonId();
int type;
switch (buttonId) {
case R.id.rb_none:
type = CRYPT_TYPE_NONE;
break;
case R.id.rb_password:
type = CRYPT_TYPE_PASS;
break;
case R.id.rb_biometrics:
type = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
}
introState.putInt("cryptType", type);
}
}
@@ -1,52 +1,68 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.github.appintro.SlidePolicy;
import com.github.appintro.SlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
private int _bgColor;
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 SecuritySetupSlide extends SlideFragment {
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
private ProgressBar _barPasswordStrength;
private TextView _textPasswordStrength;
private TextInputLayout _textPasswordWrapper;
private int _cryptType;
private VaultFileCredentials _creds;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
_barPasswordStrength = view.findViewById(R.id.progressBar);
_textPasswordStrength = view.findViewById(R.id.text_password_strength);
_textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
@@ -60,21 +76,41 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
}
});
_creds = new VaultFileCredentials();
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
_textPassword.addTextChangedListener(new TextWatcher() {
private Zxcvbn _zxcvbn = new Zxcvbn();
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), getContext()) : "");
_textPasswordWrapper.setError(strength.getFeedback().getWarning());
strength.wipe();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
return view;
}
public int getCryptType() {
return _cryptType;
}
@Override
public void onResume() {
super.onResume();
public VaultFileCredentials getCredentials() {
return _creds;
}
_cryptType = getState().getInt("cryptType", CRYPT_TYPE_INVALID);
if (_cryptType == CRYPT_TYPE_INVALID || _cryptType == CRYPT_TYPE_NONE) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d", _cryptType));
}
public void setBgColor(int color) {
_bgColor = color;
_creds = new VaultFileCredentials();
}
private void showBiometricPrompt() {
@@ -89,31 +125,21 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
private void deriveKey() {
PasswordSlot slot = new PasswordSlot();
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, EditTextHelper.getEditTextChars(_textPassword));
new KeyDerivationTask(getContext(), new PasswordDerivationListener()).execute(params);
KeyDerivationTask task = new KeyDerivationTask(getContext(), new PasswordDerivationListener());
task.execute(getLifecycle(), params);
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
}
@Override
public void onSlideDeselected() {
}
@Override
public boolean isPolicyRespected() {
public boolean isFinished() {
switch (_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
case CRYPT_TYPE_BIOMETRIC:
if (!_creds.getSlots().has(BiometricSlot.class)) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
case CRYPT_TYPE_PASS:
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
return _creds.getSlots().has(PasswordSlot.class);
}
@@ -125,23 +151,21 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
public void onNotFinishedError() {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = getString(R.string.password_equality_error);
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
} else if (_cryptType != CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
Toast.makeText(getContext(), R.string.password_equality_error, Toast.LENGTH_SHORT).show();
} else if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC) {
deriveKey();
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
showBiometricPrompt();
}
}
@Override
public void onSaveIntroState(@NonNull Bundle introState) {
introState.putSerializable("creds", _creds);
}
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
@Override
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
@@ -155,7 +179,7 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
return;
}
((IntroActivity) getActivity()).goToNextSlide();
goToNextSlide();
}
}
@@ -0,0 +1,16 @@
package com.beemdevelopment.aegis.ui.slides;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class WelcomeSlide extends SlideFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_welcome_slide, container, false);
}
}
@@ -0,0 +1,64 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ExportTask extends ProgressDialogTask<ExportTask.Params, Exception> {
private final Callback _cb;
public ExportTask(Context context, Callback cb) {
super(context, context.getString(R.string.exporting_vault));
_cb = cb;
}
@Override
protected Exception doInBackground(ExportTask.Params... args) {
setPriority();
ExportTask.Params params = args[0];
try (InputStream inStream = new FileInputStream(params.getFile());
OutputStream outStream = getDialog().getContext().getContentResolver().openOutputStream(params.getDestUri(), "w")) {
IOUtils.copy(inStream, outStream);
return null;
} catch (IOException e) {
return e;
}
}
@Override
protected void onPostExecute(Exception e) {
super.onPostExecute(e);
_cb.onTaskFinished(e);
}
public static class Params {
private final File _file;
private final Uri _destUri;
public Params(File file, Uri destUri) {
_file = file;
_destUri = destUri;
}
public File getFile() {
return _file;
}
public Uri getDestUri() {
return _destUri;
}
}
public interface Callback {
void onTaskFinished(Exception e);
}
}
@@ -1,13 +1,18 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Process;
import com.beemdevelopment.aegis.ui.Dialogs;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import com.beemdevelopment.aegis.ui.Dialogs;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
private ProgressDialog _dialog;
@@ -47,4 +52,29 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
protected final ProgressDialog getDialog() {
return _dialog;
}
@SafeVarargs
public final void execute(@Nullable Lifecycle lifecycle, Params... params) {
if (lifecycle != null) {
LifecycleObserver observer = new Observer(getDialog());
lifecycle.addObserver(observer);
}
execute(params);
}
private static class Observer implements LifecycleObserver {
private Dialog _dialog;
public Observer(Dialog dialog) {
_dialog = dialog;
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void onPause() {
if (_dialog != null && _dialog.isShowing()) {
_dialog.dismiss();
_dialog = null;
}
}
}
}
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.ui.views;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -18,6 +19,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -35,8 +37,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tempHighlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private boolean _copyOnTap;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
@@ -47,6 +51,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
private EntryHolder _dragHandleHolder; // holder with enabled drag handle
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
@@ -88,17 +93,25 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_highlightEntry = highlightEntry;
}
public void setTempHighlightEntry(boolean highlightEntry) {
_tempHighlightEntry = highlightEntry;
}
public void setIsCopyOnTapEnabled(boolean enabled) {
_copyOnTap = enabled;
}
public VaultEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
public void addEntry(VaultEntry entry) {
public int addEntry(VaultEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
return;
return -1;
}
boolean added = false;
int position = -1;
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
// insert the entry in the correct order
@@ -107,16 +120,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
_shownEntries.add(i, entry);
notifyItemInserted(i);
added = true;
position = i;
break;
}
}
}
if (!added){
if (position < 0){
_shownEntries.add(entry);
int position = getItemCount() - 1;
position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
@@ -124,10 +137,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
_view.onListChange();
checkPeriodUniformity();
return position;
}
public void addEntries(List<VaultEntry> entries) {
public void addEntries(Collection<VaultEntry> entries) {
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
@@ -142,6 +157,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyItemRemoved(position);
}
_view.onListChange();
checkPeriodUniformity();
}
@@ -218,7 +234,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyDataSetChanged();
} else {
for (EntryHolder holder : _holders) {
holder.refreshCode();
holder.refresh();
}
}
}
@@ -267,6 +283,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
Collections.sort(_shownEntries, comparator);
}
_view.onListChange();
notifyDataSetChanged();
}
@@ -330,7 +347,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
VaultEntry entry = _shownEntries.get(position);
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry;
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
holder.setData(entry, _codeGroupSize, _showAccountName, showProgress, hidden, dimmed);
holder.setFocused(_selectedEntries.contains(entry));
@@ -342,14 +359,17 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
boolean handled = false;
if (_selectedEntries.isEmpty()) {
holder.animateCopyText();
if (_copyOnTap) {
_view.onEntryCopy(entry);
holder.animateCopyText();
}
if (_highlightEntry || _tapToReveal) {
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
handled = true;
} else {
focusEntry(entry);
focusEntry(entry, _tapToRevealTime);
}
}
} else {
@@ -377,7 +397,29 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
holder.setFocusedAndAnimate(true);
}
return _view.onLongEntryClick(_shownEntries.get(position));
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(position));
boolean dragEnabled = _selectedEntries.size() == 0
|| _selectedEntries.size() == 1 && _selectedEntries.get(0) == holder.getEntry();
if (dragEnabled && isDragAndDropAllowed()) {
_view.startDrag(_dragHandleHolder);
}
return returnVal;
}
});
holder.itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// Start drag if this is the only item selected
if (event.getActionMasked() == MotionEvent.ACTION_MOVE
&& _selectedEntries.size() == 1
&& _selectedEntries.get(0) == holder.getEntry()
&& isDragAndDropAllowed()) {
_view.startDrag(_dragHandleHolder);
return true;
}
return false;
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@@ -461,20 +503,20 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return maxValue > 1 ? maxKey : -1;
}
private void focusEntry(VaultEntry entry) {
public void focusEntry(VaultEntry entry, int secondsToFocus) {
_focusedEntry = entry;
_dimHandler.removeCallbacksAndMessages(null);
for (EntryHolder holder : _holders) {
if (holder.getEntry() != _focusedEntry) {
if (_highlightEntry) {
if (_highlightEntry || _tempHighlightEntry) {
holder.dim();
}
if (_tapToReveal) {
holder.hideCode();
}
} else {
if (_highlightEntry) {
if (_highlightEntry || _tempHighlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
@@ -483,12 +525,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
_dimHandler.postDelayed(this::resetFocus, secondsToFocus * 1000);
}
private void resetFocus() {
for (EntryHolder holder : _holders) {
if (_highlightEntry) {
if (_focusedEntry != null) {
holder.highlight();
}
if (_tapToReveal) {
@@ -497,18 +539,45 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
_focusedEntry = null;
_tempHighlightEntry = false;
}
private void updateDraggableStatus() {
if (!isDragAndDropAllowed()) {
return;
}
if (_selectedEntries.size() == 1 && _dragHandleHolder == null) {
// Find and enable dragging for the single selected EntryHolder
// Not nice but this is the best method I could find
for (int i = 0; i < _holders.size(); i++) {
if (_holders.get(i).getEntry() == _selectedEntries.get(0)) {
_dragHandleHolder = _holders.get(i);
_dragHandleHolder.setShowDragHandle(true);
_view.setSelectedEntry(_selectedEntries.get(0));
return;
}
}
} else if (_dragHandleHolder != null) {
// Disable dragging if necessary when more/less than 1 selected entry
_dragHandleHolder.setShowDragHandle(false);
_dragHandleHolder = null;
_view.setSelectedEntry(null);
}
}
public void removeSelectedEntry(VaultEntry entry) {
_selectedEntries.remove(entry);
updateDraggableStatus();
}
public void addSelectedEntry(VaultEntry entry) {
if (_highlightEntry) {
if (_focusedEntry != null) {
resetFocus();
}
_selectedEntries.add(entry);
updateDraggableStatus();
}
public void deselectAllEntries() {
@@ -522,6 +591,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
_selectedEntries.clear();
updateDraggableStatus();
}
public boolean isDragAndDropAllowed() {
@@ -547,8 +618,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onEntryCopy(VaultEntry entry);
void onPeriodUniformityChanged(boolean uniform, int period);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
void onListChange();
}
}
@@ -24,10 +24,12 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.zxing.common.StringUtils;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
private static final float DIMMED_ALPHA = 0.2f;
private static final char HIDDEN_CHAR = '●';
private TextView _profileName;
private TextView _profileCode;
@@ -37,6 +39,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private VaultEntry _entry;
private ImageView _buttonRefresh;
private RelativeLayout _description;
private ImageView _dragHandle;
private final ImageView _selected;
private final Handler _selectedHandler;
@@ -45,7 +48,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private boolean _hidden;
private PeriodProgressBar _progressBar;
private TotpProgressBar _progressBar;
private View _view;
private UiRefresher _refresher;
@@ -67,7 +70,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_selected = view.findViewById(R.id.ivSelected);
_dragHandle = view.findViewById(R.id.drag_handle);
_selectedHandler = new Handler();
_animationHandler = new Handler();
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
@@ -83,8 +89,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
if (!_hidden) {
refreshCode();
}
_progressBar.refresh();
}
@Override
@@ -106,6 +110,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_selected.clearAnimation();
_selected.setVisibility(View.GONE);
_selectedHandler.removeCallbacksAndMessages(null);
_animationHandler.removeCallbacksAndMessages(null);
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
setShowProgress(showProgress);
@@ -156,6 +161,14 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_buttonRefresh.setOnClickListener(listener);
}
public void setShowDragHandle(boolean showDragHandle) {
if (showDragHandle) {
_dragHandle.setVisibility(View.VISIBLE);
} else {
_dragHandle.setVisibility(View.INVISIBLE);
}
}
public void setShowProgress(boolean showProgress) {
if (_entry.getInfo() instanceof HotpInfo) {
showProgress = false;
@@ -197,10 +210,17 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void startRefreshLoop() {
_refresher.start();
_progressBar.start();
}
public void stopRefreshLoop() {
_refresher.stop();
_progressBar.stop();
}
public void refresh() {
_progressBar.restart();
refreshCode();
}
public void refreshCode() {
@@ -214,26 +234,34 @@ public class EntryHolder extends RecyclerView.ViewHolder {
String otp = info.getOtp();
if (!(info instanceof SteamInfo)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < otp.length(); i++) {
if (i != 0 && i % _codeGroupSize == 0) {
sb.append(" ");
}
sb.append(otp.charAt(i));
}
otp = sb.toString();
otp = formatCode(otp);
}
_profileCode.setText(otp);
}
private String formatCode(String code) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < code.length(); i++) {
if (i != 0 && i % _codeGroupSize == 0) {
sb.append(" ");
}
sb.append(code.charAt(i));
}
code = sb.toString();
return code;
}
public void revealCode() {
updateCode();
_hidden = false;
}
public void hideCode() {
_profileCode.setText(R.string.tap_to_reveal);
String hiddenText = new String(new char[_entry.getInfo().getDigits()]).replace("\0", Character.toString(HIDDEN_CHAR));
hiddenText = formatCode(hiddenText);
_profileCode.setText(hiddenText);
_hidden = true;
}
@@ -246,9 +274,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void animateCopyText() {
if (_animationHandler != null) {
_animationHandler.removeCallbacksAndMessages(null);
}
_animationHandler.removeCallbacksAndMessages(null);
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
Animation slideDownFadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
@@ -257,14 +284,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileCopied.startAnimation(slideDownFadeIn);
_description.startAnimation(slideDownFadeOut);
_animationHandler = new Handler();
_animationHandler.postDelayed(new Runnable() {
@Override
public void run() {
_profileCopied.startAnimation(fadeOut);
_description.startAnimation(fadeIn);
}
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
_description.startAnimation(fadeIn);
}, 3000);
}
@@ -1,9 +1,11 @@
package com.beemdevelopment.aegis.ui.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
@@ -22,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
@@ -33,6 +36,7 @@ import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -41,11 +45,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private ItemTouchHelper _touchHelper;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private TotpProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
@@ -89,8 +94,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(layoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(_recyclerView);
_touchHelper = new ItemTouchHelper(_touchCallback);
_touchHelper.attachToRecyclerView(_recyclerView);
_recyclerView.setAdapter(_adapter);
int resId = R.anim.layout_animation_fall_down;
@@ -125,8 +130,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
_adapter.setGroupFilter(group, apply);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
if (apply) {
runEntriesAnimation();
@@ -134,7 +139,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setIsLongPressDragEnabled(boolean enabled) {
_touchCallback.setIsLongPressDragEnabled(enabled);
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
}
public void setIsCopyOnTapEnabled(boolean enabled) {
_adapter.setIsCopyOnTapEnabled(enabled);
}
public void setActionModeState(boolean enabled, VaultEntry entry) {
@@ -149,8 +158,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
if (apply) {
runEntriesAnimation();
@@ -158,8 +167,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setSearchFilter(String search) {
_touchCallback.setIsLongPressDragEnabled(search == null);
_adapter.setSearchFilter(search);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
}
public void setSelectedEntry(VaultEntry entry) {
_touchCallback.setSelectedEntry(entry);
}
public void setViewMode(ViewMode mode) {
@@ -168,10 +181,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setViewMode(_viewMode);
}
public void startDrag(RecyclerView.ViewHolder viewHolder) {
_touchHelper.startDrag(viewHolder);
}
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.refresh();
_progressBar.restart();
}
_adapter.refresh(hard);
}
@@ -204,6 +222,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_listener.onEntryChange(entry);
}
@Override
public void onEntryCopy(VaultEntry entry) {
_listener.onEntryCopy(entry);
}
@Override
public void onSelect(VaultEntry entry) { _listener.onSelect(entry); }
@@ -214,16 +237,20 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void onPeriodUniformityChanged(boolean isUniform, int period) {
setShowProgress(isUniform);
if (_showProgress) {
_refresher.stop();
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(period);
_progressBar.start();
_refresher.start();
} else {
_progressBar.setVisibility(View.GONE);
_progressBar.stop();
_refresher.stop();
}
}
@Override
public void onListChange() { _listener.onListChange(); }
public void setCodeGroupSize(int codeGrouping) {
_adapter.setCodeGroupSize(codeGrouping);
}
@@ -249,11 +276,42 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void addEntry(VaultEntry entry) {
_adapter.addEntry(entry);
updateEmptyState();
addEntry(entry, false);
}
public void addEntries(List<VaultEntry> entries) {
@SuppressLint("ClickableViewAccessibility")
public void addEntry(VaultEntry entry, boolean focusEntry) {
int position = _adapter.addEntry(entry);
updateEmptyState();
if (focusEntry && position >= 0) {
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
_recyclerView.removeOnScrollListener(this);
_adapter.setTempHighlightEntry(true);
final int secondsToFocus = 3;
_adapter.focusEntry(entry, secondsToFocus);
}
}
};
_recyclerView.addOnScrollListener(scrollListener);
_recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
_recyclerView.removeOnScrollListener(scrollListener);
_recyclerView.stopScroll();
_recyclerView.setOnTouchListener(null);
}
return false;
});
_recyclerView.smoothScrollToPosition(position);
}
}
public void addEntries(Collection<VaultEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
}
@@ -322,10 +380,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onEntryCopy(VaultEntry entry);
void onLongEntryClick(VaultEntry entry);
void onScroll(int dx, int dy);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
void onListChange();
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
@@ -333,7 +393,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private VerticalSpaceItemDecoration(float dp) {
// convert dp to pixels
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
_height = MetricsHelper.convertDpToPixels(getContext(), dp);
}
@Override
@@ -4,12 +4,12 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import androidx.recyclerview.widget.RecyclerView;
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
private GroupAdapter.Listener _listener;
private ArrayList<String> _groups;
@@ -38,7 +38,7 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
@Override
public GroupHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_group, parent, false);
return new GroupHolder(view);
}
@@ -4,17 +4,17 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
public class GroupHolder extends RecyclerView.ViewHolder {
private TextView _slotName;
private ImageView _buttonDelete;
public GroupHolder(final View view) {
super(view);
_slotName = view.findViewById(R.id.text_slot_name);
_slotName = view.findViewById(R.id.text_group_name);
_buttonDelete = view.findViewById(R.id.button_delete);
}
@@ -1,54 +0,0 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import com.beemdevelopment.aegis.otp.TotpInfo;
import androidx.annotation.RequiresApi;
public class PeriodProgressBar extends ProgressBar {
private int _period;
public PeriodProgressBar(Context context) {
super(context);
}
public PeriodProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setPeriod(int period) {
_period = period;
}
public void refresh() {
// reset the progress bar
int maxProgress = getMax();
setProgress(maxProgress);
// calculate the progress the bar should start at
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
long period = _period * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
// start progress animation
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, 0);
animation.setDuration(millisTillRotation);
animation.setInterpolator(new LinearInterpolator());
animation.start();
}
}
@@ -0,0 +1,83 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.otp.TotpInfo;
public class TotpProgressBar extends ProgressBar {
private int _period = 30;
private Handler _handler;
private float _animDurationScale;
public TotpProgressBar(Context context) {
super(context);
}
public TotpProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setPeriod(int period) {
_period = period;
}
public void start() {
stop();
_handler = new Handler();
_animDurationScale = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
refresh();
}
public void stop() {
if (_handler != null) {
_handler.removeCallbacksAndMessages(null);
_handler = null;
}
}
public void restart() {
stop();
start();
}
private void refresh() {
// calculate the current progress the bar should start at
int maxProgress = getMax();
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
int currentProgress = (int) (maxProgress * ((float) millisTillRotation / (_period * 1000)));
// start progress animation, compensating for any changes to the animator duration scale settings
float animPart = (float) maxProgress / _period;
int animEnd = (int) (Math.floor(currentProgress / animPart) * animPart);
int animPartDuration = _animDurationScale > 0 ? (int) (1000 / _animDurationScale) : 0;
float animDurationFraction = (float) (currentProgress - animEnd) / animPart;
int realAnimDuration = (int) (1000 * animDurationFraction);
int animDuration = (int) (animPartDuration * animDurationFraction);
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, animEnd);
animation.setDuration(animDuration);
animation.setInterpolator(new LinearInterpolator());
animation.start();
// the animation only lasts for (less than) one second, so restart it after that
_handler.postDelayed(this::refresh, realAnimDuration);
}
}
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.util;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -34,4 +35,21 @@ public class IOUtils {
outStream.write(buf, 0, read);
}
}
public static void clearDirectory(File dir, boolean deleteRoot) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
clearDirectory(file, true);
} else {
file.delete();
}
}
}
if (deleteRoot) {
dir.delete();
}
}
}
@@ -0,0 +1,16 @@
package com.beemdevelopment.aegis.util;
import org.json.JSONObject;
import javax.annotation.Nullable;
public class JsonUtils {
private JsonUtils() {
}
@Nullable
public static String optString(JSONObject obj, String key) {
return obj.isNull(key) ? null : obj.optString(key, null);
}
}
@@ -44,6 +44,13 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
return oldValue;
}
/**
* Clears the internal map.
*/
public void wipe() {
_map.clear();
}
/**
* Replaces an old value (with the same UUID as the new given value) in the
* internal map with the new given value.
@@ -9,8 +9,10 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -25,6 +27,8 @@ import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VaultBackupManager {
private static final String TAG = VaultBackupManager.class.getSimpleName();
@@ -34,42 +38,67 @@ public class VaultBackupManager {
public static final String FILENAME_PREFIX = "aegis-backup";
private Context _context;
private final Context _context;
private final Preferences _prefs;
private final ExecutorService _executor;
public VaultBackupManager(Context context) {
_context = context;
_prefs = new Preferences(context);
_executor = Executors.newSingleThreadExecutor();
}
public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException {
public void destroy() {
Log.i(TAG, "Shutting down backup manager thread");
_executor.shutdown();
}
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
_executor.execute(() -> {
try {
createBackup(tempFile, dirUri, versionsToKeep);
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
e.printStackTrace();
_prefs.setBackupsError(e);
}
});
}
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep) throws VaultManagerException {
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
try {
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
if (!hasPermissionsAt(dirUri)) {
Log.e(TAG, "Unable to create file for backup, no persisted URI permissions");
throw new VaultManagerException("No persisted URI permissions");
}
if (!hasPermissionsAt(dirUri)) {
throw new VaultManagerException("No persisted URI permissions");
}
// If we create a file with a name that already exists, SAF will append a number
// to the filename and write to that instead. We can't overwrite existing files, so
// just avoid that altogether by checking beforehand.
if (dir.findFile(fileInfo.toString()) != null) {
throw new VaultManagerException("Backup file already exists");
}
// If we create a file with a name that already exists, SAF will append a number
// to the filename and write to that instead. We can't overwrite existing files, so
// just avoid that altogether by checking beforehand.
if (dir.findFile(fileInfo.toString()) != null) {
throw new VaultManagerException("Backup file already exists");
}
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
if (file == null) {
Log.e(TAG, "Unable to create file for backup, createFile returned null");
throw new VaultManagerException("createFile returned null");
}
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
if (file == null) {
throw new VaultManagerException("createFile returned null");
}
try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME);
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, "Unable to create backup", e);
throw new VaultManagerException(e);
try (FileInputStream inStream = new FileInputStream(tempFile);
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
throw new VaultManagerException(e);
}
} catch (VaultManagerException e) {
Log.e(TAG, String.format("Unable to create backup: %s", e.toString()));
throw e;
} finally {
tempFile.delete();
}
enforceVersioning(dir, versionsToKeep);
@@ -88,22 +117,20 @@ public class VaultBackupManager {
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
List<File> files = new ArrayList<>();
List<BackupFile> files = new ArrayList<>();
for (DocumentFile docFile : dir.listFiles()) {
if (docFile.isFile() && !docFile.isVirtual()) {
try {
files.add(new File(docFile));
files.add(new BackupFile(docFile));
} catch (ParseException ignored) { }
}
}
Collections.sort(files, new FileComparator());
for (File file : files) {
Log.i(TAG, file.getFile().getName());
}
Log.i(TAG, String.format("Found %d backup files, keeping the %d most recent", files.size(), versionsToKeep));
Collections.sort(files, new FileComparator());
if (files.size() > versionsToKeep) {
for (File file : files.subList(0, files.size() - versionsToKeep)) {
for (BackupFile file : files.subList(0, files.size() - versionsToKeep)) {
Log.i(TAG, String.format("Deleting %s", file.getFile().getName()));
if (!file.getFile().delete()) {
Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName()));
@@ -114,17 +141,27 @@ public class VaultBackupManager {
public static class FileInfo {
private String _filename;
private String _ext;
private Date _date;
public FileInfo(String filename, Date date) {
public FileInfo(String filename, String extension, Date date) {
_filename = filename;
_ext = extension;
_date = date;
}
public FileInfo(String filename, Date date) {
this(filename, "json", date);
}
public FileInfo(String filename) {
this(filename, Calendar.getInstance().getTime());
}
public FileInfo(String filename, String extension) {
this(filename, extension, Calendar.getInstance().getTime());
}
public static FileInfo parseFilename(String filename) throws ParseException {
if (filename == null) {
throw new ParseException("The filename must not be null", 0);
@@ -163,6 +200,10 @@ public class VaultBackupManager {
return _filename;
}
public String getExtension() {
return _ext;
}
public Date getDate() {
return _date;
}
@@ -170,15 +211,15 @@ public class VaultBackupManager {
@NonNull
@Override
public String toString() {
return String.format("%s-%s.json", _filename, _dateFormat.format(_date));
return String.format("%s-%s.%s", _filename, _dateFormat.format(_date), _ext);
}
}
private static class File {
private static class BackupFile {
private DocumentFile _file;
private FileInfo _info;
public File(DocumentFile file) throws ParseException {
public BackupFile(DocumentFile file) throws ParseException {
_file = file;
_info = FileInfo.parseFilename(file.getName());
}
@@ -192,9 +233,9 @@ public class VaultBackupManager {
}
}
private static class FileComparator implements Comparator<File> {
private static class FileComparator implements Comparator<BackupFile> {
@Override
public int compare(File o1, File o2) {
public int compare(BackupFile o1, BackupFile o2) {
return o1.getInfo().getDate().compareTo(o2.getInfo().getDate());
}
}
@@ -5,13 +5,18 @@ import android.content.Context;
import androidx.core.util.AtomicFile;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.text.Collator;
import java.util.Collection;
import java.util.TreeSet;
@@ -19,8 +24,9 @@ import java.util.UUID;
public class VaultManager {
public static final String FILENAME = "aegis.json";
public static final String FILENAME_EXPORT = "aegis-export.json";
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
public static final String FILENAME_PREFIX_EXPORT = "aegis-export";
public static final String FILENAME_PREFIX_EXPORT_PLAIN = "aegis-export-plain";
public static final String FILENAME_PREFIX_EXPORT_URI = "aegis-export-uri";
private Vault _vault;
private VaultFileCredentials _creds;
@@ -41,13 +47,21 @@ public class VaultManager {
this(context, vault, null);
}
private static AtomicFile getAtomicFile(Context context) {
return new AtomicFile(new File(context.getFilesDir(), FILENAME));
}
public static boolean fileExists(Context context) {
File file = new File(context.getFilesDir(), FILENAME);
File file = getAtomicFile(context).getBaseFile();
return file.exists() && file.isFile();
}
public static void deleteFile(Context context) {
getAtomicFile(context).delete();
}
public static VaultFile readFile(Context context) throws VaultManagerException {
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
AtomicFile file = getAtomicFile(context);
try {
byte[] fileBytes = file.readFully();
@@ -81,7 +95,7 @@ public class VaultManager {
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
byte[] bytes = vaultFile.toBytes();
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
AtomicFile file = getAtomicFile(context);
FileOutputStream stream = null;
try {
@@ -96,7 +110,11 @@ public class VaultManager {
}
}
public void save() throws VaultManagerException {
public void destroy() {
_backups.destroy();
}
public void save(boolean backup) throws VaultManagerException {
try {
JSONObject obj = _vault.toJson();
@@ -108,20 +126,37 @@ public class VaultManager {
}
save(_context, file);
if (_prefs.isBackupsEnabled()) {
backup();
}
} catch (VaultFileException e) {
throw new VaultManagerException(e);
}
if (backup && _prefs.isBackupsEnabled()) {
try {
backup();
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
_prefs.setBackupsError(e);
}
}
}
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
/**
* Exports the vault bt serializing it and writing it to the given OutputStream. If encryption
* is enabled, the vault will be encrypted automatically.
*/
public void export(OutputStream stream) throws VaultManagerException {
export(stream, getCredentials());
}
/**
* Exports the vault by serializing it and writing it to the given OutputStream. If creds is
* not null, it will be used to encrypt the vault first.
*/
public void export(OutputStream stream, VaultFileCredentials creds) throws VaultManagerException {
try {
VaultFile vaultFile = new VaultFile();
if (encrypt && isEncryptionEnabled()) {
vaultFile.setContent(_vault.toJson(), _creds);
if (creds != null) {
vaultFile.setContent(_vault.toJson(), creds);
} else {
vaultFile.setContent(_vault.toJson());
}
@@ -133,8 +168,38 @@ public class VaultManager {
}
}
/**
* Exports the vault by serializing the list of entries to a newline-separated list of
* Google Authenticator URI's and writing it to the given OutputStream.
*/
public void exportGoogleUris(OutputStream outStream) throws VaultManagerException {
try (PrintStream stream = new PrintStream(outStream, false, StandardCharsets.UTF_8.name())) {
for (VaultEntry entry : getEntries()) {
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
stream.println(info.getUri().toString());
}
} catch (IOException e) {
throw new VaultManagerException(e);
}
}
public void backup() throws VaultManagerException {
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
try {
File dir = new File(_context.getCacheDir(), "backup");
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create directory %s", dir));
}
File tempFile = File.createTempFile(VaultBackupManager.FILENAME_PREFIX, ".json", dir);
try (InputStream inStream = getAtomicFile(_context).openRead();
OutputStream outStream = new FileOutputStream(tempFile)) {
IOUtils.copy(inStream, outStream);
}
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
} catch (IOException e) {
throw new VaultManagerException(e);
}
}
public void addEntry(VaultEntry entry) {
@@ -149,6 +214,10 @@ public class VaultManager {
return _vault.getEntries().remove(entry);
}
public void wipeEntries() {
_vault.getEntries().wipe();
}
public VaultEntry replaceEntry(VaultEntry entry) {
return _vault.getEntries().replace(entry);
}
@@ -190,11 +259,11 @@ public class VaultManager {
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
_creds = creds;
save();
save(true);
}
public void disableEncryption() throws VaultManagerException {
_creds = null;
save();
save(true);
}
}
+15 -6
View File
@@ -5,14 +5,23 @@ option java_outer_classname = "GoogleAuthProtos";
message MigrationPayload {
enum Algorithm {
ALGO_INVALID = 0;
ALGO_SHA1 = 1;
ALGORITHM_UNSPECIFIED = 0;
ALGORITHM_SHA1 = 1;
ALGORITHM_SHA256 = 2;
ALGORITHM_SHA512 = 3;
ALGORITHM_MD5 = 4;
}
enum DigitCount {
DIGIT_COUNT_UNSPECIFIED = 0;
DIGIT_COUNT_SIX = 1;
DIGIT_COUNT_EIGHT = 2;
}
enum OtpType {
OTP_INVALID = 0;
OTP_HOTP = 1;
OTP_TOTP = 2;
OTP_TYPE_UNSPECIFIED = 0;
OTP_TYPE_HOTP = 1;
OTP_TYPE_TOTP = 2;
}
message OtpParameters {
@@ -20,7 +29,7 @@ message MigrationPayload {
string name = 2;
string issuer = 3;
Algorithm algorithm = 4;
int32 digits = 5;
DigitCount digits = 5;
OtpType type = 6;
int64 counter = 7;
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/auth_button"/>
<stroke android:color="@color/auth_button" android:width="1dp" />
<!--corners allow us to make the rounded corners button-->
<corners android:radius="4dp" />
</shape>
</item>
</selector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#59000000" />
<size
android:width="50dp"
android:height="50dp" />
</shape>
@@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_check_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>
@@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_arrow_right_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>
@@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_arrow_left_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/divider"/>
<solid android:color="?attr/divider"/>
<size android:height="0.1dp"/>
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
</vector>
@@ -0,0 +1,9 @@
<!-- drawable/ic_arrow_left_black_24dp.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path android:fillColor="#000" android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
</vector>
@@ -0,0 +1,9 @@
<!-- drawable/ic_arrow_right_black_24dp.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path android:fillColor="#000" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
</vector>
@@ -0,0 +1,5 @@
<vector android:height="32dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
</vector>
@@ -0,0 +1,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="7dp" android:left="7dp" android:right="7dp" android:bottom="7dp">
<vector android:height="24dp" android:viewportHeight="3328" android:tint="#FFFFFF"
android:viewportWidth="3333" android:width="24.036058dp">
<path android:fillColor="#FF000000" android:fillType="evenOdd" android:pathData="M5,0h1211v1206L5,1206L5,0zM3025,3025h309v304h-309v-304zM2432,3025h309v290h-618v-592h299v-300h304v-598h309v294h294v304h-294v304h-603v299zM1515,2417h299v-304h-289v-304h289v-304h-294v304h-309v-304h304L1515,603h309v902h299v304h294v-304h309v304h-294v304h-309v598h-299v603h-309v-897zM3020,1505h309v304h-309v-304zM608,1505h309v304L608,1809v-304zM5,1505h309v304L5,1809v-304zM1515,0h309v304h-309L1515,0zM0,2118h1211v1206L0,3324L0,2118zM294,2411h623v620L294,3031v-620zM2118,0h1211v1206L2118,1206L2118,0zM2412,293h623v620h-623L2412,293zM299,293h623v620L299,913L299,293z"/>
</vector>
</item>
</layer-list>

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