Compare commits

...
557 Commits
Author SHA1 Message Date
Alexander Bakker 735c086726 Release v1.4-beta2 2020-12-04 22:10:04 +01:00
Alexander Bakker d94be2420c Downgrade Android Gradle plugin to 4.0.1 to fix a crash 2020-12-04 22:07:07 +01:00
Alexander Bakker 0b6da5a93c Prevent file provider name collision (for debug builds) 2020-12-04 22:03:58 +01:00
Alexander Bakker d24e1471a7 Release v1.4-beta1 2020-12-04 20:49:51 +01:00
Alexander Bakker 7ca072a29c Update translations from Crowdin 2020-12-04 20:29:55 +01:00
Alexander Bakker d5ac447332 Update Android Gradle plugin to fix the build 2020-12-04 20:08:53 +01:00
Alexander BakkerandGitHub 0067c79dfa Merge pull request #618 from alexbakker/backup-thread
Move the creation of exports/backups to a background thread
2020-12-04 10:48:03 +01:00
Alexander Bakker 4e198d2556 Move the creation of exports/backups to a background thread
This writes exports/backups to the cache directory first and then hands off to a
background thread to pipe the file to SAF. I'm cheating a bit, but it's the
easiest fix with our current architecture. In the future, we should skip the
first step by making VaultManager itself thread-safe.
2020-12-03 22:16:35 +01:00
Michael SchättgenandGitHub f15a0018ef Merge pull request #605 from alexbakker/highlight-on-add
After adding a new entry, scroll to it and highlight it
2020-12-03 21:48:48 +01:00
Alexander Bakker d121139676 After adding a new entry, scroll to it and highlight it 2020-11-30 20:08:09 +01:00
Alexander BakkerandGitHub 98b246ad20 Merge pull request #616 from richardk85/richardk85-tap-to-reveal-min-1
"Tap to reveal": change min duration from 3 to 1
2020-11-24 17:03:26 +01:00
Jay bce0d2c044 "Tap to reveal": change min duration from 3 to 1 2020-11-24 14:26:16 +01:00
Michael SchättgenandGitHub bf91917745 Merge pull request #604 from alexbakker/export-plain
Improve the export functionality in numerous ways
2020-11-08 21:18:50 +01:00
Michael SchättgenandGitHub a91339de73 Merge pull request #603 from alexbakker/resize-qr-image
Resize images to a max res of 640x480 before scanning for QR codes
2020-11-07 21:06:11 +01:00
Michael SchättgenandGitHub 5d9de7ac32 Merge pull request #602 from alexbakker/qr-multi-intent
Combine ACTION_PICK and ACTION_GET_CONTENT when selecting an image to scan
2020-11-07 20:51:26 +01:00
Alexander Bakker 5f2529ea33 Improve the export functionality in numerous ways
This patch improves the export functionality in the following ways:
1. Allow setting a password even if the Aegis vault is not encrypted
2. Display a scary warning when exporting an unencrypted vault
3. Support exporting to a Google Authenticator URI file
4. Option to use Android's share mechanism

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

Add minor review fixes

Add panic trigger which deletes vault

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

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

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

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

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

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

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

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

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


Layout improvements


Layout improvements


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

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

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

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

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

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

Add label to activity
2020-06-06 12:23:28 +02:00
Michael SchättgenandGitHub dd09595866 Merge pull request #435 from alexbakker/fix-363
Don't add new entries to the view if restoring from instance state
2020-06-06 12:22:47 +02:00
Michael SchättgenandGitHub b27bfc3fa9 Merge pull request #432 from alexbakker/fix-252
Improve icon editing flow
2020-06-06 11:56:54 +02:00
Michael SchättgenandGitHub c6a9716d16 Merge pull request #431 from alexbakker/update-deps-again
Update dependencies and sort them alphabetically
2020-06-06 11:54:12 +02:00
Michael SchättgenandGitHub acd7a54ee9 Merge pull request #430 from orangenbaumblatt/feature/improve-action-bar-consistency
Improve Action Bar consistency for Settings and About
2020-06-04 01:29:44 +02:00
Alexander BakkerandGitHub e7d240549e Merge pull request #452 from lotusexpeditor/master
Add Turkish Translation
2020-06-02 11:25:27 +02:00
lotusexpeditorandGitHub 70fedc53b9 Add Turkish Translation 2020-06-02 04:13:56 +03:00
Alexander Bakker bed8c9172d Release v1.2-beta4 2020-05-30 13:52:20 +02:00
Michael SchättgenandGitHub ef1ac3e5e9 Merge pull request #441 from alexbakker/fix-lock-crash
Fix crash that would occur when locking the screen with a locked vault
2020-05-30 13:06:25 +02:00
Alexander Bakker cc163f952f Fix an issue where the password reminder popup was not shown
We no longer create BiometricPrompt prematurely, but forgot to adjust the check for the password reminder.
2020-05-28 11:55:56 +02:00
Michael SchättgenandGitHub 95180ba963 Merge pull request #444 from alexbakker/fix-export-backup
Fix an issue where backups where enabled when exporting the vault
2020-05-28 10:01:19 +02:00
Alexander Bakker 56d7f0ef7b Fix an issue where backups where enabled when exporting the vault
This is really stupid.
2020-05-28 09:54:29 +02:00
Alexander Bakker fbd6a5931e Fix crash that would occur when locking the screen with a locked vault 2020-05-26 11:33:35 +02:00
Alexander Bakker 66dd3561cf Release v1.2-beta3 2020-05-25 17:32:32 +02:00
Michael SchättgenandGitHub 58db3fdaa2 Merge pull request #438 from alexbakker/fix-intro-creds
Fix issue where VaultManager was erroneously initialized with creds in intro
2020-05-25 16:41:35 +02:00
Alexander Bakker 92b6738ff4 Fix issue where VaultManager was erroneously initialized with creds in intro
Also fixes another issue where previously, if a user made it to the last intro
slide and then navigated back to change the security options, any changes would
be ignored.
2020-05-25 16:29:14 +02:00
Alexander Bakker 63f57667ba Fix issue where the copy animation continued after view holder rebind 2020-05-25 12:02:21 +02:00
Alexander Bakker 34718a7b89 Don't add new entries to the view if restoring from instance state
Also addresses other potential issues with the same cause. Fixes #363.
2020-05-24 20:24:04 +02:00
orangenbaumblatt 6ac91e6cf3 Improve Action Bar consistency for Settings and About
Change Group Manager back icon
2020-05-24 15:10:59 +02:00
Alexander Bakker a39d6ab346 Improve icon editing flow
This improves the entry icon editing flow as suggested in #252:
- Add an "Edit icon" menu item
- Save the icon even if the checkmark was not clicked
- Exit icon edit mode with the back button

Close #252.
2020-05-24 13:49:30 +02:00
Alexander Bakker 91d89670d2 Update dependencies and sort them alphabetically 2020-05-24 12:52:02 +02:00
Alexander Bakker 6a2992530e Release v1.2-beta2 2020-05-16 21:36:52 +02:00
Alexander Bakker 1f839a886d Don't replace the Bouncy Castle security provider
We only use Bouncy Castle for scrypt, so replacing the security provider was a
nice to have. It's causing issues because Proguard removes Bouncy Castle
classes. As we just released a beta, this is a quick fix and we may revisit this
later.
2020-05-16 21:32:13 +02:00
Alexander Bakker 91f9a6756a Release v1.2-beta1 2020-05-16 13:53:30 +02:00
Alexander BakkerandGitHub 89137c0873 Merge pull request #417 from michaelschattgen/feature/update-translations
Update translations
2020-05-16 13:44:42 +02:00
Michael Schättgen ffa31c7aa3 Update translations 2020-05-16 13:37:41 +02:00
Michael SchättgenandGitHub e12bfdbe58 Merge pull request #365 from alexbakker/spongy-to-bouncy
Replace SpongyCastle with BouncyCastle
2020-05-16 12:50:58 +02:00
Alexander Bakker 3c887d8392 Replace SpongyCastle with BouncyCastle
SpongyCastle is a fork of BouncyCastle. We originally used this fork to 1) have
access to scrypt and 2) prevent a package name collision with the bundled
BouncyCastle. We don't actually need to use the fork anymore, because the
package name of the bundled BouncyCastle was changed in Android. SpongyCastle
has also gotten quite outdated in recent years.

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

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

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

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

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

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

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

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

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

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


Changed indicator color to black secondary


Fix indicator flickering when scrolling


Applied patch


Fix unsharp selection icon


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

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


Rearrange item position in import dialog


Refactor SqlImportHelper to support SQLCipher


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

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

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

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

