Compare commits

..
403 Commits
Author SHA1 Message Date
Alexander Bakker f9f37d30b2 Release v3.0
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-03-24 16:47:26 +01:00
Alexander Bakker 4c4acf0cd0 Update translations from Crowdin 2024-03-24 16:42:26 +01:00
Alexander Bakker 3a66851df5 Only fetch 2 specific dependencies from JCenter 2024-03-24 14:26:59 +01:00
Michael SchättgenandGitHub 09c789b250 Merge pull request #1321 from alexbakker/about-libraries
Switch to AboutLibraries for the third-party license list
2024-03-23 16:03:25 +01:00
Alexander Bakker 60c72d48ee Switch to AboutLibraries for the third-party license list
The previous library we were using is unmaintained and can't be
customized to match the Material 3 theme.
2024-03-23 13:41:02 +01:00
Alexander BakkerandGitHub 8001ecb482 Merge pull request #1320 from michaelschattgen/fix/padding-unlock-button
Fix padding unlock button
2024-03-20 21:34:59 +01:00
Michael Schättgen 8912d75870 Fix padding unlock button 2024-03-20 21:33:15 +01:00
Michael SchättgenandGitHub e70499bcff Merge pull request #1319 from alexbakker/amoled-code-color
Make the code color white for AMOLED
2024-03-20 21:32:09 +01:00
Alexander BakkerandMichael Schättgen 4c28bf2a12 Make the code color white for AMOLED
Co-authored-by: Michael Schättgen <michael@schattgen.me>
2024-03-20 21:21:25 +01:00
Michael SchättgenandGitHub 584b0acb2b Merge pull request #1318 from alexbakker/no-compact-divider
Fix various minor inconsistencies in entry list item offsets
2024-03-19 22:50:13 +01:00
Alexander Bakker 45ced0de60 Fix various minor inconsistencies in entry list item offsets
This patch addresses the following:
- More consistent offsets between entries in the list, especially in
  relation to the action bar and the error card.
- Consistent correct application of card shapes when switching between
  favoriting and unfavoriting entries.
- Removal of CompactDividerDecoration. We no longer uses dividers, so
  this is no longer needed.
2024-03-19 22:40:47 +01:00
Michael Schättgen 7ec786231e Update featured screenshots with material 3 refresh 2024-03-17 23:23:44 +01:00
Alexander BakkerandGitHub 9737c85f86 Merge pull request #1312 from michaelschattgen/fix/favorites-ui
Fix shape of favorited entries
2024-03-16 17:40:29 +01:00
Michael Schättgen bf7c60d620 Fix shape of favorited entries 2024-03-16 17:38:56 +01:00
Alexander BakkerandGitHub 6fc9cd5a71 Merge pull request #1310 from michaelschattgen/feature/last-used
Add ability to sort based on last used timestamp
2024-03-16 16:15:26 +01:00
Michael Schättgen 9bae4d6bbc Add ability to sort based on last used timestamp 2024-03-16 16:14:47 +01:00
Alexander Bakker 7ce43a0afd Update build pipeline to resolve deprecation warnings 2024-03-16 13:08:21 +01:00
Alexander BakkerandGitHub 2b2cac1ada Merge pull request #1308 from michaelschattgen/fix/ui-inconsistencies
Fix a couple UI inconsistencies
2024-03-16 11:28:02 +01:00
Alexander BakkerandGitHub 2f18907ce1 Merge pull request #1307 from michaelschattgen/fix/copied-text
Fix copied text visibility
2024-03-16 11:24:22 +01:00
Michael SchättgenandGitHub ec01a4a96d Merge pull request #1240 from GitGitro/master
Add delta-aegis-icons to the Readme
2024-03-15 22:46:55 +01:00
GitGitro 76f6ebd216 Add delta-aegis-icons to the Readme
fix: remove preview
2024-03-15 22:35:11 +01:00
Michael Schättgen f1e14e6645 Fix a couple UI inconsistencies 2024-03-15 22:12:55 +01:00
Michael Schättgen 52ecf12576 Fix copied text visibility 2024-03-15 21:34:59 +01:00
Michael SchättgenandGitHub dfd720b406 Merge pull request #1303 from alexbakker/amoled-dynamic
Apply dark background colors when combining AMOLED and dynamic colors
2024-03-14 20:38:31 +01:00
Alexander Bakker 8995626d16 Apply dark background colors when combining AMOLED and dynamic colors
Previously, the dark background colors would not be applied for this
combination of settings.

Unfortunately, I couldn't find a way to avoid some duplication in
themes.xml.
2024-03-14 20:27:43 +01:00
Michael SchättgenandGitHub cffe15735c Merge pull request #1302 from alexbakker/fix-cam-btn-color
Set the correct color for the camera switch button
2024-03-14 20:16:18 +01:00
Michael SchättgenandGitHub 8512986a6b Merge pull request #1301 from alexbakker/fix-1300
Use MaterialColors.getColor instead of our own helper
2024-03-14 20:01:53 +01:00
Alexander Bakker c5b8ee9215 Set the correct color for the camera switch button 2024-03-13 20:50:07 +01:00
Alexander Bakker 2e44a81c69 Use MaterialColors.getColor instead of our own helper
I set CompactDividerDecoration to transparant, because that was already
effectively the case. I think we can remove this class entirely, but
I'll do that in a separate PR.
2024-03-13 20:33:09 +01:00
Alexander Bakker f76d84ef87 Release v3.0-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-03-13 17:21:14 +01:00
Alexander Bakker 3d59114230 Update translations from Crowdin 2024-03-13 16:56:54 +01:00
Michael SchättgenandGitHub 22c9ab7c03 Merge pull request #1295 from alexbakker/intro-init-crash
Don't initialize VaultManager after the intro unless saving succeeds
2024-03-13 16:51:39 +01:00
Michael SchättgenandGitHub f8ad3d16fc Merge pull request #1297 from alexbakker/entry-move-anim
Restore entry list item animations
2024-03-13 16:40:41 +01:00
Michael SchättgenandGitHub fbd3bf3ff5 Merge pull request #1296 from alexbakker/shown-entries-bold
Only bold number of shown entries if found in the translated string
2024-03-13 16:39:30 +01:00
Alexander Bakker 8bbbe3611a Don't initialize VaultManager after the intro unless saving succeeds
In rare cases where writing to disk fails after the intro, a crash could
occur if the user presses "Done" again. VaultManager would have been
initialized, and trying to initialize it again would result in a crash.
2024-03-13 16:36:09 +01:00
Michael SchättgenandGitHub 2d0e201060 Merge pull request #1294 from alexbakker/load-vaultfile
Load vault file on demand instead of juggling it around in-memory
2024-03-13 16:29:01 +01:00
Michael SchättgenandGitHub b59350337f Merge pull request #1293 from alexbakker/disable-unlock-button
Disable the unlock button until the slot decryption task is done
2024-03-13 16:27:44 +01:00
Michael SchättgenandGitHub 6d73e5101c Merge pull request #1278 from alexbakker/fix-1077
Pass down the root shell to every SuFile for the Authy importer
2024-03-13 16:24:22 +01:00
Michael SchättgenandGitHub d16d56c4b0 Merge pull request #1263 from alexbakker/icon-suggestion-prio
Prioritize normal icon issuer matches over inverse matches
2024-03-13 16:23:55 +01:00
Alexander BakkerandGitHub 59bae27556 Merge pull request #1227 from alexbakker/material3
Material 3
2024-03-13 16:07:00 +01:00
Alexander BakkerandMichael Schättgen fcde086ae3 Material 3
Co-authored-by: Michael Schättgen <michael@schattgen.me>
2024-03-13 16:03:56 +01:00
Alexander Bakker 0e2fa929e6 Restore entry list item animations
This fixes an issue where the entry list items no longer animated upon
move, insert, delete, etc.

RecyclerView's DefaultItemAnimator automatically scales the animations
according to the user's settings.

Introduced in 9ff8efab69
2024-03-10 22:18:15 +01:00
Alexander Bakker 8951c19581 Only bold number of shown entries if found in the translated string
This should fix the following crash:

```
Exception java.lang.IndexOutOfBoundsException: setSpan (-1 ... 0) starts before 0
  at android.text.SpannableStringInternal.checkRange (SpannableStringInternal.java:499)
  at android.text.SpannableStringInternal.setSpan (SpannableStringInternal.java:199)
  at android.text.SpannableStringInternal.setSpan (SpannableStringInternal.java:186)
  at android.text.SpannableString.setSpan (SpannableString.java:60)
  at com.beemdevelopment.aegis.ui.views.EntryAdapter$FooterView.refresh (EntryAdapter.java:596)
```
2024-03-10 20:43:59 +01:00
Alexander Bakker 32e462bdce Load vault file on demand instead of juggling it around in-memory
This trades performance for making VaultManager a bit easier to reason
about.

This also fixes a rare crash that could occur if the user retries to unlock
the app after the previous attempt resulted in an error related to
parsing the vault. The vault file would no longer be present in memory
after the first attempt, causing the second attempt to crash the app.
2024-03-10 19:43:40 +01:00
Alexander Bakker 6bd8521661 Disable the unlock button until the slot decryption task is done
This prevents a crash that could occur when double tapping the Unlock
button.
2024-03-10 18:29:49 +01:00
Alexander Bakker f7bac4331e Run the instrumented tests on Ubuntu since KVM is now available
See: https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
2024-03-02 14:43:15 +01:00
Alexander BakkerandGitHub 243a52ebed Merge pull request #1286 from Granddave/feature/update-vault-docs
Update vault documentation
2024-03-01 13:27:07 +01:00
David IsakssonandAlexander Bakker f91b6f0466 Update vault documentation
Here some changes to the vault documentation are made. The documentation
is updated to reflect the latest versions of both the vault and the
database, i.e. vault version 1 and database version 3.

Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2024-03-01 13:22:50 +01:00
Alexander Bakker 57ec695718 Pass down the root shell to every SuFile for the Authy importer
The issue was introduced in: 69f0bb4fbc
2024-02-18 20:03:37 +01:00
Michael SchättgenandGitHub 224ec2553c Merge pull request #1262 from alexbakker/glide-caching
Use the hash of entry icons as keys for Glide caching
2024-02-01 22:47:39 +01:00
Michael SchättgenandGitHub 5acacf63e1 Merge pull request #1249 from alexbakker/2fas-schema4
Add support for importing 2FAS schema v4 backups
2024-02-01 22:06:23 +01:00
Alexander Bakker bfbb3ef2c4 Prioritize normal icon issuer matches over inverse matches
Icon packs may have very generic issuers for their icons (like [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)).
For example, this causes the icon assigning view to suggest the "C" icon for every
entry that contains a "c".

This patch addresses that by giving inverse matches (where the entry
issuer contains the icon issuer) a lower position in the suggested icons
list.
2024-01-20 14:25:17 +01:00
Alexander Bakker f1c9c6c5fc Use the hash of entry icons as keys for Glide caching
This is mostly a cleanup of the way we do Glide in-memory caching. It
also fixes a few minor issues along the way:

- Entry icon cache keys were based on entry UUID's. This could cause
  problems when changing an entry's icon.
- A TextDrawable could get replaced by the icon of a different entry
  when scrolling through the entry list quickly.
2024-01-18 23:55:16 +01:00
Michael SchättgenandGitHub 566bcac3e0 Merge pull request #1236 from alexbakker/steam-xposed
Add support for importing decrypted Steam JSON blob
2024-01-09 23:03:05 +01:00
Alexander BakkerandGitHub 4d729d1bef Merge pull request #1204 from ranjeetchouhan/master
feat: Update references to FreeOTP and add version hint "1.x"
2023-12-27 18:03:57 +01:00
RanjeetandAlexander Bakker 1acb9db489 feat: Update references to FreeOTP and add version hint "1.x"
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-12-27 18:00:21 +01:00
Alexander Bakker 98bcdc7615 Update Gradle and dependencies 2023-12-27 17:51:56 +01:00
Alexander Bakker 7c1a954e4d Stop using deprecated startActivityAndCollapse(Intent) 2023-12-27 17:51:53 +01:00
Alexander BakkerandGitHub a1d00b47fe Merge pull request #1238 from cyb3rko/startactivityforesult-deprecation
Replace deprecated startActivityForResult
2023-12-22 22:39:21 +01:00
Niko Diamadis ca530f229b Replace startActivityForResult with result launchers 2023-12-21 22:57:39 +01:00
Alexander Bakker b86bb286e8 Add support for importing 2FAS schema v4 backups 2023-12-18 22:55:30 +01:00
Alexander Bakker 52abb08201 Update dependencies 2023-12-17 17:42:16 +01:00
Alexander Bakker ff233090f8 Add support for importing decrypted Steam JSON blob
Some people have managed to snatch the OTP details from Steam using
Xposed while it is being decrypted by the app. Aegis still won't be
able to do the decryption part, but we can add support for importing
the decrypted JSON blob, which only differs slightly from the old
format.
2023-11-30 21:01:27 +01:00
Alexander BakkerandGitHub adaae9e6d6 Merge pull request #1234 from michaelschattgen/feature/issuer-sort-account-fallback
Improve issuer and account sorting
2023-11-29 23:24:19 +01:00
Michael SchättgenandGitHub 3dd70de5df Merge pull request #1233 from alexbakker/explain-uri-perms
Explain vault backup permission error
2023-11-29 21:26:18 +01:00
Michael Schättgen da2244f511 Improve issuer and account sorting 2023-11-29 20:57:48 +01:00
Alexander Bakker 08c73922cc Explain vault backup permission error
Users understandably get confused by the "No persisted URI permissions"
error. This patch adds some text to the dialog explaining why this
happened and how the user can fix the issue.

This permission issue can happen for one of two reasons:
- The user made a change to the backup destination (renamed, moved,
  deleted, etc)
- Aegis was restored from an Android backup
2023-11-29 20:09:37 +01:00
Michael SchättgenandGitHub 88caafd61c Merge pull request #1232 from jsoberg/jsoberg/1231/fixing-configuration-change-licensedialog-crash
#1231 - Fix crash in License and Changelog dialogs on configuration change
2023-11-29 14:44:23 +01:00
Joshua Soberg 45220241aa #1231 - Use public constructors for License/Changelog dialog fragments so that they can be recreated on configuration change 2023-11-28 19:04:18 -05:00
Alexander Bakker 60e93559c3 Bump target SDK version and update dependencies 2023-11-07 20:29:45 +01:00
Alexander BakkerandGitHub e1f4696115 Merge pull request #1200 from michaelschattgen/feature/select-all
Add ability to select all tokens
2023-09-24 18:08:30 +02:00
Michael Schättgen 1c86c5fd51 Add ability to select all tokens 2023-09-24 17:12:37 +02:00
Michael SchättgenandGitHub 92e9e047a7 Merge pull request #1192 from alexbakker/agp-migration
Transition to non-final resource IDs and non-transitive R classes
2023-09-20 22:32:48 +02:00
Alexander Bakker c13d4e7f8d Transition to non-final resource IDs and non-transitive R classes
Future versions of AGP will force us to do this, so we might as well get
it over with now.
2023-09-19 23:34:08 +02:00
Michael SchättgenandGitHub d09e81232a Merge pull request #1190 from alexbakker/fix-assign-icons-menu
Introduce a separate menu for AssignIconsActivity
2023-09-19 21:01:24 +02:00
Alexander Bakker 03f1a0e8ab Introduce a separate menu for AssignIconsActivity
Apparently this was using ``menu_groups``, probably a copy-paste error.

This also moves ``AssignIconsActivity`` to the right package.
2023-09-18 22:31:38 +02:00
Alexander BakkerandGitHub 305e157fc5 Merge pull request #1078 from orange-elephant/entries-in-multiple-groups
Refer to groups by UUID
2023-09-11 22:34:14 +02:00
elenaandAlexander Bakker 5c86e5c099 Refer to groups by UUID
- Also lays the foundations for adding entries to multiple groups and changing group names

Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-09-11 22:28:53 +02:00
Michael SchättgenandGitHub 0760bfc618 Merge pull request #1188 from alexbakker/fix-anim-issues
Fix two issues related to animation duration scale
2023-09-11 21:34:33 +02:00
Alexander BakkerandGitHub 9414b5c420 Merge pull request #1172 from michaelschattgen/feature/assign-icons
Add ability to automatically assign icons to (imported) entries
2023-09-11 21:07:23 +02:00
Alexander Bakker e7a1058618 Fix two issues related to animation duration scale
This patch addresses two issues:
- The entry selection icon would flicker when a non-1x animator
  duration scale was set.
- The advanced entry field animation was not shown if the animator
  duration scale was set to .5x, due to a rounding error.

Introduced in: 9ff8efab69
2023-09-11 21:05:20 +02:00
Michael Schättgen 1a6f85ccb6 Add ability to assign icons
More progress

Open IconPicker dialog on click

Add ability to reset

Fix changing icons

Cleanup

Add ability to assign icons after import

PR fixes
2023-09-10 12:14:57 +02:00
Michael SchättgenandGitHub b84ecf15da Merge pull request #1184 from alexbakker/no-nested-recyclerview
Never wrap RecyclerView with a NestedScrollView
2023-09-10 00:32:41 +02:00
Alexander Bakker 31b8162ab4 Use 'comment' instead of 'context' to add context to strings 2023-09-09 22:09:03 +02:00
Alexander Bakker 7def7eb4f7 Remove unused strings and add context to a couple of strings
Most of these were related to slots. Also removed the card_slot layout.
2023-09-09 21:59:31 +02:00
Alexander Bakker 8ca45d2322 Fix singular form of the import_error_dialog string 2023-09-09 21:37:34 +02:00
Michael SchättgenandGitHub 1c1dee560c Merge pull request #1182 from alexbakker/clarify-intro-import
Clarify that only Aegis vaults can be imported during the intro
2023-09-09 21:05:53 +02:00
Alexander Bakker ca4a3e2f74 Never wrap RecyclerView with a NestedScrollView
Wrapping a ``RecyclerView`` with a ``NestedScrollView`` breaks its recycling
functionality because the view height is stretched to fit the full list
of entries.

We never noticed performance issues in these two cases because these
lists never get very long. Let's fix these cases anyway so that we
don't accidentally base a new use of a ``RecyclerView`` on this broken
pattern.

Also renamed ``list_slots`` to ``list_groups``. Must have been
a copy-paste error.
2023-09-09 18:37:07 +02:00
Alexander Bakker 37964da4a5 Clarify that only Aegis vaults can be imported during the intro
Some users understandably get confused when they try to import a backup
file from a different 2FA app during the intro and then get greeted
with an error dialog.

This changes the button text to "Import Aegis vault" and adds a small
hint text in the hope that this makes the limitations of the intro more
clear to the user.

<img width="200" src="https://alexbakker.me/u/jzhh3bk30w.png" />
2023-09-09 12:51:04 +02:00
Alexander Bakker dd9c307dea Release v2.2.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-09 12:15:32 +02:00
Alexander Bakker c65ecd9c54 Update translations from Crowdin 2023-09-09 12:08:06 +02:00
Michael SchättgenandGitHub 72511fc02b Merge pull request #1180 from alexbakker/fix-tile-crash
Check for null returned by getQsTile()
2023-09-08 00:26:47 +02:00
Michael SchättgenandGitHub 79ade74c0c Merge pull request #1179 from alexbakker/icon-name
Introduce optional 'name' field for iconpack icons
2023-09-08 00:26:15 +02:00
Michael SchättgenandGitHub 8164e91dd0 Merge pull request #1178 from alexbakker/fix-auth-pro
Add support for new Authenticator Pro backup format
2023-09-07 23:59:28 +02:00
Alexander Bakker 1ccbe88ce6 Check for null returned by getQsTile()
Apparently ``getQsTile()`` can return null, which resulted in a crash.
Reported through the Google Play Console:

```
Exception java.lang.NullPointerException: Attempt to invoke virtual method 'void android.service.quicksettings.Tile.setState(int)' on a null object reference
  at com.beemdevelopment.aegis.services.LaunchAppTileService.onStartListening
  at android.service.quicksettings.TileService$H.handleMessage (TileService.java:488)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loopOnce (Looper.java:205)
  at android.os.Looper.loop (Looper.java:294)
  at android.app.ActivityThread.main (ActivityThread.java:8177)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:552)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:971)
```
2023-09-07 23:51:01 +02:00
Alexander Bakker 9b3e7136bd Introduce optional 'name' field for iconpack icons
This introduces a new (optional) 'name' field for iconpack icons. It
will be used to describe the icon in the icon selection dialog. If it is
not present, the name of the icon will be derived from the filename,
like before. Using this new field allows usage of more exotic characters
in the icon name that are not allowed in a filename.
2023-09-07 22:49:52 +02:00
Alexander Bakker 9cabd9f309 Add support for new Authenticator Pro backup format
This adds support for Authenticator Pro's latest backup format changes.
The format of the content itself has not changed as far as I can tell, but
they do use a different cipher and KDF now: AES GCM and Argon2id,
respectively.

The memory cost is statically set at 64MiB. I suspect that this may
cause OOM situations on some lower-end devices, but we'll see, not much
we can do about that right now without making more changes.
2023-09-07 22:30:22 +02:00
Michael Schättgen 27e56d60b5 Release v2.2.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-06 23:49:51 +02:00
Michael SchättgenandGitHub aac77442bf Merge pull request #1175 from michaelschattgen/hotfix/biometrics-unlock
Fix biometrics unlock button on AuthActivity
2023-09-06 23:43:50 +02:00
Michael Schättgen c9cf6729e0 Fix biometrics unlock button on AuthActivity 2023-09-06 23:39:18 +02:00
Alexander BakkerandGitHub b916697391 Merge pull request #1171 from michaelschattgen/feature/import-duplicates
Add ability to skip duplicates during import
2023-09-06 12:49:23 +02:00
Michael Schättgen b205438982 Add ability to skip duplicates during import 2023-09-06 12:40:01 +02:00
Michael Schättgen 4f83706f73 Release v2.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-05 23:12:55 +02:00
Michael Schättgen 44bb11e2d8 Update translations from Crowdin 2023-09-05 22:54:29 +02:00
Michael SchättgenandGitHub 92b31c3eda Reference to new icon pack in readme 2023-09-03 18:58:15 +02:00
Alexander Bakker 2793b65786 Fix versions of two build dependencies 2023-08-31 23:46:03 +02:00
Michael Schättgen 5e35cc203f Fix bug in Japanese translation 2023-08-30 22:58:32 +02:00
Michael Schättgen 67d7f7a75d Release v2.2-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-08-30 21:37:56 +02:00
Michael Schättgen 18e6fd2561 Update translations from Crowdin 2023-08-30 21:09:24 +02:00
Alexander BakkerandGitHub c19692b416 Merge pull request #1165 from michaelschattgen/feature/disable-animations
Respect system animation setting
2023-08-30 15:17:26 +02:00
Michael SchättgenandAlexander Bakker 9ff8efab69 Respect system animation setting
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-08-30 15:07:09 +02:00
Alexander Bakker f38b6ec586 Fix the build by updating to JDK 17 2023-08-30 14:45:21 +02:00
Alexander Bakker 3278d6544b Update dependencies 2023-08-30 14:26:12 +02:00
Alexander Bakker 366c039aff Add example Crowdin CLI config file 2023-08-30 14:00:54 +02:00
Michael Schättgen 8917bb3b94 Fix the spacing between issuer and account name 2023-08-23 18:45:39 +02:00
Alexander BakkerandGitHub 3e352156c4 Merge pull request #1096 from beemdevelopment/feature/view-mode-tiles
Add tiles view mode
2023-08-23 17:31:41 +02:00
Michael SchättgenandAlexander Bakkker d90303cf0e Add tiles view mode
Minor UI improvements
Fix animations
Fix typo
Improvements made after PR review
PR improvements

