Compare commits

...
164 Commits
Author SHA1 Message Date
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 ddbe9ccfb7 Fix ADB command in CONTRIBUTING.md 2021-01-01 12:57:20 +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 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
160 changed files with 9435 additions and 1889 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
+59 -19
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 35
versionName "1.2"
targetSdkVersion 30
versionCode 44
versionName "1.4.1"
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,25 +111,31 @@ protobuf {
}
dependencies {
def cameraxVersion = '1.0.0-rc01'
def glideVersion = '4.11.0'
def guavaVersion = '29.0'
def junitVersion = '5.6.2'
def libsuVersion = '2.5.1'
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 '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.constraintlayout:constraintlayout:1.1.3'
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 "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 '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}") {
@@ -118,17 +144,31 @@ dependencies {
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.1.0'
implementation 'com.google.protobuf:protobuf-javalite:3.12.1'
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'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
implementation 'net.lingala.zip4j:zip4j:2.6.4'
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());
}
}
+43 -10
View File
@@ -29,11 +29,10 @@
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" />
@@ -51,27 +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"
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>
+58
View File
@@ -31,6 +31,64 @@
</head>
<body>
<div></div>
<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>
@@ -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() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
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() {
@@ -168,6 +205,10 @@ public class Preferences {
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);
}
@@ -0,0 +1,23 @@
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
);
}
@@ -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},};
}
}
@@ -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,6 +3,7 @@ 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;
@@ -10,6 +11,7 @@ import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
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;
@@ -17,13 +19,16 @@ 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;
@@ -55,20 +60,15 @@ public class AndOtpImporter 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 {
byte[] bytes;
try {
bytes = reader.readAll();
bytes = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
@@ -150,7 +150,8 @@ public class AndOtpImporter extends DatabaseImporter {
listener.onError(e);
}
});
task.execute(params);
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}
}
@@ -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.optString("accountType", "authy").equals("authenticator");
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, digits == 7 ? 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";
}
}
@@ -50,7 +50,7 @@ public class GoogleAuthInfo implements Serializable {
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));
@@ -82,15 +82,13 @@ public class GoogleAuthInfo implements Serializable {
throw new GoogleAuthInfoException("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);
}
// check the otp type
OtpInfo info;
try {
String type = uri.getHost();
@@ -174,6 +172,14 @@ public class GoogleAuthInfo implements Serializable {
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) {
@@ -210,13 +216,46 @@ public class GoogleAuthInfo implements Serializable {
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
int digits;
switch (params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = 6;
break;
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
default:
throw new GoogleAuthInfoException(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(String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
}
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_HOTP:
otp = new HotpInfo(secret, params.getCounter());
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
case OTP_TYPE_TOTP:
otp = new TotpInfo(secret, algo, digits, 30);
break;
case OTP_TOTP:
otp = new TotpInfo(secret);
case OTP_TYPE_HOTP:
otp = new HotpInfo(secret, algo, digits, params.getCounter());
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
@@ -12,8 +12,6 @@ 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;
@@ -21,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;
@@ -80,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");
});
}
@@ -118,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);
@@ -127,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)
@@ -6,21 +6,20 @@ import android.os.Bundle;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
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) {
@@ -28,15 +27,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;
}
@@ -56,24 +56,15 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
@Override
protected void onResume() {
super.onResume();
_resumed = true;
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
_app.setBlockAutoLock(false);
}
@Override
protected void onPause() {
super.onPause();
_resumed = false;
}
@CallSuper
@Override
public void onLocked() {
if (isOrphan()) {
setResult(RESULT_CANCELED, null);
finish();
}
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
finishAndRemoveTask();
}
protected AegisApplication getApp() {
@@ -84,9 +75,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:
@@ -98,19 +106,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) {
@@ -132,22 +128,11 @@ 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 Theme getCurrentTheme() {
return _currentTheme;
protected boolean isOrphan() {
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
}
@@ -2,9 +2,9 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -23,15 +23,15 @@ 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.Theme;
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;
@@ -52,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;
@@ -79,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) {
@@ -139,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 -> {
@@ -148,31 +161,14 @@ public class AuthActivity extends AegisActivity {
}
@Override
protected void setPreferredTheme(Theme theme) {
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
// set the theme based on the system theme
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
theme = Theme.LIGHT;
break;
case Configuration.UI_MODE_NIGHT_YES:
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
break;
}
}
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme_Light_NoActionBar);
break;
case DARK:
setTheme(R.style.AppTheme_Dark_NoActionBar);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_NoActionBar);
break;
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
private void selectPassword() {
@@ -184,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();
}
}
@@ -196,15 +191,26 @@ 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
@@ -214,6 +220,11 @@ public class AuthActivity extends AegisActivity {
}
}
@Override
protected boolean isOrphan() {
return _stateless && super.isOrphan();
}
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@@ -221,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) {
@@ -288,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) {
@@ -303,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();
}
}
}
@@ -317,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();
}
@@ -325,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);
}
}
@@ -37,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;
@@ -392,8 +393,12 @@ public class EditEntryActivity extends AegisActivity {
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));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_icon)), PICK_IMAGE_REQUEST);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
}
private void startEditingIcon(Uri data) {
@@ -448,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);
@@ -507,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");
}
@@ -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;
@@ -18,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) {
@@ -35,15 +38,28 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
// 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,107 +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.SecuritySetupSlide;
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 SecuritySetupSlide securitySetupSlide;
private SecurityPickerSlide _securityPickerSlide;
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));
_securityPickerSlide = new SecurityPickerSlide();
_securityPickerSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_securityPickerSlide);
securitySetupSlide = new SecuritySetupSlide();
securitySetupSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(securitySetupSlide);
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 == _securityPickerSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
int cryptType = getIntent().getIntExtra("cryptType", SecurityPickerSlide.CRYPT_TYPE_INVALID);
if (cryptType == SecurityPickerSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
goToNextSlide(false);
}
}
if (newFragment == _endSlide) {
setWizardMode(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 = securitySetupSlide.getCryptType();
VaultFileCredentials creds = securitySetupSlide.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 == SecurityPickerSlide.CRYPT_TYPE_NONE) {
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
vaultFile.setContent(obj, creds);
@@ -114,20 +84,16 @@ public class IntroActivity extends AppIntro2 {
return;
}
if (cryptType == SecurityPickerSlide.CRYPT_TYPE_NONE) {
_app.initVaultManager(vault, null);
if (cryptType == CRYPT_TYPE_NONE) {
getApp().initVaultManager(vault, null);
} else {
_app.initVaultManager(vault, creds);
getApp().initVaultManager(vault, creds);
}
// skip the intro from now on
_prefs.setIntroDone(true);
getPreferences().setIntroDone(true);
setResult(RESULT_OK);
finish();
}
public void goToNextSlide() {
super.goToNextSlide(false);
}
}
@@ -20,14 +20,16 @@ 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;
@@ -42,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;
@@ -76,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;
@@ -92,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);
@@ -137,6 +145,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_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);
@@ -160,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;
@@ -280,7 +285,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry);
_entryListView.addEntry(entry, true);
}
}
@@ -306,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()];
@@ -314,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());
@@ -401,11 +407,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void startScanImageActivity() {
_app.setBlockAutoLock(true);
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) {
@@ -436,10 +448,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 {
@@ -462,12 +474,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;
}
@@ -486,7 +499,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) {
@@ -518,8 +531,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;
}
@@ -591,7 +604,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) {
@@ -647,10 +660,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
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() {
@@ -735,7 +751,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();
}
@@ -743,11 +762,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) {
@@ -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();
}
}
@@ -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,15 @@ 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("*/*");
_app.setBlockAutoLock(true);
startActivityForResult(intent, CODE_IMPORT);
});
return true;
}
});
@@ -183,7 +203,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;
}
});
@@ -233,7 +256,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@@ -252,7 +275,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);
@@ -262,14 +285,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);
}
@@ -372,6 +395,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
@@ -410,6 +460,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);
@@ -419,17 +470,46 @@ 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");
}
@@ -464,8 +544,10 @@ 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);
@@ -482,42 +564,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()
@@ -527,11 +573,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();
@@ -541,9 +584,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) {
@@ -551,7 +593,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() {
@@ -604,11 +645,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);
}
@@ -633,39 +674,115 @@ 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());
_app.setBlockAutoLock(true);
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));
_app.setBlockAutoLock(true);
startActivity(chooser);
});
});
});
Dialogs.showSecureDialog(dialog);
}
private void onSlotManagerResult(int resultCode, Intent data) {
@@ -691,6 +808,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
entry.setGroup(null);
}
}
saveVault();
}
private void onSelectEntriesResult(int resultCode, Intent data) {
@@ -698,6 +817,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());
@@ -715,27 +839,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) {
@@ -773,6 +979,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();
@@ -813,9 +1020,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
_app.setBlockAutoLock(true);
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) {
@@ -828,7 +1062,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);
@@ -839,6 +1075,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
@@ -899,4 +1140,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,72 +1,82 @@
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.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) {
protected void onSetTheme() {
setTheme(R.style.AppTheme_Fullscreen);
}
@@ -74,60 +84,80 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
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, R.string.read_qr_error, e, ((dialog, which) -> bindPreview(_cameraProvider)));
_cameraProvider.unbindAll();
}
}
@@ -159,7 +189,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 +201,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();
}
@@ -9,11 +9,9 @@ import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
@@ -22,7 +20,6 @@ import com.google.zxing.qrcode.QRCodeWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TransferEntriesActivity extends AegisActivity {
private List<GoogleAuthInfo> _authInfos;
@@ -58,7 +55,6 @@ public class TransferEntriesActivity extends AegisActivity {
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
_entriesCount.setVisibility(controlVisibility);
_nextButton.setVisibility(controlVisibility);
_nextButton.setOnClickListener(v -> {
@@ -108,7 +104,7 @@ public class TransferEntriesActivity extends AegisActivity {
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
_issuer.setText(selectedEntry.getIssuer());
_accountName.setText(selectedEntry.getAccountName());
_entriesCount.setText(String.format(getString(R.string.entries_count), _currentEntryCount, _authInfos.size()));
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = null;
@@ -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;
}
}
@@ -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);
}
}
@@ -1,6 +1,5 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -8,80 +7,87 @@ import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.appintro.SlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class SecurityPickerSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
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 int _bgColor;
private RadioButton _bioButton;
private TextView _bioText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
View view = inflater.inflate(R.layout.fragment_security_picker_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);
_bioButton = view.findViewById(R.id.rb_biometrics);
_bioText = view.findViewById(R.id.text_rb_biometrics);
updateBiometricsOption(true);
return view;
}
public void setBgColor(int color) {
_bgColor = color;
@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 isPolicyRespected() {
public boolean isFinished() {
return _buttonGroup.getCheckedRadioButtonId() != -1;
}
@Override
public void onUserIllegallyRequestedNextPage() {
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.snackbar_authentication_method), Snackbar.LENGTH_LONG);
snackbar.show();
public void onNotFinishedError() {
Toast.makeText(getContext(), R.string.snackbar_authentication_method, Toast.LENGTH_SHORT).show();
}
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if (i == -1) {
return;
}
public void onSaveIntroState(@NonNull Bundle introState) {
int buttonId = _buttonGroup.getCheckedRadioButtonId();
int id;
switch (i) {
int type;
switch (buttonId) {
case R.id.rb_none:
id = CRYPT_TYPE_NONE;
type = CRYPT_TYPE_NONE;
break;
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
type = CRYPT_TYPE_PASS;
break;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
type = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
}
Intent intent = getActivity().getIntent();
intent.putExtra("cryptType", id);
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 SecuritySetupSlide 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_security_setup_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,20 +76,41 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
});
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() {
@@ -88,34 +125,21 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
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", SecurityPickerSlide.CRYPT_TYPE_INVALID);
if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_NONE) {
_creds = new VaultFileCredentials();
}
}
@Override
public void onSlideDeselected() {
}
@Override
public boolean isPolicyRespected() {
public boolean isFinished() {
switch (_cryptType) {
case SecurityPickerSlide.CRYPT_TYPE_NONE:
case CRYPT_TYPE_NONE:
return true;
case SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC:
case CRYPT_TYPE_BIOMETRIC:
if (!_creds.getSlots().has(BiometricSlot.class)) {
return false;
}
// intentional fallthrough
case SecurityPickerSlide.CRYPT_TYPE_PASS:
case CRYPT_TYPE_PASS:
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
return _creds.getSlots().has(PasswordSlot.class);
}
@@ -127,16 +151,9 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
@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();
}
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)) {
@@ -144,6 +161,11 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
}
@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) {
@@ -157,7 +179,7 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
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;
@@ -36,6 +37,7 @@ 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;
@@ -49,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<>();
@@ -90,6 +93,10 @@ 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;
}
@@ -98,13 +105,13 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
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
@@ -113,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 {
@@ -130,7 +137,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
_view.onListChange();
checkPeriodUniformity();
return position;
}
public void addEntries(Collection<VaultEntry> entries) {
@@ -148,6 +157,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyItemRemoved(position);
}
_view.onListChange();
checkPeriodUniformity();
}
@@ -273,6 +283,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
Collections.sort(_shownEntries, comparator);
}
_view.onListChange();
notifyDataSetChanged();
}
@@ -336,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));
@@ -353,12 +364,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
holder.animateCopyText();
}
if (_highlightEntry || _tapToReveal) {
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
handled = true;
} else {
focusEntry(entry);
focusEntry(entry, _tapToRevealTime);
}
}
} else {
@@ -386,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() {
@@ -470,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) {
@@ -492,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) {
@@ -506,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() {
@@ -531,6 +591,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
_selectedEntries.clear();
updateDraggableStatus();
}
public boolean isDragAndDropAllowed() {
@@ -560,5 +622,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
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;
@@ -67,6 +70,8 @@ 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();
@@ -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;
@@ -221,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;
}
@@ -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;
@@ -42,6 +45,7 @@ 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;
@@ -90,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;
@@ -167,12 +171,20 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
}
public void setSelectedEntry(VaultEntry entry) {
_touchCallback.setSelectedEntry(entry);
}
public void setViewMode(ViewMode mode) {
_viewMode = mode;
updateDividerDecoration();
_adapter.setViewMode(_viewMode);
}
public void startDrag(RecyclerView.ViewHolder viewHolder) {
_touchHelper.startDrag(viewHolder);
}
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.restart();
@@ -236,6 +248,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
@Override
public void onListChange() { _listener.onListChange(); }
public void setCodeGroupSize(int codeGrouping) {
_adapter.setCodeGroupSize(codeGrouping);
}
@@ -261,8 +276,39 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void addEntry(VaultEntry entry) {
_adapter.addEntry(entry);
addEntry(entry, false);
}
@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) {
@@ -339,6 +385,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onScroll(int dx, int dy);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
void onListChange();
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
@@ -346,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);
}
@@ -65,12 +65,13 @@ public class TotpProgressBar extends ProgressBar {
int currentProgress = (int) (maxProgress * ((float) millisTillRotation / (_period * 1000)));
// start progress animation, compensating for any changes to the animator duration scale settings
int animPart = maxProgress / _period;
int animEnd = (currentProgress / animPart) * animPart;
int animPartDuration = (int) (1000 / _animDurationScale);
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 realAnimDuration = (int) (1000 * animDurationFraction);
int animDuration = (int) (animPartDuration * animDurationFraction);
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, animEnd);
animation.setDuration(animDuration);
animation.setInterpolator(new LinearInterpolator());
@@ -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,6 +110,10 @@ public class VaultManager {
}
}
public void destroy() {
_backups.destroy();
}
public void save(boolean backup) throws VaultManagerException {
try {
JSONObject obj = _vault.toJson();
@@ -122,11 +140,23 @@ public class VaultManager {
}
}
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());
}
@@ -138,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.toString())) {
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) {
@@ -154,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);
}
+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,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>
@@ -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>
+4 -9
View File
@@ -31,9 +31,9 @@
android:fontFamily="sans-serif-light"
android:singleLine="false"
android:text="@string/authentication_multiline"
android:textColor="?attr/authText"
android:textFontWeight="500"
android:textSize="46sp" />
android:textStyle="bold"
android:textSize="46sp"
android:textColor="?attr/authText" />
<LinearLayout
android:layout_width="match_parent"
@@ -54,10 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword"
android:textColor="?attr/primaryText"
android:textColorHint="?attr/authText"
app:backgroundTint="?attr/authText" />
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
@@ -85,7 +82,6 @@
android:layout_gravity="center"
android:minWidth="125dp"
android:text="@string/auth_use_biometrics"
android:textColor="?attr/authText"
android:visibility="gone" />
<LinearLayout
@@ -111,7 +107,6 @@
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+14 -16
View File
@@ -21,13 +21,11 @@
android:layout_height="250dp">
<de.hdodenhof.circleimageview.CircleImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/profile_drawable"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
/>
android:layout_centerVertical="true" />
<com.avito.android.krop.KropView
android:id="@+id/krop_view"
@@ -45,10 +43,10 @@
android:id="@+id/iv_saveImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_gravity="bottom|end"
android:layout_margin="15dp"
android:src="@drawable/ic_check_black_24dp"
android:tint="?attr/iconColorPrimary" />
app:tint="?attr/iconColorPrimary" />
</com.avito.android.krop.KropView>
</RelativeLayout>
@@ -71,7 +69,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_person_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
@@ -127,7 +125,7 @@
android:layout_centerVertical="true"
android:layout_marginStart="14dp"
android:src="@drawable/ic_keyboard_arrow_down_black_24dp"
android:tint="?attr/iconColorPrimary"/>
app:tint="?attr/iconColorPrimary"/>
<TextView
android:id="@+id/accordian_title"
@@ -160,7 +158,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
@@ -176,9 +174,9 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="4"
android:paddingRight="6dp"
android:gravity="left"
android:layout_gravity="left"
android:paddingEnd="6dp"
android:gravity="start"
android:layout_gravity="start"
android:backgroundTint="#949494"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<androidx.appcompat.widget.AppCompatSpinner
@@ -186,7 +184,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="4"
android:paddingRight="6dp"
android:paddingEnd="6dp"
android:backgroundTint="#949494"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<EditText
@@ -194,7 +192,7 @@
android:hint="@string/digits"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingRight="6dp"
android:paddingEnd="6dp"
android:layout_weight="3"
android:backgroundTint="#949494"/>
</LinearLayout>
@@ -211,7 +209,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_timelapse_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
@@ -262,7 +260,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_counter_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
@@ -295,7 +293,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_vpn_key_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
+43 -2
View File
@@ -23,9 +23,50 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|fill_vertical"
android:id="@+id/vEmptyList"
android:visibility="gone"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="150dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_layers_black_24dp"
app:tint="?attr/primaryText" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_group_list_title"
android:paddingTop="17dp"
android:textColor="?attr/primaryText"
android:textSize="18sp" />
<TextView
android:id="@+id/textView5"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:lineSpacingExtra="5dp"
android:paddingTop="7dp"
android:text="@string/empty_group_list"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+40 -4
View File
@@ -1,10 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.ui.IntroActivity">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/btnPrevious" />
<ImageButton
android:id="@+id/btnPrevious"
android:layout_width="65dp"
android:layout_height="65dp"
android:layout_margin="10dp"
android:src="@drawable/circular_button_prev"
android:tint="?attr/iconColorPrimary"
android:background="?selectableItemBackgroundBorderless"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.beemdevelopment.aegis.ui.intro.SlideIndicator
android:id="@+id/slideIndicator"
android:layout_width="0dp"
android:layout_height="65dp"
android:layout_marginBottom="10dp"
app:layout_constraintStart_toEndOf="@+id/btnPrevious"
app:layout_constraintEnd_toStartOf="@+id/btnNext"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/btnNext"
android:layout_width="65dp"
android:layout_height="65dp"
android:layout_margin="10dp"
android:src="@drawable/circular_button_next"
android:tint="?attr/iconColorPrimary"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+3 -3
View File
@@ -28,7 +28,7 @@
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/icon_primary_inverted"
app:tint="@color/icon_primary_inverted"
android:src="@drawable/ic_alert_black_24dp" />
<TextView
android:layout_width="wrap_content"
@@ -49,6 +49,7 @@
<!-- note: the fab should always be the last element to be sure it's displayed on top -->
<com.getbase.floatingactionbutton.FloatingActionsMenu
android:id="@+id/fab"
style="@style/FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
@@ -56,8 +57,7 @@
android:src="@drawable/ic_add_black_24dp"
app:fab_addButtonColorNormal="@color/colorAccent"
app:fab_addButtonColorPressed="@color/colorAccent"
app:fab_labelStyle="@style/fab_label_style"
app:fab_labelsPosition="left">
app:fab_labelStyle="@style/fab_label_style">
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scan"
+13 -8
View File
@@ -1,14 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_scanner"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.beemdevelopment.aegis.ui.ScannerActivity">
</RelativeLayout>
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -73,7 +73,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/transparent"
android:background="?android:attr/selectableItemBackground"
android:text="@string/next"
android:textAllCaps="false"
android:textColor="@color/code_primary_text"
@@ -86,7 +86,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/transparent"
android:background="?android:attr/selectableItemBackground"
android:text="@string/previous"
android:textAllCaps="false"
android:visibility="invisible"
@@ -99,7 +99,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/entries_count"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/btnNext"
app:layout_constraintHorizontal_bias="0.506"
+3 -3
View File
@@ -41,7 +41,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_plus_black_24dp"
android:tint="@color/colorAccent"
app:tint="@color/colorAccent"
android:layout_marginEnd="15dp"/>
<TextView
@@ -69,7 +69,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_plus_black_24dp"
android:tint="@color/colorAccent"
app:tint="@color/colorAccent"
android:layout_marginEnd="15dp"/>
<TextView
@@ -98,7 +98,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp"
android:tint="?attr/iconColorPrimary"
app:tint="?attr/iconColorPrimary"
android:layout_marginEnd="15dp"/>
<TextView
+48 -40
View File
@@ -21,23 +21,20 @@
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="16dp">
android:paddingStart="16dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivTextDrawable"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
android:layout_alignParentStart="true" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/item_selected"
android:visibility="gone"
@@ -48,12 +45,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_alignParentBottom="false"
android:layout_alignParentTop="false"
android:layout_alignWithParentIfMissing="false"
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="18dp"
android:paddingBottom="16dp"
android:paddingEnd="16dp"
@@ -69,7 +62,6 @@
android:maxLines="1"
android:text="@string/copied"
android:layout_above="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:textColor="?attr/secondaryText"
android:textSize="16sp"
@@ -79,10 +71,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -95,21 +85,21 @@
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/secondary_text"
android:textSize="16sp"
tools:text=" - AccountName" />
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/profile_issuer"
android:layout_marginStart="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/secondary_text"
android:textSize="16sp"
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -117,32 +107,50 @@
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:textSize="34sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
</RelativeLayout>
<ImageView
android:id="@+id/buttonRefresh"
android:visibility="gone"
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_refresh_black_24dp"
android:tint="?attr/iconColorPrimary"
android:background="?android:attr/selectableItemBackground" />
android:layout_height="match_parent">
<ImageView
android:id="@+id/buttonRefresh"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_refresh_black_24dp"
app:tint="?attr/iconColorPrimary"
android:background="?android:attr/selectableItemBackground" />
<ImageView
android:id="@+id/drag_handle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="-12dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_menu_black_32" />
</LinearLayout>
</LinearLayout>
<LinearLayout
@@ -154,7 +162,7 @@
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="4dp"
android:id="@+id/progressBar"
android:max="5000"

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