![](https://alexbakker.me/u/115z6be7go.png)
2020-01-19 15:53:04 +01:00
Alexander Bakker 7f1ce1e645 Release v1.1.2 2020-01-18 14:42:53 +01:00
Michael SchättgenandGitHub dc6bf3795a Merge pull request #286 from alexbakker/intro-text
Add explanation for the security options
2020-01-12 07:58:17 -08:00
Michael SchättgenandGitHub a3538f95e2 Merge pull request #285 from alexbakker/storage-permissions
Remove EXTERNAL_STORAGE permissions
2020-01-12 07:53:08 -08:00
Alexander Bakker 85c358043d Add explanation for the security options
This adds some extra explanation of the security options to the intro, as well
as a warning when setting a password.

[<img width=200 src="https://alexbakker.me/u/jipc03u3hx.png">](https://alexbakker.me/u/jipc03u3hx.png)
[<img width=200 src="https://alexbakker.me/u/uw9nbsd95s.png">](https://alexbakker.me/u/uw9nbsd95s.png)
[<img width=200 src="https://alexbakker.me/u/mdoe5m8c3o.png">](https://alexbakker.me/u/mdoe5m8c3o.png)
2020-01-11 13:57:08 +01:00
Alexander Bakker ab8f01284a Remove EXTERNAL_STORAGE permissions
As we switched to the SAF, we don't need these permissions anymore.
2020-01-11 12:59:09 +01:00
Alexander BakkerandGitHub 97d824d779 Merge pull request #279 from michaelschattgen/feature/multi-select
Add ability to select multiple entries
2020-01-05 11:42:36 +01:00
Michael SchättgenandGitHub eedf9d2935 Merge pull request #278 from alexbakker/scoped-storage
Rewrite the export functionality to use the Storage Access Framework
2020-01-04 13:20:11 -08:00
Alexander Bakker 1ede56e137 Rewrite the export functionality to use the Storage Access Framework
We noticed after the v1.1 release that we need to switch our external storage
access code over to use the Storage Access Framework. The export functionality
was the only piece of code that still used the deprecated method and this patch
addresses that.

The Storage Access Framework is a little annoying to work with, but I don't
think it'll be too painful for our usecase. This switch comes with some nice
benefits for users as well. Users are now able to select a custom storage
location, which can be local or in the cloud.

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

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

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

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

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

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

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

Add review fixes

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

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

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

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

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

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

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

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

This patch addresses the issue by using the old password encode function for
passwords over 64 bytes and repairing the affected password slot.
2019-05-26 23:52:20 +02:00
mdmkandAlexander Bakker 588c1c07df Update french translation (#97)
* Update strings.xml
2019-05-26 21:55:01 +02:00
Alexander BakkerandGitHub f54639c791 Merge pull request #89 from michaelschattgen/feature-ignorespaces
Add possibility to ignore spaces in EditTexts
2019-05-24 10:20:19 +02:00
Alexander Bakker 7c78ddc929 Release v0.4.1 2019-05-22 00:19:32 +02:00
Alexander Bakker 9f3ccdc508 Force a period uniformity check when initializing EntryAdapter
This fixes a bug where the progress bar was not shown upon unlock.
We'll come up with a better solution later. This is just a hot fix.
2019-05-22 00:09:45 +02:00
Alexander Bakker 99c222cffa Release v0.4 2019-05-21 21:48:03 +02:00
Michael Schättgen 3556cd0e66 Add possibility to ignore spaces in EditTexts 2019-05-21 21:18:01 +02:00
Alexander BakkerandMichael Schättgen 57e3574693 Fix a bug where Aegis would forcibly pop up after unlocking the device 2019-05-20 22:27:28 +02:00
Alexander BakkerandGitHub 98c166a6b2 Merge pull request #86 from michaelschattgen/feature-clearpassword
Select and focus password on failed unlock attempt
2019-05-20 20:47:10 +02:00
Michael Schättgen b6a31e798f Select and focus password on failed unlock attempt 2019-05-20 20:41:56 +02:00
Michael Schättgen 5ebb714d05 Fix setting the preferred theme 2019-05-15 22:23:13 +02:00
Alexander BakkerandMichael Schättgen 9958f73926 Set the Activity theme before calling super.onCreate (#84)
This fixes a NullPointerException that would occur when restoring from
savedInstanceState due to getSupportActionBar returning null. It also removes
the theme definitions from AndroidManifest as we override those anyway.
2019-05-15 22:01:00 +02:00
Alexander BakkerandMichael Schättgen 0fb33744ae Update Krop to 0.44 (#83) 2019-05-15 21:56:14 +02:00
Alexander BakkerandMichael Schättgen 6d26d1beb0 Fix the last couple of sorting bugs (#77)
This fixes the following bugs:
- Sort category is forgotten after lock/unlock
- The sort mode is not respected for new entries

I got a little carried away while working on this patch and also included the
following other enhancements:
- Simplify the SortCategory, Theme and ViewMode enums
- Simplify usage of string resources
- Don't call notifyDataSetChanged and runLayoutAnimation unnecessarily
2019-05-15 21:29:45 +02:00
Alexander Bakker 0a8dd56306 Escape apostrophes in the French translation 2019-05-15 19:54:49 +02:00
mdmkandMichael Schättgen 8a8d714901 French translation and small changes to english strings. (#59)
* Typos in strings

- Double space.
- Missing verb in sentence. "need" makes sense here.
- Equal is a verb suitable for speaking of strings. For a final user, identical is more understandable.

* French translation
2019-05-15 10:45:08 +02:00
Alexander BakkerandGitHub c2cb5a3044 Merge pull request #74 from alexbakker/feature-encrypted-andotp
Add support for importing encrypted andOTP backups
2019-05-01 14:46:39 +02:00
Alexander Bakker 9893b2f057 Add support for importing encrypted andOTP backups
This also refactors dialog_newgroup to serve as a generic text input dialog
2019-05-01 14:42:39 +02:00
Alexander BakkerandGitHub 83b2f85ff4 Merge pull request #73 from alexbakker/refactor-import
Refactor database importer logic
2019-05-01 14:34:18 +02:00
Alexander Bakker 47df8968c7 Refactor database importer logic
Summary:
- Combine app and file importers into one type
- Split encrypted and plain text file logic into separate DatabaseImporter.State
  classes
- Add an abstract FileReader class that can handle reading from apps and files

The idea is that every importer implements its own UI logic to obtain
credentials for decryption through a dialog. Aegis databases are still an
exception to this rule as it still uses an Activity to ask for credentials. This
will be addressed in a future patch.

As a result of this change all app importers are now also available under
"Import from file".
2019-05-01 14:30:57 +02:00
Alexander BakkerandGitHub 33b4b4b7ab Merge pull request #72 from michaelschattgen/feature-selectentries
Add ability to select entries when importing
2019-05-01 13:14:16 +02:00
Michael SchättgenandAlexander Bakker 21fd8fdd8d Add ability to select entries when importing
Move onScroll to seperate class to avoid duplicate code

Move importing logic back to the PreferencesFragment

Add minor changes

Add ImportEntry to properly track checked states

Minor layout changes
2019-05-01 13:13:50 +02:00
Michael SchättgenandGitHub 7ec8391872 Merge pull request #71 from alexbakker/fix-pass-encode
Fix a bug where the password encode function added trailing null bytes
2019-04-30 15:20:01 +02:00
Alexander Bakker afb9e59711 Fix a bug where the password encode function added trailing null bytes
Funny story. Instead of	obtaining the actual bytes from the ByteBuffer in the
password encode function, we obtained the entire buffer. This caused some
trailing null bytes to be added to the encoded password. Luckily (and
strangely), PBKDF2 produces collisions for inputs with trailing null bytes and
thus scrypt does this as well. As such, this bug doesn't affect us right now,
but it would if we were to use that encode function for other purposes in the
future.

This also adds a test that checks for the expected collision behavior of scrypt.
2019-04-24 13:43:46 +02:00
Alexander BakkerandGitHub 24a93ecc9f Merge pull request #66 from michaelschattgen/feature-scanimage
Add ability to scan image for QR code
2019-04-23 21:09:15 +02:00
Michael Schättgen 2de9b96138 Add ability to scan image for QR code 2019-04-23 20:27:31 +02:00
Michael SchättgenandGitHub e0d5aba94a Update README.md 2019-04-18 17:02:56 +02:00
Michael SchättgenandGitHub f3b4433d5b Merge pull request #60 from alexbakker/doc-vault
Add documentation for Aegis' security design and vault format
2019-04-18 14:43:17 +02:00
Alexander Bakker f4678e2813 Release v0.3.3 2019-04-17 13:40:57 +02:00
Michael Schättgen b958145761 Fix bug where parsing otp codes was impossible
Y I K E S
2019-04-17 13:37:53 +02:00
Alexander Bakker feea9a2e67 Fix crash when trying to auto lock a locked vault 2019-04-17 08:45:42 +02:00
Alexander Bakker ebb9d0be3f Make app importer path lookup more dynamic
Also fixes a crash in SteamAppImporter that occurred with empty dirs
2019-04-17 01:32:20 +02:00
Alexander Bakker f5cbec21f4 Release v0.3.2 2019-04-17 00:00:41 +02:00
Alexander Bakker e55d6111cf Fix bold in full_description.txt for F-Droid 2019-04-17 00:00:10 +02:00
Alexander Bakker cb01fb21d3 Remove extra translations to fix lint errors in release mode 2019-04-16 23:48:50 +02:00
Alexander Bakker c5be99e58d Release v0.3.1 2019-04-16 23:38:05 +02:00
Alexander Bakker 92458d0d3b Move setGlobalDurationScale to AegisActivity
Also, rename the method and make the error string translatable
2019-04-16 23:24:39 +02:00
Michael SchättgenandGitHub 5d835b05ee Merge pull request #56 from ByteHamster/krop-color
Darkened Krop color
2019-04-16 22:26:55 +02:00
Michael Schättgen 2a47fa4de5 Add method to set global duration scale
Fixes #35
2019-04-16 22:12:49 +02:00
Alexander Bakker ead17949f9 Add simple example script to decrypt an Aegis vault 2019-04-16 20:21:15 +02:00
Alexander Bakker d5a848e79e Add a document detailing Aegis' security design and vault format 2019-04-16 20:21:15 +02:00
Alexander BakkerandGitHub 553cb16bc4 Merge pull request #57 from ByteHamster/arrays
Strings.xml cleanup
2019-04-14 18:03:16 +02:00
ByteHamster 47f8c9e648 Strings cleanup
- Removed unused strings
- Moved arrays to their own file, so translators do not have to deal with arrays
2019-04-14 17:18:14 +02:00
Alexander BakkerandGitHub 791138ccab Merge pull request #55 from ByteHamster/focus-password
Show keyboard automatically
2019-04-14 15:57:00 +02:00
Alexander BakkerandGitHub 6cbef27cfb Merge pull request #58 from ByteHamster/master
Added F-droid icon to readme
2019-04-14 15:56:21 +02:00
H. LehmannandGitHub e5f7dce9b8 Added F-droid icon 2019-04-14 14:24:53 +02:00
ByteHamster a415ef573e Show keyboard automatically 2019-04-14 14:15:34 +02:00
ByteHamster ffa2577cc2 Darkened Krop color 2019-04-14 00:47:21 +02:00
Alexander Bakker 2a0daae155 Fix crash that occurred when scheme of the URI in a QR code is missing 2019-04-10 11:42:35 +02:00
Michael Schättgen b41e328d22 Add ability to translate themes and viewmodes
Closes #50
2019-04-09 17:53:10 +02:00
Michael SchättgenandGitHub 1ac42d85d6 Merge pull request #53 from alexbakker/feature-autolock
Add an option to automatically lock the app
2019-04-09 17:34:47 +02:00
Michael SchättgenandGitHub 685a791aba Merge pull request #49 from zaggo0/master
Add Dutch translations
2019-04-09 17:34:35 +02:00
Michael Schättgen 89a1663e28 Fix bug where entries didn't get sorted properly 2019-04-09 17:15:25 +02:00
Alexander Bakker 0563ac917a Fix some rare crashes when restoring the app after termination
Also fixes an issue where the app shortcuts would not work for unencrypted vaults
2019-04-08 23:13:11 +02:00
Patrick van der Linden 719ef3bc71 Add Dutch translations
+ Fix preference groups not being translated
2019-04-08 15:00:24 +02:00
Alexander Bakker 18fd88a441 Add an option to automatically lock the app
This adds an option to automatically lock the app when:
* The back button is pressed
* The device is locked

It's the first step towards implementing #7
2019-04-07 18:36:13 +02:00
Alexander Bakker 6d93b78f9a Fix a crash on KitKat when clearing the KeyStore
Apparently KitKat doesn't like KeyPermanentlyInvalidatedException
2019-04-04 18:49:57 +02:00
Alexander Bakker 4365a693f2 Get Cipher back from FingerprintManager,CryptoObject after auth 2019-04-04 18:18:34 +02:00
Alexander Bakker 14fa493ac9 Remove keys from the KeyStore when disabling encryption or fingerprint 2019-04-04 15:27:24 +02:00
Alexander Bakker 36e3dd559c Normalize formatting and 'optimize' imports 2019-04-04 14:07:36 +02:00
Alexander Bakker b681d57b4e Update testdata file to use 'test' as the password 2019-04-03 15:03:56 +02:00
Alexander Bakker c5fa40166f Add some info about verifying APK's obtained from GitHub to the README 2019-04-02 21:37:49 +02:00
Michael Schättgen b7cf234db6 Add smaller view mode
Closes #39
2019-04-02 15:47:07 +02:00
Alexander Bakker f27dc54ec4 Release v0.3 2019-04-01 23:50:07 +02:00
Alexander Bakker 4c1dec613c Fallback to a 30 second period if an andOTP doesn't provide one 2019-04-01 23:48:44 +02:00
Alexander Bakker a251685002 Add a dialog that displays a list of errors that occurred during import 2019-04-01 23:36:31 +02:00
Alexander Bakker 118221e3df Add support for Steam tokens to the andOTP importer
Close #44
2019-04-01 22:37:56 +02:00
Alexander Bakker 1dd5f893da Add support for Steam OTP 2019-04-01 22:37:52 +02:00
Michael Schättgen a46640f43d Fix recreate when setting view mode 2019-04-01 21:47:41 +02:00
Michael Schättgen a293078603 Add way to set view mode 2019-04-01 01:44:38 +02:00
Michael Schättgen f3ed79dc71 Add ability to change view mode 2019-04-01 01:21:12 +02:00
Michael Schättgen ee8fd2e9f2 Hide divider in compact mode when progressbar is visible 2019-04-01 01:04:26 +02:00
Michael Schättgen 8b9ec5f976 Start working on compact mode 2019-04-01 00:58:45 +02:00
Michael Schättgen d283eae7a0 Add russian strings for authentication methods
#43
2019-04-01 00:18:44 +02:00
Michael SchättgenandGitHub a854db760c Merge pull request #41 from ferhadnecef/stringlate-ru-1048
Updated ru (Russian) translation
2019-03-31 23:57:36 +02:00
Michael Schättgen caf2ec882a Merge branch 'feature-sorting' 2019-03-31 23:30:52 +02:00
Michael Schättgen bbf967e2b7 Add a way to save current sorting method 2019-03-31 22:34:25 +02:00
Michael Schättgen b9d011b48e Disable drag and drop when list is sorted 2019-03-31 21:28:37 +02:00
Michael Schättgen ecdadf5335 Add SortCategory fixes and support for custom sorting 2019-03-31 21:23:14 +02:00
Michael Schättgen 10b80442d0 Comparators will now ignore casing 2019-03-31 21:22:20 +02:00
Alexander Bakker a2879a4558 Fix the screenshot links in the README 2019-03-31 20:39:41 +02:00
Michael Schättgen 62025d1053 Add way to check for reversed sort categories 2019-03-31 20:14:11 +02:00
Michael Schättgen 09fe550a28 Add AccountNameComparator 2019-03-31 20:12:34 +02:00
Michael Schättgen 07aac1e6a3 Add support for reversed sorting methods 2019-03-31 20:12:25 +02:00
Michael Schättgen b8939b504e Start working on entry sorting 2019-03-31 19:57:45 +02:00
Alexander Bakker 4f29d0c2e4 Fix metadata files for F-Droid (for real this time) 2019-03-31 18:21:10 +02:00
Alexander Bakker 592c6683c3 Continue importing entries even if one can't be parsed 2019-03-30 18:26:16 +01:00
ferhad.necef f8edac72ea Updated ru (Russian) translation 2019-03-29 16:00:49 +03:00
Alexander Bakker fc0e1150f6 Replace hardcoded encoding names with constants 2019-03-28 20:01:00 +01:00
Michael SchättgenandGitHub e1fd913817 Update README.md 2019-03-28 13:15:18 +00:00
Michael Schättgen ede9f93a59 Release v0.2.2 2019-03-28 13:46:39 +01:00
Michael Schättgen c6c0eb88e9 Fix true black theme in all activities
Closes #32. Also tested on API 19.
2019-03-28 13:46:33 +01:00
Michael Schättgen f929790be6 Fix metadata path by adding android folder
Closes #36
2019-03-28 13:46:07 +01:00
Michael Schättgen a4ff9d6d31 Fix crash in API 19 2019-03-28 13:45:43 +01:00
Michael SchättgenandGitHub a21d28a91f Update README.md 2019-03-28 10:47:24 +00:00
Michael Schättgen f7c32bc28b Improve theme based texts 2019-03-28 01:27:07 +01:00
Michael Schättgen 59c0ca947d Add workaround for amoled themed preferences 2019-03-28 01:08:44 +01:00
Michael Schättgen a7ac1adb80 Convert dark mode preference to select theme preference 2019-03-28 00:56:29 +01:00
Michael Schättgen 17204cc026 Add status bar color for true dark theme 2019-03-28 00:54:50 +01:00
Michael Schättgen cf4aecbd3e Refactor dark mode to allow multiple themes 2019-03-28 00:54:30 +01:00
Michael Schättgen 9baaf824c6 Start working on amoled / true black theme
#32
2019-03-28 00:15:02 +01:00
Michael Schättgen eb172fc54e Merge branch 'master' of https://github.com/alexbakker/Aegis 2019-03-27 21:18:26 +01:00
Michael Schättgen e99762b814 Change global progressbar color to improve visibility
As part of #34
2019-03-27 21:18:16 +01:00
Alexander Bakker 6b1d4a60e3 Credit Freepik from Flaticon for the icon 2019-03-27 12:32:40 +01:00
Alexander Bakker a540b3cfee Add icon to fastlane metadata 2019-03-27 12:14:08 +01:00
Michael SchättgenandGitHub b0481d6b30 Update README.md 2019-03-27 11:07:03 +00:00
Alexander Bakker 0c315c53e5 Add .idea/ and release/ to .gitignore 2019-03-27 01:03:55 +01:00
Alexander Bakker 75c37a454f Release v0.2.1 2019-03-27 00:51:44 +01:00
Michael Schättgen 554d7e17be Fix NullPointerException when deleting tokens 2019-03-27 00:47:39 +01:00
379 changed files with 22660 additions and 4452 deletions
+1
View File
@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/beemdevelopment']
+34
View File
@@ -0,0 +1,34 @@
---
name: "Bug report"
about: "Create a report to help us fix a bug"
labels: bug
---
<!-- Please read the [bug reports section of the contribution
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
###### Info
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
###### Steps to reproduce
A detailed list of reproduction steps.
###### What do you expect to happen?
...
###### What happens instead?
...
###### Log
```
If applicable, paste the debug log here.
```
+6
View File
@@ -0,0 +1,6 @@
---
name: "Feature request"
about: "Suggest a new feature for this project"
labels: proposal
---
+3 -2
View File
@@ -16,6 +16,7 @@ out/
# Gradle files
.gradle/
build/
release/
# Local configuration file (sdk path, etc)
local.properties
@@ -34,8 +35,8 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
.idea/libraries
.idea/
# Keystore files
*.jks
crowdin.properties
Generated
-1
View File
@@ -1 +0,0 @@
Aegis
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>
-3
View File
@@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT" />
</compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="F:\Android\Android Studio\gradle\gradle-2.14.1" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/Aegis.iml" filepath="$PROJECT_DIR$/Aegis.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+24
View File
@@ -0,0 +1,24 @@
language: shell
os: linux
dist: bionic
env:
global:
- ANDROID_HOME=${HOME}/android
- ANDROID_SDK_ROOT=${ANDROID_HOME}
- TOOLS=${ANDROID_HOME}/cmdline-tools
- PATH=${ANDROID_HOME}/emulator:${TOOLS}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
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
install:
- mkdir -p ${TOOLS}
- travis_retry wget https://dl.google.com/android/repository/commandlinetools-linux-6514223_latest.zip -O ${TOOLS}/tools.zip
- unzip ${TOOLS}/tools.zip -d ${TOOLS}
- yes | travis_retry sdkmanager "platform-tools" > /dev/null
- yes | travis_retry sdkmanager "tools" > /dev/null
- yes | travis_retry sdkmanager "build-tools;30.0.1" > /dev/null
- yes | travis_retry sdkmanager "platforms;android-30" > /dev/null
script:
- ./gradlew assemble check
+121
View File
@@ -0,0 +1,121 @@
# Contributing
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](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. 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. Duplicate
bug reports receive the same treatment.
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
In some cases, we ask our users to obtain a debug log from their device. This is
typically only necessary if Aegis:
- Is unable to recover from an error and crashes.
- Only shows a generic error to the user, but writes a more detailed one to the
log.
Capturing a log with the Android Debug Bridge (ADB) allows us to see the stack
trace and the exception that occurred.
#### Preparation
Before you can capture a log, you first need to go through a one-time setup
process on your Android device and computer.
##### Prerequisites
- Your Android device.
- A computer with Windows, Mac or Linux.
- A USB cable to connect your Android device to your computer.
##### Setup
__On your Android device__:
1. Navigate to ``Settings -> About``, scroll down and start tapping on the build
number until developer options are enabled.
2. Navigate to ``Settings -> System -> Developer options`` and enable ``USB
debugging``.
These navigation steps may differ slightly across Android versions and ROMs.
__On your computer__:
3. Download and extract the SDK platform tools for Android:
https://developer.android.com/studio/releases/platform-tools.
4. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
5. Execute ``adb devices``.
__On your Android device__:
6. A prompt will appear. Select "Always allow from this computer" and accept the
connection.
#### Capturing a log
__On your Android device__:
1. Start Aegis.
__On your PC__:
2. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
3. Start a log capture by executing the following commands.
```
adb logcat -c
adb logcat -f debug.log
```
The logcat command captures the full system log by default, which may expose
some sensitive information. While this information can sometimes help with
finding the root cause of the issue, it is not always necessary. To only
capture the log output of Aegis, replace the last logcat command with the
one below:
```sh
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
```
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
``com.beemdevelopment.aegis.debug``._
__On your Android device__:
4. Reproduce the issue.
__On your PC__:
5. Stop the log capture with Ctrl+C.
6. Attach the ``debug.log`` file to your issue on GitHub.
+104 -39
View File
@@ -1,64 +1,129 @@
# 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)
# Features
__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.
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)
- Fingerprint (Android Keystore)
- Biometrics (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
- Scan QR code
- Enter details manually
- Import
- andOTP
- FreeOTP
- Supported algorithms:
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
- 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
- Light and dark themes
- 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
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true).png
[<img width=200 alt="Screenshot 1"
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
[<img width=200 alt="Screenshot 2"
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
[<img width=200 alt="Screenshot 3"
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Screenshot 4"
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
[<img width=200 alt="Screenshot 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
## Downloads
Aegis is available in the Google Play Store.
Aegis is available on the Google Play Store and on F-Droid.
[<img height=80 alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" />](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
[<img height=80 alt="Get it on Google Play"
src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
/>](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
[<img height="80" alt="Get it on F-Droid"
src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
/>](https://f-droid.org/app/com.beemdevelopment.aegis)
### Verification
There are plans to make it available on F-Droid as well.
APK releases on Google Play and GitHub are signed using the same key. They can
be verified using
[apksigner](https://developer.android.com/studio/command-line/apksigner.html#options-verify):
## Libraries
```
apksigner verify --print-certs --verbose aegis.apk
```
- [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
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [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
The output should look like:
```
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
```
The certificate fingerprints should correspond to the ones listed below:
```
Owner: CN=Beem Development
Issuer: CN=Beem Development
Serial number: 172380c
Valid from: Sat Feb 09 14:05:49 CET 2019 until: Wed Feb 03 14:05:49 CET 2044
Certificate fingerprints:
MD5: AA:EE:86:DB:C7:B8:88:9F:1F:C9:D0:7A:EC:37:36:32
SHA1: 59:FB:63:B7:1F:CE:95:74:6C:EB:1E:1A:CB:2C:2E:45:E5:FF:13:50
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
```
### Community
- [aegis-icons](https://github.com/krisu5/aegis-icons)
While we're still [working on better icon
support](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)
## Contributing
Looking to contribute to Aegis? That's great! There are a couple of ways to help
out. Translations, bug reports and pull requests are all greatly appreciated.
Please refer to our [contributing guidelines](CONTRIBUTING.md) to get started.
Swing by our Matrix room to interact with other contributors:
[#aegis:matrix.org](https://matrix.to/#/#aegis:matrix.org).
## License
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
This project is licensed under the GNU General Public License v3.0. See the
[LICENSE](LICENSE) file for details.
+148 -26
View File
@@ -1,52 +1,174 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine cmd
standardOutput = stdout
}
return stdout.toString().trim()
}
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
def packageName = "com.beemdevelopment.aegis"
def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdkVersion 28
compileSdkVersion 30
defaultConfig {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 3
versionName "0.2"
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 30
versionCode 42
versionName "1.4-beta2"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.all {
useJUnitPlatform()
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
manifestPlaceholders = [title:"AegisDev", iconName:"ic_launcher_debug"]
manifestPlaceholders = [
title: "AegisDev",
iconName: "ic_launcher_debug",
fileProviderAuthority: "${fileProviderAuthorityDebug}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
resValue "bool", "pref_secure_screen_default", "false"
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode false
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [title:"Aegis", iconName:"ic_launcher"]
manifestPlaceholders = [
title: "Aegis",
iconName: "ic_launcher",
fileProviderAuthority: "${fileProviderAuthority}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
resValue "bool", "pref_secure_screen_default", "true"
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
def libsuVersion = '2.3.2'
def cameraxVersion = '1.0.0-beta11'
def glideVersion = '4.11.0'
def guavaVersion = '30.0'
def junitVersion = '5.7.0'
def libsuVersion = '2.5.1'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
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-alpha18"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
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:v4.2.2'
implementation 'com.github.avito-tech:krop:3e65e12'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'me.dm7.barcodescanner:zxing:1.9'
implementation 'com.github.avito-tech:krop:0.44'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
transitive = false
}
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
testImplementation 'junit:junit:4.12'
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.protobuf:protobuf-javalite:3.13.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.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'net.lingala.zip4j:zip4j:2.6.4'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
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.0.10'
}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
</lint>
+7
View File
@@ -15,3 +15,10 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
@@ -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);
}
}
@@ -1,13 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
@@ -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());
}
}
+77 -40
View File
@@ -3,61 +3,98 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:name="com.beemdevelopment.aegis.AegisApplication"
android:allowBackup="true"
android:name=".AegisApplication"
android:allowBackup="false"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
tools:replace="android:theme"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
<activity
android:name="com.beemdevelopment.aegis.ui.MainActivity"
android:label="${title}"
android:alwaysRetainTaskState="true"
android:launchMode="singleTask">
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about" />
<activity
android:name=".ui.SelectEntriesActivity"
android:label="@string/title_activity_select_entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
</intent-filter>
</activity>
<activity
android:name="com.beemdevelopment.aegis.ui.ScannerActivity"
android:label="Scan a QR code"
android:theme="@style/AppTheme.Fullscreen"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/title_activity_scan_qr"
android:screenOrientation="portrait" />
<activity
android:name="com.beemdevelopment.aegis.ui.EditEntryActivity"
android:label="Edit profile"
android:theme="@style/AppTheme.NoActionBar">
</activity>
android:name=".ui.EditEntryActivity"
android:label="@string/title_activity_edit_entry" />
<activity
android:name="com.beemdevelopment.aegis.ui.IntroActivity"
android:theme="@style/Theme.Intro"
android:launchMode="singleTop">
</activity>
android:name=".ui.IntroActivity"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait" />
<activity
android:name="com.beemdevelopment.aegis.ui.AuthActivity"
android:launchMode="singleTop">
</activity>
<activity android:name="com.beemdevelopment.aegis.ui.PreferencesActivity">
</activity>
android:name=".ui.AuthActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name="com.beemdevelopment.aegis.ui.SlotManagerActivity"
android:label="Manage key slots"
android:theme="@style/AppTheme.TransparentActionBar">
</activity>
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
<activity
android:name="com.beemdevelopment.aegis.ui.GroupManagerActivity"
android:label="Manage groups"
android:theme="@style/AppTheme.TransparentActionBar">
android:name=".ui.SlotManagerActivity"
android:label="@string/title_activity_manage_slots" />
<activity
android:name=".ui.GroupManagerActivity"
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>
+205
View File
@@ -0,0 +1,205 @@
<html>
<head>
<style type="text/css">
* {
word-wrap: break-word;
}
body {
background-color: %1$s;
color: %2$s;
}
ul {
list-style-position: inside;
padding: 0;
padding-left: 5px;
margin-bottom: 5px;
}
li {
padding-bottom: 8px;
list-style-position: outside;
margin-left: 1em;
}
h4 {
margin-top: 0px;
margin-bottom: 0px;
}
h3 {
margin-bottom: 7px;
padding-top: 7px;
}
</style>
</head>
<body>
<div></div>
<h3>Version 1.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>
</ul>
<h3>Version 1.3</h3>
<h4>New</h4>
<ul>
<li>Completely rewritten intro/onboarding</li>
<li>Option to show a PIN keyboard when unlocking Aegis</li>
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
<li>RTL support</li>
<li>Arabic and Portuguese translations</li>
<li>Updates to existing translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Better lifecycle handling of the biometric authentication prompt</li>
<li>The filename of exported vaults had a double .json extension</li>
<li>The navigation bar color was incorrect on devices pre API 27</li>
<li>QR code scanner performance and stability improvements</li>
<li>Various other small usability and stability improvements</li>
</ul>
<h3>Version 1.2.1</h3>
<h4>Fixes</h4>
<ul>
<li>Fix a rare issue where the intro could end up in a bad state</li>
</ul>
<h3>Version 1.2</h3>
<h4>New</h4>
<ul>
<li>Add navigation bar color to themes</li>
<li>Add support for importing from TOTP Authenticator</li>
<li>Add support for importing from Microsoft Authenticator</li>
<li>Add support for importing from Authenticator Plus</li>
<li>Add support for importing a plain text Google Authenticator URI file</li>
<li>Add support for importing from the new Google Authenticator export QR codes</li>
<li>Add support for otpauth://steam URI's</li>
<li>Add an option to copy tokens on tap (and disable it by default)</li>
<li>Improve method to notify users on copy</li>
<li>Add support for backups</li>
<li>Improve multiselect flow</li>
<li>Automatically adapt to system theme</li>
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
<li>Use most frequent period to show progress</li>
<li>Append a timestamp to the filename of exported vaults</li>
<li>Add Hungarian translation</li>
<li>Add Turkish translation</li>
<li>Display a warning if automatic time sync is not enabled</li>
<li>Minor card entry layout overhaul</li>
<li>Ability to transfer tokens with qr codes</li>
<li>Lockscreen overhaul</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Improve overall exception handling and error feedback to the user</li>
<li>Improve icon editing flow</li>
<li>Protect writes of the vault file against corruption with AtomicFile</li>
<li>Make the parsing logic of the QR code URI more robust</li>
<li>Importing from Authy now asks for password if needed</li>
<li>Update Russian localization</li>
<li>Increase password reminder period to 30 days</li>
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
<li>Respect the global animator duration scale setting</li>
</ul>
<p>Various other minor improvements</p>
<h3>Version 1.1.4</h3>
<h4>Fixes</h4>
<ul>
<li>The export filename was missing the ".json" extension in some cases</li>
</ul>
<h3>Version 1.1.3</h3>
<h4>New</h4>
<ul>
<li>Password reminder for users who use biometric unlock</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Tokens would not refresh in some rare cases</li>
</ul>
<h3>Version 1.1.2</h3>
<h4>New</h4>
<ul>
<li>Ability to select multiple entries</li>
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
<li>Explanation and warning for the security options</li>
<li>Removed external storage permissions</li>
</ul>
<h3>Version 1.1.1</h3>
<h4>Fixes</h4>
<ul>
<li>Exporting the vault did not work on Android 10</li>
</ul>
<h3>Version 1.1</h3>
<h4>New</h4>
<ul>
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
<li>Support for importing from WinAuth</li>
<li>Support for Chromebooks</li>
<li>Option to highlight entries when tapped</li>
<li>Filter for ungrouped tokens</li>
<li>Ability to search for token account names</li>
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
<li>Updated translations (thanks to all Crowdin contributers!)</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The behavior of highlighting and revealing entries was inconsistent</li>
<li>The changelog dialog didn't work</li>
<li>The persistent notification was shown even after the app was killed</li>
</ul>
<h3>Version 1.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for andOTP's new backup file format</li>
</ul>
<h3>Version 1.0.2</h3>
<h4>Fixes</h4>
<ul>
<li>Search feature on Huawei devices</li>
</ul>
<h4>Notes</h4>
<ul>
<li>Disabled automatic backups through the Google Play Store</li>
</ul>
<h3>Version 1.0.1</h3>
<h4>Notes</h4>
<ul>
<li>Temporarily disabled search feature on Huawei devices</li>
</ul>
<h3>Version 1.0</h3>
<h4>New</h4>
<ul>
<li>New icon</li>
<li>Overhaul of interaction with the entry list</li>
<li>Persistent notification while the vault is unlocked</li>
<li>Language override option</li>
<li>Support for importing from FreeOTP+</li>
<li>Ability to toggle password visibility during unlock</li>
<li>Support for deeplinking otpauth URIs</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Bad overall performance and high battery usage</li>
<li>Codes with an uneven number of digits are displayed incorrectly</li>
<li>Crash when entering a large value for OTP period</li>
</ul>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,35 +1,121 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.ui.MainActivity;
import androidx.annotation.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;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private VaultFile _vaultFile;
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
registerReceiver(receiver, intentFilter);
// 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();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}
}
public DatabaseManager getDatabaseManager() {
public boolean isVaultLocked() {
return _manager == null;
}
/**
* Loads the vault file from disk at the default location, stores an internal
* reference to it for future use and returns it. This must only be called before
* initVaultManager() or after lock().
*/
public VaultFile loadVaultFile() throws VaultManagerException {
if (!isVaultLocked()) {
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readFile(this);
}
return _vaultFile;
}
/**
* Initializes the vault manager by decrypting the given vaultFile with the given
* creds. This removes the internal reference to the raw vault file.
*/
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
_vaultFile = null;
_manager = VaultManager.init(this, vaultFile, creds);
return _manager;
}
/**
* Initializes the vault manager with the given vault and creds. This removes the
* internal reference to the raw vault file.
*/
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
_vaultFile = null;
_manager = new VaultManager(this, vault, creds);
return _manager;
}
public VaultManager getVaultManager() {
return _manager;
}
@@ -37,6 +123,33 @@ public class AegisApplication extends Application {
return _prefs;
}
public boolean isAutoLockEnabled(int autoLockType) {
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
}
public void registerLockListener(LockListener listener) {
_lockListeners.add(listener);
}
public void unregisterLockListener(LockListener listener) {
_lockListeners.remove(listener);
}
/**
* 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(userInitiated);
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
@@ -46,15 +159,56 @@ public class AegisApplication extends Application {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setIcon(Icon.createWithResource(this, R.drawable.qr_scanner))
.setShortLabel(getString(R.string.new_entry))
.setLongLabel(getString(R.string.add_new_entry))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
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)) {
lock(false);
}
}
}
private class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
lock(false);
}
}
}
public interface LockListener {
/**
* 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);
}
}
@@ -2,35 +2,113 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class Preferences {
public static final int AUTO_LOCK_OFF = 1 << 0;
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
public static final int[] AUTO_LOCK_SETTINGS = {
AUTO_LOCK_ON_BACK_BUTTON,
AUTO_LOCK_ON_MINIMIZE,
AUTO_LOCK_ON_DEVICE_LOCK
};
private SharedPreferences _prefs;
public Preferences(Context context) {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public boolean isDarkModeEnabled() {
return _prefs.getBoolean("pref_dark_mode", false);
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public boolean isSecureScreenEnabled() {
return _prefs.getBoolean("pref_secure_screen", true);
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isPasswordReminderEnabled() {
return _prefs.getBoolean("pref_password_reminder", true);
}
public boolean isPasswordReminderNeeded() {
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return isPasswordReminderEnabled() && days >= 30;
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
public void resetPasswordReminderTimestamp() {
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
return _prefs.getBoolean("pref_account_name", true);
}
public int getCodeGroupSize() {
if (_prefs.getBoolean("pref_code_group_size", false)) {
return 2;
} else {
return 3;
}
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public boolean isAutoLockEnabled() {
return 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) {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
@@ -39,12 +117,107 @@ public class Preferences {
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
}
public void setCurrentSortCategory(SortCategory category) {
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
}
public SortCategory getCurrentSortCategory() {
return SortCategory.fromInteger(_prefs.getInt("pref_current_sort_category", 0));
}
public int getTapToRevealTime() {
return _prefs.getInt("pref_tap_to_reveal_time", 30);
}
public Theme getCurrentTheme() {
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
}
public void setCurrentTheme(Theme theme) {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
public void setCurrentViewMode(ViewMode viewMode) {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Resources.getSystem().getConfiguration().getLocales().get(0);
} else {
return Resources.getSystem().getConfiguration().locale;
}
}
String[] parts = lang.split("_");
if (parts.length == 1) {
return new Locale(parts[0]);
}
return new Locale(parts[0], parts[1]);
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
return Uri.parse(str);
}
return null;
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
public int getBackupsVersionCount() {
return _prefs.getInt("pref_backups_versions", 5);
}
public void setBackupsVersionCount(int versions) {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
}
public boolean isPinKeyboardEnabled() {
return _prefs.getBoolean("pref_pin_keyboard", false);
}
public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true);
}
public void setIsTimeSyncWarningEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
}
}
@@ -0,0 +1,64 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import java.util.Collections;
import java.util.Comparator;
public enum SortCategory {
CUSTOM,
ACCOUNT,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED;
private static SortCategory[] _values;
static {
_values = values();
}
public static SortCategory fromInteger(int x) {
return _values[x];
}
public Comparator<VaultEntry> getComparator() {
Comparator<VaultEntry> comparator = null;
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator();
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator());
break;
case ISSUER:
comparator = new IssuerNameComparator();
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
break;
}
return comparator;
}
public int getMenuItem() {
switch (this) {
case CUSTOM:
return R.id.menu_sort_custom;
case ACCOUNT:
return R.id.menu_sort_alphabetically_name;
case ACCOUNT_REVERSED:
return R.id.menu_sort_alphabetically_name_reverse;
case ISSUER:
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
default:
return R.id.menu_sort_custom;
}
}
}
@@ -0,0 +1,19 @@
package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED,
SYSTEM,
SYSTEM_AMOLED;
private static Theme[] _values;
static {
_values = values();
}
public static Theme fromInteger(int x) {
return _values[x];
}
}
@@ -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
);
}
@@ -0,0 +1,44 @@
package com.beemdevelopment.aegis;
import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
private static ViewMode[] _values;
static {
_values = values();
}
public static ViewMode fromInteger(int x) {
return _values[x];
}
@LayoutRes
public int getLayoutId() {
switch (this) {
case NORMAL:
return R.layout.card_entry;
case COMPACT:
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
*/
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 0;
}
return 20;
}
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
return obj;
}
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
byte[] nonce = Hex.decode(obj.getString("nonce"));
byte[] tag = Hex.decode(obj.getString("tag"));
return new CryptParameters(nonce, tag);
@@ -2,11 +2,13 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import org.bouncycastle.crypto.generators.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -24,21 +26,24 @@ import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.spongycastle.crypto.generators.SCrypt;
public class CryptoUtils {
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
public static final int CRYPTO_SCRYPT_N = 1 << 15;
public static final int CRYPTO_SCRYPT_r = 8;
public static final int CRYPTO_SCRYPT_p = 1;
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
byte[] bytes = toBytes(password);
byte[] keyBytes = SCrypt.generate(bytes, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
return deriveKey(bytes, params);
}
public static Cipher createEncryptCipher(SecretKey key)
@@ -88,9 +93,14 @@ public class CryptoUtils {
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
return decrypt(encrypted, 0, encrypted.length, cipher, params);
}
public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
// append the tag to the ciphertext
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(encrypted);
stream.write(encrypted, encryptedOffset, encryptedLen);
stream.write(params.getTag());
encrypted = stream.toByteArray();
@@ -120,9 +130,18 @@ public class CryptoUtils {
return data;
}
private static byte[] toBytes(char[] chars) {
public static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);
return bytes;
}
@Deprecated
public static byte[] toBytesOld(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
}
}
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -12,8 +13,10 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.ProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
@@ -58,6 +61,14 @@ public class KeyStoreHandle {
.build());
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new KeyStoreHandleException(e);
}
@@ -76,22 +87,29 @@ public class KeyStoreHandle {
throw new KeyStoreHandleException(e);
}
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
if (isSupported()) {
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
return null;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException(e);
}
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
return null;
}
return key;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
return false;
}
public void deleteKey(String id) throws KeyStoreHandleException {
try {
_keyStore.deleteEntry(id);
@@ -100,6 +118,16 @@ public class KeyStoreHandle {
}
}
public void clear() throws KeyStoreHandleException {
try {
for (String alias : Collections.list(_keyStore.aliases())) {
deleteKey(alias);
}
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
public MasterKey(SecretKey key) {
if (key == null) {
throw new NullPointerException();
throw new IllegalArgumentException("Key cannot be null");
}
_key = key;
}
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.crypto.otp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -12,7 +12,7 @@ public class HOTP {
private HOTP() {
}
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
@@ -30,18 +30,11 @@ public class HOTP {
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
long bin = ((hash[offset] & 0x7f) << 24)
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long otp = bin % (long) Math.pow(10, digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(otp));
while (res.length() < digits) {
res.insert(0, "0");
}
return res.toString();
return new OTP(otp, digits);
}
}
@@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private int _code;
private int _digits;
public OTP(int code, int digits) {
_code = code;
_digits = digits;
}
public int getCode() {
return _code;
}
public int getDigits() {
return _digits;
}
@NonNull
@Override
public String toString() {
int code = _code % (int) Math.pow(10, _digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(code));
while (res.length() < _digits) {
res.insert(0, "0");
}
return res.toString();
}
public String toSteamString() {
int code = _code;
StringBuilder res = new StringBuilder();
for (int i = 0; i < _digits; i++) {
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
res.append(c);
code /= STEAM_ALPHABET.length();
}
return res.toString();
}
}
@@ -8,13 +8,13 @@ public class TOTP {
private TOTP() {
}
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
public static String generateOTP(byte[] secret, String algo, int digits, long period)
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
public GuardianProjectFDroidRSA2048() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
public GuardianProjectFDroidRSA4096() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -1,77 +0,0 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import java.util.UUID;
public class Database {
private static final int VERSION = 1;
private DatabaseEntryList _entries = new DatabaseEntryList();
public JSONObject toJson() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
array.put(e.toJson());
}
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
try {
int ver = obj.getInt("version");
if (ver != VERSION) {
throw new DatabaseException("Unsupported version");
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
db.addEntry(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
}
return db;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void removeEntry(DatabaseEntry entry) {
_entries.remove(entry);
}
public void replaceEntry(DatabaseEntry newEntry) {
_entries.replace(newEntry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
_entries.swap(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
return _entries.getList();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
return _entries.getByUUID(uuid);
}
}
@@ -1,62 +0,0 @@
package com.beemdevelopment.aegis.db;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@NonNull
@Override
public Iterator<DatabaseEntry> iterator() {
return _entries.iterator();
}
public void add(DatabaseEntry entry) {
if (getByUUID(entry.getUUID()) != null) {
throw new AssertionError("entry found with the same uuid");
}
_entries.add(entry);
}
public void remove(DatabaseEntry entry) {
entry = mustGetByUUID(entry.getUUID());
_entries.remove(entry);
}
public void replace(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
_entries.set(_entries.indexOf(oldEntry), newEntry);
}
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
}
public List<DatabaseEntry> getList() {
return Collections.unmodifiableList(_entries);
}
public DatabaseEntry getByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
private DatabaseEntry mustGetByUUID(UUID uuid) {
DatabaseEntry entry = getByUUID(uuid);
if (entry == null) {
throw new AssertionError("no entry found with the same uuid");
}
return entry;
}
}
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseException extends Exception {
public DatabaseException(Throwable cause) {
super(cause);
}
public DatabaseException(String message) {
super(message);
}
}
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseFileException extends Exception {
public DatabaseFileException(Throwable cause) {
super(cause);
}
public DatabaseFileException(String message) {
super(message);
}
}
@@ -1,236 +0,0 @@
package com.beemdevelopment.aegis.db;
import android.content.Context;
import android.os.Environment;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
public class DatabaseManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
private Database _db;
private DatabaseFile _file;
private DatabaseFileCredentials _creds;
private boolean _encrypt;
private Context _context;
public DatabaseManager(Context context) {
_context = context;
}
public boolean fileExists() {
File file = new File(_context.getFilesDir(), FILENAME);
return file.exists() && file.isFile();
}
public void load() throws DatabaseManagerException {
assertState(true, false);
try (FileInputStream file = _context.openFileInput(FILENAME)) {
byte[] fileBytes = new byte[(int) file.getChannel().size()];
DataInputStream stream = new DataInputStream(file);
stream.readFully(fileBytes);
stream.close();
_file = DatabaseFile.fromBytes(fileBytes);
_encrypt = _file.isEncrypted();
if (!isEncryptionEnabled()) {
JSONObject obj = _file.getContent();
_db = Database.fromJson(obj);
}
} catch (IOException | DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public void lock() {
assertState(false, true);
_creds = null;
_db = null;
}
public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(true, true);
try {
JSONObject obj = _file.getContent(creds);
_db = Database.fromJson(obj);
_creds = creds;
} catch (DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public static void save(Context context, DatabaseFile file) throws DatabaseManagerException {
byte[] bytes = file.toBytes();
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
stream.write(bytes);
} catch (IOException e) {
throw new DatabaseManagerException(e);
}
}
public void save() throws DatabaseManagerException {
assertState(false, true);
try {
JSONObject obj = _db.toJson();
if (isEncryptionEnabled()) {
_file.setContent(obj, _creds);
} else {
_file.setContent(obj);
}
save(_context, _file);
} catch (DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public String export(boolean encrypt) throws DatabaseManagerException {
assertState(false, true);
try {
DatabaseFile dbFile = new DatabaseFile();
if (encrypt && isEncryptionEnabled()) {
dbFile.setContent(_db.toJson(), _creds);
} else {
dbFile.setContent(_db.toJson());
}
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("error creating external storage directory");
}
byte[] bytes = dbFile.toBytes();
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
try (FileOutputStream stream = new FileOutputStream(file)) {
stream.write(bytes);
}
return file.getAbsolutePath();
} catch (IOException | DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public void addEntry(DatabaseEntry entry) {
assertState(false, true);
_db.addEntry(entry);
}
public void removeEntry(DatabaseEntry entry) {
assertState(false, true);
_db.removeEntry(entry);
}
public void replaceEntry(DatabaseEntry entry) {
assertState(false, true);
_db.replaceEntry(entry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
assertState(false, true);
_db.swapEntries(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
assertState(false, true);
return _db.getEntries();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
assertState(false, true);
return _db.getEntryByUUID(uuid);
}
public TreeSet<String> getGroups() {
assertState(false, true);
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
for (DatabaseEntry entry : getEntries()) {
String group = entry.getGroup();
if (group != null) {
groups.add(group);
}
}
return groups;
}
public DatabaseFileCredentials getCredentials() {
assertState(false, true);
return _creds;
}
public void setCredentials(DatabaseFileCredentials creds) {
assertState(false, true);
_creds = creds;
}
public DatabaseFile.Header getFileHeader() {
assertLoaded(true);
return _file.getHeader();
}
public boolean isEncryptionEnabled() {
assertLoaded(true);
return _encrypt;
}
public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(false, true);
_creds = creds;
_encrypt = true;
save();
}
public void disableEncryption() throws DatabaseManagerException {
assertState(false, true);
_creds = null;
_encrypt = false;
save();
}
public boolean isLoaded() {
return _file != null;
}
public boolean isLocked() {
return _db == null;
}
private void assertState(boolean locked, boolean loaded) {
assertLoaded(loaded);
if (isLocked() && !locked) {
throw new AssertionError("database file has not been unlocked yet");
} else if (!isLocked() && locked) {
throw new AssertionError("database file has already been unlocked");
}
}
private void assertLoaded(boolean loaded) {
if (isLoaded() && !loaded) {
throw new AssertionError("database file has already been loaded");
} else if (!isLoaded() && loaded) {
throw new AssertionError("database file has not been loaded yet");
}
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseManagerException extends Exception {
public DatabaseManagerException(Throwable cause) {
super(cause);
}
}
@@ -1,20 +0,0 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
super();
}
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_FINGERPRINT;
}
}
@@ -1,155 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// modified for use in Aegis
import com.google.common.io.BaseEncoding;
/* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info.
*
* As modified by Patrick Woodworth:
*
* Copyright 2011 Patrick Woodworth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
/**
* Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html )
*
* @author Robert Kaye
* @author Gordon Mohr
*/
public class Base32 {
private static final String base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static final int[] base32Lookup =
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
};
private Base32() {
/**
* Encodes byte array to Base32 String.
*
* @param bytes Bytes to encode.
* @return Encoded byte array <code>bytes</code> as a String.
*
*/
public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte;
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
/* Is the current digit going to span a byte boundary? */
if (index > 3) {
if ((i + 1) < bytes.length) {
nextByte =
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
} else {
nextByte = 0;
}
digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= nextByte >> (8 - index);
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;
if (index == 0)
i++;
}
base32[j++] = base32Chars.charAt(digit);
}
return Arrays.copyOf(base32, j);
}
/**
* Decodes the given Base32 String to a raw byte array.
*
* @param base32
* @return Decoded <code>base32</code> String as a raw byte array.
*/
public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
// stop decoding when a padding char is encountered
if (base32[i] == '=') {
// make sure the rest is also padding, but don't bother verifying the length
for (int j = i + 1; j < base32.length; j++) {
if (base32[j] != '=') {
throw new Base32Exception("bad padding");
}
}
break;
}
lookup = base32[i] - '0';
digit = decodeDigit(lookup);
if (index <= 3) {
index = (index + 5) % 8;
if (index == 0) {
bytes[offset] |= digit;
offset++;
if (offset >= bytes.length)
break;
} else {
bytes[offset] |= digit << (8 - index);
}
} else {
index = (index + 5) % 8;
bytes[offset] |= (digit >>> index);
offset++;
if (offset >= bytes.length) {
break;
}
bytes[offset] |= digit << (8 - index);
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return bytes;
}
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}
@@ -1,29 +1,27 @@
package com.beemdevelopment.aegis.encoding;
import java.io.UnsupportedEncodingException;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
private Base64() {
}
public static byte[] decode(String s) throws Base64Exception {
public static byte[] decode(String s) throws EncodingException {
try {
return android.util.Base64.decode(s, _flags);
return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException e) {
throw new Base64Exception(e);
throw new EncodingException(e);
}
}
public static byte[] decode(byte[] s) throws EncodingException {
return decode(new String(s, StandardCharsets.UTF_8));
}
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
try {
return new String(encoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return BaseEncoding.base64().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base64Exception extends Exception {
public Base64Exception(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,9 @@
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
}
@@ -1,46 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
// It is licensed under GPLv2 with a classpath exception.
import com.google.common.io.BaseEncoding;
public class Hex {
private Hex() {
}
private static int hexToBin(char ch) {
if ('0' <= ch && ch <= '9') return ch - '0';
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
return -1;
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static byte[] decode(String s) throws HexException {
final int len = s.length();
if (len % 2 != 0)
throw new HexException("hexBinary needs to be even-length: " + s);
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int h = hexToBin(s.charAt(i));
int l = hexToBin(s.charAt(i + 1));
if (h == -1 || l == -1)
throw new HexException("contains illegal character for hexBinary: " + s);
out[i / 2] = (byte) (h * 16 + l);
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return out;
}
public static String encode(byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
return BaseEncoding.base16().lowerCase().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}
@@ -0,0 +1,136 @@
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricConstants.ERROR_CANCELED
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
@@ -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,10 +9,6 @@ public class EditTextHelper {
private EditTextHelper() {
}
public static void clearEditText(EditText text) {
text.getText().clear();
}
public static char[] getEditTextChars(EditText text) {
Editable editable = text.getText();
char[] chars = new char[editable.length()];
@@ -0,0 +1,58 @@
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
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;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
}
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.Manifest;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class FingerprintHelper {
private FingerprintHelper() {
}
public static FingerprintManager getManager(Context context) {
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
@@ -1,156 +0,0 @@
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
// It has been modified to suit Aegis' needs
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import androidx.annotation.RequiresApi;
import android.widget.TextView;
import com.mattprecious.swirl.SwirlView;
import com.beemdevelopment.aegis.R;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 100;
private final FingerprintManager mFingerprintManager;
private final SwirlView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
private boolean mSelfCancelled;
/**
* Constructor for {@link FingerprintUiHelper}.
*/
public FingerprintUiHelper(FingerprintManager fingerprintManager,
SwirlView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setState(SwirlView.State.ON);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setState(SwirlView.State.OFF);
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mIcon.setState(SwirlView.State.ON);
}
}, 500);
}
private void showError(CharSequence error) {
mIcon.setState(SwirlView.State.ERROR);
mErrorTextView.setText(error);
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setState(SwirlView.State.ON);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}
@@ -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];
}
}
@@ -3,12 +3,13 @@ package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionHelper {
private PermissionHelper() {
@@ -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);
}
}
@@ -1,10 +1,15 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
@@ -22,6 +27,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(VaultEntry entry) {
_selectedEntry = entry;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
@@ -30,7 +39,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
int swipeFlags = 0;
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
{
dragFlags = 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
@@ -56,4 +73,6 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_positionChanged = false;
}
}
}
@@ -1,12 +1,13 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.annotation.ArrayRes;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
import androidx.annotation.ArrayRes;
public class SpinnerHelper {
private SpinnerHelper() {
@@ -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);
}
}
@@ -0,0 +1,23 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
public class ThemeHelper {
private ThemeHelper() {
}
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
TypedValue typedValue = new TypedValue();
currentTheme.resolveAttribute(attributeId, typedValue, true);
@ColorInt int color = typedValue.data;
return color;
}
}
@@ -12,6 +12,11 @@ public class UiRefresher {
_handler = new Handler();
}
public void destroy() {
stop();
_listener = null;
}
public void start() {
if (_running) {
return;
@@ -22,15 +27,14 @@ public class UiRefresher {
_handler.postDelayed(new Runnable() {
@Override
public void run() {
if (_running) {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
}
public void stop() {
_handler.removeCallbacksAndMessages(null);
_running = false;
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
@@ -1,64 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONObject;
import java.util.List;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseException;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisFileImporter extends DatabaseFileImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
Database db = Database.fromJson(obj);
return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -0,0 +1,108 @@
package com.beemdevelopment.aegis.importers;
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 {
public AegisImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (VaultFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private VaultFile _file;
private EncryptedState(VaultFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
JSONObject obj = _file.getContent(creds);
return new DecryptedState(obj);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
}
}
public static class DecryptedState extends State {
private JSONObject _obj;
private DecryptedState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -1,87 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AndOtpFileImporter extends DatabaseFileImporter {
private JSONArray _obj;
public AndOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = "";
String name = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
} catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -0,0 +1,313 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return read(bytes);
} catch (JSONException e) {
// andOTP doesn't have a proper way to indicate whether a file is encrypted
// so, if we can't parse it as JSON, we'll have to assume it is
return new EncryptedState(bytes);
}
}
private static DecryptedState read(byte[] bytes) throws JSONException {
JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
return new DecryptedState(array);
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
try {
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
DecryptedState state = decryptData(key, 0);
listener.onStateDecrypted(state);
} else {
int offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
try {
DecryptedState state = decryptData(key1, offset);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
})
.create());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _obj;
private DecryptedState(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public static class Params {
private char[] _password;
private byte[] _salt;
private int _iterations;
public Params(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
}
@@ -0,0 +1,70 @@
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;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
public AuthenticatorPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
DatabaseImporter.State state = importer.read(zipStream);
listener.onStateDecrypted(state);
return;
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
listener.onError(e);
}
});
}
}
}
@@ -0,0 +1,287 @@
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;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs";
private static final String _pkgName = "com.authy.authy";
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
private static final String _authyFilename = "com.authy.storage.tokens.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
public AuthyImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
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(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(String.format("%s.key", _authFilename))
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
array = new JSONArray(entry.Value);
break;
}
}
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;
private EncryptedState(JSONArray array) {
super(true);
_array = array;
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
DecryptedState state = new DecryptedState(_array);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
listener.onError(e);
}
});
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false);
_array = array;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
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");
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, 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 = " - ";
} 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.Name;
info.Name = "";
}
}
}
private static class AuthyEntryInfo {
String OriginalName;
String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
}
}
@@ -1,50 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class DatabaseAppImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseAppImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseAppImporter>> importers = new LinkedHashMap<>();
importers.put("Google Authenticator", GoogleAuthAppImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected DatabaseAppImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseAppImporter create(Context context, Class<? extends DatabaseAppImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseAppImporter>> getImporters() {
return _importers;
}
}
@@ -1,55 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
public abstract class DatabaseFileImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseFileImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseFileImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisFileImporter.class);
importers.put("andOTP", AndOtpFileImporter.class);
importers.put("FreeOTP", FreeOtpFileImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected ByteInputStream _stream;
protected DatabaseFileImporter(Context context, ByteInputStream stream) {
_context = context;
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class<? extends DatabaseFileImporter> type) {
try {
return type.getConstructor(Context.class, ByteInputStream.class).newInstance(context, stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseFileImporter>> getImporters() {
return _importers;
}
}
@@ -1,14 +1,174 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.db.DatabaseEntry;
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.List;
import java.util.stream.Collectors;
public interface DatabaseImporter {
void parse() throws DatabaseImporterException;
List<DatabaseEntry> convert() throws DatabaseImporterException;
boolean isEncrypted();
Context getContext();
public abstract class DatabaseImporter {
private Context _context;
private static List<Definition> _importers;
static {
// note: keep these lists sorted alphabetically
_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) {
_context = context;
}
protected Context getContext() {
return _context;
}
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 State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
public State read(InputStream stream) throws DatabaseImporterException {
return read(stream, false);
}
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 {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
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 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 {
private boolean _encrypted;
public State(boolean encrypted) {
_encrypted = encrypted;
}
public boolean isEncrypted() {
return _encrypted;
}
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
if (!_encrypted) {
throw new RuntimeException("Attempted to decrypt a plain text database");
}
throw new UnsupportedOperationException();
}
public Result convert() throws DatabaseImporterException {
if (_encrypted) {
throw new RuntimeException("Attempted to convert database before decrypting it");
}
throw new UnsupportedOperationException();
}
}
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
}
}
@@ -0,0 +1,19 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(String message, String text) {
super(message);
_text = text;
}
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
}
public String getText() {
return _text;
}
}
@@ -1,161 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class FreeOtpFileImporter extends DatabaseFileImporter {
private List<XmlEntry> _xmlEntries;
public FreeOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
private static class XmlEntry {
String Name;
String Value;
}
@Override
public void parse() throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.nextTag();
_xmlEntries = parse(parser);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (XmlEntry xmlEntry : _xmlEntries) {
if (xmlEntry.Name.equals("tokenOrder")) {
// TODO: order
JSONArray array = new JSONArray(xmlEntry.Value);
} else {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
}
} catch (OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
entries.add(parseEntry(parser));
}
return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
bytes[i] = (byte)array.getInt(i);
}
return bytes;
}
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
XmlEntry entry = new XmlEntry();
entry.Name = name;
entry.Value = value;
return entry;
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
@@ -0,0 +1,191 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
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;
import org.json.JSONObject;
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;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public FreeOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
List<JSONObject> entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (!entry.Name.equals("tokenOrder")) {
entries.add(new JSONObject(entry.Value));
}
}
return new State(entries);
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private List<JSONObject> _entries;
public State(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
OtpInfo info;
switch (type) {
case "totp":
int period = obj.getInt("period");
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
info = new TotpInfo(secret, algo, digits, period);
}
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
bytes[i] = (byte)array.getInt(i);
}
return bytes;
}
private static JSONObject parseXmlEntry(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
@@ -0,0 +1,57 @@
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;
public class FreeOtpPlusImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.liberty.android.freeotpplus";
public FreeOtpPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
State state;
if (isInternal) {
state = new FreeOtpImporter(getContext()).read(stream);
} else {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.State(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
return state;
}
}
@@ -1,124 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthAppImporter extends DatabaseAppImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@SuppressLint("SdCardPath")
private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases";
private List<DatabaseEntry> _entries = new ArrayList<>();
public GoogleAuthAppImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (SuFileInputStream in = new SuFileInputStream(new SuFile(_filename))) {
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(in, out);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
if (!cursor.moveToFirst()) {
return;
}
do {
int type = getInt(cursor, "type");
byte[] secret = Base32.decode(getString(cursor, "secret").toCharArray());
OtpInfo info;
switch (type) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, getInt(cursor, "counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name = getString(cursor, "email", "");
String issuer = getString(cursor, "issuer", "");
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
_entries.add(entry);
} while(cursor.moveToNext());
}
} catch (SQLiteException | OtpInfoException | Base32Exception e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@Override
public List<DatabaseEntry> convert() {
return _entries;
}
@Override
public boolean isEncrypted() {
return false;
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
}
@@ -0,0 +1,139 @@
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.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 {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new VaultEntry(info, name, entry.getIssuer());
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _email;
private String _issuer;
private long _counter;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_secret = SqlImporterHelper.getString(cursor, "secret");
_email = SqlImporterHelper.getString(cursor, "email", "");
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
_counter = SqlImporterHelper.getLong(cursor, "counter");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getEmail() {
return _email;
}
public String getIssuer() {
return _issuer;
}
public long getCounter() {
return _counter;
}
}
}
@@ -0,0 +1,78 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class GoogleAuthUriImporter extends DatabaseImporter {
public GoogleAuthUriImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (!line.isEmpty()) {
lines.add(line);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new GoogleAuthUriImporter.State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public DatabaseImporter.Result convert() {
DatabaseImporter.Result result = new DatabaseImporter.Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
return new VaultEntry(info);
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -0,0 +1,132 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
private static final String _subPath = "databases/PhoneFactor";
private static final String _pkgName = "com.azure.authenticator";
private static final int TYPE_TOTP = 0;
private static final int TYPE_MICROSOFT = 1;
public MicrosoftAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
int type = sqlEntry.getType();
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret;
int digits = 6;
switch (entry.getType()) {
case TYPE_TOTP:
secret = Base32.decode(entry.getSecret());
break;
case TYPE_MICROSOFT:
digits = 8;
secret = Base64.decode(entry.getSecret());
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _issuer;
private String _userName;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "account_type");
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
_issuer = SqlImporterHelper.getString(cursor, "name");
_userName = SqlImporterHelper.getString(cursor, "username");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getIssuer() {
return _issuer;
}
public String getUserName() {
return _userName;
}
}
}
@@ -0,0 +1,145 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.common.io.Files;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class SqlImporterHelper {
private Context _context;
public SqlImporterHelper(Context context) {
_context = context;
}
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
File dir = Files.createTempDir();
File mainFile = new File(dir, path.getName());
List<File> fileCopies = new ArrayList<>();
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
// create temporary copies of the database files so that SQLiteDatabase can open them
File fileCopy = null;
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
}
fileCopies.add(fileCopy);
} catch (IOException e) {
if (fileCopy != null) {
fileCopy.delete();
}
for (File fileCopy2 : fileCopies) {
fileCopy2.delete();
}
throw new DatabaseImporterException(e);
}
}
try {
return read(type, mainFile, table);
} finally {
for (File fileCopy : fileCopies) {
fileCopy.delete();
}
}
}
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
}
return files;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file = null;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("db-import-", "", _context.getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
IOUtils.copy(inStream, out);
}
} catch (IOException e) {
if (file != null) {
file.delete();
}
throw new DatabaseImporterException(e);
}
try {
return read(type, file, table);
} finally {
// always delete the temporary file
file.delete();
}
}
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
entries.add(entry);
} while (cursor.moveToNext());
}
return entries;
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
}
}
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
public static abstract class Entry {
public Entry(Cursor cursor) {
}
}
}
@@ -0,0 +1,87 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
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;
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 {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
public SteamImporter(Context context) {
super(context);
}
@Override
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(_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 files[0];
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONObject _obj;
private State(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() {
Result result = new Result();
try {
VaultEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -0,0 +1,234 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TotpAuthenticatorImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
private static final String _pkgName = "com.authenticator.authservice2";
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
public TotpAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
String data = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(PREF_KEY)) {
data = entry.Value;
}
}
if (data == null) {
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
}
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = IOUtils.readAll(stream);
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
} catch (IOException | XmlPullParserException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List<JSONObject> parse(String data) throws JSONException {
JSONArray array = new JSONArray(data);
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
String s = array.getString(i);
entries.add(new JSONObject(s));
}
return entries;
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = cipher.doFinal(_data);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
JSONArray keys = obj.names();
List<JSONObject> entries = new ArrayList<>();
if (keys != null && keys.length() > 0) {
entries = parse((String) keys.get(0));
}
return new DecryptedState(entries);
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException
| JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
});
})
.setNegativeButton(R.string.no, (dialog, which) -> {
decrypt(PASSWORD, listener);
})
.create());
}
private void decrypt(char[] password, DecryptListener listener) {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}
}
public static class DecryptedState extends DatabaseImporter.State {
private List<JSONObject> _objs;
private DecryptedState(List<JSONObject> objs) {
super(false);
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int base = obj.getInt("base");
String secretString = obj.getString("key");
byte[] secret;
switch (base) {
case 16:
secret = Hex.decode(secretString);
break;
case 32:
secret = Base32.decode(secretString);
break;
case 64:
secret = Base64.decode(secretString);
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
return new VaultEntry(info, name, issuer);
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -0,0 +1,47 @@
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) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private DatabaseImporter.State _state;
private State(DatabaseImporter.State state) {
super(false);
_state = state;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = _state.convert();
for (VaultEntry entry : result.getEntries()) {
entry.setIssuer(entry.getName());
entry.setName("WinAuth");
}
return result;
}
}
}
@@ -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 GlideLicense extends License {
@Override
public String getName() {
return "Glide License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://github.com/bumptech/glide/blob/master/LICENSE";
}
}
@@ -0,0 +1,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";
}
}

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