Co-authored-by: Alexander Bakkker <ab@alexbakker.me>
2023-08-23 17:29:18 +02:00
Alexander BakkerandGitHub 94a38e82e4 Merge pull request #1167 from michaelschattgen/feature/hide-account-name
Add ability to only show names when necessary
2023-08-22 23:26:20 +02:00
Michael Schättgen edb1d8d76f Add ability to only show names when necessary 2023-08-21 00:24:10 +02:00
Alexander BakkerandGitHub 6009c09607 Merge pull request #1161 from beemdevelopment/feature/copy-behavior
Add ability to change copy behavior
2023-08-20 16:51:30 +02:00
Michael Schättgen 3ff242e6b6 Add ability to change copy behavior 2023-08-20 15:51:32 +02:00
Michael Schättgen 51897a4ab7 Update featured screenshots in README.md
this was well needed
2023-08-20 13:05:26 +02:00
Alexander BakkerandGitHub 84abffb2ad Merge pull request #1163 from michaelschattgen/feature/password-reminder-dialog
Add dialog when password reminder is active
2023-08-20 10:43:36 +02:00
Michael Schättgen ef069e49af Add dialog when password reminder is activated 2023-08-18 10:46:08 +02:00
Michael SchättgenandGitHub d3c48848be Merge pull request #1162 from michaelschattgen/feature/account-name-position
Add ability to change account name position
2023-08-17 02:20:17 +02:00
Michael Schättgen 813fd62dc7 Add ability to change account name position 2023-08-16 23:40:19 +02:00
Michael SchättgenandGitHub 8c81ec0739 Merge pull request #1140 from nerodevo/master
Remove unwanted character
2023-08-16 01:47:58 +02:00
Michael SchättgenandGitHub b08a2f5f93 Fix minor typo in FAQ
Closes #1158
2023-08-09 00:31:46 +02:00
nerodevo 3026a3eab1 Remove unwanted character 2023-06-04 06:38:04 -05:00
Alexander Bakker efd8e2d9ff Run key derivation for Authenticator Pro importer on background thread 2023-03-03 23:04:33 +01:00
Alexander Bakker 58b8edf318 Various minor touchups for the Authenticator Pro importer 2023-03-03 21:44:21 +01:00
Alexander BakkerandGitHub abe7af1549 Merge pull request #1080 from NepNep21/authpro
Add Authenticator Pro encrypted import support, fixes #1035
2023-03-03 20:53:17 +01:00
NepNep21 289d5409a5 Add Authenticator Pro encrypted import support 2023-03-02 17:46:45 -03:00
Alexander Bakker e906131f1b Disable Glide notification permission lint check
Fixes the build
2023-03-01 14:38:29 +01:00
Alexander Bakker b6bfc5b15f Update Gradle and dependencies 2023-03-01 13:49:01 +01:00
Alexander BakkerandGitHub 90f2ea79d9 Merge pull request #1100 from beemdevelopment/bugfix/empty-state
Fix empty state while using search filter
2023-03-01 12:11:23 +01:00
Michael Schättgen 3c9e5a9fdb Fix empty state while using search filter 2023-02-27 23:49:35 +01:00
Alexander Bakker f0f9653ace Release v2.1.3
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-02-27 23:19:04 +01:00
Alexander Bakker e2df97514c Update translations from Crowdin 2023-02-27 23:11:07 +01:00
Alexander BakkerandGitHub ed5ac4730f Merge pull request #1093 from beemdevelopment/feature/backup-reminder-disable
Add ability to disable backup reminder
2023-02-27 22:39:01 +01:00
Michael Schättgen 61d41a26fa Add ability to disable backup reminder 2023-02-05 18:31:31 +01:00
Alexander Bakker fd42c5c893 Update build pipeline to resolve deprecation warnings 2022-12-27 22:19:56 +01:00
Alexander BakkerandGitHub 0eccc875b0 Merge pull request #1067 from orange-elephant/export-dialog-ux
Only enable export buttons if options are in a valid state
2022-12-24 14:47:16 +01:00
elena d3e5472ef2 Only enable export buttons if options are in a valid state 2022-12-20 17:27:46 +00:00
Alexander Bakker bf825df221 Fix crash caused by incorrect Toast creation 2022-12-18 18:39:46 +01:00
Alexander Bakker 6de007e3b1 Fix the build status badge 2022-12-18 18:32:34 +01:00
Alexander BakkerandGitHub 121c1dada9 Merge pull request #1054 from orange-elephant/checkboxes-dropdown
Create checkboxes dropdown component
2022-12-18 18:20:56 +01:00
elena 1c9931b1c8 Display export groups selection as dropdown 2022-12-13 15:22:00 +00:00
elena 51698947aa Create checkboxes dropdown component 2022-12-13 15:20:00 +00:00
Alexander Bakker ceaf52e238 Update dependencies 2022-12-09 22:37:45 +01:00
Alexander Bakker cad2ecfe1b Release v2.1.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2022-12-06 18:58:44 +01:00
Alexander Bakker 36b1f91140 Update translations from Crowdin 2022-12-06 18:56:27 +01:00
Alexander Bakker 4bd12f5abe Don't try to move entries that are filtered out
This fixes a crash that could occur when changing an entry in such a
way that it is filtered out from the entry list after the change.
2022-12-06 18:41:29 +01:00
Alexander Bakker 780a215a6b Release v2.1.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2022-12-05 22:44:06 +01:00
Alexander Bakker f6f05dee6e Update translations from Crowdin 2022-12-05 22:17:40 +01:00
Alexander Bakker 7a1e4e1d77 Move HTML export logic to a separate file and finish it up 2022-12-05 22:10:35 +01:00
Alexander BakkerandGitHub 2c36149a3d Merge pull request #1032 from JordanPlayz158/master
Battle.net Authenticator Import Support
2022-12-05 22:00:05 +01:00
JordanPlayz158 f3731c23a3 Battle.net Importing Support
Fixed issue caused by supplying the VaultEntry with the Base32 encoded string rather than the raw secret

Added blizzard package to manifest so Aegis is allowed to query whether the app is installed

Fixed VaultEntry to be more inline with other entries

Removed the unnecessary encoding of the secret as it is used as is without encoding and changed the way the TotpInfo object is supplied with the relevant information.

Credits to alexbakker (https://github.com/beemdevelopment/Aegis/pull/1032#pullrequestreview-1203477313)
2022-12-05 14:55:27 -05:00
Alexander BakkerandGitHub bebda569de Merge pull request #1039 from CristianAUnisa/export-to-html
Export vault to HTML
2022-12-04 23:01:25 +01:00
Alexander Bakker 0112431269 Abort andOTP import early if number of iterations is suspicious 2022-12-04 20:34:48 +01:00
CristianAUnisa 59c887e6a4 Export vault to HTML 2022-12-04 19:30:32 +01:00
Alexander Bakker ee6a020f4d Disallow empty strings to pass through text input dialogs
This also fixes a crash for certain importers than could occur if the
user entered an empty password.
2022-12-04 19:20:43 +01:00
Alexander Bakker ee15a61403 Prevent the use of MD5 for anything other than mOTP
This forcefully resets any HOTP/TOTP entries that were using MD5 back to
SHA1, because users could only configure this by mistake. No website should be
using it, as the HOTP algorithm was not made to be compatible with the
hash length of MD5.
2022-12-04 18:49:54 +01:00
Alexander BakkerandGitHub 927a27943b Merge pull request #1045 from Toadsta/copyURI
Copy URI to clipboard when using transfer entries
2022-12-04 17:00:12 +01:00
Alexander Bakker 69f0bb4fbc Request root access from separate thread and don't use global Shell
This should help prevent some of the ANR's reported through Google Play
2022-12-04 16:55:19 +01:00
Alexander Bakker ac51996896 Move XML comment to attribute in the string tag 2022-12-04 15:46:52 +01:00
Alexander Bakker 8cf48a94aa Clarify backup reminder with time elapsed since last backup/export 2022-12-04 15:44:45 +01:00
Alexander Bakker d7e2114811 Pass Context instead of Activity where possible 2022-12-03 21:45:39 +01:00
Alexander Bakker caad516a6e Don't show the password reminder popup if the activity is finished 2022-12-03 21:30:10 +01:00
Alexander Bakker 12683e3ff0 Don't process QR code if ScannerActivity is finished 2022-12-03 21:27:43 +01:00
Alexander Bakker 74ecdec637 Ignore any menu clicks before the camera is ready in ScannerActivity 2022-12-03 21:19:02 +01:00
Alexander Bakker b98a5c55bc Simplify tracking of draggable entries
This fixes a crash and a case where rebinding a ViewHolder to an entry
that is not selected would still show a drag handle
2022-12-03 21:11:33 +01:00
Toadstaandorange-elephant 96852528f4 Copy URI to clipboard when using transfer entries, copied text is marked as sensitive
Co-authored-by: orange-elephant <88595467+orange-elephant@users.noreply.github.com>
2022-12-03 20:06:25 +00:00
Alexander Bakker bcbe5f33d4 Fix crash in Bitwarden importer when encountering a bad URI 2022-12-03 20:03:51 +01:00
Alexander Bakker 5cf8f73193 Check for nulls in intent extra's sent to MainActivity 2022-12-03 19:31:25 +01:00
Alexander Bakker 6ea9502ad1 Check for null before calling the EntryListView listener 2022-12-03 19:18:01 +01:00
Alexander Bakker 65790166b6 Set fallbackLineSpacing to false for all entry view modes
I fixed an issue in e77df1eee4 but
apparently I only applied it to the default view mode
2022-12-03 14:15:48 +01:00
Alexander Bakker bb4450a00b Completely disable the lock notification for now
See: #1047
2022-12-03 14:12:31 +01:00
Alexander Bakker c9a27b830d Add null check before shutting down executor in ScannerActivity 2022-12-02 13:11:06 +01:00
Alexander Bakker aff441a7ee Include URI in ImportFileTask error messages 2022-11-28 18:50:49 +01:00
Alexander Bakker 9d318a0d54 Always take favorites into account when sorting the entry list 2022-11-28 18:13:33 +01:00
Alexander Bakker ef759eb15e Start NotificationService as a foreground service
This should prevent the notification from remaining after the app has
been killed by Android.

Fixes #1037.
2022-11-23 16:05:22 +01:00
Alexander BakkerandGitHub f8d89d5754 Merge pull request #1016 from orange-elephant/copy-token-click-behaviour
Only copy code if not hidden
2022-11-20 18:54:34 +01:00
elenaandAlexander Bakker 542a8c6f8c Only copy code if not hidden 2022-11-20 18:53:13 +01:00
Alexander Bakker 282f85fb3b Don't show "Export all groups" option if there are no groups 2022-11-20 18:49:47 +01:00
Alexander BakkerandGitHub fd5a0390f0 Merge pull request #1014 from orange-elephant/export-selected-groups
Allow exporting specific groups
2022-11-20 18:37:54 +01:00
Alexander BakkerandGitHub 01e59d79a1 Merge pull request #1013 from orange-elephant/no-icons-view-mode
Add 'No Icons' view mode
2022-11-20 18:36:42 +01:00
Alexander Bakker 506d9efab3 Release v2.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2022-11-20 16:19:37 +01:00
Alexander Bakker 5ab36d72a4 Add a test for password and backup password changes 2022-11-20 15:05:11 +01:00
Alexander Bakker ac75c346ec Update translations from Crowdin 2022-11-20 13:23:40 +01:00
Alexander Bakker 5dfdbabf30 Patch scrypt implementation to directly use Java's Integer.rotateLeft
This should improve performance in some rare cases where the wrapper
function that BouncyCastle has for Integer.rotateLeft is not inlined.

See: #1024
2022-11-20 12:54:55 +01:00
Alexander Bakker e7cc3e6ca3 Fix a crash by only creating toasts using Toast.makeText() 2022-11-16 10:48:21 +01:00
Alexander BakkerandGitHub f8d60fb1b7 Merge pull request #1027 from alexbakker/2fas-schema3
Add support for importing 2FAS schema v3 backups
2022-11-15 22:45:18 +01:00
Alexander Bakker f081cfa77b Add support for importing 2FAS schema v3 backups
Fixes #1026
2022-11-15 22:44:13 +01:00
Alexander Bakker 55dc4b22d5 Set RESULT_CANCELED before calling finishAffinity()
This fixes a crash in AuthActivity:

Exception java.lang.IllegalStateException: Can not be called to deliver a result
2022-11-12 11:50:01 +01:00
Alexander Bakker 9f55d4f659 Do not print a trace when calling the private finish() overload fails 2022-11-09 20:42:59 +01:00
Alexander Bakker 715c5112ab Fix a crash caused by ViewHolder.getAdapterPosition returning -1 2022-11-09 20:18:18 +01:00
Alexander Bakker e77df1eee4 Set fallbackLineSpacing to false for the OTP TextView
This prevents any dynamic changes in height that may occur while using
tap to reveal on Android 13 and up.
2022-11-09 18:50:38 +01:00
Alexander Bakker f6f549aaeb Be even more clear about why we can't import from certain apps anymore 2022-11-09 18:50:38 +01:00
Alexander Bakker 2f8d66843f Add vault encryption field back to the new issue template 2022-11-09 18:32:13 +01:00
Alexander Bakker 12dad56fe3 Update test dependencies to stable releases 2022-11-09 15:49:33 +01:00
Alexander Bakker ffbea9ea32 Use the new syntax for GitHub issue forms 2022-11-09 15:32:56 +01:00
Alexander Bakker 286d74b69a Switch comments of tile_open_vault and tile_open_scanner 2022-11-07 21:01:11 +01:00
Alexander Bakker 10ecd513fc Add context to tile_open_vault and tile_open_scanner strings 2022-11-07 21:00:08 +01:00
elena c45564d852 Allow exporting specific groups 2022-11-06 18:38:34 +00:00
Alexander BakkerandGitHub 98e802a534 Merge pull request #1019 from beemdevelopment/feature/quick-settings
Add quick setting tiles
2022-11-06 15:34:10 +01:00
Michael Schättgen 6a1e6db486 Add quick setting tiles 2022-11-06 15:21:33 +01:00
elena 237e6744a5 Add option to hide icons 2022-11-06 13:26:47 +00:00
Alexander BakkerandGitHub 599be44369 Merge pull request #1017 from orange-elephant/reveal-on-hotp-increment
Reveal hidden code on HOTP increment click
2022-11-06 12:18:40 +01:00
Alexander Bakker b2a98d9799 Release v2.1-beta2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2022-11-03 22:50:55 +01:00
Alexander Bakker bee490d091 Check for nulls when opening ContentResolver streams 2022-11-03 22:41:30 +01:00
Michael Schättgen 1f69bf558e Temporarily disable failed test output in Github Actions 2022-11-03 13:41:17 +01:00
Michael Schättgen dd88f5bb0c Fix crash when using non latin language 2022-11-03 13:37:15 +01:00
elena af9be15ae0 Reveal hidden code on HOTP increment click 2022-11-02 21:07:37 +00:00
Alexander Bakker 8562fafda7 Release v2.1-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2022-11-02 17:11:39 +01:00
Alexander Bakker 708a5f4a19 Update translations from Crowdin 2022-11-02 15:45:27 +01:00
Alexander Bakker fcc7b7baa2 Use MaterialDividerItemDecoration for compact view mode
This allows us to disable the divider for the "Showing x entries" footer
2022-11-02 15:18:07 +01:00
Alexander Bakker 7c0b22920d Fix minor entry margin issues
Accidentally introduced with the entry pinning feature

This also moves the favorite field to the vault instead of shared
preference in order to avoid some complexity
2022-11-02 15:14:59 +01:00
Alexander Bakker c27d080a11 Fix call to notifyItemMoved when entry change causes sort change
This fixes an issue introduced in ffcbaffcfc
2022-11-02 15:09:42 +01:00
Alexander Bakker 0ee74bfeb2 Install HAXM before running the instrumented tests 2022-11-02 12:44:29 +01:00
Alexander Bakker 289b8775d3 Update ZXing to 3.5.1 2022-11-02 12:40:20 +01:00
Alexander Bakker c12397a509 Update dependencies 2022-11-01 20:19:00 +01:00
Alexander Bakker 0aef57e610 Add context to favorite strings to indicate that they're verbs 2022-10-27 22:12:49 +02:00
Alexander Bakker 7993142cc5 Show a warning when a separate export/backup password is configured 2022-10-27 22:12:44 +02:00
Alexander BakkerandGitHub 031a11250a Merge pull request #1006 from beemdevelopment/feature/favorites
Add ability to pin/favorite entries
2022-10-26 14:14:51 +02:00
Michael Schättgen 5ec1e33a31 Add ability to favorite/pin entries 2022-10-26 14:07:49 +02:00
Alexander Bakker c58b773878 Rename some things related to plaintext backup warning 2022-10-25 18:38:07 +02:00
Alexander Bakker 681326839c Fix back navigation in preferences when pressing the home action button 2022-10-16 13:27:20 +02:00
Alexander Bakker 3d124d225e Use VaultLockReceiver in a Context-registered manner
Turns out we can't listen from ACTION_SCREEN_OFF from a
manifest-registered broadcast receiver
2022-10-15 20:48:36 +02:00
Alexander Bakker 8292173297 Remove unused code for storage read permission request results 2022-10-12 17:39:40 +02:00
Alexander Bakker 84e179f9d3 Add helper to simplify animation end listeners 2022-10-12 17:36:46 +02:00
Alexander BakkerandGitHub 41eba76b05 Merge pull request #1005 from alexbakker/predictive-back
Add support for predictive back gesture
2022-10-12 17:30:05 +02:00
Alexander Bakker ad77bd687f Add support for predictive back gesture 2022-10-12 17:25:40 +02:00
Alexander Bakker c6888b11df Add an icon to the backup status in preferences 2022-10-10 22:33:42 +02:00
Alexander BakkerandGitHub 427be4d56b Merge pull request #992 from orange-elephant/code-grouping-options
Additional code grouping options
2022-10-10 17:36:53 +02:00
elena c206d4bee8 Additional code grouping options 2022-10-09 21:24:01 +01:00
Alexander Bakker 7de7fba2c8 Disable R8 obfuscation 2022-10-09 18:28:56 +02:00
Alexander Bakker 8ea28327db Optimize APK size by fixing R8 rules and excluding some BC resources 2022-10-09 17:18:13 +02:00
Alexander Bakker 80c37556c7 Prevent double launches of the intro and auth activities
Since API level 33: When opening Aegis right after installing it,
Android will send a configuration change event (CONFIG_ASSETS_PATHS),
causing MainActivity to restart almost immediately. This would also
cause two IntroActivity's to be started on top of eachother.
2022-10-09 16:33:28 +02:00
Alexander Bakker 35d3675561 Update GitHub Actions 2022-10-09 16:33:26 +02:00
Alexander Bakker 7517cfc975 Update dependencies 2022-10-09 12:53:46 +02:00
Alexander Bakker 4198ca3bb7 Update Gradle 2022-10-09 12:51:06 +02:00
Alexander Bakker 3927ddec3e Make vault lock intent used in notifications more explicit
Aegis can display a notification to the user as a reminder that the vault is
unlocked. If the user taps the notification, the vault is locked. CodeQL
reported that Aegis may be vulnerable to CWE-927, because of the use of an
implicit intent wrapped by a PendingIntent in that notification.

This does not appear to be exploitable in our case, because we use
``PendingIntent.getBroadcast`` and explicitly set the action of the wrapped
intent. Aegis also does not read or act on any information from the received
intent. This means that a malicious app cannot launch activities or send a
broadcast with a different action, as is common with these type of weakness. The
worst an app with notification access can do, is lock the vault.

Either way, it's good to make the intent explicit, so this patch addresses that.
Additionally, for API level 23 and up, we've made the wrapped intent immutable a
while back.

We'd like to thank John Rune, who ran a CodeQL scan on the Aegis codebase and
privately disclosed this finding to us.
2022-10-09 11:56:09 +02:00
Alexander Bakker 8c9ab38153 Add a workflow for CodeQL analysis 2022-10-08 20:31:40 +02:00
Alexander BakkerandGitHub 3c10b234ea Merge pull request #1000 from xconverge/fixSomeTypos
Fix some typos in docs
2022-10-08 11:49:59 +02:00
Sean Kelly 8a9a0856fb Fix some typos in docs 2022-10-06 21:34:37 -07:00
Alexander Bakker adc9179364 Always copy VaultFileCredentials when accessing it
This makes sure that mutations to the credentails can only ever be persisted
intentionally.
2022-10-05 19:42:18 +02:00
Alexander Bakker 8ae8130b71 Store and display backup error messages more clearly 2022-10-05 18:21:50 +02:00
Alexander Bakker 4427498d5e Rename the instrumented tests to be more consistent 2022-10-05 13:44:12 +02:00
Alexander Bakker aad6d78750 Move the panic trigger setting to the Security fragment 2022-10-05 13:27:28 +02:00
Alexander Bakker ef2e803e04 Make the entry count text a bit smaller 2022-10-05 13:25:17 +02:00
Alexander Bakker c718ea79c5 Update dependencies 2022-10-03 19:29:06 +02:00
Alexander BakkerandGitHub acc6b9a3de Merge pull request #973 from orange-elephant/entries-shown-count
Add entries showing count
2022-09-28 13:48:35 +02:00
elena 4bf807c46a Add entries showing count 2022-09-23 18:49:23 +01:00
Alexander Bakker e4798703f0 Make the FAB scroll listener a less sensitive
This should help minimize the glitchiness that can occur when scrolling
through the entry list.

Close #963.
2022-09-17 18:27:56 +02:00
Alexander Bakker 80624f844c Perform a version check when importing from the Steam app
Fixes #986.
2022-09-17 18:08:21 +02:00
Alexander BakkerandGitHub 714459c528 Merge pull request #969 from digitalheir/minimize-after-copy
Minimize or Close app after copying OTP #805
2022-09-17 17:55:25 +02:00
Maarten TrompperandAlexander Bakker 78feb25543 Minimize or Close app after copying OTP
Close #805
2022-09-17 17:51:03 +02:00
Alexander Bakker 3bc10bd4b8 Disallow importing empty secrets
Fixes #975

And display "ERROR" for any existing bad entries. This is kind of ugly,
but it's better than crashing, and there are probably very few (or zero)
users who have bad entries anyway.
2022-09-17 17:31:59 +02:00
Alexander BakkerandGitHub 66b7fd38d6 Merge pull request #964 from orange-elephant/google-auth-style-export
Google Authenticator compatible export
2022-09-17 15:46:56 +02:00
elena bc665b6980 Google Authenticator compatible export 2022-09-16 17:57:42 +01:00
Alexander Bakker 06405d10ad Only request the POST_NOTIFICATIONS permission in tests on API 33 2022-09-14 21:36:31 +02:00
Alexander Bakker 1e3ceefeec Request permission to show notifications
This is needed since API 33, because we show a "Vault unlocked"
notification if the user has enabled encryption and has unlocked the
vault.
2022-09-14 20:54:08 +02:00
Alexander Bakker 642864fca1 Mark clipboard data as "sensitive" when copying tokens 2022-09-14 18:33:28 +02:00
Alexander Bakker 3ec6e35011 Update dependencies
This also updates the test dependencies to an alpha version. We have to
use a pre-stable version, because these include some fixes for running
on API 33.
2022-09-14 18:08:47 +02:00
Alexander BakkerandGitHub e19ceb0761 Merge pull request #981 from jas14/monochromatic-icon
Add monochrome launcher icon
2022-09-14 17:33:53 +02:00
Alexander BakkerandGitHub 30b8e35de8 Merge pull request #968 from digitalheir/adaptive-text-size
Text is cutoff because of small display
2022-09-14 16:34:12 +02:00
Joe Stein 012b66fe9b Pin Robolectric target SDKs to 32 2022-09-05 17:09:48 -04:00
Joe Stein 4cd0d2de04 Bump targetSdkVersion to 33 2022-09-05 17:09:43 -04:00
Joe Stein bb3f4d733f Add monochrome adaptive icon for Android 13 2022-09-03 23:08:40 -04:00
Maarten Trompper 05a8c7c51f Fixes #794 2022-08-26 20:31:29 +02:00
Alexander Bakker 029ca81bbf Don't catch the IllegalArgumentException when calling getMissingIndices
We always call isSingleBatch beforehand, so if the exception occurs,
it's a bug.
2022-08-22 19:14:51 +02:00
Alexander BakkerandGitHub 70ceca6a7b Merge pull request #958 from orange-elephant/fix-scan-google-auth-export-from-image
Import Google Authenticator exports by image
2022-08-22 19:06:59 +02:00
elena 2767727ad4 Import google auth export from image 2022-08-21 18:47:54 +01:00
Alexander BakkerandGitHub 2dc01adc48 Merge pull request #925 from alexbakker/intro-import
Add an import button to the intro
2022-08-17 19:24:36 +02:00
Alexander Bakker 79022be3b6 Add an import button to the intro 2022-08-17 19:11:35 +02:00
Alexander Bakker dcda668671 Disable postprocessing for debug builds to speed them up 2022-08-17 12:00:17 +02:00
Alexander BakkerandGitHub ff0cd99170 Merge pull request #961 from orange-elephant/handle-intent-with-text-extra
Enable sharing text to Aegis
2022-08-17 10:44:10 +02:00
elena 5bb32a982b Enable sharing text to Aegis 2022-08-11 21:32:33 +01:00
Alexander BakkerandGitHub 85e230c46a Merge pull request #957 from orange-elephant/add-plaintext-export-warning
Add warning banner after plaintext export
2022-08-11 21:03:20 +02:00
elena 045b8280bf Add warning banner after plaintext export 2022-08-10 17:17:51 +01:00
Alexander Bakker 6fd8a3b6b4 Allow sharing multiple QR code images to Aegis through a single intent
This also refactors the logic for handling incoming intents a bit
2022-08-10 17:48:36 +02:00
Alexander Bakker e46857a26e Add support for importing multiple QR code images in one go
This is also part of the preparation needed for scanning Google
Authenticator Export QR codes from images.
2022-08-10 17:22:09 +02:00
Alexander BakkerandGitHub b875baacef Merge pull request #948 from orange-elephant/Add-mOTP-support
Add MOTP support
2022-08-07 18:31:51 +02:00
elena 3441deee64 Add MOTP support 2022-08-07 16:40:08 +01:00
Alexander Bakker bd3697659f Try harder to find QR codes in image files
And refactor a bit by moving some of the QR scanning related logic to a
separate helper class.
2022-08-07 17:13:58 +02:00
Alexander Bakker 5f12eae678 Add tests for scanning QR codes 2022-08-04 21:47:51 +02:00
Alexander BakkerandGitHub 3b715d58cf Merge pull request #952 from Hovn/master
Set the searchview to occupy all Toolbar widths when it expands
2022-08-04 18:49:59 +02:00
Alexander Bakker 76245677b0 Downgrade ResourceType linter error to a warning 2022-08-03 22:42:56 +02:00
Alexander Bakker f73a64acd6 Update gradle and dependencies 2022-08-03 20:38:10 +02:00
HovnCompany b4f12af23a Set the searchview to occupy all Toolbar widths when it expands 2022-08-01 16:00:40 +08:00
Alexander Bakker 043b17909c Wait for ViewPager to be idle in instrumented tests
This fixes the flakiness issues with the intro tests
2022-06-06 19:07:02 +02:00
Alexander Bakker 5226def653 Run UI tests as part of our CI
Third attempt's the charm? This time we're running the tests on macOS
hosts, which is supposedly more stable.
2022-06-06 19:06:42 +02:00
Alexander Bakker 1dcf56a0fa Capture screenshots of failing instrumented tests 2022-06-06 14:52:17 +02:00
Alexander Bakker fff81b0ee3 Upload APK artifact once CI is done 2022-06-06 14:51:35 +02:00
Alexander Bakker f32ac0e8ab Make ProgressDialog adhere to the "Secure screen" setting 2022-06-06 14:05:16 +02:00
Alexander Bakker 8d3b2e2c9d Rename slot type DERIVED to PASSWORD 2022-06-06 13:55:19 +02:00
Alexander BakkerandGitHub 0cfd78ace1 Merge pull request #926 from alexbakker/backup-password
Add an option to set a separate password for backups and exports
2022-06-06 13:33:22 +02:00
Alexander Bakker 5db365a999 Add instrumented tests for backup and export 2022-06-06 13:19:46 +02:00
Alexander Bakker a492bcbde3 Add an option to set a separate password for backups and exports 2022-06-05 23:03:11 +02:00
Alexander Bakker d40ebf0f67 Use a higher resolution app icon in the intro 2022-06-05 12:33:53 +02:00
Alexander Bakker 1373a67ea0 Only re-sort entry list when not using custom sorting
This fixes a crash introduced in #910.
2022-06-04 19:01:37 +02:00
Alexander BakkerandGitHub 11d7493245 Merge pull request #924 from CristianAUnisa/double-click-copy-crash
Avoid crash when double clicking on "Copy" button
2022-06-04 14:49:22 +02:00
CristianAUnisa d2f0f71be5 Check if there's a selected item after pressing on an Action menu item 2022-06-04 14:48:14 +02:00
Alexander Bakker 49b334ff70 Show dialog when trying to import from a recent Google Auth version 2022-06-04 14:35:36 +02:00
Alexander BakkerandGitHub ac123918c2 Merge pull request #922 from CristianAUnisa/google-auth-ignore-encrypted
Check if Google Authenticator entries are encrypted
2022-06-04 13:02:21 +02:00
CristianAUnisa 4cf0465a32 Check if Google Authenticator entry is encrypted 2022-06-04 12:47:44 +02:00
Alexander BakkerandGitHub 98de86b53e Merge pull request #910 from CristianAUnisa/sort-on-replace
Sort list after editing entry
2022-05-22 13:36:42 +02:00
Alexander Bakker dfa3ae4d4e Reorganize README badges 2022-05-22 13:31:35 +02:00
CristianAUnisa ffcbaffcfc Sort list after editing entry 2022-05-22 13:19:55 +02:00
Alexander Bakker ce9a15b7bc Allow filtering by "No group"
This is a continuation of #830

Close #826, #830
2022-05-22 13:11:34 +02:00
Alexander Bakker e73de453c6 Update Matrix badge 2022-05-22 13:11:34 +02:00
Alexander Bakker 3b1035b5e1 Reset the action bar subtitle when the back button is pressed 2022-05-22 12:02:23 +02:00
Alexander BakkerandGitHub 239d8740be Merge pull request #912 from CristianAUnisa/screen-rotation-bugfix-686
Restore search query on screen rotation
2022-05-22 11:55:01 +02:00
CristianAUnisa 49130cee73 Restore search query on screen rotation 2022-05-07 16:15:30 +02:00
Alexander Bakker 0909341caa Improve QR reader efficiency and support inverted QR codes 2022-05-01 19:22:20 +02:00
Alexander BakkerandGitHub 81d5915969 Merge pull request #908 from CristianAUnisa/bitwarden-remote
Add Bitwarden importer
2022-04-30 13:13:42 +02:00
CristianAUnisa 8d4a687817 Add Bitwarden importer
Added license notice for Simple Flat Mapper

Added Steam secret support to Bitwarden importer

https://bitwarden.com/help/authenticator-keys/#steam-guard-totps

Added tests
2022-04-24 16:55:38 +02:00
Alexander BakkerandGitHub b0e8d041d0 Merge pull request #904 from CristianAUnisa/dont-hide-fab-on-keydown
Don't hide "Add entry" button when a DPad key is pressed
2022-04-21 19:00:52 +02:00
CristianAUnisaandAlexander Bakker ca49cd4df5 Don't hide "Add entry" button when DPAD key is pressed 2022-04-21 18:49:41 +02:00
Alexander Bakker beadf4db8b Use requireContext, requireActivity and requirePreference where possible 2022-04-10 18:44:25 +02:00
Alexander Bakker f7b4cceb81 Remove unused SlotAdapter and SlotHolder 2022-04-10 18:14:00 +02:00
Alexander Bakker 86b86fef10 Pass current values to Dialog functions instead of reading from prefs 2022-04-10 17:48:26 +02:00
Alexander Bakker 927ea50c33 Ignore JSON exceptions in getUsageCounts instead of printing them 2022-04-10 17:12:19 +02:00
Alexander Bakker 339a31b0f3 Show a backup reminder if auto backups are not enabled
If the user doesn't have auto backups enabled, the reminder will pop up
up every time a significant change is made to the vault.

Users can get rid of the reminder by:
- Creating an export
- Enabling automatic backups (either ours or Android backups will do)
2022-04-10 17:12:12 +02:00
Alexander Bakker cf9fbf081c Don't print the exception of readVaultFile if the file was not found 2022-04-10 14:46:41 +02:00
Alexander Bakker d4557065ea Disable the MissingQuantity lint check to fix the build 2022-04-10 14:41:55 +02:00
Alexander Bakker d0c8c5fae4 Update Gradle and dependencies 2022-04-09 18:28:15 +02:00
Alexander Bakker b8fa67304a Release v2.0.3
(cherry picked from commit 32ee0915a7)
2022-03-27 21:58:31 +02:00
Alexander Bakker 903948d57c Stop using dependency injection in AegisBackupAgent
Because the app launches in a restricted mode when restoring a backup,
dependency injection with Dagger Hilt doesn't work inside BackupAgent. This
would cause backup restore operations to fail. This issue got introduced by the
recent switch to Dagger Hilt, which has not been included in a release of Aegis
yet.
2022-03-16 14:43:13 +01:00
Alexander Bakker 71eb6b133c Remove SlotManagerActivity from AndroidManifest 2022-03-06 14:54:36 +01:00
Alexander Bakker 6f66706685 Don't pass MIME type along when selecting icon pack file to import
Fixes #757
2022-03-06 13:55:12 +01:00
Alexander Bakker 6d8a8c066c Remove unused string resources 2022-03-06 13:49:40 +01:00
Alexander Bakker baa3cac5a4 Remove slot manager activity
We don't really use this anymore, and it was only available for debug builds anyway
2022-03-06 13:33:57 +01:00
Alexander Bakker 71f2b54deb Use Dagger Hilt for dependency injection
This gets rid of our own janky dependency injection through the AegisApplication class
2022-03-06 13:06:34 +01:00
Alexander BakkerandGitHub 927f5f2bd5 Merge pull request #845 from Hacker437/Dialog
Updated Delete Dialog
2022-02-27 18:18:56 +01:00
Praveen KumarandAlexander Bakker c0020684de Change the layout of the entry delete dialog and include more info 2022-02-27 17:46:47 +01:00
Alexander Bakker b8066c1925 Use Aegis icon as lock status notification icon 2022-02-18 12:54:30 +01:00
Alexander Bakker 0f3e5c537c Allow customization of password reminder frequency
Close #769
2022-02-17 18:14:08 +01:00
Alexander Bakker 5ce21a94ea Only skip panic trigger signature check if running under a test 2022-02-17 15:47:27 +01:00
Alexander Bakker d1d5df8051 Add tests for the Ripple panic trigger action 2022-02-17 14:25:32 +01:00
Alexander Bakker 43c064bb26 Disable the Crowdin workflow for forks
This was always failing in forks because the Crowdin API key is only available for the upstream repository. As it should be.
2022-02-17 13:29:59 +01:00
Alexander Bakker b9a81bdd75 Always allow D2D transfers in backup agent 2022-02-17 13:16:05 +01:00
Alexander Bakker 0938d3688a Set dataExtractionRules in AndroidManifest for Android 12+ 2022-02-17 13:16:05 +01:00
Alexander Bakker fcb7bf032b Add support for importing 2FAS schema v2 backups 2022-02-10 20:58:06 +01:00
Alexander Bakker acfb70c267 Move Yandex secret validation tests to a different folder 2022-02-06 12:55:02 +01:00
Alexander Bakker 119c3838e1 Silence lint error related to calling private method through reflection 2022-02-04 21:23:35 +01:00
Alexander Bakker 39a5dc3ee8 Update the vault format documentation and improve the diagram 2022-02-04 20:53:08 +01:00
Alexander Bakker 473cb7bfeb Set FLAG_IMMUTABLE on the intent of the lock status notification
This fixes a crash on API 31
2022-02-02 21:06:21 +01:00
Alexander Bakker 3d13117752 Refactor Yandex support a bit and fix some style issues 2022-02-02 17:55:09 +01:00
Alexander BakkerandGitHub 76c89f0fcc Merge pull request #869 from RandomRoot/feature-yandexotp
Feature: Add Yandex OTP support
2022-02-02 13:40:48 +01:00
Alexander Bakker 185e22a82e Downgrade camera-view back to 1.0.0-alpha32
Can't mix and match 1.0.0 and 1.1.0.
2022-01-31 22:07:40 +01:00
Alexander Bakker e88c3ea6db Update dependencies 2022-01-31 21:59:02 +01:00
Alexander Bakker a04020aed0 Add an FAQ document 2022-01-31 21:19:22 +01:00
Mikhail Prokofev af2bf6f683 Add Yandex OTP support 2022-01-26 18:43:55 +03:00
Alexander BakkerandGitHub 72c165749f Merge pull request #875 from moesheyab/aegis-import-message-fix
Fix Aegis import dialog message title
2022-01-22 11:41:54 +01:00
moesheyab f14bd99f63 Fix Aegis import dialog message title 2022-01-16 12:21:18 -08:00
Alexander Bakker e54ac9aba4 Update FUNDING.yml 2022-01-03 21:20:09 +01:00
Alexander Bakker f9b4e8fa3e Use the first grapheme for the TextDrawable icon
Fixes #862
2022-01-03 20:23:04 +01:00
Alexander Bakker 86580e38af Update crowdin-cli to 3.7.2 2021-12-06 23:07:59 +01:00
Alexander Bakker 6955eb10da Add a GitHub Action for uploading sources to Crowdin 2021-12-06 22:31:12 +01:00
Alexander Bakker 153f5e09d8 Update dependencies 2021-12-04 11:54:02 +01:00
Alexander Bakker cf63f5bef2 Fix the build 2021-12-04 11:42:41 +01:00
Alexander Bakker aec639cc53 Add a test for the Duo importer 2021-12-04 11:42:28 +01:00
Alexander BakkerandGitHub 5c196f1ca5 Merge pull request #835 from anibyl/feature-duo
Add DUO importer
2021-12-04 11:09:20 +01:00
Sieva Kimajeŭ 56ddefa246 Add DUO importer. #68 2021-12-02 15:47:43 -08:00
Alexander Bakker 4c977a6371 Disable the PIN keyboard when enabling encryption
Fixes #854
2021-11-28 19:27:40 +01:00
Alexander BakkerandGitHub 07b0b26caf Merge pull request #844 from Hacker437/FocusSearch
Added Focus Search on App Start
2021-11-16 18:47:45 +01:00
Praveen Kumar 0652eaa5cd Added Focus Search on App Start
Renamed key

Removed unnecessary code
2021-11-08 17:34:49 +00:00
Alexander Bakker adfc472a39 Bump target SDK version to 31 and update dependencies 2021-11-03 12:23:06 +01:00
Alexander BakkerandGitHub 9ca4de8b60 Merge pull request #838 from Dar9586/master
Vauld.md inconsistency fix
2021-10-30 11:10:06 +02:00
Dar9586andGitHub 0b104cf285 Update vault.md 2021-10-30 08:35:25 +00:00
Alexander BakkerandGitHub d65982aaa1 Merge pull request #832 from strugee/upgrade-android-gradle-plugin
Upgrade Android Gradle plugin
2021-10-20 21:44:00 +02:00
AJ Jordan fbc390b714 Upgrade to Gradle/Gradle Android plugin 7.x
Additionally, enable some more linting (per the Android plugin release
notes' recommendation) and fix a lint warning.
2021-10-20 12:07:24 -07:00
Michael SchättgenandGitHub a14dd382bf Merge pull request #831 from strugee/checked-filter
Make group chips checked when selected
2021-10-20 13:19:27 +02:00
AJ Jordan a302f3daac Make group chips checked when selected
This brings us in line with upstream Material UI guidelines - apparently
before we were showing "choice chips" and with this change we show
"filter chips".

See https://material.io/components/chips/android#filter-chip.
2021-10-19 23:01:27 -07:00
Alexander Bakker 80960e23fc Release v2.0.2
build / build (push) Canceled after 0s
2021-09-15 15:40:46 +02:00
Alexander Bakker a32ecdaf9b Update translations from Crowdin 2021-09-15 15:32:39 +02:00
Alexander BakkerandGitHub 6967e42ab5 Merge pull request #816 from Hacker437/Chip
Changed the Chip to match the background in Amoled and Dark Mode
2021-09-15 14:23:11 +02:00
Praveen Kumar 385475aa8b Changed the Chip to match the background in Amoled and Dark Mode 2021-09-03 11:24:22 +05:30
Alexander BakkerandGitHub a9d850aa01 Merge pull request #808 from Hacker437/migrate-buttons
Migrate App Compat Buttons To Material Buttons
2021-09-01 18:50:46 +02:00
Praveen Kumar a9806fcfb7 Migrate App Compat Buttons To Material Buttons 2021-08-31 09:40:39 +05:30
Alexander BakkerandGitHub 1e6175854c Merge pull request #793 from modelorona/feature-note_field_annotation
add note text field when editing entry
2021-08-30 21:08:46 +02:00
modelorona 97f28c6089 add note field to entry 2021-08-30 10:21:54 +01:00
Alexander BakkerandGitHub 3464f93c39 Merge pull request #791 from bitscuity/feature-pause
Option to Disable Automatic Refresh for Selected Code
2021-08-11 14:28:34 +02:00
bitscuity 02ee52a31f Added code to pause updating of focused entry 2021-08-11 14:01:24 +02:00
Alexander Bakker 6bafd9e9ae Ensure the issuer textbox and group dropdown have equal height
Fixes #751
2021-07-31 14:01:21 +02:00
Alexander Bakker 74e460d9a9 Update link to aegis-icons 2021-07-29 18:42:31 +02:00
Alexander Bakker 11705866e3 Fix the build by fixing some linter complaints 2021-07-22 22:25:40 +02:00
Alexander Bakker db0452e42a Update gradle 2021-07-22 21:43:51 +02:00
Alexander Bakker 177a964d3a Update dependencies 2021-07-22 21:17:04 +02:00
Alexander Bakker eb4b7b5d61 Accept spaces and dashes in secrets from Microsoft Authenticator
A user reported that importing was failing due to this issue
2021-07-22 21:07:32 +02:00
Alexander Bakker 68eb87b096 Check if searchView is not null before closing it in onLocked
This fixes a crash we've been seeing in the Google Play Console
2021-06-26 14:30:25 +02:00
Alexander Bakker eb22101b02 Release v2.0.1
build / build (push) Canceled after 0s
2021-06-23 18:41:26 +02:00
Alexander Bakker 44f69e755c Update translations from Crowdin 2021-06-23 18:27:04 +02:00
Alexander Bakker 327b7ccbae Perform case changes using Locale.ROOT where applicable
Fixes #777
2021-06-23 18:06:01 +02:00
Alexander BakkerandGitHub 6585513120 Merge pull request #780 from jhenninger/suggested-icons-matching
Improve icon suggestions
2021-06-23 12:33:08 +02:00
Johannes Henninger acbe73834d Improve icon suggestions
Check if the icon issuers contain the token issuer or vice versa.

Fixes #761
2021-06-23 01:31:50 +02:00
Alexander BakkerandGitHub c27c4f0ac5 Merge pull request #760 from michaelschattgen/feature/usage-count
Add usage count to entries
2021-06-17 20:04:51 +02:00
Michael Schättgen 34cd16f240 Add usage count to entries
Update Preferences.java

Fix build error and review improvements
2021-06-16 16:22:11 +02:00
Alexander BakkerandGitHub 78fc37d1e9 Merge pull request #763 from jhenninger/fix-blank-text-delete-dialog
Fix blank text in delete dialog for entries with no issuer
2021-06-06 11:33:50 +02:00
Johannes Henninger df9684a31c Fix blank text in delete dialog for entries with no issuer
Fixes #758
2021-06-04 14:29:53 +02:00
Alexander Bakker 520c587d09 Collapse the search view when locking the app
Addresses part of #750
2021-06-02 18:14:18 +02:00
472 changed files with 30671 additions and 8329 deletions
+10
View File
@@ -0,0 +1,10 @@
project_id: "372633"
api_token: "<api-token-here>"
base_path: "../app/src/main"
base_url: "https://api.crowdin.com"
preserve_hierarchy: true
files:
- source: "res/values/strings.xml"
dest: "strings.xml"
translation: "res/values-%android_code%/%original_file_name%"
+4 -1
View File
@@ -1 +1,4 @@
custom: ['https://www.buymeacoffee.com/beemdevelopment']
custom:
- "https://www.buymeacoffee.com/beemdevelopment"
- "https://www.blockchain.com/btc/address/bc1q26kyxqjkc6tu477pzy0whagwhs4ypv93qls22n"
- "https://nanocrawler.cc/explorer/account/nano_1aegisc559b1x4p3839egnu579jkd4htpidy14eo9e31gzqmwuafypnj4q94"
-34
View File
@@ -1,34 +0,0 @@
---
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.
```
+89
View File
@@ -0,0 +1,89 @@
name: Bug Report
description: Create a report to help us fix a bug
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please read the [bug reports section of the contribution guidelines](https://github.com/beemdevelopment/Aegis/blob/master/CONTRIBUTING.md#bug-reports) before submitting an issue.
- type: input
id: version
attributes:
label: Version
description: Which version of Aegis are you using?
placeholder: "Example: v2.1"
validations:
required: true
- type: dropdown
id: source
attributes:
label: Source
description: Where did you get Aegis from?
options:
- Google Play
- F-Droid
- GitHub
- Other
validations:
required: true
- type: dropdown
id: encryption
attributes:
label: Vault encryption
description: Do you have encryption enabled for your Aegis vault?
options:
- "Yes (with biometric unlock)"
- "Yes"
- "No"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: Which device are you using Aegis on?
placeholder: "Example: Pixel 5"
validations:
required: true
- type: input
id: android_version
attributes:
label: Android version
description: Which Android version is running on your device?
placeholder: "Example: Android 13"
validations:
required: true
- type: input
id: rom
attributes:
label: ROM
description: Are you using a custom ROM? If so, which one and which version? If you're using the stock OS that came with your device, you can leave this field empty.
placeholder: "Example: GrapheneOS"
validations:
required: false
- type: textarea
id: reproduction_steps
attributes:
label: Steps to reproduce
description: A detailed list of reproduction steps.
validations:
required: true
- type: textarea
id: expectations
attributes:
label: What do you expect to happen?
validations:
required: true
- type: textarea
id: reality
attributes:
label: What happens instead?
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: If applicable, paste the debug log that you captured using ADB here.
validations:
required: false
+53 -2
View File
@@ -5,8 +5,59 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e2c57acffb2c9aa5a8dc6eda2bbae0b6e495bd4c
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Build the app
run: ./gradlew build
- uses: actions/upload-artifact@v4
with:
name: apk
path: app/build/outputs/apk/debug/app-debug.apk
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Tests
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
with:
api-level: 31
arch: x86_64
profile: pixel_3a
heap-size: 512M
ram-size: 4096M
emulator-options: -memory 4096 -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
disk-size: 8G
script: |
mkdir -p artifacts/report
adb logcat -c
adb logcat -G 16M && adb logcat -g
./gradlew connectedCheck || touch tests_failing
adb logcat -d > artifacts/logcat.txt
cp -r app/build/reports/androidTests/connected/* artifacts/report/
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
test ! -f tests_failing
- uses: actions/upload-artifact@v4
if: always()
with:
name: instrumented-test-report
path: |
artifacts/*
if-no-files-found: ignore
+41
View File
@@ -0,0 +1,41 @@
name: codeql
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '25 16 * * 2'
jobs:
analyze:
name: analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Exclude paths
# The importers are excluded from analysis, because some of the apps Aegis
# can import from don't have such great crypto, which will cause false
# positive security alerts.
run: |
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: java
- name: Build
run: ./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+31
View File
@@ -0,0 +1,31 @@
name: crowdin
on:
push:
branches:
- master
# run sequentially (per branch)
concurrency: "crowdin-upload-${{ github.ref }}"
jobs:
upload-sources:
runs-on: ubuntu-latest
if: github.repository == 'beemdevelopment/Aegis'
steps:
- uses: actions/checkout@v4
- name: Install crowdin-cli
run: |
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
unzip crowdin-cli.zip -d crowdin-cli
- name: Upload to Crowdin
env:
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
run: |
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
--no-progress \
--token "$CROWDIN_TOKEN" \
--project-id 372633 \
--base-path app/src/main \
--source res/values/strings.xml \
--translation "res/values-%android_code%/%original_file_name%" \
--dest strings.xml \
--branch master
+1
View File
@@ -40,3 +40,4 @@ captures/
# Keystore files
*.jks
crowdin.properties
.crowdin/config.yml
+6 -4
View File
@@ -4,6 +4,9 @@ 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.
Please review [the FAQ](FAQ.md) before reporting a bug, asking a question or
requesting a feature.
## Translations
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
@@ -27,10 +30,9 @@ 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.
follow up on for us, please fill out the form as accurately as possible. If a
bug report 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
+101
View File
@@ -0,0 +1,101 @@
# FAQ
## General
### How can I contribute?
There are lots of ways! Please refer to our [contributing
guide](https://github.com/beemdevelopment/Aegis/blob/master/CONTRIBUTING.md).
### Why is the latest version not on F-Droid yet?
We don't release new versions of Aegis on F-Droid ourselves. Once we've released
a new version on GitHub, F-Droid will usually kick off their automatic build
process a day later and publish the app to their repository a couple of days
afterwards. It can sometimes take up to a week for a new version to appear on
F-Droid.
### Can you port Aegis to iOS/Windows/MacOS/Browser Extension?
We don't have plans to port Aegis to other platforms.
### Can you add support for Autofill?
On Android, only one app can be active in the Autofill slot at a time, and since
this is typically occupied by the password manager, we don't see much value in
adding support for this feature in Aegis.
### What is the difference between exporting and backing up?
Exporting is done manually and backups are done automatically. The format of the
vault file is exactly the same for both.
## Security
### I can no longer use biometrics to unlock the app. What should I do?
If you could previously unlock Aegis with biometrics, but suddenly can't do so
anymore, this is probably caused by a change made to the security settings of
your device. The app will tell you when this happened in most cases. To resolve
this, unlock the app with your password, disable biometric unlock in the
settings of Aegis and re-enable it.
### Why does Aegis keep prompting me for my password, even though I have enabled biometric authentication?
You're probably encountering the password reminder. Try entering your password
to unlock the vault once. After that, Aegis will prompt for biometrics by
default again until it's time for another password reminder.
Since forgetting your password will result in loss of access to the contents of
the vault, __we do NOT recommend disabling the password reminder__.
### Aegis uses SHA1 for most/all of my tokens. Isn't that insecure?
The hash algorithm is imposed by the service you're setting up 2FA for (e.g.
Google, Facebook, GitHub, etc). There is nothing we can do about that. If we
were to change this on Aegis' end, the tokens would stop working. Furthermore,
when using SHA1 in an HMAC calculation, the currently known issues in SHA1 are
not of concern.
### Why doesn't Aegis support biometric unlock for my device, even though it works with other apps?
The reason for this is pretty technical. In short, since you're not entering
your password when using biometric unlock, Aegis needs some other way to decrypt
the vault. For this purpose, we generate and use a key in the Android Keystore,
telling it to only allow us to use that key if the user authenticates using
their biometrics first. Some devices have buggy implementations of this feature,
resulting in the error displayed to you by Aegis in an error dialog.
If biometrics works with other apps, but not with Aegis, that means those other
apps probably perform a weaker form of biometric authentication.
## Backups
### How can I back up my Aegis vault to the cloud automatically?
Aegis can only automatically back up to the cloud if the app of your cloud
provider is installed on your device and fully participates in the Android
Storage Access Framework. Aegis doesn't have access to the internet and we don't
have plans to change this, so adding support for specific cloud providers in the
app is not possible.
Cloud providers currently known to be supported:
- Nextcloud
Another common setup is to configure Aegis to back up to a folder on local
storage of your device and then have a separate app (like
[Syncthing](https://syncthing.net/)) sync that folder anywhere you want.
## Importing
### When importing from Authenticator Plus, an error is shown claiming that Accounts.txt is missing
Make sure you supply an Authenticator Plus export file obtained through
__Settings -> Backup & Restore -> Export as Text and HTML__. The ``.db`` format
is not supported.
If it still doesn't work, please report the issue to us. As a temporary
workaround, you can try extracting the ZIP archive on a computer, recreating it
without a password and then importing that into Aegis. Another option is
extracting the ZIP archive on a computer and importing the resulting
Accounts.txt file into Aegis with the "Plain text" import option.
+27 -10
View File
@@ -1,14 +1,20 @@
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
alt="App icon">
# Aegis Authenticator [![CI](https://github.com/beemdevelopment/Aegis/workflows/build/badge.svg)](https://github.com/beemdevelopment/Aegis/actions?query=workflow%3Abuild) [![Crowdin](https://badges.crowdin.net/aegis-authenticator/localized.svg)](https://crowdin.com/project/aegis-authenticator) [![Donate](https://img.shields.io/badge/donate-buy%20us%20a%20beer-%23FF813F)](https://www.buymeacoffee.com/beemdevelopment) [![Matrix](https://img.shields.io/badge/chat-Matrix-blue)](https://matrix.to/#/#aegis:matrix.org)
# Aegis Authenticator
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
<br>
[![Build](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml/badge.svg)](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml?query=branch%3Amaster) [![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/matrix/aegis:matrix.org?color=blue)](https://matrix.to/#/#aegis:matrix.org)
**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.
For a list of frequently asked questions, please check out [the FAQ](FAQ.md).
The security design of the app and the vault format is described in detail in
[this document](docs/vault.md).
@@ -47,19 +53,20 @@ The security design of the app and the vault format is described in detail in
[<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)
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)
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot3.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)
[<img width=200 alt="Screenshot 8"
src="metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true)
## Downloads
@@ -71,7 +78,7 @@ src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
[<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
APK releases on Google Play and GitHub are signed using the same key. They can
@@ -109,7 +116,7 @@ Aegis supports icon packs to make it easier to assign icons to the entries in
your vault. There are no official icon packs, but the community maintains a
number of third-party icon packs you may want to check out. To learn how to
create your own Aegis-compatible icon pack, see [the
documenation](docs/iconpacks.md).
documentation](docs/iconpacks.md).
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
@@ -118,10 +125,20 @@ documenation](docs/iconpacks.md).
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/). The icons are automatically generated, so
Icons](https://simpleicons.org/).
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
\* The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
+80 -62
View File
@@ -1,5 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
@@ -18,27 +20,25 @@ def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdkVersion 30
compileSdk 34
namespace packageName
defaultConfig {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 30
versionCode 48
versionName "2.0"
targetSdkVersion 34
versionCode 64
versionName "3.0"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
buildConfigField "java.util.concurrent.atomic.AtomicBoolean", "TEST", "new java.util.concurrent.atomic.AtomicBoolean(false)"
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
@@ -71,13 +71,6 @@ android {
]
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 {
manifestPlaceholders = [
@@ -87,13 +80,17 @@ android {
]
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'
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
resources {
excludes += ['/org/bouncycastle/pqc/**/*.properties']
}
}
@@ -102,11 +99,16 @@ android {
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
lint {
abortOnError true
checkDependencies true
disable 'MissingQuantity', 'MissingTranslation'
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
artifact = 'com.google.protobuf:protoc:3.25.1'
}
generateProtoTasks {
all().each { task ->
@@ -119,33 +121,46 @@ protobuf {
}
}
dependencies {
def androidTestVersion = '1.3.0'
def cameraxVersion = '1.0.0'
def glideVersion = '4.12.0'
def guavaVersion = '30.1.1'
def junitVersion = '4.13.2'
def libsuVersion = '3.1.2'
aboutLibraries {
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
prettyPrint = true
configPath = "app/config"
fetchRemoteFunding = false
registerAndroidTasks = false
exclusionPatterns = [~"javax.annotation.*"]
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
}
annotationProcessor 'androidx.annotation:annotation:1.2.0'
dependencies {
def cameraxVersion = '1.3.1'
def glideVersion = '4.16.0'
def guavaVersion = '33.0.0'
def hiltVersion = '2.50'
def junitVersion = '4.13.2'
def libsuVersion = '5.2.2'
annotationProcessor 'androidx.annotation:annotation:1.7.1'
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.activity:activity:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:1.0.0-alpha24"
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.core:core:1.3.2"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation "androidx.core:core:1.12.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.3.1"
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation "com.google.dagger:hilt-android:$hiltVersion"
implementation 'com.github.avito-tech:krop:0.52'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
@@ -155,34 +170,37 @@ dependencies {
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.protobuf:protobuf-javalite:3.16.0'
implementation 'com.google.zxing:core:3.4.1'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'com.nulab-inc:zxcvbn:1.5.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
implementation 'com.google.zxing:core:3.5.2'
implementation("com.mikepenz:aboutlibraries:11.1.0") {
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
}
implementation "com.mikepenz:aboutlibraries-core-android:11.1.0"
implementation 'com.nulab-inc:zxcvbn:1.8.2'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
androidTestImplementation "androidx.test:core:${androidTestVersion}"
androidTestImplementation "androidx.test:runner:${androidTestVersion}"
androidTestImplementation "androidx.test:rules:${androidTestVersion}"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.3.0'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation "androidx.test:core:${androidTestVersion}"
testImplementation 'androidx.test:core:1.5.0'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "junit:junit:${junitVersion}"
testImplementation "org.json:json:20210307"
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'org.json:json:20231013'
testImplementation 'org.robolectric:robolectric:4.11.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
+6
View File
@@ -0,0 +1,6 @@
{
"uniqueId": "com.github.avito-tech:krop",
"licenses": [
"MIT"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
"licenses": [
"Apache-2.0"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
"licenses": [
"MIT"
]
}
+4
View File
@@ -3,4 +3,8 @@
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
<issue id="NotificationPermission">
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
</issue>
<issue id="ResourceType" severity="Warning" />
</lint>
+4 -22
View File
@@ -1,25 +1,7 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/alex/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.ui.fragments.*
-keep class com.beemdevelopment.aegis.ui.fragments.preferences.*
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
-dontobfuscate
-dontwarn javax.naming.**
@@ -2,52 +2,121 @@ package com.beemdevelopment.aegis;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
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.ui.views.EntryHolder;
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.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import java.lang.reflect.InvocationTargetException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.inject.Inject;
import dagger.hilt.android.testing.HiltAndroidRule;
public abstract class AegisTest {
public static final String VAULT_PASSWORD = "test";
public static final String VAULT_PASSWORD_CHANGED = "test2";
public static final String VAULT_BACKUP_PASSWORD = "something";
public static final String VAULT_BACKUP_PASSWORD_CHANGED = "something2";
protected AegisApplication getApp() {
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
@Rule
public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@Rule
public final GrantPermissionRule permRule = getGrantPermissionRule();
@Inject
protected VaultManager _vaultManager;
@Inject
protected Preferences _prefs;
@Before
public void init() {
hiltRule.inject();
}
protected VaultManager getVault() {
return getApp().getVaultManager();
private static GrantPermissionRule getGrantPermissionRule() {
List<String> perms = new ArrayList<>();
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
perms.add(Manifest.permission.POST_NOTIFICATIONS);
}*/
return GrantPermissionRule.grant(perms.toArray(new String[0]));
}
protected VaultManager initVault() {
protected AegisApplicationBase getApp() {
return (AegisApplicationBase) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
protected VaultRepository initEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
return initVault(creds, VaultEntries.get());
}
protected VaultRepository initEmptyEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
return initVault(creds, null);
}
protected VaultRepository initPlainVault() {
return initVault(null, VaultEntries.get());
}
protected VaultRepository initEmptyPlainVault() {
return initVault(null, null);
}
private VaultRepository initVault(@Nullable VaultFileCredentials creds, @Nullable List<VaultEntry> entries) {
VaultRepository vault;
try {
vault.save(false);
} catch (VaultManagerException e) {
vault = _vaultManager.initNew(creds);
} catch (VaultRepositoryException e) {
throw new RuntimeException(e);
}
getApp().getPreferences().setIntroDone(true);
if (entries != null) {
for (VaultEntry entry : entries) {
_vaultManager.getVault().addEntry(entry);
}
}
try {
_vaultManager.save();
} catch (VaultRepositoryException e) {
throw new RuntimeException(e);
}
_prefs.setIntroDone(true);
return vault;
}
@@ -78,7 +147,11 @@ public abstract class AegisTest {
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
byte[] secret = CryptoUtils.generateRandomBytes(20);
return generateEntry(type, name, issuer, 20);
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer, int secretLength) {
byte[] secret = CryptoUtils.generateRandomBytes(secretLength);
OtpInfo info;
try {
@@ -110,4 +183,21 @@ public abstract class AegisTest {
}
};
}
@NonNull
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
@Override
public boolean matchesSafely(EntryHolder holder) {
return holder != null
&& holder.getEntry() != null
&& holder.getEntry().getInfo().getClass().equals(otpClass);
}
@Override
public void describeTo(Description description) {
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
}
};
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
import dagger.hilt.android.testing.CustomTestApplication;
@CustomTestApplication(AegisApplicationBase.class)
public interface AegisTestApplication {
}
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.preference.PreferenceManager;
@@ -10,6 +11,16 @@ import androidx.test.runner.AndroidJUnitRunner;
import com.beemdevelopment.aegis.util.IOUtils;
public class AegisTestRunner extends AndroidJUnitRunner {
static {
BuildConfig.TEST.set(true);
}
@Override
public Application newApplication(ClassLoader cl, String name, Context context)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Instrumentation.newApplication(AegisTestApplication_Application.class, context);
}
@Override
public void callApplicationOnCreate(Application app) {
Context context = app.getApplicationContext();
@@ -0,0 +1,400 @@
package com.beemdevelopment.aegis;
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.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.GoogleAuthUriImporter;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.PreferencesActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
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.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class BackupExportTest extends AegisTest {
private final ActivityScenarioRule<PreferencesActivity> _activityRule = new ActivityScenarioRule<>(PreferencesActivity.class);
@Rule
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Before
public void setUp() {
Intents.init();
}
@After
public void tearDown() {
Intents.release();
}
@Test
public void testPlainVaultExportPlainJson() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testPlainVaultExportPlainTxt() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testPlainVaultExportEncryptedJson() {
initPlainVault();
openExportDialog();
File file = doExport();
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());
readVault(file, VAULT_PASSWORD);
}
@Test
public void testEncryptedVaultExportPlainJson() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testEncryptedVaultExportPlainTxt() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testEncryptedVaultExportEncryptedJson() {
initEncryptedVault();
openExportDialog();
File file = doExport();
readVault(file, VAULT_PASSWORD);
}
@Test
public void testPlainVaultExportHtml() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
}
@Test
public void testEncryptedVaultExportHtml() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
}
@Test
public void testSeparateExportPassword() {
initEncryptedVault();
setSeparateBackupExportPassword();
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD);
}
@Test
public void testChangeBackupPassword() throws SlotIntegrityException {
initEncryptedVault();
setSeparateBackupExportPassword();
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_change_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_BACKUP_PASSWORD, VAULT_BACKUP_PASSWORD_CHANGED);
}
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
decryptPasswordSlot(slot, VAULT_PASSWORD);
}
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD_CHANGED);
}
@Test
public void testChangePasswordHavingBackupPassword() throws SlotIntegrityException {
initEncryptedVault();
setSeparateBackupExportPassword();
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_set_password_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_PASSWORD_CHANGED);
}
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
decryptPasswordSlot(slot, VAULT_BACKUP_PASSWORD);
}
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD);
}
private void setSeparateBackupExportPassword() {
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 0);
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_BACKUP_PASSWORD);
}
}
private void verifyPasswordSlotChange(VaultFileCredentials creds, PasswordSlot slot, String oldPassword, String newPassword) {
assertThrows(SlotIntegrityException.class, () -> decryptPasswordSlot(slot, oldPassword));
MasterKey masterKey;
try {
masterKey = decryptPasswordSlot(slot, newPassword);
} catch (SlotIntegrityException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
assertArrayEquals(creds.getKey().getBytes(), masterKey.getBytes());
}
private File doExport() {
File file = getExportFileUri();
Intent resultData = new Intent();
resultData.setData(Uri.fromFile(file));
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
onView(withId(android.R.id.button1)).perform(click());
return file;
}
private void openExportDialog() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_import_export_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_export_title)), click()));
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, String password) throws SlotIntegrityException {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
try {
Cipher cipher = slot.createDecryptCipher(derivedKey);
return slot.getKey(cipher);
} catch (SlotException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
}
private File getExportFileUri() {
String dirName = Hex.encode(CryptoUtils.generateRandomBytes(8));
File dir = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), String.format("export-%s", dirName));
if (!dir.mkdirs()) {
throw new RuntimeException(String.format("Unable to create export directory: %s", dir));
}
VaultBackupManager.FileInfo fileInfo = new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT);
return new File(dir, fileInfo.toString());
}
private VaultRepository readVault(File file, @Nullable String password) {
VaultRepository repo;
try (InputStream inStream = new FileInputStream(file)) {
byte[] bytes = IOUtils.readAll(inStream);
VaultFile vaultFile = VaultFile.fromBytes(bytes);
VaultFileCredentials creds = null;
if (password != null) {
SlotList slots = vaultFile.getHeader().getSlots();
for (PasswordSlot slot : slots.findAll(PasswordSlot.class)) {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
Cipher cipher = slot.createDecryptCipher(derivedKey);
MasterKey masterKey = slot.getKey(cipher);
creds = new VaultFileCredentials(masterKey, slots);
break;
}
}
repo = VaultRepository.fromFile(getInstrumentation().getContext(), vaultFile, creds);
} catch (SlotException | SlotIntegrityException | VaultRepositoryException | VaultFileException | IOException e) {
throw new RuntimeException("Unable to read back vault file", e);
}
checkReadEntries(repo.getEntries());
return repo;
}
private void readTxtExport(File file) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getInstrumentation().getContext());
Collection<VaultEntry> entries;
try (InputStream inStream = new FileInputStream(file)) {
DatabaseImporter.State state = importer.read(inStream);
DatabaseImporter.Result result = state.convert();
entries = result.getEntries().getValues();
} catch (DatabaseImporterException | IOException e) {
throw new RuntimeException("Unable to read txt export file", e);
}
checkReadEntries(entries);
}
private void checkReadEntries(Collection<VaultEntry> entries) {
List<VaultEntry> vectors = VaultEntries.get();
assertEquals(vectors.size(), entries.size());
int i = 0;
for (VaultEntry entry : entries) {
VaultEntry vector = vectors.get(i);
String message = String.format("Entries are not equivalent: (%s) (%s)", vector.toJson().toString(), entry.toJson().toString());
assertTrue(message, vector.equivalates(entry));
try {
assertEquals(message, vector.getInfo().getOtp(), entry.getInfo().getOtp());
} catch (OtpInfoException e) {
throw new RuntimeException("Unable to generate OTP", e);
}
i++;
}
}
}
@@ -1,5 +1,10 @@
package com.beemdevelopment.aegis;
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;
import android.content.Intent;
import android.net.Uri;
@@ -16,38 +21,36 @@ 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;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class IntentTest extends AegisTest {
public class DeepLinkTest extends AegisTest {
@Before
public void before() {
initVault();
initEmptyEncryptedVault();
}
@Test
public void doDeepLinkIntent() {
public void testDeepLinkIntent() {
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];
VaultEntry createdEntry = (VaultEntry) _vaultManager.getVault().getEntries().toArray()[0];
assertTrue(createdEntry.equivalates(entry));
}
@Test
public void doDeepLinkIntent_Empty() {
public void testDeepLinkIntent_Empty() {
launch(null);
}
@Test
public void doDeepLinkIntent_Bad() {
public void testDeepLinkIntent_Bad() {
launch(Uri.parse("otpauth://bad"));
onView(withId(android.R.id.button1)).perform(click());
}
@@ -0,0 +1,54 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class EmptySecretTest extends AegisTest {
private ActivityScenario<MainActivity> _scenario;
@Before
public void before() throws OtpInfoException {
initEmptyPlainVault();
_vaultManager.getVault().addEntry(new VaultEntry(new TotpInfo(new byte[0])));
_scenario = ActivityScenario.launch(MainActivity.class);
}
@After
public void after() {
_scenario.close();
}
@Test
public void testVaultEntryEmptySecret() {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.error_all_caps)), click()));
}
}
@@ -1,41 +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.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.not;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.net.Uri;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.intent.Intents;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.viewpager2.widget.ViewPager2;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class IntroTest extends AegisTest {
private final ActivityScenarioRule<IntroActivity> _activityRule = new ActivityScenarioRule<>(IntroActivity.class);
private ViewPager2IdlingResource _viewPager2IdlingResource;
@Rule
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Before
public void setUp() {
Intents.init();
_activityRule.getScenario().onActivity(activity -> {
_viewPager2IdlingResource = new ViewPager2IdlingResource(activity.findViewById(R.id.pager), "viewPagerIdlingResource");
IdlingRegistry.getInstance().register(_viewPager2IdlingResource);
});
}
@After
public void tearDown() {
Intents.release();
IdlingRegistry.getInstance().unregister(_viewPager2IdlingResource);
}
@Test
public void doIntro_None() {
public void testIntro_None() {
assertFalse(_prefs.isIntroDone());
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
@@ -48,15 +95,16 @@ public class IntroTest extends AegisTest {
next.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
VaultRepository vault = _vaultManager.getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(getVault().getCredentials());
assertNull(vault.getCredentials());
assertTrue(_prefs.isIntroDone());
}
@Test
public void doIntro_Password() {
public void testIntro_Password() {
assertFalse(_prefs.isIntroDone());
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
@@ -79,10 +127,102 @@ public class IntroTest extends AegisTest {
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
SlotList slots = getVault().getCredentials().getSlots();
VaultRepository vault = _vaultManager.getVault();
SlotList slots = vault.getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
assertTrue(_prefs.isIntroDone());
}
@Test
public void testIntro_Import_Plain() {
assertFalse(_prefs.isIntroDone());
Uri uri = getResourceUri("aegis_plain.json");
Intent resultData = new Intent();
resultData.setData(uri);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
ViewInteraction next = onView(withId(R.id.btnNext));
onView(withId(R.id.btnImport)).perform(click());
next.perform(click());
VaultRepository vault = _vaultManager.getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
assertTrue(_prefs.isIntroDone());
}
@Test
public void testIntro_Import_Encrypted() {
assertFalse(_prefs.isIntroDone());
Uri uri = getResourceUri("aegis_encrypted.json");
Intent resultData = new Intent();
resultData.setData(uri);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
ViewInteraction next = onView(withId(R.id.btnNext));
onView(withId(R.id.btnImport)).perform(click());
onView(withId(R.id.text_input)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
next.perform(click());
VaultRepository vault = _vaultManager.getVault();
SlotList slots = vault.getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
assertTrue(_prefs.isIntroDone());
}
private Uri getResourceUri(String resourceName) {
File targetFile = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), resourceName);
try (InputStream inStream = getClass().getResourceAsStream(resourceName);
FileOutputStream outStream = new FileOutputStream(targetFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Uri.fromFile(targetFile);
}
// Source: https://stackoverflow.com/a/32763454/12972657
private static class ViewPager2IdlingResource implements IdlingResource {
private final String _resName;
private boolean _isIdle = true;
private IdlingResource.ResourceCallback _resourceCallback = null;
public ViewPager2IdlingResource(ViewPager2 viewPager, String resName) {
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrollStateChanged(int state) {
_isIdle = (state == ViewPager2.SCROLL_STATE_IDLE || state == ViewPager2.SCROLL_STATE_DRAGGING);
if (_isIdle && _resourceCallback != null) {
_resourceCallback.onTransitionToIdle();
}
}
});
_resName = resName;
}
@Override
public String getName() {
return _resName;
}
@Override
public boolean isIdleNow() {
return _isIdle;
}
@Override
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
_resourceCallback = resourceCallback;
}
}
}
@@ -1,30 +1,5 @@
package com.beemdevelopment.aegis;
import androidx.annotation.IdRes;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
@@ -34,41 +9,88 @@ 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.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
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.allOf;
import static org.hamcrest.Matchers.containsString;
import androidx.annotation.IdRes;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.MotpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class OverallTest extends AegisTest {
private static final String _groupName = "Test";
private final ActivityScenarioRule<MainActivity> _activityRule = new ActivityScenarioRule<>(MainActivity.class);
@Rule
public final ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Test
public void doOverallTest() {
public void testOverall() {
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)).perform(click()).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();
VaultRepository vault = _vaultManager.getVault();
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
assertTrue(_prefs.isIntroDone());
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")
generateEntry(SteamInfo.class, "Gaben", "Steam"),
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16),
generateEntry(MotpInfo.class, "Jimmy McGill", "PfSense", 16)
);
for (VaultEntry entry : entries) {
addEntry(entry);
@@ -81,13 +103,19 @@ public class OverallTest extends AegisTest {
}
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.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
}
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
_activityRule.getScenario().onActivity(activity -> {
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
});
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.dropdown_group)).perform(click());
@@ -106,21 +134,22 @@ public class OverallTest extends AegisTest {
changeGroupFilter(_groupName);
changeGroupFilter(null);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, 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(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
onView(withText(R.string.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();
vault = _vaultManager.getVault();
openContextualActionModeOverflowMenu();
onView(withText(R.string.action_settings)).perform(click());
@@ -169,6 +198,10 @@ public class OverallTest extends AegisTest {
otpType = "HOTP";
} else if (entry.getInfo() instanceof SteamInfo) {
otpType = "Steam";
} else if (entry.getInfo() instanceof YandexInfo) {
otpType = "Yandex";
} else if (entry.getInfo() instanceof MotpInfo) {
otpType = "MOTP";
} else if (entry.getInfo() instanceof TotpInfo) {
otpType = "TOTP";
} else {
@@ -179,9 +212,25 @@ public class OverallTest extends AegisTest {
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
}
String secret = Base32.encode(entry.getInfo().getSecret());
String secret;
if (Objects.equals(entry.getInfo().getTypeId(), MotpInfo.ID)) {
secret = Hex.encode(entry.getInfo().getSecret());
} else {
secret = Base32.encode(entry.getInfo().getSecret());
}
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
if (entry.getInfo() instanceof YandexInfo) {
String pin = "123456";
((YandexInfo) entry.getInfo()).setPin(pin);
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
} else if (entry.getInfo() instanceof MotpInfo) {
String pin = "1234";
((MotpInfo) entry.getInfo()).setPin(pin);
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
}
onView(withId(R.id.action_save)).perform(click());
}
}
@@ -0,0 +1,58 @@
package com.beemdevelopment.aegis;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
import com.beemdevelopment.aegis.ui.PanicResponderActivity;
import com.beemdevelopment.aegis.vault.VaultRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class PanicTriggerTest extends AegisTest {
@Before
public void before() {
initEncryptedVault();
}
@Test
public void testPanicTriggerDisabled() {
assertFalse(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertTrue(_vaultManager.isVaultLoaded());
_vaultManager.getVault();
assertTrue(VaultRepository.fileExists(getApp()));
}
@Test
public void testPanicTriggerEnabled() {
_prefs.setIsPanicTriggerEnabled(true);
assertTrue(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertFalse(_vaultManager.isVaultLoaded());
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
assertFalse(VaultRepository.fileExists(getApp()));
}
private void launchPanic() {
Intent intent = new Intent(PanicResponderActivity.PANIC_TRIGGER_ACTION);
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
ActivityTestRule<PanicResponderActivity> rule = new ActivityTestRule<>(PanicResponderActivity.class);
rule.launchActivity(intent);
}
}
@@ -0,0 +1,36 @@
package com.beemdevelopment.aegis.rules;
import android.graphics.Bitmap;
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
import androidx.test.runner.screenshot.Screenshot;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import java.io.IOException;
import java.util.HashSet;
public class ScreenshotTestRule extends TestWatcher {
@Override
protected void failed(Throwable e, Description description) {
super.failed(e, description);
String filename = description.getTestClass().getSimpleName() + "-" + description.getMethodName();
ScreenCapture capture = Screenshot.capture();
capture.setName(filename);
capture.setFormat(Bitmap.CompressFormat.PNG);
HashSet<ScreenCaptureProcessor> processors = new HashSet<>();
processors.add(new BasicScreenCaptureProcessor());
try {
capture.process(processors);
} catch (IOException e2) {
e.printStackTrace();
}
}
}
@@ -1,5 +1,11 @@
package com.beemdevelopment.aegis.vault;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -10,30 +16,28 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class VaultManagerTest extends AegisTest {
public class VaultRepositoryTest extends AegisTest {
@Before
public void before() {
initVault();
initEncryptedVault();
}
@Test
public void testToggleEncryption() throws VaultManagerException {
getVault().disableEncryption();
assertFalse(getVault().isEncryptionEnabled());
assertNull(getVault().getCredentials());
public void testToggleEncryption() throws VaultRepositoryException {
VaultRepository vault = _vaultManager.getVault();
_vaultManager.disableEncryption();
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
VaultFileCredentials creds = generateCredentials();
getVault().enableEncryption(creds);
assertTrue(getVault().isEncryptionEnabled());
assertNotNull(getVault().getCredentials());
assertEquals(getVault().getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
_vaultManager.enableEncryption(creds);
assertTrue(vault.isEncryptionEnabled());
assertNotNull(vault.getCredentials());
assertEquals(vault.getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
}
}
@@ -0,0 +1 @@
../../../../../../test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java
@@ -0,0 +1 @@
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_encrypted.json
@@ -0,0 +1 @@
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_plain.json
+54 -10
View File
@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- NOTE: Disabled for now. See issue: #1047
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
@@ -17,13 +20,14 @@
android:name=".AegisApplication"
android:allowBackup="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules"
android:fullBackupContent="@xml/backup_rules_old"
android:dataExtractionRules="@xml/backup_rules"
android:backupAgent=".AegisBackupAgent"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/Theme.Aegis.Launch"
tools:replace="android:theme">
tools:targetApi="tiramisu">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
<activity
@@ -34,6 +38,7 @@
android:label="@string/title_activity_import_entries" />
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:label="${title}">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -50,10 +55,10 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
@@ -72,14 +77,16 @@
<activity
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
<activity
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.AssignIconsActivity"
android:label="@string/title_activity_assign_icons"/>
<activity android:name=".ui.LicensesActivity"
android:label="@string/title_activity_licenses"/>
<activity
android:name=".ui.PanicResponderActivity"
android:exported="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
@@ -90,7 +97,41 @@
</activity>
<activity android:name=".ui.ExitActivity" />
<!-- NOTE: Disabled for now. See issue: #1047
<service android:name=".services.NotificationService" />
-->
<service
android:name=".services.LaunchAppTileService"
android:label="@string/tile_open_vault"
android:icon="@drawable/ic_aegis_quicksettings"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>
<service
android:name=".services.LaunchScannerTileService"
android:label="@string/tile_open_scanner"
android:icon="@drawable/ic_aegis_quicksettings"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>
<receiver android:name=".receivers.VaultLockReceiver" android:exported="false">
<intent-filter>
<action android:name="${applicationId}.LOCK_VAULT" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
@@ -107,6 +148,7 @@
</application>
<queries>
<package android:name="me.jmh.authenticatorpro" />
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />
@@ -114,6 +156,8 @@
<package android:name="com.azure.authenticator" />
<package android:name="com.valvesoftware.android.steam.community" />
<package android:name="com.authenticator.authservice2" />
<package android:name="com.duosecurity.duomobile" />
<package android:name="com.blizzard.bma" />
</queries>
</manifest>
+168
View File
@@ -31,6 +31,174 @@
</head>
<body>
<div></div>
<h3>Version 3.0</h3>
<h4>New</h4>
<ul>
<li>Material 3 (and Material You)</li>
<li>Automatic assignment of icons to entries</li>
<li>Ability to select all entries in one go</li>
<li>Support for importing 2FAS schema v4 backups</li>
<li>Sort entries based on the last time they were used</li>
<li>Some clarifications related to importing and backup permission errors</li>
<li>Preparations for the ability to assign a single entry to multiple groups</li>
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
<li>A new look for the third-party licenses list</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Directly importing from Authy using root would fail</li>
<li>Minor glitches related to animation duration scale settings</li>
<li>Various stability improvements</li>
</ul>
<h3>Version 2.2.2</h3>
<h4>New</h4>
<ul>
<li>An optional name field for icon packs to bypass filename character restrictions</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The Authenticator Pro importer only supported the legacy backup format</li>
<li>A crash could occur in the tile service</li>
</ul>
<h3>Version 2.2.1</h3>
<h4>New</h4>
<ul>
<li>Ability to automatically skip potential duplicates when importing entries</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Biometrics button on the unlock screen was unresponsive</li>
</ul>
<h3>Version 2.2</h3>
<h4>New</h4>
<ul>
<li>Authenticator Pro encrypted import support</li>
<li>Ability to change account name position</li>
<li>A new dialog explaining how our password reminder works</li>
<li>Ability to change copy behavior</li>
<li>Ability to only show account names when necessary</li>
<li>New view mode: Tiles/Grid</li>
<li>Added translation: Dutch (Frysian)</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Deleting an entry while a search filter is active now shows the correct state</li>
<li>Aegis now fully respects system animation settings</li>
</ul>
<h3>Version 2.1.3</h3>
<h4>New</h4>
<ul>
<li>Option to disable the backup reminder</li>
<li>Improved group selection dropdown during vault export</li>
<li>New translation: Hebrew</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>A crash could occur because a Toast was incorrectly created</li>
</ul>
<h3>Version 2.1.2</h3>
<h4>Fixes</h4>
<ul>
<li>A crash could occur when changing an entry in such a way that it is filtered out from the entry list</li>
</ul>
<h3>Version 2.1.1</h3>
<h4>New</h4>
<ul>
<li>An option to export the vault as an HTML file</li>
<li>Support for importing from Battle.net Authenticator (root required)</li>
<li>An option to hide entry icons</li>
<li>An option to only include certain groups in an export</li>
<li>Copying a token now takes a second tap if tap to reveal is enabled</li>
<li>The ability to copy the URI when transferring entries through QR codes</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The lock notification would remain after locking the vault in certain cases. For now, we've disabled the notification entirely.</li>
<li>Making changes to an entry while having one or more favorited entries in the vault could result in buggy ordering</li>
<li>Tapping to the reveal a token could increase the height of the entry in certain view modes on recent Android versions</li>
<li>The backup reminder was unclear about when the last successful backup took place</li>
<li>Users could accidentally select MD5 as the hash algorithm for non-mOTP entry types, causing crashes at seemingly random intervals. Any users who have gotten themselves into this situation will see these bad entries get reset to SHA1.</li>
<li>Importing from certain apps would cause a crash if an empty password was entered</li>
<li>The andOTP importer could hang indefinitely if the user accidentally selected a non-andOTP file.</li>
<li>Various other stability improvements</li>
</ul>
<h3>Version 2.1</h3>
<h4>New</h4>
<ul>
<li>Support for mOTP</li>
<li>Support for Yandex OTP (Experimental)</li>
<li>An Adaptive Icon for Material You</li>
<li>Ability to favorite certain entries and pin them to the top of the entry list</li>
<li>Ability to filter by entries that are not in a group</li>
<li>Ability to set a separate password that is used for encrypting backups and exports</li>
<li>Support for predictive back gesture</li>
<li>Improved overview of backup status in preferences</li>
<li>Additional options for code digit grouping</li>
<li>Support for importing from Duo</li>
<li>Support for importing from Bitwarden</li>
<li>Support for importing multiple QR code images in one go</li>
<li>Support for scanning Google Authenticator export QR codes from image files</li>
<li>Display some extra information in the dialog displayed when deleting an entry</li>
<li>An option to export through Google Authenticator export QR code images</li>
<li>An option to import an existing vault file from the first page in the intro</li>
<li>An option to minimize the app after copying a token</li>
<li>A count of the total number of entries is displayed at the bottom of the entry list</li>
<li>A backup reminder is shown if changes were made to the vault, but no backup or export has been created yet since then</li>
<li>A warning is shown after a plaintext export has been made</li>
<li>An option to focus search immediately after the app starts</li>
<li>Allow customization of the frequency of the password reminder</li>
<li>Allow sharing text to Aegis in the format of a Google Authenticator URI to add as a new entry</li>
<li>Always allow D2D (device-to-device) Android backups regardless of backup settings</li>
<li>Mark clipboard data as sensitive when copying tokens so that Android will mask them in the UI</li>
<li>Updated translations for almost all languages</li>
<li>New languages: Asturian, Catalan, Galician</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Various reliability improvements for the QR code scanner</li>
<li>The floating action button was glitchy when making small entry list scroll movements</li>
<li>The vault unlocked notification was never shown and was still using the old app icon</li>
<li>The automatically generated entry icon was broken if the entry name/issuer is a multi-codepoint character (certain emoji's, for example)</li>
<li>The PIN keyboard was not disabled after enabling encryption</li>
<li>The password prompt message was unclear when importing from a file</li>
<li>The entry list was not sorted correctly if a change to an entry caused its location to change</li>
<li>Quickly double-tapping on the copy button would cause a crash</li>
<li>Importing an entry with an empty secret would cause a crash loop</li>
<li>On certain devices, it was not possible to import icon packs because the .ZIP files would be grayed out</li>
<li>An unclear error message was shown when trying to import from Steam and Google Authenticator</li>
<li>Various other minor UI and stability improvements</li>
</ul>
<h3>Version 2.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for importing 2FAS Authenticator's new backup format</li>
</ul>
<h3>Version 2.0.2</h3>
<h4>New</h4>
<ul>
<li>Add a note field to entries</li>
<li>An option to pause code updating of highlighted entries</li>
<li>New translation: Lithuanian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Minor UI and stability improvements</li>
<li>The Microsoft Authenticator importer did not accept spaces and dashes in secrets</li>
</ul>
<h3>Version 2.0.1</h3>
<h4>New</h4>
<ul>
<li>Support for sorting on most used tokens</li>
<li>Some minor UX and stability improvements</li>
<li>New translation: Vietnamese</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>QR code information was decoded incorrectly in some cases if the app was set to a certain language (Turkish, for example)</li>
</ul>
<h3>Version 2.0</h3>
<h4>New</h4>
<ul>
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum AccountNamePosition {
HIDDEN,
END,
BELOW;
private static AccountNamePosition[] _values;
static {
_values = values();
}
public static AccountNamePosition fromInteger(int x) {
return _values[x];
}
}
@@ -1,239 +1,8 @@
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 dagger.hilt.android.HiltAndroidApp;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
@HiltAndroidApp
public class AegisApplication extends AegisApplicationBase {
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class AegisApplication extends Application {
private VaultFile _vaultFile;
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private boolean _blockAutoLock;
private IconPackManager _iconPackManager;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
static {
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER));
}
@Override
public void onCreate() {
super.onCreate();
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
_iconPackManager = new IconPackManager(this);
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 boolean isVaultLocked() {
return _manager == null;
}
/**
* Loads the vault file from disk at the default location, stores an internal
* reference to it for future use and returns it. This must only be called before
* initVaultManager() or after lock().
*/
public VaultFile loadVaultFile() throws VaultManagerException {
if (!isVaultLocked()) {
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readVaultFile(this);
}
return _vaultFile;
}
/**
* Initializes the vault manager by decrypting the given vaultFile with the given
* creds. This removes the internal reference to the raw vault file.
*/
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
_vaultFile = null;
_manager = VaultManager.init(this, vaultFile, creds);
return _manager;
}
/**
* Initializes the vault manager with the given vault and creds. This removes the
* internal reference to the raw vault file.
*/
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
_vaultFile = null;
_manager = new VaultManager(this, vault, creds);
return _manager;
}
public VaultManager getVaultManager() {
return _manager;
}
public IconPackManager getIconPackManager() {
return _iconPackManager;
}
public Preferences getPreferences() {
return _prefs;
}
public boolean isAutoLockEnabled(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);
}
/**
* Sets whether to block automatic lock on minimization. This should only be called
* by activities before invoking an intent that shows a DocumentsUI, because that
* action leads AppLifecycleObserver to believe that the app has been minimized.
*/
public void setBlockAutoLock(boolean block) {
_blockAutoLock = block;
}
/**
* Locks the vault and the app.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
public void lock(boolean userInitiated) {
_manager.destroy();
_manager = null;
for (LockListener listener : _lockListeners) {
listener.onLocked(userInitiated);
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
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_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)
&& !_blockAutoLock) {
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);
}
}
@@ -0,0 +1,121 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
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 androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.topjohnwu.superuser.Shell;
import java.util.Collections;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
public abstract class AegisApplicationBase extends Application {
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private VaultManager _vaultManager;
static {
// Enable verbose libsu logging in debug builds
Shell.enableVerboseLogging = BuildConfig.DEBUG;
}
@Override
public void onCreate() {
super.onCreate();
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
VaultLockReceiver lockReceiver = new VaultLockReceiver();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}*/
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
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_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
&& _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
&& !_vaultManager.isAutoLockBlocked()) {
_vaultManager.lock(false);
}
}
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
interface EntryPoint {
VaultManager getVaultManager();
}
}
@@ -9,22 +9,27 @@ import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = BackupAgent.class.getSimpleName();
private static final String TAG = AegisBackupAgent.class.getSimpleName();
private Preferences _prefs;
@Override
public void onCreate() {
super.onCreate();
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this);
}
@@ -34,36 +39,51 @@ public class AegisBackupAgent extends BackupAgent {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
if (!_prefs.isAndroidBackupsEnabled()) {
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
if (isD2D) {
Log.i(TAG, "onFullBackup(): allowing D2D transfer");
} else if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// first copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir();
throw e;
}
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
// We perform a catch of any Exception here to make sure we also
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
super.onFullBackup(data);
} catch (IOException e) {
fullBackup(data);
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
throw e;
} finally {
deleteBackupDir();
}
Log.i(TAG, "onFullBackup() finished");
}
private void fullBackup(FullBackupDataOutput data) throws IOException {
// First copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
VaultFile vaultFile = VaultRepository.readVaultFile(this);
byte[] bytes = vaultFile.exportable().toBytes();
outputStream.write(bytes);
} catch (VaultRepositoryException | IOException e) {
deleteBackupDir();
throw new IOException(e);
}
// Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} finally {
deleteBackupDir();
}
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
@@ -72,7 +92,7 @@ public class AegisBackupAgent extends BackupAgent {
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultManager.writeToFile(this, inStream);
VaultRepository.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
@@ -102,17 +122,19 @@ public class AegisBackupAgent extends BackupAgent {
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
if (dir == null || (!dir.exists() && !dir.mkdir())) {
throw new IOException(String.format("Unable to create backup directory: %s", dir));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true);
if (dir != null) {
IOUtils.clearDirectory(dir, true);
}
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
return new File(new File(getFilesDir(), "backup"), VaultRepository.FILENAME);
}
}
@@ -0,0 +1,35 @@
package com.beemdevelopment.aegis;
import android.content.Context;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;
@Module
@InstallIn(SingletonComponent.class)
public class AegisModule {
@Provides
@Singleton
public static IconPackManager provideIconPackManager(@ApplicationContext Context context) {
return new IconPackManager(context);
}
@Provides
@Singleton
public static VaultManager provideVaultManager(@ApplicationContext Context context) {
return new VaultManager(context);
}
@Provides
public static Preferences providePreferences(@ApplicationContext Context context) {
return new Preferences(context);
}
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum CopyBehavior {
NEVER,
SINGLETAP,
DOUBLETAP;
private static CopyBehavior[] _values;
static {
_values = values();
}
public static CopyBehavior fromInteger(int x) {
return _values[x];
}
}
@@ -0,0 +1,56 @@
package com.beemdevelopment.aegis;
import androidx.annotation.StringRes;
import java.util.concurrent.TimeUnit;
public enum PassReminderFreq {
NEVER,
WEEKLY,
BIWEEKLY,
MONTHLY,
QUARTERLY;
public long getDurationMillis() {
long weeks;
switch (this) {
case WEEKLY:
weeks = 1;
break;
case BIWEEKLY:
weeks = 2;
break;
case MONTHLY:
weeks = 4;
break;
case QUARTERLY:
weeks = 13;
break;
default:
weeks = 0;
break;
}
return TimeUnit.MILLISECONDS.convert(weeks * 7L, TimeUnit.DAYS);
}
@StringRes
public int getStringRes() {
switch (this) {
case WEEKLY:
return R.string.password_reminder_freq_weekly;
case BIWEEKLY:
return R.string.password_reminder_freq_biweekly;
case MONTHLY:
return R.string.password_reminder_freq_monthly;
case QUARTERLY:
return R.string.password_reminder_freq_quarterly;
default:
return R.string.password_reminder_freq_never;
}
}
public static PassReminderFreq fromInteger(int i) {
return PassReminderFreq.values()[i];
}
}
@@ -7,15 +7,26 @@ import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class Preferences {
public static final int AUTO_LOCK_OFF = 1 << 0;
@@ -37,6 +48,22 @@ public class Preferences {
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
migratePreferences();
}
public void migratePreferences() {
// Change copy on tap to copy behavior to new preference and delete the old key
String prefCopyOnTapKey = "pref_copy_on_tap";
if (_prefs.contains(prefCopyOnTapKey)) {
boolean isCopyOnTapEnabled = _prefs.getBoolean(prefCopyOnTapKey, false);
if (isCopyOnTapEnabled) {
setCopyBehavior(CopyBehavior.SINGLETAP);
}
_prefs.edit().remove(prefCopyOnTapKey).apply();
}
}
public boolean isTapToRevealEnabled() {
@@ -47,43 +74,79 @@ public class Preferences {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false;
return _prefs.getBoolean("pref_pause_entry", false);
}
public boolean isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public void setIsPanicTriggerEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_panic_trigger", enabled).apply();
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isPasswordReminderEnabled() {
return _prefs.getBoolean("pref_password_reminder", true);
public PassReminderFreq getPasswordReminderFrequency() {
final String key = "pref_password_reminder_freq";
if (_prefs.contains(key) || _prefs.getBoolean("pref_password_reminder", true)) {
int i = _prefs.getInt(key, PassReminderFreq.BIWEEKLY.ordinal());
return PassReminderFreq.fromInteger(i);
}
return PassReminderFreq.NEVER;
}
public void setPasswordReminderFrequency(PassReminderFreq freq) {
_prefs.edit().putInt("pref_password_reminder_freq", freq.ordinal()).apply();
}
public boolean isPasswordReminderNeeded() {
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return isPasswordReminderEnabled() && days >= 30;
return isPasswordReminderNeeded(new Date().getTime());
}
boolean isPasswordReminderNeeded(long currTime) {
PassReminderFreq freq = getPasswordReminderFrequency();
if (freq == PassReminderFreq.NEVER) {
return false;
}
long duration = currTime - getPasswordReminderTimestamp().getTime();
return duration >= freq.getDurationMillis();
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
void setPasswordReminderTimestamp(long timestamp) {
_prefs.edit().putLong("pref_password_reminder_counter", timestamp).apply();
}
public void resetPasswordReminderTimestamp() {
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
setPasswordReminderTimestamp(new Date().getTime());
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", true);
public boolean onlyShowNecessaryAccountNames() { return _prefs.getBoolean("pref_shared_issuer_account_name", false); }
public boolean isIconVisible() {
return _prefs.getBoolean("pref_show_icons", true);
}
public int getCodeGroupSize() {
if (_prefs.getBoolean("pref_code_group_size", false)) {
return 2;
} else {
return 3;
}
public CodeGrouping getCodeGroupSize() {
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
return CodeGrouping.valueOf(value);
}
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
}
public boolean isIntroDone() {
@@ -139,6 +202,10 @@ public class Preferences {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public boolean isDynamicColorsEnabled() {
return _prefs.getBoolean("pref_dynamic_colors", false);
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
@@ -147,12 +214,117 @@ public class Preferences {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public AccountNamePosition getAccountNamePosition() {
return AccountNamePosition.fromInteger(_prefs.getInt("pref_account_name_position", AccountNamePosition.END.ordinal()));
}
public void setAccountNamePosition(AccountNamePosition accountNamePosition) {
_prefs.edit().putInt("pref_account_name_position", accountNamePosition.ordinal()).apply();
}
public Integer getUsageCount(UUID uuid) {
Integer usageCount = getUsageCounts().get(uuid);
return usageCount != null ? usageCount : 0;
}
public void resetUsageCount(UUID uuid) {
Map<UUID, Integer> usageCounts = getUsageCounts();
usageCounts.put(uuid, 0);
setUsageCount(usageCounts);
}
public long getLastUsedTimestamp(UUID uuid) {
Map<UUID, Long> timestamps = getLastUsedTimestamps();
if (timestamps != null && timestamps.size() > 0){
Long timestamp = timestamps.get(uuid);
return timestamp != null ? timestamp : 0;
}
return 0;
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Long> getLastUsedTimestamps() {
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
try {
JSONArray arr = new JSONArray(lastUsedTimestamp);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
}
} catch (JSONException ignored) {
}
return lastUsedTimestamps;
}
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
JSONArray lastUsedTimestampJson = new JSONArray();
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("timestamp", entry.getValue());
lastUsedTimestampJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
try {
JSONArray arr = new JSONArray(usageCount);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
}
} catch (JSONException ignored) {
}
return usageCounts;
}
public void setUsageCount(Map<UUID, Integer> usageCounts) {
JSONArray usageCountJson = new JSONArray();
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("count", entry.getValue());
usageCountJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
public String getLanguage() {
return _prefs.getString("pref_lang", "system");
}
public void setLanguage(String lang) {
_prefs.edit().putString("pref_lang", lang).apply();
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
String lang = getLanguage();
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -176,6 +348,7 @@ public class Preferences {
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
setAndroidBackupResult(null);
}
public boolean isBackupsEnabled() {
@@ -184,6 +357,15 @@ public class Preferences {
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
setBuiltInBackupResult(null);
}
public boolean isBackupReminderEnabled() {
return _prefs.getBoolean("pref_backup_reminder", true);
}
public void setIsBackupReminderEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backup_reminder", enabled).apply();
}
public Uri getBackupsLocation() {
@@ -195,6 +377,44 @@ public class Preferences {
return null;
}
public boolean getFocusSearchEnabled() {
return _prefs.getBoolean("pref_focus_search", false);
}
public void setFocusSearch(boolean enabled) {
_prefs.edit().putBoolean("pref_focus_search", enabled).apply();
}
public void setLatestExportTimeNow() {
_prefs.edit().putLong("pref_export_latest", new Date().getTime()).apply();
setIsBackupReminderNeeded(false);
}
public Date getLatestBackupOrExportTime() {
List<Date> dates = new ArrayList<>();
long l = _prefs.getLong("pref_export_latest", 0);
if (l > 0) {
dates.add(new Date(l));
}
BackupResult builtinRes = getBuiltInBackupResult();
if (builtinRes != null) {
dates.add(builtinRes.getTime());
}
BackupResult androidRes = getAndroidBackupResult();
if (androidRes != null) {
dates.add(androidRes.getTime());
}
if (dates.size() == 0) {
return null;
}
return Collections.max(dates, Date::compareTo);
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
@@ -207,12 +427,91 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
public void setAndroidBackupResult(@Nullable BackupResult res) {
setBackupResult(false, res);
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
public void setBuiltInBackupResult(@Nullable BackupResult res) {
setBackupResult(true, res);
}
@Nullable
public BackupResult getAndroidBackupResult() {
return getBackupResult(false);
}
@Nullable
public BackupResult getBuiltInBackupResult() {
return getBackupResult(true);
}
@Nullable
public Preferences.BackupResult getErroredBackupResult() {
Preferences.BackupResult res = getBuiltInBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
res = getAndroidBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
return null;
}
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
String json = null;
if (res != null) {
res.setIsBuiltIn(isBuiltInBackup);
json = res.toJson();
}
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
}
@Nullable
private BackupResult getBackupResult(boolean isBuiltInBackup) {
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
if (json == null) {
return null;
}
try {
BackupResult res = BackupResult.fromJson(json);
res.setIsBuiltIn(isBuiltInBackup);
return res;
} catch (JSONException e) {
return null;
}
}
private static String getBackupResultKey(boolean isBuiltInBackup) {
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
}
public void setIsBackupReminderNeeded(boolean needed) {
if (isBackupsReminderNeeded() != needed) {
_prefs.edit().putBoolean("pref_backups_reminder_needed", needed).apply();
}
}
public boolean isBackupsReminderNeeded() {
return _prefs.getBoolean("pref_backups_reminder_needed", false);
}
public void setIsPlaintextBackupWarningNeeded(boolean needed) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_needed", needed).apply();
}
public boolean isPlaintextBackupWarningNeeded() {
return !isPlaintextBackupWarningDisabled()
&& _prefs.getBoolean("pref_plaintext_backup_warning_needed", false);
}
public void setIsPlaintextBackupWarningDisabled(boolean disabled) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_disabled", disabled).apply();
}
public boolean isPlaintextBackupWarningDisabled() {
return _prefs.getBoolean("pref_plaintext_backup_warning_disabled", false);
}
public boolean isPinKeyboardEnabled() {
@@ -227,30 +526,123 @@ public class Preferences {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
public CopyBehavior getCopyBehavior() {
return CopyBehavior.fromInteger(_prefs.getInt("pref_current_copy_behavior", 0));
}
public void setGroupFilter(List<String> groupFilter) {
public void setCopyBehavior(CopyBehavior copyBehavior) {
_prefs.edit().putInt("pref_current_copy_behavior", copyBehavior.ordinal()).apply();
}
public boolean isMinimizeOnCopyEnabled() {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.getString(i));
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
return Collections.emptySet();
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
private final boolean _isPermissionError;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
}
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
_time = time;
_error = error;
_isPermissionError = isPermissionError;
}
@Nullable
public String getError() {
return _error;
}
public boolean isSuccessful() {
return _error == null;
}
public Date getTime() {
return _time;
}
public String getElapsedSince(Context context) {
return TimeUtils.getElapsedSince(context, _time);
}
public boolean isBuiltIn() {
return _isBuiltIn;
}
private void setIsBuiltIn(boolean isBuiltIn) {
_isBuiltIn = isBuiltIn;
}
public boolean isPermissionError() {
return _isPermissionError;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
obj.put("isPermissionError", _isPermissionError);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj.toString();
}
public static BackupResult fromJson(String json) throws JSONException {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
boolean isPermissionError = obj.optBoolean("isPermissionError");
return new BackupResult(new Date(time), error, isPermissionError);
}
}
public enum CodeGrouping {
HALVES(-1),
NO_GROUPING(-2),
GROUPING_TWOS(2),
GROUPING_THREES(3),
GROUPING_FOURS(4);
private final int _value;
CodeGrouping(int value) {
_value = value;
}
public int getValue() {
return _value;
}
}
}
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
@@ -12,7 +14,9 @@ public enum SortCategory {
ACCOUNT,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED;
ISSUER_REVERSED,
USAGE_COUNT,
LAST_USED;
private static SortCategory[] _values;
@@ -29,17 +33,22 @@ public enum SortCategory {
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator();
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator());
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
break;
case ISSUER:
comparator = new IssuerNameComparator();
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
break;
case USAGE_COUNT:
comparator = Collections.reverseOrder(new UsageCountComparator());
break;
case LAST_USED:
comparator = Collections.reverseOrder(new LastUsedComparator());
}
return comparator;
@@ -57,6 +66,10 @@ public enum SortCategory {
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
case USAGE_COUNT:
return R.id.menu_sort_usage_count;
case LAST_USED:
return R.id.menu_sort_last_used;
default:
return R.id.menu_sort_custom;
}
@@ -10,20 +10,14 @@ public class ThemeMap {
}
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
);
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
Theme.LIGHT, R.style.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
);
}
@@ -5,7 +5,8 @@ import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
SMALL,
TILES;
private static ViewMode[] _values;
@@ -26,19 +27,39 @@ public enum ViewMode {
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
* Retrieves the offset (in dp) that should exist between entries in this view mode.
*/
public float getDividerHeight() {
public float getItemOffset() {
if (this == ViewMode.COMPACT) {
return 0;
return 1;
} else if (this == ViewMode.TILES) {
return 4;
}
return 20;
return 8;
}
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import org.bouncycastle.crypto.generators.SCrypt;
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -0,0 +1,255 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Integers;
import org.bouncycastle.util.Pack;
/**
* Implementation of the scrypt a password-based key derivation function.
* <p>
* Scrypt was created by Colin Percival and is specified in <a
* href="https://tools.ietf.org/html/rfc7914">RFC 7914 - The scrypt Password-Based Key Derivation Function</a>
*/
public class SCrypt
{
private SCrypt()
{
// not used.
}
/**
* Generate a key using the scrypt key derivation function.
*
* @param P the bytes of the pass phrase.
* @param S the salt to use for this invocation.
* @param N CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than
* <code>2^(128 * r / 8)</code>.
* @param r the block size, must be &gt;= 1.
* @param p Parallelization parameter. Must be a positive integer less than or equal to
* <code>Integer.MAX_VALUE / (128 * r * 8)</code>.
* @param dkLen the length of the key to generate.
* @return the generated key.
*/
public static byte[] generate(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
if (P == null)
{
throw new IllegalArgumentException("Passphrase P must be provided.");
}
if (S == null)
{
throw new IllegalArgumentException("Salt S must be provided.");
}
if (N <= 1 || !isPowerOf2(N))
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and a power of 2");
}
// Only value of r that cost (as an int) could be exceeded for is 1
if (r == 1 && N >= 65536)
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and < 65536.");
}
if (r < 1)
{
throw new IllegalArgumentException("Block size r must be >= 1.");
}
int maxParallel = Integer.MAX_VALUE / (128 * r * 8);
if (p < 1 || p > maxParallel)
{
throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
+ " (based on block size r of " + r + ")");
}
if (dkLen < 1)
{
throw new IllegalArgumentException("Generated key length dkLen must be >= 1.");
}
return MFcrypt(P, S, N, r, p, dkLen);
}
private static byte[] MFcrypt(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
int MFLenBytes = r * 128;
byte[] bytes = SingleIterationPBKDF2(P, S, p * MFLenBytes);
int[] B = null;
try
{
int BLen = bytes.length >>> 2;
B = new int[BLen];
Pack.littleEndianToInt(bytes, 0, B);
/*
* Chunk memory allocations; We choose 'd' so that there will be 2**d chunks, each not
* larger than 32KiB, except that the minimum chunk size is 2 * r * 32.
*/
int d = 0, total = N * r;
while ((N - d) > 2 && total > (1 << 10))
{
++d;
total >>>= 1;
}
int MFLenWords = MFLenBytes >>> 2;
for (int BOff = 0; BOff < BLen; BOff += MFLenWords)
{
// TODO These can be done in parallel threads
SMix(B, BOff, N, d, r);
}
Pack.intToLittleEndian(B, bytes, 0);
return SingleIterationPBKDF2(P, bytes, dkLen);
}
finally
{
Clear(bytes);
Clear(B);
}
}
private static byte[] SingleIterationPBKDF2(byte[] P, byte[] S, int dkLen)
{
PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(new SHA256Digest());
pGen.init(P, S, 1);
KeyParameter key = (KeyParameter)pGen.generateDerivedMacParameters(dkLen * 8);
return key.getKey();
}
private static void SMix(int[] B, int BOff, int N, int d, int r)
{
int powN = Integers.numberOfTrailingZeros(N);
int blocksPerChunk = N >>> d;
int chunkCount = 1 << d, chunkMask = blocksPerChunk - 1, chunkPow = powN - d;
int BCount = r * 32;
int[] blockX1 = new int[16];
int[] blockX2 = new int[16];
int[] blockY = new int[BCount];
int[] X = new int[BCount];
int[][] VV = new int[chunkCount][];
try
{
System.arraycopy(B, BOff, X, 0, BCount);
for (int c = 0; c < chunkCount; ++c)
{
int[] V = new int[blocksPerChunk * BCount];
VV[c] = V;
int off = 0;
for (int i = 0; i < blocksPerChunk; i += 2)
{
System.arraycopy(X, 0, V, off, BCount);
off += BCount;
BlockMix(X, blockX1, blockX2, blockY, r);
System.arraycopy(blockY, 0, V, off, BCount);
off += BCount;
BlockMix(blockY, blockX1, blockX2, X, r);
}
}
int mask = N - 1;
for (int i = 0; i < N; ++i)
{
int j = X[BCount - 16] & mask;
int[] V = VV[j >>> chunkPow];
int VOff = (j & chunkMask) * BCount;
System.arraycopy(V, VOff, blockY, 0, BCount);
Xor(blockY, X, 0, blockY);
BlockMix(blockY, blockX1, blockX2, X, r);
}
System.arraycopy(X, 0, B, BOff, BCount);
}
finally
{
ClearAll(VV);
ClearAll(new int[][]{X, blockX1, blockX2, blockY});
}
}
private static void BlockMix(int[] B, int[] X1, int[] X2, int[] Y, int r)
{
System.arraycopy(B, B.length - 16, X1, 0, 16);
int BOff = 0, YOff = 0, halfLen = B.length >>> 1;
for (int i = 2 * r; i > 0; --i)
{
Xor(X1, B, BOff, X2);
Salsa20Engine.salsaCore(8, X2, X1);
System.arraycopy(X1, 0, Y, YOff, 16);
YOff = halfLen + BOff - YOff;
BOff += 16;
}
}
private static void Xor(int[] a, int[] b, int bOff, int[] output)
{
for (int i = output.length - 1; i >= 0; --i)
{
output[i] = a[i] ^ b[bOff + i];
}
}
private static void Clear(byte[] array)
{
if (array != null)
{
Arrays.fill(array, (byte)0);
}
}
private static void Clear(int[] array)
{
if (array != null)
{
Arrays.fill(array, 0);
}
}
private static void ClearAll(int[][] arrays)
{
for (int i = 0; i < arrays.length; ++i)
{
Clear(arrays[i]);
}
}
// note: we know X is non-zero
private static boolean isPowerOf2(int x)
{
return ((x & (x - 1)) == 0);
}
}
@@ -0,0 +1,118 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
/**
* Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
*/
public class Salsa20Engine {
private Salsa20Engine()
{
}
public static void salsaCore(int rounds, int[] input, int[] x)
{
if (input.length != 16)
{
throw new IllegalArgumentException();
}
if (x.length != 16)
{
throw new IllegalArgumentException();
}
if (rounds % 2 != 0)
{
throw new IllegalArgumentException("Number of rounds must be even");
}
int x00 = input[ 0];
int x01 = input[ 1];
int x02 = input[ 2];
int x03 = input[ 3];
int x04 = input[ 4];
int x05 = input[ 5];
int x06 = input[ 6];
int x07 = input[ 7];
int x08 = input[ 8];
int x09 = input[ 9];
int x10 = input[10];
int x11 = input[11];
int x12 = input[12];
int x13 = input[13];
int x14 = input[14];
int x15 = input[15];
for (int i = rounds; i > 0; i -= 2)
{
x04 ^= Integer.rotateLeft(x00 + x12, 7);
x08 ^= Integer.rotateLeft(x04 + x00, 9);
x12 ^= Integer.rotateLeft(x08 + x04, 13);
x00 ^= Integer.rotateLeft(x12 + x08, 18);
x09 ^= Integer.rotateLeft(x05 + x01, 7);
x13 ^= Integer.rotateLeft(x09 + x05, 9);
x01 ^= Integer.rotateLeft(x13 + x09, 13);
x05 ^= Integer.rotateLeft(x01 + x13, 18);
x14 ^= Integer.rotateLeft(x10 + x06, 7);
x02 ^= Integer.rotateLeft(x14 + x10, 9);
x06 ^= Integer.rotateLeft(x02 + x14, 13);
x10 ^= Integer.rotateLeft(x06 + x02, 18);
x03 ^= Integer.rotateLeft(x15 + x11, 7);
x07 ^= Integer.rotateLeft(x03 + x15, 9);
x11 ^= Integer.rotateLeft(x07 + x03, 13);
x15 ^= Integer.rotateLeft(x11 + x07, 18);
x01 ^= Integer.rotateLeft(x00 + x03, 7);
x02 ^= Integer.rotateLeft(x01 + x00, 9);
x03 ^= Integer.rotateLeft(x02 + x01, 13);
x00 ^= Integer.rotateLeft(x03 + x02, 18);
x06 ^= Integer.rotateLeft(x05 + x04, 7);
x07 ^= Integer.rotateLeft(x06 + x05, 9);
x04 ^= Integer.rotateLeft(x07 + x06, 13);
x05 ^= Integer.rotateLeft(x04 + x07, 18);
x11 ^= Integer.rotateLeft(x10 + x09, 7);
x08 ^= Integer.rotateLeft(x11 + x10, 9);
x09 ^= Integer.rotateLeft(x08 + x11, 13);
x10 ^= Integer.rotateLeft(x09 + x08, 18);
x12 ^= Integer.rotateLeft(x15 + x14, 7);
x13 ^= Integer.rotateLeft(x12 + x15, 9);
x14 ^= Integer.rotateLeft(x13 + x12, 13);
x15 ^= Integer.rotateLeft(x14 + x13, 18);
}
x[ 0] = x00 + input[ 0];
x[ 1] = x01 + input[ 1];
x[ 2] = x02 + input[ 2];
x[ 3] = x03 + input[ 3];
x[ 4] = x04 + input[ 4];
x[ 5] = x05 + input[ 5];
x[ 6] = x06 + input[ 6];
x[ 7] = x07 + input[ 7];
x[ 8] = x08 + input[ 8];
x[ 9] = x09 + input[ 9];
x[10] = x10 + input[10];
x[11] = x11 + input[11];
x[12] = x12 + input[12];
x[13] = x13 + input[13];
x[14] = x14 + input[14];
x[15] = x15 + input[15];
}
}
@@ -14,18 +14,7 @@ public class HOTP {
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] hash = mac.doFinal(counterBytes);
byte[] hash = getHash(secret, algo, counter);
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
@@ -37,4 +26,20 @@ public class HOTP {
return new OTP(otp, digits);
}
public static byte[] getHash(byte[] secret, String algo, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
return mac.doFinal(counterBytes);
}
}
@@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.beemdevelopment.aegis.encoding.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MOTP {
private final String _code;
private final int _digits;
private MOTP(String code, int digits) {
_code = code;
_digits = digits;
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
throws NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
throws NoSuchAlgorithmException {
long timeBasedCounter = time / period;
String secretAsString = Hex.encode(secret);
String toDigest = timeBasedCounter + secretAsString + pin;
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
return new MOTP(code, digits);
}
@VisibleForTesting
@NonNull
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algo);
byte[] digest = md.digest(toDigest);
return Hex.encode(digest);
}
@NonNull
@Override
public String toString() {
return _code.substring(0, _digits);
}
}
@@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private int _code;
private int _digits;
private final int _code;
private final int _digits;
public OTP(int code, int digits) {
_code = code;
@@ -0,0 +1,71 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class YAOTP {
private static final int EN_ALPHABET_LENGTH = 26;
private final long _code;
private final int _digits;
private YAOTP(long code, int digits) {
_code = code;
_digits = digits;
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
long seconds = System.currentTimeMillis() / 1000;
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
byte[] pinWithHash;
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
stream.write(pinBytes);
stream.write(secret);
pinWithHash = stream.toByteArray();
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(pinWithHash);
if (keyHash[0] == 0) {
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
}
long counter = (long) Math.floor((double) seconds / period);
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
int offset = periodHash[periodHash.length - 1] & 0xf;
periodHash[offset] &= 0x7f;
long otp = ByteBuffer.wrap(periodHash)
.order(ByteOrder.BIG_ENDIAN)
.getLong(offset);
return new YAOTP(otp, digits);
}
@NonNull
@Override
public String toString() {
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
char[] chars = new char[_digits];
for (int i = _digits - 1; i >= 0; i--) {
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
code /= EN_ALPHABET_LENGTH;
}
return new String(chars);
}
}
@@ -1,14 +0,0 @@
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},};
}
}
@@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public class Base32 {
private Base32() {
@@ -9,7 +12,7 @@ public class Base32 {
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase());
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
@@ -18,4 +21,9 @@ public class Base32 {
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
public static String encode(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return encode(bytes);
}
}
@@ -6,4 +6,8 @@ public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
public EncodingException(String message) {
super(message);
}
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
public class Hex {
private Hex() {
@@ -9,7 +11,7 @@ public class Hex {
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase());
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
@@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.provider.Settings;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
public class AnimationsHelper {
private AnimationsHelper() {
}
public static Animation loadScaledAnimation(Context context, int animationResId) {
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
}
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
animation.setDuration(newDuration);
return animation;
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
Animation animation = controller.getAnimation();
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
return controller;
}
public enum Scale {
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
private final String _setting;
Scale(String setting) {
_setting = setting;
}
public float getValue(Context context) {
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
}
public boolean isZero(Context context) {
return getValue(context) == 0;
}
}
}
@@ -16,9 +16,9 @@ public class FabScrollHelper {
}
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
if (dy > 2 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
setVisible(false);
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
} else if (dy < -2 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
setVisible(true);
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Build;
import android.widget.ImageView;
import com.beemdevelopment.aegis.icons.IconType;
public class IconViewHelper {
private IconViewHelper() {
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
public static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import static android.graphics.ImageFormat.YUV_420_888;
import android.util.Log;
import android.util.Size;
@@ -9,21 +9,12 @@ import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.nio.ByteBuffer;
import static android.graphics.ImageFormat.YUV_420_888;
import static android.graphics.ImageFormat.YUV_422_888;
import static android.graphics.ImageFormat.YUV_444_888;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
public static final Size RESOLUTION = new Size(1200, 1600);
@@ -37,56 +28,39 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
@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));
if (format != YUV_420_888) {
Log.e(TAG, String.format("Unexpected YUV image format: %d", format));
image.close();
return;
}
byte[] data = getLuminancePlaneData(image);
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
);
QRCodeReader reader = new QRCodeReader();
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = reader.decode(bitmap);
if (_listener != null) {
new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result));
}
} catch (ChecksumException | FormatException | NotFoundException ignored) {
} finally {
image.close();
}
}
private static byte[] getLuminancePlaneData(ImageProxy image) {
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
ByteBuffer buf = plane.getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
int width = image.getWidth();
int height = image.getHeight();
int rowStride = plane.getRowStride();
int pixelStride = plane.getPixelStride();
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data,
plane.getRowStride(),
image.getHeight(),
0,
0,
image.getWidth(),
image.getHeight(),
false
);
if (width != rowStride || pixelStride != 1) {
// remove padding from the Y plane data
byte[] cleanData = new byte[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cleanData[y * width + x] = data[y * rowStride + x * pixelStride];
}
try {
Result result = QrCodeHelper.decodeFromSource(source);
if (_listener != null) {
_listener.onQrCodeDetected(result);
}
} catch (NotFoundException ignored) {
return cleanData;
} finally {
image.close();
}
return data;
}
public interface Listener {
@@ -0,0 +1,96 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class QrCodeHelper {
private QrCodeHelper() {
}
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
hints.put(DecodeHintType.ALSO_INVERTED, true);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
MultiFormatReader reader = new MultiFormatReader();
return reader.decode(bitmap, hints);
}
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
if (bitmap == null) {
throw new DecodeError("Unable to decode stream to bitmap");
}
// If ZXing is not able to decode the image on the first try, we try a couple of
// more times with smaller versions of the same image.
for (int i = 0; i <= 2; i++) {
if (i != 0) {
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
}
try {
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
return decodeFromSource(source);
} catch (NotFoundException ignored) {
}
}
throw new DecodeError(NotFoundException.getNotFoundInstance());
}
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
public static class DecodeError extends Exception {
public DecodeError(String message) {
super(message);
}
public DecodeError(Throwable cause) {
super(cause);
}
}
}
@@ -0,0 +1,47 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.webkit.MimeTypeMap;
import androidx.documentfile.provider.DocumentFile;
public class SafHelper {
private SafHelper() {
}
public static String getFileName(Context context, Uri uri) {
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (i != -1) {
return cursor.getString(i);
}
}
}
}
return uri.getLastPathSegment();
}
public static String getMimeType(Context context, Uri uri) {
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
if (file != null) {
String fileType = file.getType();
if (fileType != null) {
return fileType;
}
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (ext != null) {
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
}
return null;
}
}
@@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.helpers;
import android.view.animation.Animation;
public class SimpleAnimationEndListener implements Animation.AnimationListener {
private final Listener _listener;
public SimpleAnimationEndListener(Listener listener) {
_listener = listener;
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (_listener != null) {
_listener.onAnimationEnd(animation);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
public interface Listener {
void onAnimationEnd(Animation animation);
}
}
@@ -1,20 +1,24 @@
package com.beemdevelopment.aegis.helpers;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
}
@@ -28,7 +32,14 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
}
public void setSelectedEntry(VaultEntry entry) {
_selectedEntry = entry;
if (entry == null) {
_selectedEntry = null;
return;
}
if (!entry.isFavorite()) {
_selectedEntry = entry;
}
}
@Override
@@ -36,32 +47,55 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
return false;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = 0;
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
{
dragFlags = 0;
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
// that's passed to this function has a position of -1, leading
// to a crash down the line.
int position = viewHolder.getBindingAdapterPosition();
if (position == NO_POSITION) {
return 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter == null) {
return 0;
}
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPos(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
if (targetIndex < _adapter.getShownFavoritesCount()) {
return false;
}
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getBindingAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
@@ -69,10 +103,9 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}
@@ -0,0 +1,33 @@
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.text.TextWatcher;
public final class SimpleTextWatcher implements TextWatcher {
private final Listener _listener;
public SimpleTextWatcher(Listener listener) {
_listener = listener;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (_listener != null) {
_listener.afterTextChanged(s);
}
}
public interface Listener {
void afterTextChanged(Editable s);
}
}
@@ -5,6 +5,7 @@ import android.view.View;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.text.BreakIterator;
import java.util.Arrays;
public class TextDrawableHelper {
@@ -48,6 +49,18 @@ public class TextDrawableHelper {
.width(view.getLayoutParams().width)
.height(view.getLayoutParams().height)
.endConfig()
.buildRound(text.substring(0, 1).toUpperCase(), color);
.buildRound(getFirstGrapheme(text).toUpperCase(), color);
}
private static String getFirstGrapheme(String text) {
BreakIterator iter = BreakIterator.getCharacterInstance();
iter.setText(text);
int start = iter.first(), end = iter.next();
if (end == BreakIterator.DONE) {
return "";
}
return text.substring(start, end);
}
}
@@ -1,23 +1,55 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import android.content.res.Configuration;
import androidx.annotation.ColorInt;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.color.DynamicColorsOptions;
import java.util.Map;
public class ThemeHelper {
private ThemeHelper() {
private final AppCompatActivity _activity;
private final Preferences _prefs;
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
_activity = activity;
_prefs = prefs;
}
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
TypedValue typedValue = new TypedValue();
currentTheme.resolveAttribute(attributeId, typedValue, true);
@ColorInt int color = typedValue.data;
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
public void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
_activity.setTheme(theme);
return color;
if (_prefs.isDynamicColorsEnabled()) {
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
if (getConfiguredTheme().equals(Theme.AMOLED)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
}
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
}
}
public Theme getConfiguredTheme() {
Theme theme = _prefs.getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class FavoriteComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return -1 * Boolean.compare(a.isFavorite(), b.isFavorite());
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class LastUsedComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class UsageCountComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Integer.compare(a.getUsageCount(), b.getUsageCount());
}
}
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.base.Objects;
import com.google.common.io.Files;
@@ -17,7 +18,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPack {
private UUID _uuid;
@@ -58,9 +58,21 @@ public class IconPack {
return new ArrayList<>();
}
return _icons.stream()
.filter(i -> i.isSuggestedFor(issuer))
.collect(Collectors.toList());
List<Icon> icons = new ArrayList<>();
for (Icon icon : _icons) {
MatchType matchType = icon.getMatchFor(issuer);
if (matchType != null) {
// Inverse matches (entry issuer contains icon name) are less likely
// to be good, so position them at the end of the list.
if (matchType.equals(MatchType.NORMAL)) {
icons.add(0, icon);
} else if (matchType.equals(MatchType.INVERSE)) {
icons.add(icon);
}
}
}
return icons;
}
@Nullable
@@ -120,13 +132,15 @@ public class IconPack {
public static class Icon implements Serializable {
private final String _relFilename;
private final String _name;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String category, List<String> issuers) {
protected Icon(String filename, String name, String category, List<String> issuers) {
_relFilename = filename;
_name = name;
_category = category;
_issuers = issuers;
}
@@ -148,8 +162,10 @@ public class IconPack {
return IconType.fromFilename(_relFilename);
}
@SuppressWarnings("UnstableApiUsage")
public String getName() {
if (_name != null) {
return _name;
}
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
@@ -157,17 +173,29 @@ public class IconPack {
return _category;
}
public List<String> getIssuers() {
return Collections.unmodifiableList(_issuers);
}
private MatchType getMatchFor(String issuer) {
String lowerEntryIssuer = issuer.toLowerCase();
public boolean isSuggestedFor(String issuer) {
return getIssuers().stream()
.anyMatch(is -> is.toLowerCase().contains(issuer.toLowerCase()));
boolean inverseMatch = false;
for (String is : _issuers) {
String lowerIconIssuer = is.toLowerCase();
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
return MatchType.NORMAL;
}
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
inverseMatch = true;
}
}
if (inverseMatch) {
return MatchType.INVERSE;
}
return null;
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String name = JsonUtils.optString(obj, "name");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
@@ -177,7 +205,12 @@ public class IconPack {
issuers.add(issuer);
}
return new Icon(filename, category, issuers);
return new Icon(filename, name, category, issuers);
}
}
private enum MatchType {
NORMAL,
INVERSE
}
}
@@ -42,6 +42,10 @@ public class IconPackManager {
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
import java.util.Locale;
public enum IconType {
INVALID,
SVG,
@@ -21,9 +23,8 @@ public enum IconType {
}
}
@SuppressWarnings("UnstableApiUsage")
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase()) {
switch (Files.getFileExtension(filename).toLowerCase(Locale.ROOT)) {
case "svg":
return SVG;
case "png":
@@ -3,8 +3,10 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
@@ -14,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@@ -25,6 +28,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
@@ -71,7 +75,7 @@ public class AegisImporter extends DatabaseImporter {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj);
return new DecryptedState(obj, creds);
}
public State decrypt(char[] password) throws DatabaseImporterException {
@@ -83,7 +87,7 @@ public class AegisImporter extends DatabaseImporter {
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, (Dialogs.TextInputListener) password -> {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
@@ -108,10 +112,21 @@ public class AegisImporter extends DatabaseImporter {
public static class DecryptedState extends State {
private JSONObject _obj;
private VaultFileCredentials _creds;
private DecryptedState(JSONObject obj) {
this(obj, null);
}
private DecryptedState(JSONObject obj, VaultFileCredentials creds) {
super(false);
_obj = obj;
_creds = creds;
}
@Nullable
public VaultFileCredentials getCredentials() {
return _creds;
}
@Override
@@ -119,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -143,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -18,9 +18,10 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -35,17 +36,14 @@ 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 java.util.Locale;
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 {
@@ -116,15 +114,21 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
private PBKDFTask.Params getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
// If number of iterations is this high, it's probably not an andOTP file, so
// abort early in order to prevent having to wait for an extremely long key derivation
// process, only to find out that the user picked the wrong file
if (iterations > 10_000_000L) {
throw new DatabaseImporterException(String.format("Unexpectedly high number of iterations: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new KeyDerivationParams(password, salt, iterations);
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
@@ -148,8 +152,8 @@ public class AndOtpImporter extends DatabaseImporter {
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
KeyDerivationParams params = getKeyDerivationParams(password);
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decryptNewFormat(key);
}
@@ -158,8 +162,8 @@ public class AndOtpImporter extends DatabaseImporter {
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
KeyDerivationParams params = getKeyDerivationParams(password);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
@@ -179,7 +183,7 @@ public class AndOtpImporter extends DatabaseImporter {
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
@@ -225,7 +229,7 @@ public class AndOtpImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret"));
@@ -262,71 +266,10 @@ public class AndOtpImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
} catch (DatabaseImporterException | EncodingException | OtpInfoException |
JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
setPriority();
AndOtpImporter.KeyDerivationParams params = args[0];
return deriveKey(params);
}
protected static SecretKey deriveKey(KeyDerivationParams params) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
protected static class KeyDerivationParams {
private final char[] _password;
private final byte[] _salt;
private final int _iterations;
public KeyDerivationParams(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
}
@@ -0,0 +1,380 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
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.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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;
public class AuthenticatorProImporter extends DatabaseImporter {
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "me.jmh.authenticatorpro";
private static final String PKG_DB_PATH = "files/proauth.db3";
private enum Algorithm {
SHA1,
SHA256,
SHA512
}
public AuthenticatorProImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(PKG_NAME, PKG_DB_PATH);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
return isInternal ? readInternal(stream) : readExternal(stream);
}
private State readInternal(InputStream stream) throws DatabaseImporterException {
List<SqlEntry> entries = new SqlImporterHelper(requireContext()).read(SqlEntry.class, stream, "authenticator");
return new SqlState(entries);
}
private static State readExternal(InputStream stream) throws DatabaseImporterException {
byte[] data;
try {
data = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return new JsonState(new JSONObject(new String(data, StandardCharsets.UTF_8)));
} catch (JSONException e) {
return readEncrypted(new DataInputStream(new ByteArrayInputStream(data)));
}
}
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
try {
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
stream.readFully(headerBytes);
String header = new String(headerBytes, StandardCharsets.UTF_8);
switch (header) {
case HEADER:
return EncryptedState.parseHeader(stream);
case HEADER_LEGACY:
return LegacyEncryptedState.parseHeader(stream);
default:
throw new DatabaseImporterException("Invalid file header");
}
} catch (UTFDataFormatException e) {
throw new DatabaseImporterException("Invalid file header");
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new DatabaseImporterException(e);
}
}
private static OtpInfo parseOtpInfo(int type, byte[] secret, Algorithm algo, int digits, int period, int counter)
throws OtpInfoException, DatabaseImporterEntryException {
switch (type) {
case 1:
return new HotpInfo(secret, algo.name(), digits, counter);
case 2:
return new TotpInfo(secret, algo.name(), digits, period);
case 4:
return new SteamInfo(secret, algo.name(), digits, period);
default:
throw new DatabaseImporterEntryException(String.format("Unsupported otp type: %d", type), null);
}
}
static class EncryptedState extends State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
private static final int PARALLELISM = 4;
private static final int ITERATIONS = 3;
private static final int SALT_SIZE = 16;
private static final int IV_SIZE = 12;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public EncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
Argon2Task.Params params = getKeyDerivationParams(password);
SecretKey key = Argon2Task.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private Argon2Task.Params getKeyDerivationParams(char[] password) {
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryPowOfTwo(MEMORY_COST)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}
private static EncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
byte[] iv = new byte[IV_SIZE];
stream.readFully(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
static class LegacyEncryptedState extends State {
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final int SALT_SIZE = 20;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
}
private static LegacyEncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
private static class JsonState extends State {
private final JSONObject _obj;
public JsonState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
try {
JSONArray array = _obj.getJSONArray("Authenticators");
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
try {
res.addEntry(convertEntry(obj));
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return res;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int type = obj.getInt("Type");
String issuer = obj.getString("Issuer");
Object nullableUsername = obj.get("Username");
String username = nullableUsername == JSONObject.NULL ? "" : nullableUsername.toString();
byte[] secret = Base32.decode(obj.getString("Secret"));
Algorithm algo = Algorithm.values()[obj.getInt("Algorithm")];
int digits = obj.getInt("Digits");
int period = obj.getInt("Period");
int counter = obj.getInt("Counter");
OtpInfo info = parseOtpInfo(type, secret, algo, digits, period, counter);
return new VaultEntry(info, username, issuer);
} catch (OtpInfoException | EncodingException | JSONException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
private static class SqlState extends State {
private final List<SqlEntry> _entries;
public SqlState(List<SqlEntry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
for (SqlEntry entry : _entries) {
try {
res.addEntry(entry.convert());
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
return res;
}
}
private static class SqlEntry extends SqlImporterHelper.Entry {
private final int _type;
private final String _issuer;
private final String _username;
private final String _secret;
private final Algorithm _algo;
private final int _digits;
private final int _period;
private final int _counter;
public SqlEntry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_issuer = SqlImporterHelper.getString(cursor, "issuer");
_username = SqlImporterHelper.getString(cursor, "username");
_secret = SqlImporterHelper.getString(cursor, "secret");
_algo = Algorithm.values()[SqlImporterHelper.getInt(cursor, "algorithm")];
_digits = SqlImporterHelper.getInt(cursor, "digits");
_period = SqlImporterHelper.getInt(cursor, "period");
_counter = SqlImporterHelper.getInt(cursor, "counter");
}
public VaultEntry convert() throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(_secret);
OtpInfo info = parseOtpInfo(_type, secret, _algo, _digits, _period, _counter);
return new VaultEntry(info, _username, _issuer);
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
}
@@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@@ -67,14 +68,20 @@ public class AuthyImporter extends DatabaseImporter {
}
@Override
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
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));
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
file1.setShell(shell);
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
file2.setShell(shell);
array = readFile(file1, String.format("%s.key", _authFilename));
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
@@ -129,7 +136,7 @@ public class AuthyImporter extends DatabaseImporter {
}
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
try (InputStream inStream = SuFileInputStream.open(file)) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(inStream, null);
@@ -0,0 +1,116 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
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.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
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 BattleNetImporter extends DatabaseImporter {
private static final String _pkgName = "com.blizzard.bma";
private static final String _subPath = "shared_prefs/com.blizzard.bma.AUTH_STORE.xml";
private static final byte[] _key;
public BattleNetImporter(Context context) {
super(context);
}
static {
try {
_key = Hex.decode("398e27fc50276a656065b0e525f4c06c04c61075286b8e7aeda59da9813b5dd6c80d2fb38068773fa59ba47c17ca6c6479015c1d5b8b8f6b9a");
} catch (EncodingException e) {
throw new RuntimeException(e);
}
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected 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<String> entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.blizzard.bma.AUTH_STORE.HASH")) {
entries.add(entry.Value);
break;
}
}
return new BattleNetImporter.State(entries);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final List<String> _entries;
public State(List<String> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (String str : _entries) {
try {
VaultEntry entry = convertEntry(str);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String hashString) throws DatabaseImporterEntryException {
try {
byte[] hash = Hex.decode(hashString);
if (hash.length != _key.length) {
throw new DatabaseImporterEntryException(String.format("Unexpected hash length: %d", hash.length), hashString);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.length; i++) {
char c = (char) (hash[i] ^ _key[i]);
sb.append(c);
}
final int secretLen = 40;
byte[] secret = Hex.decode(sb.substring(0, secretLen));
String serial = sb.substring(secretLen);
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, serial, "Battle.net");
} catch (OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, hashString);
}
}
}
}
@@ -0,0 +1,127 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
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.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.simpleflatmapper.csv.CsvParser;
import org.simpleflatmapper.lightningcsv.Row;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
public class BitwardenImporter extends DatabaseImporter {
public BitwardenImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
String fileString;
try {
fileString = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
JSONObject obj = new JSONObject(fileString);
JSONArray array = obj.getJSONArray("items");
List<String> entries = new ArrayList<>();
String entry;
for (int i = 0; i < array.length(); i++) {
entry = array.getJSONObject(i).getJSONObject("login").getString("totp");
if (!entry.isEmpty()) {
entries.add(entry);
}
}
return new BitwardenImporter.State(entries);
} catch (JSONException e) {
try {
Iterator<Row> rowIterator = CsvParser.separator(',').rowIterator(fileString);
List<String> entries = new ArrayList<>();
rowIterator.forEachRemaining((row -> {
String entry = row.get("login_totp");
if (entry != null && !entry.isEmpty()) {
entries.add(entry);
}
}));
return new BitwardenImporter.State(entries);
} catch (IOException e2) {
throw new DatabaseImporterException(e2);
}
}
}
public static class State extends DatabaseImporter.State {
private final List<String> _entries;
public State(List<String> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (String obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String obj) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = BitwardenImporter.parseUri(obj);
return new VaultEntry(info);
} catch (GoogleAuthInfoException | EncodingException | OtpInfoException | URISyntaxException e) {
throw new DatabaseImporterEntryException(e, obj);
}
}
}
private static GoogleAuthInfo parseUri(String s) throws EncodingException, OtpInfoException, URISyntaxException, GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (Objects.equals(uri.getScheme(), "steam")) {
String secretString = uri.getAuthority();
if (secretString == null) {
throw new GoogleAuthInfoException(uri, "Empty secret (empty authority)");
}
byte[] secret = Base32.decode(secretString);
return new GoogleAuthInfo(new SteamInfo(secret), "Steam account", "Steam");
}
return GoogleAuthInfo.parseUri(uri);
}
}
@@ -8,11 +8,14 @@ import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
@@ -31,7 +34,11 @@ public abstract class DatabaseImporter {
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
@@ -46,26 +53,32 @@ public abstract class DatabaseImporter {
_context = context;
}
protected Context getContext() {
protected Context requireContext() {
return _context;
}
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
PackageManager man = requireContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
public boolean isInstalledAppVersionSupported() {
return true;
}
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 {
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile file = getAppPath();
try (SuFileInputStream stream = new SuFileInputStream(file)) {
file.setShell(shell);
try (InputStream stream = SuFileInputStream.open(file)) {
return read(stream, true);
} catch (IOException e) {
throw new DatabaseImporterException(e);
@@ -89,12 +102,19 @@ public abstract class DatabaseImporter {
return Collections.unmodifiableList(_importers);
}
public static class Definition {
public static class Definition implements Serializable {
private final String _name;
private final Class<? extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
/**
*
* @param name The name of the Authenticator the importer handles.
* @param type The class which does the importing.
* @param help The string that explains the type of file needed (and optionally where it can be obtained).
* @param supportsDirect Whether the importer can directly import the entries from the app's internal storage using root access.
*/
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
@@ -149,12 +169,17 @@ public abstract class DatabaseImporter {
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
@@ -163,6 +188,10 @@ public abstract class DatabaseImporter {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
@@ -0,0 +1,99 @@
package com.beemdevelopment.aegis.importers;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import androidx.annotation.NonNull;
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.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;
public class DuoImporter extends DatabaseImporter {
private static final String _pkgName = "com.duosecurity.duomobile";
private static final String _subPath = "files/duokit/accounts.json";
public DuoImporter(Context context) {
super(context);
}
@Override
protected @NonNull SuFile getAppPath() throws DatabaseImporterException, NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected @NonNull State read(
@NonNull InputStream stream, boolean isInternal
) throws DatabaseImporterException {
try {
String contents = new String(IOUtils.readAll(stream), UTF_8);
return new DecryptedState(new JSONArray(contents));
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final JSONArray _array;
public DecryptedState(@NonNull JSONArray array) {
super(false);
_array = array;
}
@Override
public @NonNull Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entry = _array.getJSONObject(i);
try {
result.addEntry(convertEntry(entry));
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static @NonNull VaultEntry convertEntry(
@NonNull JSONObject entry
) throws DatabaseImporterEntryException {
try {
String label = entry.optString("name");
JSONObject otpData = entry.getJSONObject("otpGenerator");
byte[] secret = Base32.decode(otpData.getString("otpSecret"));
Long counter = otpData.has("counter") ? otpData.getLong("counter") : null;
OtpInfo otp = counter == null
? new TotpInfo(secret)
: new HotpInfo(secret, counter);
return new VaultEntry(otp, label, "");
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
@@ -83,7 +84,7 @@ public class FreeOtpImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
@@ -34,7 +34,7 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
State state;
if (isInternal) {
state = new FreeOtpImporter(getContext()).read(stream);
state = new FreeOtpImporter(requireContext()).read(stream);
} else {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
@@ -1,9 +1,11 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
@@ -11,6 +13,7 @@ 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.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
@@ -29,30 +32,49 @@ public class GoogleAuthImporter extends DatabaseImporter {
@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);
SuFile file = getAppPath(_pkgName, _subPath);
return file;
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode <= 5000100;
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries, context);
}
@Override
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
path.setShell(shell);
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
return new State(entries, context);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private Context _context;
private State(List<Entry> entries) {
private State(List<Entry> entries, Context context) {
super(false);
_entries = entries;
_context = context;
}
@Override
@@ -61,7 +83,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
for (Entry sqlEntry : _entries) {
try {
VaultEntry entry = convertEntry(sqlEntry);
VaultEntry entry = convertEntry(sqlEntry, _context);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -71,8 +93,11 @@ public class GoogleAuthImporter extends DatabaseImporter {
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(Entry entry, Context context) throws DatabaseImporterEntryException {
try {
if (entry.isEncrypted()) {
throw new DatabaseImporterException(context.getString(R.string.importer_encrypted_exception_google_authenticator, entry.getEmail()));
}
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
@@ -102,6 +127,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private boolean _isEncrypted;
private String _secret;
private String _email;
private String _issuer;
@@ -114,12 +140,18 @@ public class GoogleAuthImporter extends DatabaseImporter {
_email = SqlImporterHelper.getString(cursor, "email", "");
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
_counter = SqlImporterHelper.getLong(cursor, "counter");
_isEncrypted = (cursor.getColumnIndex("isencrypted") != -1 && SqlImporterHelper.getInt(cursor, "isencrypted") > 0);
}
public int getType() {
return _type;
}
public boolean isEncrypted() {
return _isEncrypted;
}
public String getSecret() {
return _secret;
}
@@ -4,13 +4,14 @@ 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.GoogleAuthInfo;
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.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
@@ -34,15 +35,17 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
path.setShell(shell);
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
@@ -81,7 +84,7 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
switch (entry.getType()) {
case TYPE_TOTP:
secret = Base32.decode(entry.getSecret());
secret = GoogleAuthInfo.parseSecret(entry.getSecret());
break;
case TYPE_MICROSOFT:
digits = 8;
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis.importers;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
@@ -19,8 +21,6 @@ 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;
@@ -36,7 +36,7 @@ public class SqlImporterHelper {
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)) {
try (InputStream inStream = SuFileInputStream.open(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.encoding.Base64;
@@ -17,6 +18,10 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
@@ -39,34 +44,60 @@ public class SteamImporter extends DatabaseImporter {
return files[0];
}
@Override
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode < 7460894;
}
@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);
List<JSONObject> objs = new ArrayList<>();
if (obj.has("accounts")) {
JSONObject accounts = obj.getJSONObject("accounts");
Iterator<String> keys = accounts.keys();
while (keys.hasNext()) {
String key = keys.next();
objs.add(accounts.getJSONObject(key));
}
} else {
objs.add(obj);
}
return new State(objs);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONObject _obj;
private final List<JSONObject> _objs;
private State(JSONObject obj) {
private State(List<JSONObject> objs) {
super(false);
_obj = obj;
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
try {
VaultEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
@@ -4,8 +4,6 @@ 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;
@@ -18,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -154,7 +153,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
@@ -2,12 +2,19 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
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.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
@@ -18,10 +25,27 @@ import org.json.JSONObject;
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 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.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class TwoFASImporter extends DatabaseImporter {
private static final int ITERATION_COUNT = 10_000;
private static final int KEY_SIZE = 256; // bits
public TwoFASImporter(Context context) {
super(context);
}
@@ -37,26 +61,96 @@ public class TwoFASImporter extends DatabaseImporter {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 1) {
if (version > 4) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
JSONArray array = obj.getJSONArray("services");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
String encryptedString = JsonUtils.optString(obj, "servicesEncrypted");
if (encryptedString == null) {
JSONArray array = obj.getJSONArray("services");
List<JSONObject> entries = arrayToList(array);
return new DecryptedState(entries);
}
return new TwoFASImporter.State(entries);
String[] parts = encryptedString.split(":");
if (parts.length < 3) {
throw new DatabaseImporterException(String.format("Unexpected format of encrypted data (parts: %d)", parts.length));
}
byte[] data = Base64.decode(parts[0]);
byte[] salt = Base64.decode(parts[1]);
byte[] iv = Base64.decode(parts[2]);
return new EncryptedState(data, salt, iv);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private static List<JSONObject> arrayToList(JSONArray array) throws JSONException {
List<JSONObject> list = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
list.add(array.getJSONObject(i));
}
return list;
}
public static class EncryptedState extends State {
private final byte[] _data;
private final byte[] _salt;
private final byte[] _iv;
private EncryptedState(byte[] data, byte[] salt, byte[] iv) {
super(true);
_data = data;
_salt = salt;
_iv = iv;
}
private SecretKey deriveKey(char[] password)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, _salt, ITERATION_COUNT, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
SecretKey key = deriveKey(password);
Cipher cipher = CryptoUtils.createDecryptCipher(key, _iv);
byte[] decrypted = cipher.doFinal(_data);
String json = new String(decrypted, StandardCharsets.UTF_8);
return new DecryptedState(arrayToList(new JSONArray(json)));
} catch (BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| NoSuchPaddingException
| InvalidKeyException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_2fas_message, 0, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final List<JSONObject> _entries;
public State(List<JSONObject> entries) {
public DecryptedState(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@@ -81,10 +175,26 @@ public class TwoFASImporter extends DatabaseImporter {
try {
byte[] secret = Base32.decode(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = info.getString("issuer");
String issuer = info.optString("issuer");
String name = info.optString("account");
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
OtpInfo otp;
String tokenType = JsonUtils.optString(info, "tokenType");
if (tokenType == null || tokenType.equals("TOTP")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new TotpInfo(secret, algorithm, digits, period);
} else if (tokenType.equals("HOTP")) {
long counter = info.optLong("counter", 0);
otp = new HotpInfo(secret, algorithm, digits, counter);
} else if (tokenType.equals("STEAM")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new SteamInfo(secret, algorithm, digits, period);
} else {
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
}
OtpInfo otp = new TotpInfo(secret);
return new VaultEntry(otp, name, issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
@@ -19,7 +19,7 @@ public class WinAuthImporter extends DatabaseImporter {
@Override
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
@@ -1,34 +0,0 @@
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";
}
}
@@ -1,34 +0,0 @@
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class ProtobufLicense extends License {
@Override
public String getName() {
return "Protocol Buffers License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
}
}
@@ -2,17 +2,24 @@ package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.GoogleAuthProtos;
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.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class GoogleAuthInfo implements Serializable {
public class GoogleAuthInfo implements Transferable, Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
@@ -26,42 +33,6 @@ public class GoogleAuthInfo implements Serializable {
_issuer = issuer;
}
public OtpInfo getOtpInfo() {
return _info;
}
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
builder.scheme(SCHEME);
if (_info instanceof TotpInfo) {
if (_info instanceof SteamInfo) {
builder.authority("steam");
} else {
builder.authority("totp");
}
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
} else if (_info instanceof HotpInfo) {
builder.authority("hotp");
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
}
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
if (_issuer != null && !_issuer.equals("")) {
builder.path(String.format("%s:%s", _issuer, _accountName));
builder.appendQueryParameter("issuer", _issuer);
} else {
builder.path(_accountName);
}
return builder.build();
}
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
@@ -72,7 +43,7 @@ public class GoogleAuthInfo implements Serializable {
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME)) {
if (scheme == null || !(scheme.equals(SCHEME) || scheme.equals(MotpInfo.SCHEME))) {
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
}
@@ -84,14 +55,18 @@ public class GoogleAuthInfo implements Serializable {
byte[] secret;
try {
secret = parseSecret(encodedSecret);
secret = (scheme.equals(MotpInfo.SCHEME)) ? Hex.decode(encodedSecret) : parseSecret(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException(uri, "Bad secret", e);
}
if (secret.length == 0) {
throw new GoogleAuthInfoException(uri, "Secret is empty");
}
OtpInfo info;
String issuer = "";
try {
String type = uri.getHost();
String type = (scheme.equals(MotpInfo.SCHEME)) ? MotpInfo.ID : uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
}
@@ -122,10 +97,22 @@ public class GoogleAuthInfo implements Serializable {
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
case YandexInfo.HOST_ID:
String pin = uri.getQueryParameter("pin");
if (pin != null) {
pin = new String(parseSecret(pin), StandardCharsets.UTF_8);
}
info = new YandexInfo(secret, pin);
issuer = info.getType();
break;
case MotpInfo.ID:
info = new MotpInfo(secret);
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException | NumberFormatException e) {
} catch (OtpInfoException | NumberFormatException | EncodingException e) {
throw new GoogleAuthInfoException(uri, e);
}
@@ -134,7 +121,6 @@ public class GoogleAuthInfo implements Serializable {
String label = path != null && path.length() > 0 ? path.substring(1) : "";
String accountName = "";
String issuer = "";
if (label.contains(":")) {
// a label can only contain one colon
@@ -151,7 +137,9 @@ public class GoogleAuthInfo implements Serializable {
// label only contains the account name
// grab the issuer's info from the 'issuer' parameter if it's present
String issuerParam = uri.getQueryParameter("issuer");
issuer = issuerParam != null ? issuerParam : "";
if (issuer.isEmpty()) {
issuer = issuerParam != null ? issuerParam : "";
}
accountName = label;
}
@@ -248,6 +236,10 @@ public class GoogleAuthInfo implements Serializable {
}
byte[] secret = params.getSecret().toByteArray();
if (secret.length == 0) {
throw new GoogleAuthInfoException(uri, "Secret is empty");
}
switch (params.getType()) {
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
@@ -260,7 +252,7 @@ public class GoogleAuthInfo implements Serializable {
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e){
} catch (OtpInfoException e) {
throw new GoogleAuthInfoException(uri, e);
}
@@ -279,6 +271,55 @@ public class GoogleAuthInfo implements Serializable {
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
}
public OtpInfo getOtpInfo() {
return _info;
}
@Override
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
if (_info instanceof MotpInfo) {
builder.scheme(MotpInfo.SCHEME);
builder.appendQueryParameter("secret", Hex.encode(_info.getSecret()));
} else {
builder.scheme(SCHEME);
if (_info instanceof TotpInfo) {
if (_info instanceof SteamInfo) {
builder.authority("steam");
} else if (_info instanceof YandexInfo) {
builder.authority(YandexInfo.HOST_ID);
} else {
builder.authority("totp");
}
builder.appendQueryParameter("period", Integer.toString(((TotpInfo) _info).getPeriod()));
} else if (_info instanceof HotpInfo) {
builder.authority("hotp");
builder.appendQueryParameter("counter", Long.toString(((HotpInfo) _info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
}
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
if (_info instanceof YandexInfo) {
builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPin()));
}
}
if (_issuer != null && !_issuer.equals("")) {
builder.path(String.format("%s:%s", _issuer, _accountName));
builder.appendQueryParameter("issuer", _issuer);
} else {
builder.path(_accountName);
}
return builder.build();
}
public String getIssuer() {
return _issuer;
}
@@ -287,7 +328,7 @@ public class GoogleAuthInfo implements Serializable {
return _accountName;
}
public static class Export {
public static class Export implements Transferable, Serializable {
private int _batchId;
private int _batchIndex;
private int _batchSize;
@@ -315,5 +356,110 @@ public class GoogleAuthInfo implements Serializable {
public int getBatchId() {
return _batchId;
}
public static List<Integer> getMissingIndices(@NonNull List<Export> exports) throws IllegalArgumentException {
if (!isSingleBatch(exports)) {
throw new IllegalArgumentException("Export list contains entries from different batches");
}
List<Integer> indicesMissing = new ArrayList<>();
if (exports.isEmpty()) {
return indicesMissing;
}
Set<Integer> indicesPresent = exports.stream()
.map(Export::getBatchIndex)
.collect(Collectors.toSet());
for (int i = 0; i < exports.get(0).getBatchSize(); i++) {
if (!indicesPresent.contains(i)) {
indicesMissing.add(i);
}
}
return indicesMissing;
}
public static boolean isSingleBatch(@NonNull List<Export> exports) {
if (exports.isEmpty()) {
return true;
}
int batchId = exports.get(0).getBatchId();
for (Export export : exports) {
if (export.getBatchId() != batchId) {
return false;
}
}
return true;
}
@Override
public Uri getUri() throws GoogleAuthInfoException {
GoogleAuthProtos.MigrationPayload.Builder builder = GoogleAuthProtos.MigrationPayload.newBuilder();
builder.setBatchId(_batchId)
.setBatchIndex(_batchIndex)
.setBatchSize(_batchSize)
.setVersion(1);
for (GoogleAuthInfo info: _entries) {
GoogleAuthProtos.MigrationPayload.OtpParameters.Builder parameters = GoogleAuthProtos.MigrationPayload.OtpParameters.newBuilder()
.setSecret(ByteString.copyFrom(info.getOtpInfo().getSecret()))
.setName(info.getAccountName())
.setIssuer(info.getIssuer());
switch (info.getOtpInfo().getAlgorithm(false)) {
case "SHA1":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA1);
break;
case "SHA256":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA256);
break;
case "SHA512":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA512);
break;
case "MD5":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_MD5);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported Algorithm: %s", info.getOtpInfo().getAlgorithm(false)));
}
switch (info.getOtpInfo().getDigits()) {
case 6:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_SIX);
break;
case 8:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported number of digits: %s", info.getOtpInfo().getDigits()));
}
switch (info.getOtpInfo().getType().toLowerCase()) {
case HotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_HOTP);
parameters.setCounter(((HotpInfo) info.getOtpInfo()).getCounter());
break;
case TotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_TOTP);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Type unsupported by GoogleAuthProtos: %s", info.getOtpInfo().getType()));
}
builder.addOtpParameters(parameters.build());
}
Uri.Builder exportUriBuilder = new Uri.Builder()
.scheme(SCHEME_EXPORT)
.authority("offline");
String data = Base64.encode(builder.build().toByteArray());
exportUriBuilder.appendQueryParameter("data", data);
return exportUriBuilder.build();
}
}
}
@@ -30,7 +30,9 @@ public class GoogleAuthInfoException extends Exception {
@Override
public String getMessage() {
Throwable cause = getCause();
if (cause == null) {
if (cause == null
|| this == cause
|| (super.getMessage() != null && super.getMessage().equals(cause.getMessage()))) {
return super.getMessage();
}
@@ -30,7 +30,9 @@ public class HotpInfo extends OtpInfo {
}
@Override
public String getOtp() {
public String getOtp() throws OtpInfoException {
checkSecret();
try {
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
@@ -0,0 +1,95 @@
package com.beemdevelopment.aegis.otp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.crypto.otp.MOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
public class MotpInfo extends TotpInfo {
public static final String ID = "motp";
public static final String SCHEME = "motp";
public static final String ALGORITHM = "MD5";
public static final int PERIOD = 10;
public static final int DIGITS = 6;
private String _pin;
public MotpInfo(@NonNull byte[] secret) throws OtpInfoException {
this(secret, null);
}
public MotpInfo(byte[] secret, String pin) throws OtpInfoException {
super(secret, ALGORITHM, DIGITS, PERIOD);
setPin(pin);
}
@Override
public String getOtp() {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin());
return otp.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getOtp(long time) {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin(), time);
return otp.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getTypeId() {
return ID;
}
@Override
public JSONObject toJson() {
JSONObject result = super.toJson();
try {
result.put("pin", getPin());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return result;
}
@Nullable
public String getPin() {
return _pin;
}
public void setPin(@NonNull String pin) {
this._pin = pin;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof MotpInfo)) {
return false;
}
MotpInfo info = (MotpInfo) o;
return super.equals(o) && Objects.equals(getPin(), info.getPin());
}
}
@@ -8,6 +8,7 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Locale;
public abstract class OtpInfo implements Serializable {
public static final int DEFAULT_DIGITS = 6;
@@ -27,19 +28,25 @@ public abstract class OtpInfo implements Serializable {
setDigits(digits);
}
public abstract String getOtp();
public abstract String getOtp() throws OtpInfoException;
protected void checkSecret() throws OtpInfoException {
if (getSecret().length == 0) {
throw new OtpInfoException("Secret is empty");
}
}
public abstract String getTypeId();
public String getType() {
return getTypeId().toUpperCase();
return getTypeId().toUpperCase(Locale.ROOT);
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("secret", new String(Base32.encode(getSecret())));
obj.put("secret", Base32.encode(getSecret()));
obj.put("algo", getAlgorithm(false));
obj.put("digits", getDigits());
} catch (JSONException e) {
@@ -69,14 +76,15 @@ public abstract class OtpInfo implements Serializable {
}
public static boolean isAlgorithmValid(String algorithm) {
return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
return algorithm.equals("SHA1") || algorithm.equals("SHA256") ||
algorithm.equals("SHA512") || algorithm.equals("MD5");
}
public void setAlgorithm(String algorithm) throws OtpInfoException {
if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4);
}
algorithm = algorithm.toUpperCase();
algorithm = algorithm.toUpperCase(Locale.ROOT);
if (!isAlgorithmValid(algorithm)) {
throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm));
@@ -104,6 +112,12 @@ public abstract class OtpInfo implements Serializable {
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
// Special case to work around a bug where a user could accidentally
// set the hash algorithm of a non-mOTP entry to MD5
if (!type.equals(MotpInfo.ID) && algo.equals("MD5")) {
algo = DEFAULT_ALGORITHM;
}
switch (type) {
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
@@ -114,6 +128,12 @@ public abstract class OtpInfo implements Serializable {
case HotpInfo.ID:
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case YandexInfo.ID:
info = new YandexInfo(secret, obj.getString("pin"));
break;
case MotpInfo.ID:
info = new MotpInfo(secret, obj.getString("pin"));
break;
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
@@ -5,6 +5,7 @@ import com.beemdevelopment.aegis.crypto.otp.TOTP;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
@@ -19,7 +20,9 @@ public class SteamInfo extends TotpInfo {
}
@Override
public String getOtp() {
public String getOtp() throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toSteamString();
@@ -36,6 +39,6 @@ public class SteamInfo extends TotpInfo {
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase() + id.substring(1);
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
}
}
@@ -26,7 +26,9 @@ public class TotpInfo extends OtpInfo {
}
@Override
public String getOtp() {
public String getOtp() throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toString();
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public interface Transferable {
Uri getUri() throws GoogleAuthInfoException;
}
@@ -0,0 +1,188 @@
package com.beemdevelopment.aegis.otp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.crypto.otp.YAOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
public class YandexInfo extends TotpInfo {
public static final String DEFAULT_ALGORITHM = "SHA256";
public static final int DIGITS = 8;
public static final int SECRET_LENGTH = 16;
public static final int SECRET_FULL_LENGTH = 26;
public static final String ID = "yandex";
public static final String HOST_ID = "yaotp";
@Nullable
private String _pin;
public YandexInfo(@NonNull byte[] secret) throws OtpInfoException {
this(secret, null);
}
public YandexInfo(@NonNull byte[] secret, @Nullable String pin) throws OtpInfoException {
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
setSecret(parseSecret(secret));
_pin = pin;
}
@Override
public String getOtp() {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod());
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
throw new RuntimeException(e);
}
}
@Nullable
public String getPin() {
return _pin;
}
public void setPin(@NonNull String pin) {
_pin = pin;
}
@Override
public String getTypeId() {
return ID;
}
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
}
@Override
public JSONObject toJson() {
JSONObject result = super.toJson();
try {
result.put("pin", getPin());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof YandexInfo)) {
return false;
}
YandexInfo info = (YandexInfo) o;
return super.equals(o) && Objects.equals(getPin(), info.getPin());
}
public static byte[] parseSecret(byte[] secret) throws OtpInfoException {
validateSecret(secret);
if (secret.length != SECRET_LENGTH) {
return Arrays.copyOfRange(secret, 0, SECRET_LENGTH);
}
return secret;
}
/**
* Java implementation of ChecksumIsValid
* From: https://github.com/norblik/KeeYaOtp/blob/188a1a99f13f82e4ef8df8a1b9b9351ba236e2a1/KeeYaOtp/Core/Secret.cs
* License: GPLv3+
*/
public static void validateSecret(byte[] secret) throws OtpInfoException {
if (secret.length != SECRET_LENGTH && secret.length != SECRET_FULL_LENGTH) {
throw new OtpInfoException(String.format("Invalid Yandex secret length: %d bytes", secret.length));
}
// Secrets originating from a QR code do not have a checksum, so we assume those are valid
if (secret.length == SECRET_LENGTH) {
return;
}
char originalChecksum = (char) ((secret[secret.length - 2] & 0x0F) << 8 | secret[secret.length - 1] & 0xff);
char accum = 0;
int accumBits = 0;
int inputTotalBitsAvailable = secret.length * 8 - 12;
int inputIndex = 0;
int inputBitsAvailable = 8;
while (inputTotalBitsAvailable > 0) {
int requiredBits = 13 - accumBits;
if (inputTotalBitsAvailable < requiredBits) {
requiredBits = inputTotalBitsAvailable;
}
while (requiredBits > 0) {
int curInput = (secret[inputIndex] & (1 << inputBitsAvailable) - 1) & 0xff;
int bitsToRead = Math.min(requiredBits, inputBitsAvailable);
curInput >>= inputBitsAvailable - bitsToRead;
accum = (char) (accum << bitsToRead | curInput);
inputTotalBitsAvailable -= bitsToRead;
requiredBits -= bitsToRead;
inputBitsAvailable -= bitsToRead;
accumBits += bitsToRead;
if (inputBitsAvailable == 0) {
inputIndex += 1;
inputBitsAvailable = 8;
}
}
if (accumBits == 13) {
accum ^= 0b1_1000_1111_0011;
}
accumBits = 16 - getNumberOfLeadingZeros(accum);
}
if (accum != originalChecksum) {
throw new OtpInfoException("Yandex secret checksum invalid");
}
}
private static int getNumberOfLeadingZeros(char value) {
if (value == 0) {
return 16;
}
int n = 0;
if ((value & 0xFF00) == 0) {
n += 8;
value <<= 8;
}
if ((value & 0xF000) == 0) {
n += 4;
value <<= 4;
}
if ((value & 0xC000) == 0) {
n += 2;
value <<= 2;
}
if ((value & 0x8000) == 0) {
n++;
}
return n;
}
}

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