Compare commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Changed indicator color to black secondary


Fix indicator flickering when scrolling


Applied patch


Fix unsharp selection icon


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

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


Rearrange item position in import dialog


Refactor SqlImportHelper to support SQLCipher


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

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

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

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

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

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

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

This also removes ``requestLegacyExternalStorage`` from the manifest, as we
don't need it anymore.
2020-01-04 22:09:13 +01:00
Michael Schättgen f8c106bcb9 Add ability to select multiple entries
Improve code and add select listeners
2020-01-04 21:15:17 +01:00
Alexander Bakker 10c206270a Release v1.1.1 2020-01-04 12:57:52 +01:00
Michael SchättgenandGitHub f7a0ee7174 Merge pull request #275 from alexbakker/thanks-for-the-storage-regressions-google
Set requestLegacyExternalStorage to true in AndroidManifest
2020-01-04 03:33:08 -08:00
Alexander Bakker f5e8ba96e6 Set requestLegacyExternalStorage to true in AndroidManifest
This is a temporary workaround while we work on support for the Storage Access Framework.
2020-01-03 19:15:04 +01:00
129 changed files with 5769 additions and 1726 deletions
+1
View File
@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/beemdevelopment']
+1
View File
@@ -39,3 +39,4 @@ captures/
# Keystore files
*.jks
crowdin.properties
+64 -25
View File
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
@@ -17,10 +18,11 @@ android {
defaultConfig {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 29
versionCode 24
versionName "1.1"
versionCode 36
versionName "1.2.1"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
}
@@ -43,16 +45,27 @@ android {
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
resValue "bool", "pref_secure_screen_default", "false"
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode false
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
resValue "bool", "pref_secure_screen_default", "true"
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
}
@@ -62,34 +75,60 @@ android {
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
def glideVersion = '4.11.0'
def guavaVersion = '29.0'
def junitVersion = '5.6.2'
def libsuVersion = '2.5.1'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:5.1.0'
implementation 'com.github.apl-devs:appintro:6.0.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation 'com.github.bumptech.glide:annotations:4.10.0'
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
transitive = false
}
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.protobuf:protobuf-javalite:3.12.1'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
</lint>
+5
View File
@@ -15,3 +15,8 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
+15 -8
View File
@@ -5,11 +5,13 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:name=".AegisApplication"
@@ -20,9 +22,11 @@
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about"></activity>
android:label="@string/title_activity_about" />
<activity
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
@@ -51,12 +55,15 @@
android:screenOrientation="portrait" />
<activity
android:name=".ui.EditEntryActivity"
android:label="Edit profile" />
android:label="Edit entry" />
<activity
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
<activity android:name=".ui.AuthActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
<activity
android:name=".ui.SlotManagerActivity"
android:label="Manage key slots" />
+70
View File
@@ -31,6 +31,76 @@
</head>
<body>
<div></div>
<h3>Version 1.2.1</h3>
<h4>Fixes</h4>
<ul>
<li>Fix a rare issue where the intro could end up in a bad state</li>
</ul>
<h3>Version 1.2</h3>
<h4>New</h4>
<ul>
<li>Add navigation bar color to themes</li>
<li>Add support for importing from TOTP Authenticator</li>
<li>Add support for importing from Microsoft Authenticator</li>
<li>Add support for importing from Authenticator Plus</li>
<li>Add support for importing a plain text Google Authenticator URI file</li>
<li>Add support for importing from the new Google Authenticator export QR codes</li>
<li>Add support for otpauth://steam URI's</li>
<li>Add an option to copy tokens on tap (and disable it by default)</li>
<li>Improve method to notify users on copy</li>
<li>Add support for backups</li>
<li>Improve multiselect flow</li>
<li>Automatically adapt to system theme</li>
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
<li>Use most frequent period to show progress</li>
<li>Append a timestamp to the filename of exported vaults</li>
<li>Add Hungarian translation</li>
<li>Add Turkish translation</li>
<li>Display a warning if automatic time sync is not enabled</li>
<li>Minor card entry layout overhaul</li>
<li>Ability to transfer tokens with qr codes</li>
<li>Lockscreen overhaul</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Improve overall exception handling and error feedback to the user</li>
<li>Improve icon editing flow</li>
<li>Protect writes of the vault file against corruption with AtomicFile</li>
<li>Make the parsing logic of the QR code URI more robust</li>
<li>Importing from Authy now asks for password if needed</li>
<li>Update Russian localization</li>
<li>Increase password reminder period to 30 days</li>
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
<li>Respect the global animator duration scale setting</li>
</ul>
<p>Various other minor improvements</p>
<h3>Version 1.1.4</h3>
<h4>Fixes</h4>
<ul>
<li>The export filename was missing the ".json" extension in some cases</li>
</ul>
<h3>Version 1.1.3</h3>
<h4>New</h4>
<ul>
<li>Password reminder for users who use biometric unlock</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Tokens would not refresh in some rare cases</li>
</ul>
<h3>Version 1.1.2</h3>
<h4>New</h4>
<ul>
<li>Ability to select multiple entries</li>
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
<li>Explanation and warning for the security options</li>
<li>Removed external storage permissions</li>
</ul>
<h3>Version 1.1.1</h3>
<h4>Fixes</h4>
<ul>
<li>Exporting the vault did not work on Android 10</li>
</ul>
<h3>Version 1.1</h3>
<h4>New</h4>
<ul>
@@ -16,7 +16,11 @@ import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
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;
@@ -25,6 +29,7 @@ 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;
@@ -35,7 +40,6 @@ public class AegisApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
_manager = new VaultManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
@@ -58,6 +62,47 @@ public class AegisApplication extends Application {
}
}
public boolean isVaultLocked() {
return _manager == null;
}
/**
* Loads the vault file from disk at the default location, stores an internal
* reference to it for future use and returns it. This must only be called before
* initVaultManager() or after lock().
*/
public VaultFile loadVaultFile() throws VaultManagerException {
if (!isVaultLocked()) {
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readFile(this);
}
return _vaultFile;
}
/**
* Initializes the vault manager by decrypting the given vaultFile with the given
* creds. This removes the internal reference to the raw vault file.
*/
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
_vaultFile = null;
_manager = VaultManager.init(this, vaultFile, creds);
return _manager;
}
/**
* Initializes the vault manager with the given vault and creds. This removes the
* internal reference to the raw vault file.
*/
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
_vaultFile = null;
_manager = new VaultManager(this, vault, creds);
return _manager;
}
public VaultManager getVaultManager() {
return _manager;
}
@@ -67,7 +112,7 @@ public class AegisApplication extends Application {
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
return _prefs.isAutoLockEnabled() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
}
public void registerLockListener(LockListener listener) {
@@ -79,7 +124,7 @@ public class AegisApplication extends Application {
}
public void lock() {
_manager.lock();
_manager = null;
for (LockListener listener : _lockListeners) {
listener.onLocked();
}
@@ -99,8 +144,8 @@ public class AegisApplication extends Application {
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setShortLabel(getString(R.string.new_entry))
.setLongLabel(getString(R.string.add_new_entry))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
@@ -3,16 +3,23 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class Preferences {
private SharedPreferences _prefs;
public Preferences(Context context) {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
}
public boolean isTapToRevealEnabled() {
@@ -32,10 +39,36 @@ public class Preferences {
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isPasswordReminderEnabled() {
return _prefs.getBoolean("pref_password_reminder", true);
}
public boolean isPasswordReminderNeeded() {
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return isPasswordReminderEnabled() && days >= 30;
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
public void resetPasswordReminderTimestamp() {
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", true);
}
public int getCodeGroupSize() {
if (_prefs.getBoolean("pref_code_group_size", false)) {
return 2;
} else {
return 3;
}
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
@@ -65,7 +98,7 @@ public class Preferences {
}
public Theme getCurrentTheme() {
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
}
public void setCurrentTheme(Theme theme) {
@@ -97,4 +130,53 @@ public class Preferences {
return new Locale(lang);
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
return Uri.parse(str);
}
return null;
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
public int getBackupsVersionCount() {
return _prefs.getInt("pref_backups_versions", 5);
}
public void setBackupsVersionCount(int versions) {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
}
public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true);
}
public void setIsTimeSyncWarningEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
}
}
@@ -3,7 +3,9 @@ package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED;
AMOLED,
SYSTEM,
SYSTEM_AMOLED;
private static Theme[] _values;
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
return obj;
}
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
byte[] nonce = Hex.decode(obj.getString("nonce"));
byte[] tag = Hex.decode(obj.getString("tag"));
return new CryptParameters(nonce, tag);
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import org.spongycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.generators.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -12,6 +13,7 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.ProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
@@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import androidx.annotation.RequiresApi;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String STORE_NAME = "AndroidKeyStore";
@@ -61,6 +61,14 @@ public class KeyStoreHandle {
.build());
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause instanceof KeyStoreException) {
throw new KeyStoreHandleException(cause);
}
throw e;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new KeyStoreHandleException(e);
}
@@ -88,18 +96,14 @@ public class KeyStoreHandle {
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
// so instead, check for it here
if (e instanceof KeyPermanentlyInvalidatedException) {
return true;
}
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
public MasterKey(SecretKey key) {
if (key == null) {
throw new NullPointerException();
throw new IllegalArgumentException("Key cannot be null");
}
_key = key;
}
@@ -1,155 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// modified for use in Aegis
import com.google.common.io.BaseEncoding;
/* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info.
*
* As modified by Patrick Woodworth:
*
* Copyright 2011 Patrick Woodworth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
/**
* Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html )
*
* @author Robert Kaye
* @author Gordon Mohr
*/
public class Base32 {
private static final String base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static final int[] base32Lookup =
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
};
private Base32() {
/**
* Encodes byte array to Base32 String.
*
* @param bytes Bytes to encode.
* @return Encoded byte array <code>bytes</code> as a String.
*
*/
public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte;
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
/* Is the current digit going to span a byte boundary? */
if (index > 3) {
if ((i + 1) < bytes.length) {
nextByte =
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
} else {
nextByte = 0;
}
digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= nextByte >> (8 - index);
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;
if (index == 0)
i++;
}
base32[j++] = base32Chars.charAt(digit);
}
return Arrays.copyOf(base32, j);
}
/**
* Decodes the given Base32 String to a raw byte array.
*
* @param base32
* @return Decoded <code>base32</code> String as a raw byte array.
*/
public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
// stop decoding when a padding char is encountered
if (base32[i] == '=') {
// make sure the rest is also padding, but don't bother verifying the length
for (int j = i + 1; j < base32.length; j++) {
if (base32[j] != '=') {
throw new Base32Exception("bad padding");
}
}
break;
}
lookup = base32[i] - '0';
digit = decodeDigit(lookup);
if (index <= 3) {
index = (index + 5) % 8;
if (index == 0) {
bytes[offset] |= digit;
offset++;
if (offset >= bytes.length)
break;
} else {
bytes[offset] |= digit << (8 - index);
}
} else {
index = (index + 5) % 8;
bytes[offset] |= (digit >>> index);
offset++;
if (offset >= bytes.length) {
break;
}
bytes[offset] |= digit << (8 - index);
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return bytes;
}
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}
@@ -1,24 +1,27 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
private Base64() {
}
public static byte[] decode(String s) throws Base64Exception {
public static byte[] decode(String s) throws EncodingException {
try {
return android.util.Base64.decode(s, _flags);
return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException e) {
throw new Base64Exception(e);
throw new EncodingException(e);
}
}
public static byte[] decode(byte[] s) throws EncodingException {
return decode(new String(s, StandardCharsets.UTF_8));
}
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
return new String(encoded, StandardCharsets.UTF_8);
return BaseEncoding.base64().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base64Exception extends Exception {
public Base64Exception(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,9 @@
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
}
@@ -1,46 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
// It is licensed under GPLv2 with a classpath exception.
import com.google.common.io.BaseEncoding;
public class Hex {
private Hex() {
}
private static int hexToBin(char ch) {
if ('0' <= ch && ch <= '9') return ch - '0';
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
return -1;
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static byte[] decode(String s) throws HexException {
final int len = s.length();
if (len % 2 != 0)
throw new HexException("hexBinary needs to be even-length: " + s);
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int h = hexToBin(s.charAt(i));
int l = hexToBin(s.charAt(i + 1));
if (h == -1 || l == -1)
throw new HexException("contains illegal character for hexBinary: " + s);
out[i / 2] = (byte) (h * 16 + l);
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return out;
}
public static String encode(byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
return BaseEncoding.base16().lowerCase().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}
@@ -2,13 +2,13 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
@@ -102,7 +102,7 @@ public class AegisImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@@ -8,15 +8,16 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray;
import org.json.JSONException;
@@ -49,8 +50,6 @@ public class AndOtpImporter extends DatabaseImporter {
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
private static final int MAX_ITERATIONS = 10000;
public AndOtpImporter(Context context) {
super(context);
}
@@ -96,38 +95,12 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
try {
SecretKey key;
int offset = 0;
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
key = new SecretKeySpec(keyBytes, "AES");
} else {
offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1 || iterations > MAX_ITERATIONS) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
key = factory.generateSecret(spec);
}
// extract nonce and tag
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
@@ -137,13 +110,50 @@ public class AndOtpImporter extends DatabaseImporter {
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
DecryptedState state = decryptData(key, 0);
listener.onStateDecrypted(state);
} else {
int offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
try {
DecryptedState state = decryptData(key1, offset);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
task.execute(params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
@@ -158,8 +168,7 @@ public class AndOtpImporter extends DatabaseImporter {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
@@ -201,7 +210,7 @@ public class AndOtpImporter extends DatabaseImporter {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
@@ -221,18 +230,83 @@ public class AndOtpImporter extends DatabaseImporter {
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
name = parts[0];
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public static class Params {
private char[] _password;
private byte[] _salt;
private int _iterations;
public Params(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
}
@@ -0,0 +1,73 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.Dialogs;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
public AuthenticatorPlusImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
return new EncryptedState(reader.readAll());
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
FileReader reader = new FileReader(zipStream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
GoogleAuthUriImporter.State state = importer.read(reader);
listener.onStateDecrypted(state);
return;
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
listener.onError(e);
}
});
}
}
}
@@ -3,13 +3,16 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray;
import org.json.JSONException;
@@ -18,11 +21,33 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _pkgName = "com.authy.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
public AuthyImporter(Context context) {
super(context);
}
@@ -52,18 +77,76 @@ public class AuthyImporter extends DatabaseImporter {
}
}
return new State(array);
for (int i = 0; i < array.length(); i++) {
if (!array.getJSONObject(i).has("decryptedSecret")) {
return new EncryptedState(array);
}
}
return new DecryptedState(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONArray _obj;
public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _array;
private State(JSONArray obj) {
private EncryptedState(JSONArray array) {
super(true);
_array = array;
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = obj.optString("encryptedSecret", null);
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
DecryptedState state = new DecryptedState(_array);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
listener.onError(e);
}
});
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false);
_obj = obj;
_array = array;
}
@Override
@@ -71,8 +154,8 @@ public class AuthyImporter extends DatabaseImporter {
Result result = new Result();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i);
for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
@@ -90,42 +173,46 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName");
authyEntryInfo.OriginalName = entry.optString("originalName", null);
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
int digits = entry.getInt("digits");
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | Base32Exception e) {
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String seperator = "";
String separator = "";
if (info.OriginalName.contains(":")) {
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
seperator = ":";
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
seperator = " - ";
separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + seperator, "");
info.Name = info.Name.replace(info.Issuer + separator, "");
}
}
private static class AuthyEntryInfo {
String OriginalName;
String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
@@ -4,7 +4,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.io.SuFile;
@@ -27,12 +27,16 @@ public abstract class DatabaseImporter {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.class);
_importers.put("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_importers.put("Plain text", GoogleAuthUriImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
@@ -40,7 +44,9 @@ public abstract class DatabaseImporter {
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
}
public DatabaseImporter(Context context) {
@@ -146,9 +152,7 @@ public abstract class DatabaseImporter {
}
public byte[] readAll() throws IOException {
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
return stream.getBytes();
}
return IOUtils.readAll(_stream);
}
public InputStream getStream() {
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(String message, String text) {
super(message);
_text = text;
}
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
@@ -2,26 +2,17 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@@ -45,37 +36,9 @@ public class GoogleAuthImporter extends DatabaseImporter {
@Override
public State read(FileReader reader) throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(reader.getStream(), out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
List<Entry> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
Entry entry = new Entry(cursor);
entries.add(entry);
} while(cursor.moveToNext());
}
return new State(entries);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
return new State(entries);
}
public static class State extends DatabaseImporter.State {
@@ -104,7 +67,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
byte[] secret = Base32.decode(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
@@ -125,33 +88,13 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
private static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
private static class Entry {
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _email;
@@ -159,11 +102,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
private long _counter;
public Entry(Cursor cursor) {
_type = getInt(cursor, "type");
_secret = getString(cursor, "secret");
_email = getString(cursor, "email", "");
_issuer = getString(cursor, "issuer", "");
_counter = getLong(cursor, "counter");
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_secret = SqlImporterHelper.getString(cursor, "secret");
_email = SqlImporterHelper.getString(cursor, "email", "");
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
_counter = SqlImporterHelper.getLong(cursor, "counter");
}
public int getType() {
@@ -0,0 +1,81 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class GoogleAuthUriImporter extends DatabaseImporter {
public GoogleAuthUriImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return null;
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public GoogleAuthUriImporter.State read(DatabaseImporter.FileReader reader) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(reader.getStream());
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (!line.isEmpty()) {
lines.add(line);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new GoogleAuthUriImporter.State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public DatabaseImporter.Result convert() {
DatabaseImporter.Result result = new DatabaseImporter.Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
return new VaultEntry(info);
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -0,0 +1,126 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
private static final String _subPath = "databases/PhoneFactor";
private static final String _pkgName = "com.azure.authenticator";
private static final int TYPE_TOTP = 0;
private static final int TYPE_MICROSOFT = 1;
public MicrosoftAuthImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
return new State(entries);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
int type = sqlEntry.getType();
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret;
int digits = 6;
switch (entry.getType()) {
case TYPE_TOTP:
secret = Base32.decode(entry.getSecret());
break;
case TYPE_MICROSOFT:
digits = 8;
secret = Base64.decode(entry.getSecret());
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _issuer;
private String _userName;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "account_type");
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
_issuer = SqlImporterHelper.getString(cursor, "name");
_userName = SqlImporterHelper.getString(cursor, "username");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getIssuer() {
return _issuer;
}
public String getUserName() {
return _userName;
}
}
}
@@ -0,0 +1,89 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.topjohnwu.superuser.ShellUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class SqlImporterHelper {
private Context _context;
public SqlImporterHelper(Context context) {
_context = context;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("db-import-", "", _context.getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(inStream, out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
entries.add(entry);
} while (cursor.moveToNext());
}
return entries;
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
public static abstract class Entry {
public Entry(Cursor cursor) {
}
}
}
@@ -3,12 +3,11 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONException;
@@ -45,8 +44,9 @@ public class SteamImporter extends DatabaseImporter {
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
try {
byte[] bytes = reader.readAll();
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
@@ -82,7 +82,7 @@ public class SteamImporter extends DatabaseImporter {
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
} catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@@ -0,0 +1,235 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TotpAuthenticatorImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
private static final String _pkgName = "com.authenticator.authservice2";
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
public TotpAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
if (reader.isInternal()) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.nextTag();
String data = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(PREF_KEY)) {
data = entry.Value;
}
}
if (data == null) {
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
}
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = reader.readAll();
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
} catch (IOException | XmlPullParserException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List<JSONObject> parse(String data) throws JSONException {
JSONArray array = new JSONArray(data);
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
String s = array.getString(i);
entries.add(new JSONObject(s));
}
return entries;
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = cipher.doFinal(_data);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
JSONArray keys = obj.names();
List<JSONObject> entries = new ArrayList<>();
if (keys != null && keys.length() > 0) {
entries = parse((String) keys.get(0));
}
return new DecryptedState(entries);
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException
| JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
});
})
.setNegativeButton(R.string.no, (dialog, which) -> {
decrypt(PASSWORD, listener);
})
.create());
}
private void decrypt(char[] password, DecryptListener listener) {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}
}
public static class DecryptedState extends DatabaseImporter.State {
private List<JSONObject> _objs;
private DecryptedState(List<JSONObject> objs) {
super(false);
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int base = obj.getInt("base");
String secretString = obj.getString("key");
byte[] secret;
switch (base) {
case 16:
secret = Hex.decode(secretString);
break;
case 32:
secret = Base32.decode(secretString);
break;
case 64:
secret = Base64.decode(secretString);
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
return new VaultEntry(info, name, issuer);
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -1,14 +1,8 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import com.beemdevelopment.aegis.vault.VaultEntry;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
@@ -27,57 +21,29 @@ public class WinAuthImporter extends DatabaseImporter {
@Override
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new State(lines);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
GoogleAuthUriImporter.State state = importer.read(reader);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private GoogleAuthUriImporter.State _state;
private State(ArrayList<String> lines) {
private State(GoogleAuthUriImporter.State state) {
super(false);
_lines = lines;
_state = state;
}
@Override
public Result convert() {
Result result = new Result();
Result result = _state.convert();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
for (VaultEntry entry : result.getEntries()) {
entry.setIssuer(entry.getName());
entry.setName("WinAuth");
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
VaultEntry vaultEntry = new VaultEntry(info);
vaultEntry.setIssuer(vaultEntry.getName());
vaultEntry.setName("WinAuth");
return vaultEntry;
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -2,10 +2,20 @@ package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import com.beemdevelopment.aegis.GoogleAuthProtos;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class GoogleAuthInfo implements Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
public class GoogleAuthInfo {
private OtpInfo _info;
private String _accountName;
private String _issuer;
@@ -22,16 +32,20 @@ public class GoogleAuthInfo {
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
builder.scheme(SCHEME);
if (_info instanceof TotpInfo) {
builder.authority("totp");
if (_info instanceof SteamInfo) {
builder.authority("steam");
} else {
builder.authority("totp");
}
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
} else if (_info instanceof HotpInfo) {
builder.authority("hotp");
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
} else {
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
}
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
@@ -51,35 +65,39 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("bad uri format");
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("otpauth")) {
throw new GoogleAuthInfoException("unsupported protocol");
if (scheme == null || !scheme.equals(SCHEME)) {
throw new GoogleAuthInfoException("Unsupported protocol");
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException("'secret' is not set");
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
}
// decode secret
byte[] secret;
try {
secret = Base32.decode(encodedSecret.toCharArray());
} catch (Base32Exception e) {
throw new GoogleAuthInfoException("bad secret", e);
secret = Base32.decode(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException("Bad secret", e);
}
// check the otp type
OtpInfo info;
try {
String type = uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
case "totp":
TotpInfo totpInfo = new TotpInfo(secret);
@@ -89,19 +107,27 @@ public class GoogleAuthInfo {
}
info = totpInfo;
break;
case "steam":
SteamInfo steamInfo = new SteamInfo(secret);
period = uri.getQueryParameter("period");
if (period != null) {
steamInfo.setPeriod(Integer.parseInt(period));
}
info = steamInfo;
break;
case "hotp":
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("'counter' was not set");
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
default:
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException e) {
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
}
@@ -141,13 +167,79 @@ public class GoogleAuthInfo {
if (digits != null) {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException e) {
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
}
return new GoogleAuthInfo(info, accountName, issuer);
}
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException("Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException("Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException("Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
try {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(e);
}
List<GoogleAuthInfo> infos = new ArrayList<>();
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_HOTP:
otp = new HotpInfo(secret, params.getCounter());
break;
case OTP_TOTP:
otp = new TotpInfo(secret);
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e){
throw new GoogleAuthInfoException(e);
}
String name = params.getName();
String issuer = params.getIssuer();
int colonI = name.indexOf(':');
if (issuer.isEmpty() && colonI != -1) {
issuer = name.substring(0, colonI);
name = name.substring(colonI + 1);
}
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
infos.add(info);
}
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
}
public String getIssuer() {
return _issuer;
}
@@ -155,4 +247,34 @@ public class GoogleAuthInfo {
public String getAccountName() {
return _accountName;
}
public static class Export {
private int _batchId;
private int _batchIndex;
private int _batchSize;
private List<GoogleAuthInfo> _entries;
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
_batchId = batchId;
_batchIndex = batchIndex;
_batchSize = batchSize;
_entries = entries;
}
public List<GoogleAuthInfo> getEntries() {
return _entries;
}
public int getBatchSize() {
return _batchSize;
}
public int getBatchIndex() {
return _batchIndex;
}
public int getBatchId() {
return _batchId;
}
}
}
@@ -34,7 +34,7 @@ public class HotpInfo extends OtpInfo {
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
throw new RuntimeException(e);
}
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -93,7 +93,7 @@ public abstract class OtpInfo implements Serializable {
OtpInfo info;
try {
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
byte[] secret = Base32.decode(obj.getString("secret"));
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
@@ -110,7 +110,7 @@ public abstract class OtpInfo implements Serializable {
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
} catch (Base32Exception | JSONException e) {
} catch (EncodingException | JSONException e) {
throw new OtpInfoException(e);
}
@@ -6,10 +6,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
@@ -40,6 +44,11 @@ public class AboutActivity extends AegisActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
@@ -110,11 +119,12 @@ public class AboutActivity extends AegisActivity {
private void showLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
String textColor = getThemeColorAsHex(R.attr.primaryText);
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
LicenseResolver.registerLicense(new GlideLicense());
new LicensesDialog.Builder(this)
@@ -125,4 +135,21 @@ public class AboutActivity extends AegisActivity {
.build()
.show();
}
private String getThemeColorAsHex(@AttrRes int attributeId) {
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,20 +1,19 @@
package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.provider.Settings;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import java.util.Locale;
@@ -46,9 +45,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// apply a dirty hack to make progress bars work even if animations are disabled
setGlobalAnimationDurationScale();
// register a callback to listen for lock events
_app.registerLockListener(this);
}
@@ -89,6 +85,19 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
protected void setPreferredTheme(Theme theme) {
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
// set the theme based on the system theme
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
theme = Theme.LIGHT;
break;
case Configuration.UI_MODE_NIGHT_YES:
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
break;
}
}
_currentTheme = theme;
switch (theme) {
@@ -110,7 +119,17 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
Configuration config = new Configuration();
config.locale = locale;
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
}
protected boolean saveVault(boolean backup) {
try {
getApp().getVaultManager().save(backup);
return true;
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
return false;
}
}
/**
@@ -125,18 +144,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
* the vault was locked by an external trigger while the Activity was still open.
*/
private boolean isOrphan() {
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
}
private void setGlobalAnimationDurationScale() {
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
if (durationScale != 1) {
try {
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
} catch (Throwable t) {
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
}
}
return !(this instanceof MainActivity) && _app.isVaultLocked();
}
protected Theme getCurrentTheme() {
@@ -2,55 +2,75 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private BiometricPrompt.CryptoObject _bioCryptoObj;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
private Preferences _prefs;
private boolean _stateless;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_prefs = new Preferences(this);
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button biometricsButton = findViewById(R.id.button_biometrics);
TextView biometricsButton = findViewById(R.id.button_biometrics);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
@@ -60,8 +80,21 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
});
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
_slots = (SlotList) intent.getSerializableExtra("slots");
_stateless = _slots != null;
if (!_stateless) {
VaultFile vaultFile;
try {
vaultFile = getApp().loadVaultFile();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
return;
}
_slots = vaultFile.getHeader().getSlots();
}
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
@@ -80,57 +113,66 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
continue;
}
Cipher cipher = slot.createDecryptCipher(key);
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
_bioSlot = slot;
_bioKey = key;
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
} catch (KeyStoreHandleException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxBiometricInfo.setVisibility(View.VISIBLE);
biometricsButton.setVisibility(View.GONE);
}
}
decryptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
decryptButton.setOnClickListener(v -> {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
char[] password = EditTextHelper.getEditTextChars(_textPassword);
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
});
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
});
}
if (_bioCryptoObj == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@Override
protected void setPreferredTheme(Theme theme) {
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
// set the theme based on the system theme
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
theme = Theme.LIGHT;
break;
case Configuration.UI_MODE_NIGHT_YES:
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
break;
}
}
}
private void showError() {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
}
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
new SlotListTask<>(type, this, this).execute(params);
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme_Light_NoActionBar);
break;
case DARK:
setTheme(R.style.AppTheme_Dark_NoActionBar);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_NoActionBar);
break;
}
}
private void selectPassword() {
@@ -145,10 +187,7 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
Intent intent = new Intent();
setResult(RESULT_CANCELED, intent);
finish();
}
}
@@ -157,17 +196,60 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
public void onResume() {
super.onResume();
if (_bioPrompt != null) {
showBiometricPrompt();
if (_bioKey != null) {
if (_prefs.isPasswordReminderNeeded()) {
focusPasswordField();
} else {
showBiometricPrompt();
}
} else {
focusPasswordField();
}
}
@Override
public void onAttachedToWindow() {
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
showPasswordReminder();
}
}
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
private void showPasswordReminder() {
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setFocusable(false);
popup.setOutsideTouchable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
popup.setElevation(5.0f);
}
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
_textPassword.postDelayed(popup::dismiss, 5000);
}
public void showBiometricPrompt() {
Cipher cipher;
try {
cipher = _bioSlot.createDecryptCipher(_bioKey);
} catch (SlotException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
return;
}
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(false)
.build();
_bioPrompt.authenticate(info, _bioCryptoObj);
_bioPrompt.authenticate(info, cryptoObj);
}
@Override
@@ -179,22 +261,55 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
}
}
@Override
public void onTaskFinished(SlotListTask.Result result) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
private void finish(MasterKey key, boolean isSlotRepaired) {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
if (_stateless) {
// send the master key back to the calling activity
Intent intent = new Intent();
intent.putExtra("creds", creds);
setResult(RESULT_OK, intent);
} else {
try {
AegisApplication app = getApp();
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
saveVault(true);
}
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
}
// send the master key back to the main activity
Intent intent = new Intent();
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
intent.putExtra("repairedSlot", result.isSlotRepaired());
setResult(RESULT_OK, intent);
finish();
} else {
showError();
setResult(RESULT_OK);
}
finish();
}
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
}
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
_prefs.resetPasswordReminderTimestamp();
}
finish(result.getKey(), result.isSlotRepaired());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
}
}
}
@@ -210,7 +325,19 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
MasterKey key;
BiometricSlot slot = _slots.find(BiometricSlot.class);
try {
key = slot.getKey(result.getCryptoObject().getCipher());
} catch (SlotException | SlotIntegrityException e) {
e.printStackTrace();
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
return;
}
finish(key, false);
}
@Override
@@ -2,8 +2,12 @@ package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.app.Dialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.provider.Settings;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
@@ -16,17 +20,18 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.TextView;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import java.util.concurrent.atomic.AtomicReference;
@@ -57,6 +62,24 @@ public class Dialogs {
.create());
}
public static void showDeleteEntriesDialog(Activity activity, DialogInterface.OnClickListener onDelete, int totalEntries) {
String title, message;
if (totalEntries > 1) {
title = activity.getString(R.string.delete_entries);
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
} else {
title = activity.getString(R.string.delete_entry);
message = activity.getString(R.string.delete_entry_description);
}
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
}
public static void showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.discard_changes))
@@ -75,11 +98,12 @@ public class Dialogs {
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
if (isChecked) {
textPassword.setTransformationMethod(null);
textPasswordConfirm.setTransformationMethod(null);
textPassword.clearFocus();
textPasswordConfirm.setEnabled(false);
textPasswordConfirm.clearFocus();
} else {
textPassword.setTransformationMethod(new PasswordTransformationMethod());
textPasswordConfirm.setEnabled(true);
textPasswordConfirm.setTransformationMethod(new PasswordTransformationMethod());
}
});
@@ -98,13 +122,13 @@ public class Dialogs {
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
return;
}
char[] password = EditTextHelper.getEditTextChars(textPassword);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(activity, key -> {
KeyDerivationTask task = new KeyDerivationTask(activity, (passSlot, key) -> {
Cipher cipher;
try {
cipher = Slot.createEncryptCipher(key);
@@ -116,14 +140,14 @@ public class Dialogs {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params(slot, password));
task.execute(new KeyDerivationTask.Params(slot, password));
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
buttonOK.get().setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
@@ -138,7 +162,7 @@ public class Dialogs {
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
if (isSecret) {
@@ -146,18 +170,25 @@ public class Dialogs {
}
input.setHint(hintId);
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
})
.create();
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
});
if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
}
@@ -166,6 +197,10 @@ public class Dialogs {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
@@ -184,6 +219,107 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showBackupVersionsPickerDialog(Activity activity, NumberInputListener listener) {
final int max = 30;
String[] numbers = new String[max / 5];
for (int i = 0; i < numbers.length; i++) {
numbers[i] = Integer.toString(i * 5 + 5);
}
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
numberPicker.setDisplayedValues(numbers);
numberPicker.setMaxValue(numbers.length - 1);
numberPicker.setMinValue(0);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getBackupsVersionCount() / 5 - 1);
numberPicker.setWrapSelectorWheel(false);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onNumberInputResult(numberPicker.getValue()))
.create();
showSecureDialog(dialog);
}
public static void showErrorDialog(Context context, @StringRes int message, Exception e) {
showErrorDialog(context, message, e, null);
}
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error) {
showErrorDialog(context, message, error, null);
}
public static void showErrorDialog(Context context, @StringRes int message, Exception e, DialogInterface.OnClickListener listener) {
showErrorDialog(context, message, e.toString(), listener);
}
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error, DialogInterface.OnClickListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null);
TextView textDetails = view.findViewById(R.id.error_details);
textDetails.setText(error);
TextView textMessage = view.findViewById(R.id.error_message);
textMessage.setText(message);
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.error_occurred)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
if (listener != null) {
listener.onClick(dialog1, which);
}
})
.setNeutralButton(R.string.details, (dialog1, which) -> {
textDetails.setVisibility(View.VISIBLE);
})
.create();
dialog.setOnShowListener(d -> {
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
button.setOnClickListener(v -> {
if (textDetails.getVisibility() == View.GONE) {
textDetails.setVisibility(View.VISIBLE);
button.setText(R.string.copy);
} else {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
ClipData clip = ClipData.newPlainText("text/plain", error);
clipboard.setPrimaryClip(clip);
}
}
});
});
Dialogs.showSecureDialog(dialog);
}
public static void showTimeSyncWarningDialog(Context context, Dialog.OnClickListener listener) {
Preferences prefs = new Preferences(context);
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.time_sync_warning_title)
.setView(view)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (checkBox.isChecked()) {
prefs.setIsTimeSyncWarningEnabled(false);
}
if (listener != null) {
listener.onClick(dialog, which);
}
})
.setNegativeButton(R.string.no, (dialog, which) -> {
if (checkBox.isChecked()) {
prefs.setIsTimeSyncWarningEnabled(false);
}
})
.create());
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
@@ -6,6 +6,7 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@@ -22,12 +23,17 @@ import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TableRow;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
@@ -36,24 +42,21 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.beemdevelopment.aegis.util.Cloner;
import java.io.ByteArrayOutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
public class EditEntryActivity extends AegisActivity {
@@ -65,6 +68,7 @@ public class EditEntryActivity extends AegisActivity {
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private boolean _isEditingIcon;
private CircleImageView _iconView;
private ImageView _saveImageButton;
@@ -88,23 +92,29 @@ public class EditEntryActivity extends AegisActivity {
private RelativeLayout _advancedSettingsHeader;
private RelativeLayout _advancedSettings;
private VaultManager _vault;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_entry);
_vault = getApp().getVaultManager();
_groups = _vault.getGroups();
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
// retrieve info from the calling activity
Intent intent = getIntent();
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
if (_isNew) {
setTitle(R.string.add_new_profile);
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
if (entryUUID != null) {
_origEntry = _vault.getEntryByUUID(entryUUID);
} else {
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
_isNew = true;
setTitle(R.string.add_new_entry);
}
String selectedGroup = intent.getStringExtra("selectedGroup");
@@ -157,14 +167,14 @@ public class EditEntryActivity extends AegisActivity {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
String secretString = Base32.encode(secretBytes);
_textSecret.setText(secretString);
}
String type = _origEntry.getInfo().getType();
@@ -184,9 +194,8 @@ public class EditEntryActivity extends AegisActivity {
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String type = _spinnerType.getSelectedItem().toString();
switch (type.toLowerCase()) {
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
switch (type) {
case TotpInfo.ID:
case SteamInfo.ID:
_rowCounter.setVisibility(View.GONE);
@@ -197,7 +206,7 @@ public class EditEntryActivity extends AegisActivity {
_rowCounter.setVisibility(View.VISIBLE);
break;
default:
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
}
@@ -238,11 +247,7 @@ public class EditEntryActivity extends AegisActivity {
});
_iconView.setOnClickListener(v -> {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
startIconSelectionActivity();
});
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
@@ -250,7 +255,6 @@ public class EditEntryActivity extends AegisActivity {
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
setGroup(selectedGroup);
}
}
@@ -263,6 +267,7 @@ public class EditEntryActivity extends AegisActivity {
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -319,6 +324,11 @@ public class EditEntryActivity extends AegisActivity {
@Override
public void onBackPressed() {
if (_isEditingIcon) {
stopEditingIcon(false);
return;
}
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<VaultEntry> entry = new AtomicReference<>();
@@ -343,7 +353,7 @@ public class EditEntryActivity extends AegisActivity {
return;
}
finish(entry.get(), false);
addAndFinish(entry.get());
},
(dialog, which) -> super.onBackPressed()
);
@@ -360,9 +370,12 @@ public class EditEntryActivity extends AegisActivity {
break;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
finish(_origEntry, true);
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelectionActivity();
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
@@ -375,6 +388,52 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void startIconSelectionActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_icon)), PICK_IMAGE_REQUEST);
}
private void startEditingIcon(Uri data) {
Glide.with(this)
.asBitmap()
.load(data)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(v -> {
stopEditingIcon(true);
});
_isEditingIcon = true;
}
private void stopEditingIcon(boolean save) {
if (save) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
}
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save;
_isEditingIcon = false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit, menu);
@@ -388,46 +447,36 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void finish(VaultEntry entry, boolean delete) {
private void addAndFinish(VaultEntry entry) {
if (_isNew) {
_vault.addEntry(entry);
} else {
_vault.replaceEntry(entry);
}
saveAndFinish(entry, false);
}
private void deleteAndFinish(VaultEntry entry) {
_vault.removeEntry(entry);
saveAndFinish(entry, true);
}
private void saveAndFinish(VaultEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entry", entry);
intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete);
setResult(RESULT_OK, intent);
finish();
if (saveVault(true)) {
setResult(RESULT_OK, intent);
finish();
}
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Glide.with(this)
.asBitmap()
.load(data.getData())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = true;
_hasChangedIcon = true;
}
});
startEditingIcon(data.getData());
}
super.onActivityResult(requestCode, resultCode, data);
@@ -458,8 +507,11 @@ public class EditEntryActivity extends AegisActivity {
byte[] secret;
try {
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
} catch (Base32Exception e) {
secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
if (secret.length == 0) {
throw new ParseException("Secret cannot be empty");
}
} catch (EncodingException e) {
throw new ParseException("Secret is not valid base32.");
}
@@ -482,7 +534,7 @@ public class EditEntryActivity extends AegisActivity {
info = new HotpInfo(secret, algo, digits, counter);
break;
default:
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
info.setDigits(digits);
@@ -528,6 +580,10 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
if (_isEditingIcon) {
stopEditingIcon(true);
}
VaultEntry entry;
try {
entry = parseEntry();
@@ -536,7 +592,7 @@ public class EditEntryActivity extends AegisActivity {
return false;
}
finish(entry, false);
addAndFinish(entry);
return true;
}
@@ -11,7 +11,6 @@ import java.text.Collator;
import java.util.ArrayList;
import java.util.TreeSet;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -29,9 +28,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
// set up the recycler view
_adapter = new GroupAdapter(this);
@@ -1,13 +1,15 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
@@ -16,39 +18,30 @@ import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.github.paolorotolo.appintro.AppIntro2;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
import com.github.appintro.AppIntro2;
import com.github.appintro.AppIntroFragment;
import com.github.appintro.model.SliderPage;
import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
private CustomAuthenticatedSlide _authenticatedSlide;
private CustomAuthenticationSlide _authenticationSlide;
public class IntroActivity extends AppIntro2 {
private SecuritySetupSlide securitySetupSlide;
private SecurityPickerSlide _securityPickerSlide;
private Fragment _endSlide;
private Vault _vault;
private VaultFile _vaultFile;
private PasswordSlot _passwordSlot;
private Cipher _passwordCipher;
private AegisApplication _app;
private Preferences _prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
@@ -56,10 +49,9 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
setWizardMode(true);
showSkipButton(false);
pager.setPagingEnabled(false);
//showPagerIndicator(false);
setGoBackLock(true);
setSkipButtonEnabled(false);
showStatusBar(true);
setSystemBackButtonLocked(true);
setBarColor(getResources().getColor(R.color.colorPrimary));
SliderPage homeSliderPage = new SliderPage();
@@ -67,130 +59,93 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_authenticationSlide = new CustomAuthenticationSlide();
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
//_authenticationSlide.setDescColor(getResources().getColor(R.color.primary_text_dark));
addSlide(_authenticationSlide);
_authenticatedSlide = new CustomAuthenticatedSlide();
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticatedSlide);
_securityPickerSlide = new SecurityPickerSlide();
_securityPickerSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_securityPickerSlide);
securitySetupSlide = new SecuritySetupSlide();
securitySetupSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(securitySetupSlide);
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
_vault = new Vault();
_vaultFile = new VaultFile();
}
private void setException(Exception e) {
Intent result = new Intent();
result.putExtra("exception", e);
setResult(RESULT_EXCEPTION, result);
finish();
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
Intent intent = getIntent();
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_passwordSlot = new PasswordSlot();
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
new DerivationTask(this, this).execute(params);
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
if (oldFragment == _securityPickerSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
int cryptType = getIntent().getIntExtra("cryptType", CRYPT_TYPE_INVALID);
if (cryptType == CRYPT_TYPE_NONE) {
// TODO: no magic indices
getPager().setCurrentItem(4);
goToNextSlide(false);
}
}
if (newFragment == _endSlide) {
setWizardMode(false);
}
setSwipeLock(true);
}
@Override
public void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
int cryptType = _authenticatedSlide.getCryptType();
// wait for the key derivation background task
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
(_passwordSlot == null || _passwordCipher == null)) {
int cryptType = securitySetupSlide.getCryptType();
VaultFileCredentials creds = securitySetupSlide.getCredentials();
if (cryptType == CRYPT_TYPE_INVALID
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
Dialogs.showErrorDialog(this, R.string.intro_crypto_error, String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds), (dialog, which) -> finishIntro(CRYPT_TYPE_NONE, null));
return;
}
// generate the master key
VaultFileCredentials creds = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
creds = new VaultFileCredentials();
}
finishIntro(cryptType, creds);
}
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
if (_passwordSlot == null || _passwordCipher == null) {
throw new RuntimeException();
}
try {
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
creds.getSlots().add(_passwordSlot);
} catch (SlotException e) {
setException(e);
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
try {
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
} catch (SlotException e) {
setException(e);
}
creds.getSlots().add(slot);
}
// finally, save the vault
private void finishIntro(int cryptType, VaultFileCredentials creds) {
Vault vault = new Vault();
VaultFile vaultFile = new VaultFile();
try {
JSONObject obj = _vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_vaultFile.setContent(obj);
JSONObject obj = vault.toJson();
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
_vaultFile.setContent(obj, creds);
vaultFile.setContent(obj, creds);
}
VaultManager.save(getApplicationContext(), _vaultFile);
VaultManager.save(getApplicationContext(), vaultFile);
} catch (VaultManagerException | VaultFileException e) {
setException(e);
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
return;
}
// send the master key back to the main activity
Intent result = new Intent();
result.putExtra("creds", creds);
setResult(RESULT_OK, result);
if (cryptType == CRYPT_TYPE_NONE) {
_app.initVaultManager(vault, null);
} else {
_app.initVaultManager(vault, creds);
}
// skip the intro from now on
_prefs.setIntroDone(true);
setResult(RESULT_OK);
finish();
}
@Override
public void onTaskFinished(SecretKey key) {
if (key != null) {
try {
_passwordCipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
setException(e);
}
} else {
setException(new NullPointerException());
}
public void goToNextSlide() {
super.goToNextSlide(false);
}
}
@@ -10,11 +10,14 @@ import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.view.ActionMode;
@@ -31,7 +34,7 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
@@ -51,17 +54,17 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
private static final int CODE_SCAN = 0;
private static final int CODE_ADD_ENTRY = 1;
private static final int CODE_EDIT_ENTRY = 2;
private static final int CODE_ENTER_ENTRY = 3;
private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6;
private static final int CODE_SCAN_IMAGE = 7;
private static final int CODE_DO_INTRO = 3;
private static final int CODE_DECRYPT = 4;
private static final int CODE_PREFERENCES = 5;
private static final int CODE_SCAN_IMAGE = 6;
// permission request codes
private static final int CODE_PERM_CAMERA = 0;
@@ -73,13 +76,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private String _selectedGroup;
private boolean _searchSubmitted;
private VaultEntry _selectedEntry;
private List<VaultEntry> _selectedEntries;
private ActionMode _actionMode;
private Menu _menu;
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private LinearLayout _btnBackupError;
private FabScrollHelper _fabScrollHelper;
@@ -99,6 +103,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// set up the entry view
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
@@ -106,12 +111,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
@@ -122,7 +128,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startScanActivity();
});
_btnBackupError = findViewById(R.id.btn_backup_error);
_btnBackupError.setOnClickListener(view -> {
startPreferencesActivity("pref_backups");
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
_selectedEntries = new ArrayList<>();
}
@Override
@@ -158,39 +170,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data == null) {
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
return;
}
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
if (resultCode != RESULT_OK) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(resultCode, data);
onScanResult(data);
break;
case CODE_ADD_ENTRY:
onAddEntryResult(resultCode, data);
onAddEntryResult(data);
break;
case CODE_EDIT_ENTRY:
onEditEntryResult(resultCode, data);
break;
case CODE_ENTER_ENTRY:
onEnterEntryResult(resultCode, data);
onEditEntryResult(data);
break;
case CODE_DO_INTRO:
onDoIntroResult(resultCode, data);
onDoIntroResult();
break;
case CODE_DECRYPT:
onDecryptResult(resultCode, data);
onDecryptResult();
break;
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
onPreferencesResult(data);
break;
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
onScanImageResult(data);
}
super.onActivityResult(requestCode, resultCode, data);
@@ -213,103 +222,108 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void onPreferencesResult(int resultCode, Intent data) {
private void onPreferencesResult(Intent data) {
// refresh the entire entry list if needed
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
if (_loaded) {
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
_entryListView.refresh(true);
}
}
}
private void startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
intent.putExtra("isNew", isNew);
if (isNew) {
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
} else {
intent.putExtra("entryUUID", entry.getUUID());
}
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, requestCode);
}
private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveVault();
}
}
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
if (data.getBooleanExtra("delete", false)) {
deleteEntry(entry);
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
VaultEntry oldEntry = _vault.replaceEntry(entry);
_entryListView.replaceEntry(oldEntry, entry);
saveVault();
}
}
}
private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveVault();
}
}
private void onScanImageResult(int resultCode, Intent intent) {
if (resultCode == RESULT_OK) {
Uri inputFile = (intent.getData());
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
private void onScanResult(Intent data) {
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
if (entries.size() == 1) {
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
} else {
for (VaultEntry entry : entries) {
_vault.addEntry(entry);
if (_loaded) {
_entryListView.addEntry(entry);
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new MultiFormatReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
saveVault(true);
}
}
private void onAddEntryResult(Intent data) {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry);
}
}
private void onEditEntryResult(Intent data) {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
private void onScanImageResult(Intent intent) {
Uri inputFile = (intent.getData());
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new MultiFormatReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
}
@@ -355,33 +369,26 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setGroupFilter(group, true);
}
private void addEntry(VaultEntry entry) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
private void onDoIntroResult() {
_vault = _app.getVaultManager();
loadEntries();
checkTimeSyncSetting();
}
private void onDoIntroResult(int resultCode, Intent data) {
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
// TODO: user feedback
Exception e = (Exception) data.getSerializableExtra("exception");
throw new RuntimeException(e);
private void checkTimeSyncSetting() {
boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1;
if (!autoTime && getPreferences().isTimeSyncWarningEnabled()) {
Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
startActivity(intent);
});
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
unlockVault(creds);
}
private void onDecryptResult(int resultCode, Intent intent) {
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
boolean unlocked = unlockVault(creds);
// save the vault in case a slot was repaired
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
_vault.setCredentials(creds);
saveVault();
}
doShortcutActions();
private void onDecryptResult() {
_vault = _app.getVaultManager();
loadEntries();
checkTimeSyncSetting();
}
private void startScanActivity() {
@@ -394,10 +401,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void startScanImageActivity() {
if (!PermissionHelper.request(this, CODE_PERM_READ_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return;
}
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
@@ -405,10 +408,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
}
private void startPreferencesActivity(String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
}
private void doShortcutActions() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null || _vault.isLocked()) {
if (action == null || _app.isVaultLocked()) {
return;
}
@@ -422,7 +431,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void handleDeeplink() {
if (_vault.isLocked()) {
if (_app.isVaultLocked()) {
return;
}
@@ -436,35 +445,48 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
try {
info = GoogleAuthInfo.parseUri(uri);
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
if (info != null) {
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
}
}
}
@Override
protected void onResume() {
super.onResume();
if (_vault.isLocked()) {
if (_vault == null) {
// start the intro if the vault file doesn't exist
if (!_vault.isLoaded() && !_vault.fileExists()) {
// the vault doesn't exist, start the intro
if (!VaultManager.fileExists(this)) {
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
Intent intro = new Intent(this, IntroActivity.class);
startActivityForResult(intro, CODE_DO_INTRO);
return;
} else {
unlockVault(null);
}
// read the vault from disk
// if this fails, show the error to the user and close the app
try {
VaultFile vaultFile = _app.loadVaultFile();
if (!vaultFile.isEncrypted()) {
_vault = _app.initVaultManager(vaultFile, null);
}
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog1, which) -> finish());
return;
}
}
if (_app.isVaultLocked()) {
startAuthActivity();
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
@@ -475,11 +497,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.refresh(false);
} else {
loadEntries();
checkTimeSyncSetting();
}
handleDeeplink();
updateLockIcon();
doShortcutActions();
updateBackupErrorBar();
}
@Override
@@ -502,11 +526,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onBackPressed();
}
private void deleteEntry(VaultEntry entry) {
VaultEntry oldEntry = _vault.removeEntry(entry);
saveVault();
private void deleteEntries(List<VaultEntry> entries) {
for (VaultEntry entry: entries) {
VaultEntry oldEntry = _vault.removeEntry(entry);
_entryListView.removeEntry(oldEntry);
}
_entryListView.removeEntry(oldEntry);
saveVault(true);
}
@Override
@@ -556,8 +582,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
startPreferencesActivity(null);
return true;
}
case R.id.action_about: {
@@ -614,81 +639,73 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_searchView.setIconified(true);
}
private boolean unlockVault(VaultFileCredentials creds) {
try {
if (!_vault.isLoaded()) {
_vault.load();
}
if (_vault.isLocked()) {
if (creds == null) {
startAuthActivity();
return false;
} else {
_vault.unlock(creds);
}
}
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
startAuthActivity();
return false;
}
loadEntries();
return true;
}
private void loadEntries() {
// load all entries
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.getEntries());
_entryListView.addEntries(entries);
_entryListView.runEntriesAnimation();
_loaded = true;
if (!_loaded) {
_entryListView.addEntries(_vault.getEntries());
_entryListView.runEntriesAnimation();
_loaded = true;
}
}
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("slots", _vault.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
}
private void saveVault() {
try {
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
}
}
private void updateLockIcon() {
// hide the lock icon if the vault is not unlocked
if (_menu != null && !_vault.isLocked()) {
if (_menu != null && !_app.isVaultLocked()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_vault.isEncryptionEnabled());
}
}
private void updateBackupErrorBar() {
String error = null;
if (_app.getPreferences().isBackupsEnabled()) {
error = _app.getPreferences().getBackupsError();
}
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
}
@Override
public void onEntryClick(VaultEntry entry) {
if (_selectedEntry != null) {
if (_selectedEntry == entry) {
if (_actionMode != null) {
if (_selectedEntries.isEmpty()) {
_actionMode.finish();
} else {
setIsMultipleSelected(_selectedEntries.size() > 1);
}
return;
}
}
copyEntryCode(entry);
@Override
public void onSelect(VaultEntry entry) {
_selectedEntries.add(entry);
}
@Override
public void onDeselect(VaultEntry entry) {
_selectedEntries.remove(entry);
}
private void setIsMultipleSelected(boolean multipleSelected) {
_entryListView.setIsLongPressDragEnabled(!multipleSelected);
_actionMode.getMenu().findItem(R.id.action_edit).setVisible(!multipleSelected);
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
}
@Override
public void onLongEntryClick(VaultEntry entry) {
if (_selectedEntry != null) {
if (!_selectedEntries.isEmpty()) {
return;
}
_selectedEntry = entry;
_selectedEntries.add(entry);
_entryListView.setActionModeState(true, entry);
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
}
@@ -700,12 +717,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryDrop(VaultEntry entry) {
saveVault();
saveVault(false);
}
@Override
public void onEntryChange(VaultEntry entry) {
saveVault();
saveVault(true);
}
public void onEntryCopy(VaultEntry entry) {
copyEntryCode(entry);
}
@Override
@@ -733,7 +754,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
}
private class ActionModeCallbacks implements ActionMode.Callback {
@@ -753,26 +773,43 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_copy:
copyEntryCode(_selectedEntry);
copyEntryCode(_selectedEntries.get(0));
mode.finish();
return true;
case R.id.action_edit:
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
mode.finish();
return true;
case R.id.action_share_qr:
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
for (VaultEntry entry : _selectedEntries) {
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
authInfos.add(authInfo);
}
intent.putExtra("authInfos", authInfos);
startActivity(intent);
mode.finish();
return true;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
deleteEntry(_selectedEntry);
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
deleteEntries(_selectedEntries);
if (_selectedEntry.getGroup() != null) {
if (!_vault.getGroups().contains(_selectedEntry.getGroup())) {
updateGroupFilterMenu();
for (VaultEntry entry : _selectedEntries) {
if (entry.getGroup() != null) {
if (!_vault.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
}
}
mode.finish();
});
}, _selectedEntries.size());
return true;
default:
return false;
@@ -782,7 +819,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_selectedEntry = null;
_selectedEntries.clear();
_actionMode = null;
}
}
@@ -1,9 +1,8 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import android.view.MenuItem;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@@ -12,9 +11,30 @@ public class PreferencesActivity extends AegisActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_fragment = new PreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (savedInstanceState == null) {
_fragment = new PreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
} else {
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
}
}
@Override
public void onResume() {
super.onResume();
Intent intent = getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
_fragment.scrollToPreference(preference);
intent.removeExtra("pref");
}
}
@Override
@@ -41,19 +61,15 @@ public class PreferencesActivity extends AegisActivity {
}
@Override
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_Preferences);
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,12 +1,11 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
@@ -17,15 +16,28 @@ import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
@@ -36,17 +48,6 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@@ -54,6 +55,7 @@ import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -70,12 +72,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private static final int CODE_SLOTS = 2;
private static final int CODE_GROUPS = 3;
private static final int CODE_SELECT_ENTRIES = 4;
// permission request codes
private static final int CODE_PERM_IMPORT = 0;
private static final int CODE_PERM_EXPORT = 1;
private static final int CODE_EXPORT = 5;
private static final int CODE_EXPORT_ENCRYPT = 6;
private static final int CODE_BACKUPS = 7;
private Intent _result;
private Preferences _prefs;
private VaultManager _vault;
// keep a reference to the type of database converter the user selected
@@ -89,30 +91,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_prefs = app.getPreferences();
_vault = app.getVaultManager();
// set the result intent in advance
setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
int currentTheme = _prefs.getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
int currentTheme = _prefs.getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
_prefs.setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
@@ -127,25 +135,30 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
});
Preference langPreference = findPreference("pref_lang");
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
} else {
// Setting locale doesn't work on Marshmallow or below
langPreference.setVisible(false);
}
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
int currentViewMode = _prefs.getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
int currentViewMode = _prefs.getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
_prefs.setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
_result.putExtra("needsRefresh", true);
dialog.dismiss();
@@ -161,7 +174,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
startImport();
return true;
}
});
@@ -175,12 +188,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference exportPreference = findPreference("pref_export");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onExport();
startExport();
return true;
}
});
@@ -195,6 +207,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
});
timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());*/
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference issuerPreference = findPreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@@ -210,6 +228,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return true;
});
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@@ -269,7 +293,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
try {
_vault.disableEncryption();
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
return;
}
@@ -358,30 +383,61 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
_backupsPreference = findPreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
} else {
_prefs.setIsBackupsEnabled(false);
updateBackupPreference();
}
return false;
});
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = findPreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
return false;
});
_backupsTriggerPreference = findPreference("pref_backups_trigger");
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (_prefs.isBackupsEnabled()) {
try {
_vault.backup();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
}
}
return true;
});
_backupsVersionsPreference = findPreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
number = number * 5 + 5;
_prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
});
return false;
});
_autoLockPreference = findPreference("pref_auto_lock");
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onResume() {
super.onResume();
updateEncryptionPreferences();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!PermissionHelper.checkResults(grantResults)) {
Toast.makeText(getActivity(), R.string.permission_denied, Toast.LENGTH_SHORT).show();
return;
}
switch (requestCode) {
case CODE_PERM_IMPORT:
onImport();
break;
case CODE_PERM_EXPORT:
onExport();
break;
}
updateBackupPreference();
}
@Override
@@ -406,6 +462,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_ENCRYPT:
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
break;
case CODE_BACKUPS:
onSelectBackupsLocationResult(resultCode, data);
break;
}
}
@@ -418,11 +482,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
getActivity().setResult(Activity.RESULT_OK, _result);
}
private void onImport() {
if (!PermissionHelper.request(getActivity(), CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return;
}
private void startImport() {
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[0]);
@@ -502,7 +562,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onError(Exception e) {
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
}
});
}
@@ -511,8 +572,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
}
}
@@ -528,7 +588,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (VaultFileException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
return;
}
@@ -550,7 +610,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
}
}
@@ -560,8 +620,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
return;
}
@@ -577,28 +636,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
startActivityForResult(intent, CODE_SELECT_ENTRIES);
}
private void onExport() {
if (!PermissionHelper.request(getActivity(), CODE_PERM_EXPORT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
}
private void startExport() {
// TODO: create a custom layout to show a message AND a checkbox
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename;
try {
filename = _vault.export(checked.get());
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
String filename = checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN;
filename = new VaultBackupManager.FileInfo(filename).toString();
// make sure the new file is visible
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, filename);
Toast.makeText(getActivity(), getString(R.string.export_vault_location) + filename, Toast.LENGTH_SHORT).show();
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
})
.setNegativeButton(android.R.string.cancel, null);
if (_vault.isEncryptionEnabled()) {
@@ -669,11 +721,45 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRecreate", true);
}
private void onExportResult(int resultCode, Intent data, boolean encrypt) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
_vault.export(stream, encrypt);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
}
private boolean saveVault() {
try {
_vault.save();
_vault.save(true);
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
return false;
}
@@ -698,12 +784,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
} else {
_setPasswordPreference.setEnabled(false);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
_passwordReminderPreference.setVisible(false);
}
updateBackupPreference();
}
private void updateBackupPreference() {
boolean encrypted = _vault.isEncryptionEnabled();
boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted;
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
startActivityForResult(intent, CODE_BACKUPS);
}
private class SetPasswordListener implements Dialogs.SlotListener {
@@ -733,8 +843,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onException(Exception e) {
e.printStackTrace();
updateEncryptionPreferences();
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
@@ -745,6 +856,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
e.printStackTrace();
onSlotInitializationFailed(0, e.toString());
return;
}
@@ -758,7 +870,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
}
}
}
@@ -783,7 +895,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
}
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
@@ -10,14 +11,16 @@ import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
@@ -30,10 +33,15 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
private Menu _menu;
private int _facing = CAMERA_FACING_BACK;
private int _batchId = 0;
private int _batchIndex = -1;
private List<VaultEntry> _entries;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
_entries = new ArrayList<>();
_scannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
@@ -107,19 +115,61 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
@Override
public void handleResult(Result rawResult) {
try {
// parse google auth uri
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
VaultEntry entry = new VaultEntry(info);
Uri uri = Uri.parse(rawResult.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
}
Intent intent = new Intent();
intent.putExtra("entry", entry);
setResult(RESULT_OK, intent);
finish();
_scannerView.resumeCameraPreview(this);
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
_scannerView.resumeCameraPreview(this);
});
}
}
private void handleUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
List<VaultEntry> entries = new ArrayList<>();
entries.add(new VaultEntry(info));
finish(entries);
}
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
if (_batchId == 0) {
_batchId = export.getBatchId();
}
_scannerView.resumeCameraPreview(this);
int batchIndex = export.getBatchIndex();
if (_batchId != export.getBatchId()) {
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
for (GoogleAuthInfo info : export.getEntries()) {
VaultEntry entry = new VaultEntry(info);
_entries.add(entry);
}
_batchIndex = batchIndex;
if (_batchIndex + 1 == export.getBatchSize()) {
finish(_entries);
}
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
} else if (_batchIndex != batchIndex) {
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
}
}
private void finish(List<VaultEntry> entries) {
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<VaultEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
private void updateCameraIcon() {
@@ -0,0 +1,136 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TransferEntriesActivity extends AegisActivity {
private List<GoogleAuthInfo> _authInfos;
private ImageView _qrImage;
private TextView _issuer;
private TextView _accountName;
private TextView _entriesCount;
private Button _nextButton;
private Button _previousButton;
private VaultManager _vault;
private int _currentEntryCount = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_entry);
_vault = getApp().getVaultManager();
_qrImage = findViewById(R.id.ivQrCode);
_issuer = findViewById(R.id.tvIssuer);
_accountName = findViewById(R.id.tvAccountName);
_entriesCount = findViewById(R.id.tvEntriesCount);
_nextButton = findViewById(R.id.btnNext);
_previousButton = findViewById(R.id.btnPrevious);
if (getSupportActionBar() != null){
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Intent intent = getIntent();
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
_entriesCount.setVisibility(controlVisibility);
_nextButton.setVisibility(controlVisibility);
_nextButton.setOnClickListener(v -> {
if (_currentEntryCount < _authInfos.size()) {
_previousButton.setVisibility(View.VISIBLE);
_currentEntryCount++;
generateQR();
if (_currentEntryCount == _authInfos.size()) {
_nextButton.setText(R.string.done);
}
} else {
finish();
}
});
_previousButton.setOnClickListener(v -> {
if (_currentEntryCount > 1 ) {
_nextButton.setText(R.string.next);
_currentEntryCount--;
generateQR();
if (_currentEntryCount == 1) {
_previousButton.setVisibility(View.INVISIBLE);
}
}
});
generateQR();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
private void generateQR() {
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
_issuer.setText(selectedEntry.getIssuer());
_accountName.setText(selectedEntry.getAccountName());
_entriesCount.setText(String.format(getString(R.string.entries_count), _currentEntryCount, _authInfos.size()));
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = null;
try {
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
} catch (WriterException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
_qrImage.setImageBitmap(bitmap);
}
}
@@ -13,10 +13,10 @@ import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.appintro.SlidePolicy;
import com.google.android.material.snackbar.Snackbar;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public class SecurityPickerSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
@@ -27,7 +27,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authentication_slide, container, false);
final View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
_buttonGroup.setOnCheckedChangeListener(this);
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
@@ -38,6 +38,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
_buttonGroup.check(R.id.rb_biometrics);
}
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
@@ -77,7 +78,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
id = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
}
Intent intent = getActivity().getIntent();
@@ -8,36 +8,42 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.github.appintro.SlidePolicy;
import com.github.appintro.SlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
private int _bgColor;
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
private int _bgColor;
private BiometricSlot _bioSlot;
private Cipher _bioCipher;
private int _cryptType;
private VaultFileCredentials _creds;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
final View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
@@ -45,11 +51,12 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
_textPassword.setTransformationMethod(null);
_textPasswordConfirm.setTransformationMethod(null);
_textPassword.clearFocus();
_textPasswordConfirm.setEnabled(false);
_textPasswordConfirm.clearFocus();
} else {
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
_textPasswordConfirm.setEnabled(true);
_textPasswordConfirm.setTransformationMethod(new PasswordTransformationMethod());
}
});
@@ -61,23 +68,15 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
return _cryptType;
}
public BiometricSlot getBiometricSlot() {
return _bioSlot;
}
public Cipher getBiometriCipher() {
return _bioCipher;
}
public char[] getPassword() {
return EditTextHelper.getEditTextChars(_textPassword);
public VaultFileCredentials getCredentials() {
return _creds;
}
public void setBgColor(int color) {
_bgColor = color;
}
public void showBiometricPrompt() {
private void showBiometricPrompt() {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
@@ -86,13 +85,18 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
initializer.authenticate(info);
}
private void deriveKey() {
PasswordSlot slot = new PasswordSlot();
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, EditTextHelper.getEditTextChars(_textPassword));
new KeyDerivationTask(getContext(), new PasswordDerivationListener()).execute(params);
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
showBiometricPrompt();
_cryptType = intent.getIntExtra("cryptType", SecurityPickerSlide.CRYPT_TYPE_INVALID);
if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_NONE) {
_creds = new VaultFileCredentials();
}
}
@@ -104,16 +108,16 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
@Override
public boolean isPolicyRespected() {
switch (_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case SecurityPickerSlide.CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
if (_bioSlot == null) {
case SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC:
if (!_creds.getSlots().has(BiometricSlot.class)) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
case SecurityPickerSlide.CRYPT_TYPE_PASS:
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
return _creds.getSlots().has(PasswordSlot.class);
}
return false;
@@ -125,7 +129,7 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = getString(R.string.password_equality_error);
View view = getView();
@@ -133,23 +137,49 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
} else if (_bioSlot == null) {
} else if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC) {
deriveKey();
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
showBiometricPrompt();
}
}
private class BiometricsListener implements BiometricSlotInitializer.Listener {
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
@Override
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
try {
Cipher cipher = Slot.createEncryptCipher(key);
slot.setKey(_creds.getKey(), cipher);
_creds.getSlots().add(slot);
} catch (SlotException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.enable_encryption_error, e);
return;
}
((IntroActivity) getActivity()).goToNextSlide();
}
}
private class BiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
_bioSlot = slot;
_bioCipher = cipher;
try {
slot.setKey(_creds.getKey(), cipher);
_creds.getSlots().add(slot);
} catch (SlotException e) {
e.printStackTrace();
onSlotInitializationFailed(0, e.toString());
return;
}
deriveKey();
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
}
}
}
@@ -9,16 +9,16 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import javax.crypto.SecretKey;
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
public class KeyDerivationTask extends ProgressDialogTask<KeyDerivationTask.Params, KeyDerivationTask.Result> {
private Callback _cb;
public DerivationTask(Context context, Callback cb) {
public KeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.encrypting_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(DerivationTask.Params... args) {
protected Result doInBackground(KeyDerivationTask.Params... args) {
setPriority();
Params params = args[0];
@@ -29,13 +29,16 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
CryptoUtils.CRYPTO_SCRYPT_p,
salt
);
return params.getSlot().deriveKey(params.getPassword(), scryptParams);
PasswordSlot slot = params.getSlot();
SecretKey key = slot.deriveKey(params.getPassword(), scryptParams);
return new Result(slot, key);
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
protected void onPostExecute(Result result) {
super.onPostExecute(result);
_cb.onTaskFinished(result.getSlot(), result.getKey());
}
public static class Params {
@@ -56,7 +59,25 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
}
}
public static class Result {
private PasswordSlot _slot;
private SecretKey _key;
public Result(PasswordSlot slot, SecretKey key) {
_slot = slot;
_key = key;
}
public PasswordSlot getSlot() {
return _slot;
}
public SecretKey getKey() {
return _key;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
void onTaskFinished(PasswordSlot slot, SecretKey key);
}
}
@@ -5,41 +5,32 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, SlotListTask.Result> {
public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecryptTask.Params, PasswordSlotDecryptTask.Result> {
private Callback _cb;
private Class<T> _type;
public SlotListTask(Class<T> type, Context context, Callback cb) {
public PasswordSlotDecryptTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
_type = type;
}
@Override
protected Result doInBackground(SlotListTask.Params... args) {
protected Result doInBackground(PasswordSlotDecryptTask.Params... args) {
setPriority();
Params params = args[0];
SlotList slots = params.getSlots();
for (Slot slot : slots.findAll(_type)) {
for (PasswordSlot slot : params.getSlots()) {
try {
if (slot instanceof PasswordSlot) {
char[] password = (char[]) params.getObj();
return decryptPasswordSlot((PasswordSlot) slot, password);
} else if (slot instanceof BiometricSlot) {
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
}
return decryptPasswordSlot(slot, params.getPassword());
} catch (SlotException e) {
throw new RuntimeException(e);
} catch (SlotIntegrityException ignored) {
@@ -50,12 +41,6 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
return null;
}
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
throws SlotException, SlotIntegrityException {
MasterKey key = slot.getKey(cipher);
return new Result(key, slot);
}
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
throws SlotIntegrityException, SlotException {
MasterKey masterKey;
@@ -103,35 +88,35 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
}
public static class Params {
private SlotList _slots;
private Object _obj;
private List<PasswordSlot> _slots;
private char[] _password;
public Params(SlotList slots, Object obj) {
public Params(List<PasswordSlot> slots, char[] password) {
_slots = slots;
_obj = obj;
_password = password;
}
public SlotList getSlots() {
public List<PasswordSlot> getSlots() {
return _slots;
}
public Object getObj() {
return _obj;
public char[] getPassword() {
return _password;
}
}
public static class Result {
private MasterKey _key;
private Slot _slot;
private PasswordSlot _slot;
private boolean _repaired;
public Result(MasterKey key, Slot slot, boolean repaired) {
public Result(MasterKey key, PasswordSlot slot, boolean repaired) {
_key = key;
_slot = slot;
_repaired = repaired;
}
public Result(MasterKey key, Slot slot) {
public Result(MasterKey key, PasswordSlot slot) {
this(key, slot, false);
}
@@ -10,34 +10,41 @@ import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private EntryListView _view;
private List<VaultEntry> _entries;
private List<VaultEntry> _shownEntries;
private VaultEntry _selectedEntry;
private List<VaultEntry> _selectedEntries;
private VaultEntry _focusedEntry;
private int _codeGroupSize;
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private boolean _copyOnTap;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private String _searchFilter;
private boolean _isPeriodUniform = true;
private int _uniformPeriod = -1;
private Handler _dimHandler;
// keeps track of the viewholders that are currently bound
@@ -46,6 +53,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_selectedEntries = new ArrayList<>();
_holders = new ArrayList<>();
_dimHandler = new Handler();
_view = view;
@@ -58,6 +66,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_view = null;
}
public void setCodeGroupSize(int codeGroupeSize) {
_codeGroupSize = codeGroupeSize;
}
public void setShowAccountName(boolean showAccountName) {
_showAccountName = showAccountName;
}
@@ -78,6 +90,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_highlightEntry = highlightEntry;
}
public void setIsCopyOnTapEnabled(boolean enabled) {
_copyOnTap = enabled;
}
public VaultEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
@@ -117,7 +133,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
public void addEntries(List<VaultEntry> entries) {
public void addEntries(Collection<VaultEntry> entries) {
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
@@ -135,6 +151,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
public void removeEntry(UUID uuid) {
VaultEntry entry = getEntryByUUID(uuid);
removeEntry(entry);
}
public void clearEntries() {
_entries.clear();
_shownEntries.clear();
@@ -142,7 +163,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
VaultEntry oldEntry = getEntryByUUID(uuid);
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
@@ -165,6 +187,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
private VaultEntry getEntryByUUID(UUID uuid) {
for (VaultEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
private boolean isEntryFiltered(VaultEntry entry) {
String group = entry.getGroup();
String issuer = entry.getIssuer().toLowerCase();
@@ -192,7 +224,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyDataSetChanged();
} else {
for (EntryHolder holder : _holders) {
holder.refreshCode();
holder.refresh();
}
}
}
@@ -302,12 +334,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
VaultEntry entry = _shownEntries.get(position);
holder.setFocused(entry == _selectedEntry);
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
holder.setData(entry, _codeGroupSize, _showAccountName, showProgress, hidden, dimmed);
holder.setFocused(_selectedEntries.contains(entry));
holder.loadIcon(_view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@@ -315,7 +347,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void onClick(View v) {
boolean handled = false;
if (_selectedEntry == null) {
if (_selectedEntries.isEmpty()) {
if (_copyOnTap) {
_view.onEntryCopy(entry);
holder.animateCopyText();
}
if (_highlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
@@ -324,6 +361,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
focusEntry(entry);
}
}
} else {
if (_selectedEntries.contains(entry)) {
_view.onDeselect(entry);
removeSelectedEntry(entry);
holder.setFocusedAndAnimate(false);
} else {
holder.setFocusedAndAnimate(true);
addSelectedEntry(entry);
_view.onSelect(entry);
}
}
if (!handled) {
@@ -335,9 +382,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
if (_selectedEntry == null) {
setSelectedEntry(_shownEntries.get(position));
holder.setFocused(true);
if (_selectedEntries.isEmpty()) {
holder.setFocusedAndAnimate(true);
}
return _view.onLongEntryClick(_shownEntries.get(position));
@@ -370,20 +416,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
private void checkPeriodUniformity(boolean force) {
int mostFrequentPeriod = getMostFrequentPeriod();
boolean uniform = isPeriodUniform();
if (!force && uniform == _isPeriodUniform) {
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
return;
}
_isPeriodUniform = uniform;
_uniformPeriod = mostFrequentPeriod;
for (EntryHolder holder : _holders) {
holder.setShowProgress(!_isPeriodUniform);
if ((holder.getEntry().getInfo() instanceof TotpInfo)) {
holder.setShowProgress(((TotpInfo) holder.getEntry().getInfo()).getPeriod() != mostFrequentPeriod);
}
}
_view.onPeriodUniformityChanged(_isPeriodUniform);
_view.onPeriodUniformityChanged(_isPeriodUniform, _uniformPeriod);
}
public int getUniformPeriod() {
public int getMostFrequentPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (VaultEntry entry : _shownEntries) {
OtpInfo info = entry.getInfo();
@@ -396,14 +448,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return -1;
}
int period = infos.get(0).getPeriod();
Map<Integer, Integer> occurrences = new HashMap<>();
for (TotpInfo info : infos) {
if (period != info.getPeriod()) {
return -1;
int period = info.getPeriod();
if(occurrences.containsKey(period)) {
occurrences.put(period, occurrences.get(period) + 1);
} else {
occurrences.put(period, 1);
}
}
return period;
Integer maxValue = 0;
Integer maxKey = 0;
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()){
if(entry.getValue() > maxValue){
maxValue = entry.getValue();
maxKey = entry.getKey();
}
}
return maxValue > 1 ? maxKey : -1;
}
private void focusEntry(VaultEntry entry) {
@@ -444,14 +508,29 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_focusedEntry = null;
}
public void setSelectedEntry(VaultEntry entry) {
if (entry == null) {
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
} else if (_highlightEntry) {
public void removeSelectedEntry(VaultEntry entry) {
_selectedEntries.remove(entry);
}
public void addSelectedEntry(VaultEntry entry) {
if (_highlightEntry) {
resetFocus();
}
_selectedEntry = entry;
_selectedEntries.add(entry);
}
public void deselectAllEntries() {
for (VaultEntry entry: _selectedEntries) {
for (EntryHolder holder : _holders) {
if (holder.getEntry() == entry) {
holder.setFocusedAndAnimate(false);
break;
}
}
}
_selectedEntries.clear();
}
public boolean isDragAndDropAllowed() {
@@ -459,7 +538,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public boolean isPeriodUniform() {
return getUniformPeriod() != -1;
return isPeriodUniform(getMostFrequentPeriod());
}
private static boolean isPeriodUniform(int period) {
return period != -1;
}
@Override
@@ -473,6 +556,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onPeriodUniformityChanged(boolean uniform);
void onEntryCopy(VaultEntry entry);
void onPeriodUniformityChanged(boolean uniform, int period);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
}
}
@@ -1,8 +1,12 @@
package com.beemdevelopment.aegis.ui.views;
import android.graphics.PorterDuff;
import android.os.Handler;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
@@ -10,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
@@ -18,6 +21,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@@ -28,16 +32,27 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private TextView _profileCopied;
private ImageView _profileDrawable;
private VaultEntry _entry;
private ImageView _buttonRefresh;
private RelativeLayout _description;
private final ImageView _selected;
private final Handler _selectedHandler;
private int _codeGroupSize = 6;
private boolean _hidden;
private PeriodProgressBar _progressBar;
private TotpProgressBar _progressBar;
private View _view;
private UiRefresher _refresher;
private Handler _animationHandler;
private Animation _scaleIn;
private Animation _scaleOut;
public EntryHolder(final View view) {
super(view);
@@ -47,46 +62,64 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
_profileCopied = view.findViewById(R.id.profile_copied);
_description = view.findViewById(R.id.description);
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_selected = view.findViewById(R.id.ivSelected);
_selectedHandler = new Handler();
_animationHandler = new Handler();
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
_scaleIn = AnimationUtils.loadAnimation(view.getContext(), R.anim.item_scale_in);
_scaleOut = AnimationUtils.loadAnimation(view.getContext(), R.anim.item_scale_out);
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
if (_hidden) {
if (!_hidden) {
refreshCode();
}
_progressBar.refresh();
}
@Override
public long getMillisTillNextRefresh() {
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
}
});
}
public void setData(VaultEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
public void setData(VaultEntry entry, int codeGroupSize, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
_entry = entry;
_hidden = hidden;
if (codeGroupSize <= 0)
throw new IllegalArgumentException("Code group size cannot be zero or negative");
_codeGroupSize = codeGroupSize;
_selected.clearAnimation();
_selected.setVisibility(View.GONE);
_selectedHandler.removeCallbacksAndMessages(null);
_animationHandler.removeCallbacksAndMessages(null);
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
setShowProgress(showProgress);
// only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
_profileIssuer.setText(entry.getIssuer());
_profileName.setText("");
if (showAccountName) {
_profileName.setText(" - " + entry.getName());
String profileIssuer = entry.getIssuer();
String profileName = showAccountName ? entry.getName() : "";
if (!profileIssuer.isEmpty() && !profileName.isEmpty()) {
profileName = String.format(" (%s)", profileName);
}
_profileIssuer.setText(profileIssuer);
_profileName.setText(profileName);
if (_hidden) {
hideCode();
@@ -139,6 +172,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void setFocused(boolean focused) {
if (focused) {
_selected.setVisibility(View.VISIBLE);
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
} else {
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
@@ -146,16 +180,34 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_view.setSelected(focused);
}
public void setFocusedAndAnimate(boolean focused) {
setFocused(focused);
if (focused) {
_selected.startAnimation(_scaleIn);
} else {
_selected.startAnimation(_scaleOut);
_selectedHandler.postDelayed(() -> _selected.setVisibility(View.GONE), 150);
}
}
public void destroy() {
_refresher.destroy();
}
public void startRefreshLoop() {
_refresher.start();
_progressBar.start();
}
public void stopRefreshLoop() {
_refresher.stop();
_progressBar.stop();
}
public void refresh() {
_progressBar.restart();
refreshCode();
}
public void refreshCode() {
@@ -171,7 +223,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
if (!(info instanceof SteamInfo)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < otp.length(); i++) {
if (i != 0 && i % 3 == 0) {
if (i != 0 && i % _codeGroupSize == 0) {
sb.append(" ");
}
sb.append(otp.charAt(i));
@@ -200,6 +252,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
animateAlphaTo(DEFAULT_ALPHA);
}
public void animateCopyText() {
_animationHandler.removeCallbacksAndMessages(null);
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
Animation slideDownFadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in);
_profileCopied.startAnimation(slideDownFadeIn);
_description.startAnimation(slideDownFadeOut);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
_description.startAnimation(fadeIn);
}, 3000);
}
private void animateAlphaTo(float alpha) {
itemView.animate().alpha(alpha).setDuration(200).start();
}
@@ -22,10 +22,10 @@ import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
@@ -33,8 +33,10 @@ import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
@@ -44,7 +46,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private TotpProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
@@ -104,7 +106,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public long getMillisTillNextRefresh() {
return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
}
});
@@ -124,23 +126,36 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
_adapter.setGroupFilter(group, apply);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
if (apply) {
runEntriesAnimation();
}
}
public void setIsLongPressDragEnabled(boolean enabled) {
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
}
public void setIsCopyOnTapEnabled(boolean enabled) {
_adapter.setIsCopyOnTapEnabled(enabled);
}
public void setActionModeState(boolean enabled, VaultEntry entry) {
_touchCallback.setSelectedEntry(entry);
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
_adapter.setSelectedEntry(entry);
if (enabled) {
_adapter.addSelectedEntry(entry);
} else {
_adapter.deselectAllEntries();
}
}
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
if (apply) {
runEntriesAnimation();
@@ -148,8 +163,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setSearchFilter(String search) {
_touchCallback.setIsLongPressDragEnabled(search == null);
_adapter.setSearchFilter(search);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
}
public void setViewMode(ViewMode mode) {
@@ -160,8 +175,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.refresh();
_progressBar.restart();
}
_adapter.refresh(hard);
}
@@ -195,18 +211,35 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
@Override
public void onPeriodUniformityChanged(boolean isUniform) {
public void onEntryCopy(VaultEntry entry) {
_listener.onEntryCopy(entry);
}
@Override
public void onSelect(VaultEntry entry) { _listener.onSelect(entry); }
@Override
public void onDeselect(VaultEntry entry) { _listener.onDeselect(entry); }
@Override
public void onPeriodUniformityChanged(boolean isUniform, int period) {
setShowProgress(isUniform);
if (_showProgress) {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
_progressBar.setPeriod(period);
_progressBar.start();
_refresher.start();
} else {
_progressBar.setVisibility(View.GONE);
_progressBar.stop();
_refresher.stop();
}
}
public void setCodeGroupSize(int codeGrouping) {
_adapter.setCodeGroupSize(codeGrouping);
}
public void setShowAccountName(boolean showAccountName) {
_adapter.setShowAccountName(showAccountName);
}
@@ -232,7 +265,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
updateEmptyState();
}
public void addEntries(List<VaultEntry> entries) {
public void addEntries(Collection<VaultEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
}
@@ -242,12 +275,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
updateEmptyState();
}
public void removeEntry(UUID uuid) {
_adapter.removeEntry(uuid);
updateEmptyState();
}
public void clearEntries() {
_adapter.clearEntries();
}
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
_adapter.replaceEntry(oldEntry, newEntry);
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
_adapter.replaceEntry(uuid, newEntry);
}
public void runEntriesAnimation() {
@@ -296,8 +334,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onEntryCopy(VaultEntry entry);
void onLongEntryClick(VaultEntry entry);
void onScroll(int dx, int dy);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
@@ -1,54 +0,0 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import com.beemdevelopment.aegis.otp.TotpInfo;
import androidx.annotation.RequiresApi;
public class PeriodProgressBar extends ProgressBar {
private int _period;
public PeriodProgressBar(Context context) {
super(context);
}
public PeriodProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setPeriod(int period) {
_period = period;
}
public void refresh() {
// reset the progress bar
int maxProgress = getMax();
setProgress(maxProgress);
// calculate the progress the bar should start at
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
long period = _period * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
// start progress animation
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, 0);
animation.setDuration(millisTillRotation);
animation.setInterpolator(new LinearInterpolator());
animation.start();
}
}
@@ -51,7 +51,7 @@ public class SlotHolder extends RecyclerView.ViewHolder {
_slotName.setText(R.string.authentication_method_raw);
_slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
} else {
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported Slot type: %s", slot.getClass()));
}
}
@@ -0,0 +1,82 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.otp.TotpInfo;
public class TotpProgressBar extends ProgressBar {
private int _period = 30;
private Handler _handler;
private float _animDurationScale;
public TotpProgressBar(Context context) {
super(context);
}
public TotpProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setPeriod(int period) {
_period = period;
}
public void start() {
stop();
_handler = new Handler();
_animDurationScale = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
refresh();
}
public void stop() {
if (_handler != null) {
_handler.removeCallbacksAndMessages(null);
_handler = null;
}
}
public void restart() {
stop();
start();
}
private void refresh() {
// calculate the current progress the bar should start at
int maxProgress = getMax();
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
int currentProgress = (int) (maxProgress * ((float) millisTillRotation / (_period * 1000)));
// start progress animation, compensating for any changes to the animator duration scale settings
int animPart = maxProgress / _period;
int animEnd = (currentProgress / animPart) * animPart;
int animPartDuration = (int) (1000 / _animDurationScale);
float animDurationFraction = (float) (currentProgress - animEnd) / animPart;
int realAnimDuration = (int) (1000 * animDurationFraction);
int animDuration = (int) (animPartDuration * animDurationFraction);
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, animEnd);
animation.setDuration(animDuration);
animation.setInterpolator(new LinearInterpolator());
animation.start();
// the animation only lasts for (less than) one second, so restart it after that
_handler.postDelayed(this::refresh, realAnimDuration);
}
}
@@ -1,27 +0,0 @@
package com.beemdevelopment.aegis.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteInputStream extends ByteArrayInputStream {
private ByteInputStream(byte[] buf) {
super(buf);
}
public static ByteInputStream create(InputStream fileStream) throws IOException {
int read;
byte[] buf = new byte[4096];
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
outStream.write(buf, 0, read);
}
return new ByteInputStream(outStream.toByteArray());
}
public byte[] getBytes() {
return this.buf;
}
}
@@ -0,0 +1,37 @@
package com.beemdevelopment.aegis.util;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOUtils {
private IOUtils() {
}
public static byte[] readFile(FileInputStream inStream) throws IOException {
try (DataInputStream outStream = new DataInputStream(inStream)) {
byte[] fileBytes = new byte[(int) inStream.getChannel().size()];
outStream.readFully(fileBytes);
return fileBytes;
}
}
public static byte[] readAll(InputStream inStream) throws IOException {
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
copy(inStream, outStream);
return outStream.toByteArray();
}
}
public static void copy(InputStream inStream, OutputStream outStream) throws IOException {
int read;
byte[] buf = new byte[4096];
while ((read = inStream.read(buf, 0, buf.length)) != -1) {
outStream.write(buf, 0, read);
}
}
}
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.UUIDMap;
@@ -43,7 +43,7 @@ public class Vault {
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
entries.add(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
} catch (EncodingException | OtpInfoException | JSONException e) {
throw new VaultException(e);
}
@@ -0,0 +1,223 @@
package com.beemdevelopment.aegis.vault;
import android.content.Context;
import android.content.UriPermission;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class VaultBackupManager {
private static final String TAG = VaultBackupManager.class.getSimpleName();
private static final StrictDateFormat _dateFormat =
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
public static final String FILENAME_PREFIX = "aegis-backup";
private Context _context;
public VaultBackupManager(Context context) {
_context = context;
}
public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException {
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
if (!hasPermissionsAt(dirUri)) {
Log.e(TAG, "Unable to create file for backup, no persisted URI permissions");
throw new VaultManagerException("No persisted URI permissions");
}
// If we create a file with a name that already exists, SAF will append a number
// to the filename and write to that instead. We can't overwrite existing files, so
// just avoid that altogether by checking beforehand.
if (dir.findFile(fileInfo.toString()) != null) {
throw new VaultManagerException("Backup file already exists");
}
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
if (file == null) {
Log.e(TAG, "Unable to create file for backup, createFile returned null");
throw new VaultManagerException("createFile returned null");
}
try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME);
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, "Unable to create backup", e);
throw new VaultManagerException(e);
}
enforceVersioning(dir, versionsToKeep);
}
public boolean hasPermissionsAt(Uri uri) {
for (UriPermission perm : _context.getContentResolver().getPersistedUriPermissions()) {
if (perm.getUri().equals(uri)) {
return perm.isReadPermission() && perm.isWritePermission();
}
}
return false;
}
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
List<File> files = new ArrayList<>();
for (DocumentFile docFile : dir.listFiles()) {
if (docFile.isFile() && !docFile.isVirtual()) {
try {
files.add(new File(docFile));
} catch (ParseException ignored) { }
}
}
Collections.sort(files, new FileComparator());
for (File file : files) {
Log.i(TAG, file.getFile().getName());
}
if (files.size() > versionsToKeep) {
for (File file : files.subList(0, files.size() - versionsToKeep)) {
Log.i(TAG, String.format("Deleting %s", file.getFile().getName()));
if (!file.getFile().delete()) {
Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName()));
}
}
}
}
public static class FileInfo {
private String _filename;
private Date _date;
public FileInfo(String filename, Date date) {
_filename = filename;
_date = date;
}
public FileInfo(String filename) {
this(filename, Calendar.getInstance().getTime());
}
public static FileInfo parseFilename(String filename) throws ParseException {
if (filename == null) {
throw new ParseException("The filename must not be null", 0);
}
final String ext = ".json";
if (!filename.endsWith(ext)) {
throwBadFormat(filename);
}
filename = filename.substring(0, filename.length() - ext.length());
final String delim = "-";
String[] parts = filename.split(delim);
if (parts.length < 3) {
throwBadFormat(filename);
}
filename = TextUtils.join(delim, Arrays.copyOf(parts, parts.length - 2));
if (!filename.equals(FILENAME_PREFIX)) {
throwBadFormat(filename);
}
Date date = _dateFormat.parse(parts[parts.length - 2] + delim + parts[parts.length - 1]);
if (date == null) {
throwBadFormat(filename);
}
return new FileInfo(filename, date);
}
private static void throwBadFormat(String filename) throws ParseException {
throw new ParseException(String.format("Bad backup filename format: %s", filename), 0);
}
public String getFilename() {
return _filename;
}
public Date getDate() {
return _date;
}
@NonNull
@Override
public String toString() {
return String.format("%s-%s.json", _filename, _dateFormat.format(_date));
}
}
private static class File {
private DocumentFile _file;
private FileInfo _info;
public File(DocumentFile file) throws ParseException {
_file = file;
_info = FileInfo.parseFilename(file.getName());
}
public DocumentFile getFile() {
return _file;
}
public FileInfo getInfo() {
return _info;
}
}
private static class FileComparator implements Comparator<File> {
@Override
public int compare(File o1, File o2) {
return o1.getInfo().getDate().compareTo(o2.getInfo().getDate());
}
}
// https://stackoverflow.com/a/19503019
private static class StrictDateFormat extends SimpleDateFormat {
public StrictDateFormat(String pattern, Locale locale) {
super(pattern, locale);
setLenient(false);
}
@Override
public Date parse(@NonNull String text, ParsePosition pos) {
int posIndex = pos.getIndex();
Date d = super.parse(text, pos);
if (!isLenient() && d != null) {
String format = this.format(d);
if (posIndex + format.length() != text.length() ||
!text.endsWith(format)) {
d = null; // Not exact match
}
}
return d;
}
}
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -38,6 +38,13 @@ public class VaultEntry extends UUIDMap.Value {
setIssuer(issuer);
}
public VaultEntry(OtpInfo info, String name, String issuer, String group) {
this(info);
setName(name);
setIssuer(issuer);
setGroup(group);
}
public VaultEntry(GoogleAuthInfo info) {
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
}
@@ -60,7 +67,7 @@ public class VaultEntry extends UUIDMap.Value {
return obj;
}
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, EncodingException {
// if there is no uuid, generate a new one
UUID uuid;
if (!obj.has("uuid")) {
@@ -3,11 +3,10 @@ package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vault.slots.SlotListException;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -95,7 +94,7 @@ public class VaultFile {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
} catch (MasterKeyException | JSONException | Base64Exception e) {
} catch (MasterKeyException | JSONException | EncodingException e) {
throw new VaultFileException(e);
}
}
@@ -136,7 +135,7 @@ public class VaultFile {
SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
return new Header(slots, params);
} catch (SlotListException | JSONException | HexException e) {
} catch (SlotListException | JSONException | EncodingException e) {
throw new VaultFileException(e);
}
}
@@ -1,112 +1,128 @@
package com.beemdevelopment.aegis.vault;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.services.NotificationService;
import androidx.core.util.AtomicFile;
import com.beemdevelopment.aegis.Preferences;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.Collator;
import java.util.Collection;
import java.util.TreeSet;
import java.util.UUID;
public class VaultManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
public static final String FILENAME = "aegis.json";
public static final String FILENAME_EXPORT = "aegis-export.json";
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
private Vault _vault;
private VaultFile _file;
private VaultFileCredentials _creds;
private boolean _encrypt;
private Context _context;
private Preferences _prefs;
private VaultBackupManager _backups;
public VaultManager(Context context) {
public VaultManager(Context context, Vault vault, VaultFileCredentials creds) {
_context = context;
_prefs = new Preferences(context);
_backups = new VaultBackupManager(context);
_vault = vault;
_creds = creds;
}
public boolean fileExists() {
File file = new File(_context.getFilesDir(), FILENAME);
public VaultManager(Context context, Vault vault) {
this(context, vault, null);
}
public static boolean fileExists(Context context) {
File file = new File(context.getFilesDir(), FILENAME);
return file.exists() && file.isFile();
}
public void load() throws VaultManagerException {
assertState(true, false);
try (FileInputStream file = _context.openFileInput(FILENAME)) {
byte[] fileBytes = new byte[(int) file.getChannel().size()];
DataInputStream stream = new DataInputStream(file);
stream.readFully(fileBytes);
stream.close();
_file = VaultFile.fromBytes(fileBytes);
_encrypt = _file.isEncrypted();
if (!isEncryptionEnabled()) {
JSONObject obj = _file.getContent();
_vault = Vault.fromJson(obj);
}
} catch (IOException | VaultFileException | VaultException e) {
throw new VaultManagerException(e);
}
}
public void lock() {
assertState(false, true);
_creds = null;
_vault = null;
}
public void unlock(VaultFileCredentials creds) throws VaultManagerException {
assertState(true, true);
public static VaultFile readFile(Context context) throws VaultManagerException {
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
try {
JSONObject obj = _file.getContent(creds);
_vault = Vault.fromJson(obj);
_creds = creds;
_context.startService(new Intent(_context, NotificationService.class));
} catch (VaultFileException | VaultException e) {
byte[] fileBytes = file.readFully();
return VaultFile.fromBytes(fileBytes);
} catch (IOException | VaultFileException e) {
throw new VaultManagerException(e);
}
}
public static void save(Context context, VaultFile file) throws VaultManagerException {
byte[] bytes = file.toBytes();
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
public static VaultManager init(Context context, VaultFile file, VaultFileCredentials creds) throws VaultManagerException {
if (file.isEncrypted() && creds == null) {
throw new IllegalArgumentException("The VaultFile is encrypted but the given VaultFileCredentials is null");
}
Vault vault;
try {
JSONObject obj;
if (!file.isEncrypted()) {
obj = file.getContent();
} else {
obj = file.getContent(creds);
}
vault = Vault.fromJson(obj);
} catch (VaultException | VaultFileException e) {
throw new VaultManagerException(e);
}
return new VaultManager(context, vault, creds);
}
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
byte[] bytes = vaultFile.toBytes();
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
FileOutputStream stream = null;
try {
stream = file.startWrite();
stream.write(bytes);
file.finishWrite(stream);
} catch (IOException e) {
if (stream != null) {
file.failWrite(stream);
}
throw new VaultManagerException(e);
}
}
public void save() throws VaultManagerException {
assertState(false, true);
public void save(boolean backup) throws VaultManagerException {
try {
JSONObject obj = _vault.toJson();
VaultFile file = new VaultFile();
if (isEncryptionEnabled()) {
_file.setContent(obj, _creds);
file.setContent(obj, _creds);
} else {
_file.setContent(obj);
file.setContent(obj);
}
save(_context, _file);
save(_context, file);
} catch (VaultFileException e) {
throw new VaultManagerException(e);
}
if (backup && _prefs.isBackupsEnabled()) {
try {
backup();
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
_prefs.setBackupsError(e);
}
}
}
public String export(boolean encrypt) throws VaultManagerException {
assertState(false, true);
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
try {
VaultFile vaultFile = new VaultFile();
if (encrypt && isEncryptionEnabled()) {
@@ -115,57 +131,46 @@ public class VaultManager {
vaultFile.setContent(_vault.toJson());
}
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("error creating external storage directory");
}
byte[] bytes = vaultFile.toBytes();
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
try (FileOutputStream stream = new FileOutputStream(file)) {
stream.write(bytes);
}
return file.getAbsolutePath();
stream.write(bytes);
} catch (IOException | VaultFileException e) {
throw new VaultManagerException(e);
}
}
public void backup() throws VaultManagerException {
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
}
public void addEntry(VaultEntry entry) {
assertState(false, true);
_vault.getEntries().add(entry);
}
public VaultEntry getEntryByUUID(UUID uuid) {
return _vault.getEntries().getByUUID(uuid);
}
public VaultEntry removeEntry(VaultEntry entry) {
assertState(false, true);
return _vault.getEntries().remove(entry);
}
public VaultEntry replaceEntry(VaultEntry entry) {
assertState(false, true);
return _vault.getEntries().replace(entry);
}
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
assertState(false, true);
_vault.getEntries().swap(entry1, entry2);
}
public boolean isEntryDuplicate(VaultEntry entry) {
assertState(false, true);
return _vault.getEntries().has(entry);
}
public Collection<VaultEntry> getEntries() {
assertState(false, true);
return _vault.getEntries().getValues();
}
public TreeSet<String> getGroups() {
assertState(false, true);
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
for (VaultEntry entry : getEntries()) {
String group = entry.getGroup();
@@ -177,62 +182,24 @@ public class VaultManager {
}
public VaultFileCredentials getCredentials() {
assertState(false, true);
return _creds;
}
public void setCredentials(VaultFileCredentials creds) {
assertState(false, true);
_creds = creds;
}
public VaultFile.Header getFileHeader() {
assertLoaded(true);
return _file.getHeader();
}
public boolean isEncryptionEnabled() {
assertLoaded(true);
return _encrypt;
return _creds != null;
}
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
assertState(false, true);
_creds = creds;
_encrypt = true;
save();
save(true);
}
public void disableEncryption() throws VaultManagerException {
assertState(false, true);
_creds = null;
_encrypt = false;
save();
}
public boolean isLoaded() {
return _file != null;
}
public boolean isLocked() {
return _vault == null;
}
private void assertState(boolean locked, boolean loaded) {
assertLoaded(loaded);
if (isLocked() && !locked) {
throw new AssertionError("vault file has not been unlocked yet");
} else if (!isLocked() && locked) {
throw new AssertionError("vault file has already been unlocked");
}
}
private void assertLoaded(boolean loaded) {
if (isLoaded() && !loaded) {
throw new AssertionError("vault file has already been loaded");
} else if (!isLoaded() && loaded) {
throw new AssertionError("vault file has not been loaded yet");
}
save(true);
}
}
@@ -4,4 +4,8 @@ public class VaultManagerException extends Exception {
public VaultManagerException(Throwable cause) {
super(cause);
}
public VaultManagerException(String message) {
super(message);
}
}
@@ -5,8 +5,8 @@ import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
@@ -144,7 +144,7 @@ public abstract class Slot extends UUIDMap.Value {
default:
throw new SlotException("unrecognized slot type");
}
} catch (JSONException | HexException e) {
} catch (JSONException | EncodingException e) {
throw new SlotException(e);
}
+33
View File
@@ -0,0 +1,33 @@
syntax = "proto3";
option java_package = "com.beemdevelopment.aegis";
option java_outer_classname = "GoogleAuthProtos";
message MigrationPayload {
enum Algorithm {
ALGO_INVALID = 0;
ALGO_SHA1 = 1;
}
enum OtpType {
OTP_INVALID = 0;
OTP_HOTP = 1;
OTP_TOTP = 2;
}
message OtpParameters {
bytes secret = 1;
string name = 2;
string issuer = 3;
Algorithm algorithm = 4;
int32 digits = 5;
OtpType type = 6;
int64 counter = 7;
}
repeated OtpParameters otp_parameters = 1;
int32 version = 2;
int32 batch_size = 3;
int32 batch_index = 4;
int32 batch_id = 5;
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="0"
android:fromYDelta="-100%"
android:toYDelta="0" />
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="300"/>
</set>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="300"/>
</set>
+11
View File
@@ -0,0 +1,11 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="150">
<scale
android:fromXScale="0"
android:fromYScale="0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="100%"
android:toYScale="100%" />
</set>
+11
View File
@@ -0,0 +1,11 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="150">
<scale
android:fromXScale="100%"
android:fromYScale="100%"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0"
android:toYScale="0" />
</set>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<translate
android:duration="300"
android:fromYDelta="0"
android:toYDelta="100%" />
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="500"/>
</set>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<translate
android:duration="300"
android:fromYDelta="0"
android:toYDelta="100%" />
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="500"/>
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="500"/>
</set>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/auth_button"/>
<stroke android:color="@color/auth_button" android:width="1dp" />
<!--corners allow us to make the rounded corners button-->
<corners android:radius="4dp" />
</shape>
</item>
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
</vector>
@@ -0,0 +1,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="7dp" android:left="7dp" android:right="7dp" android:bottom="7dp">
<vector android:height="24dp" android:viewportHeight="3328" android:tint="#FFFFFF"
android:viewportWidth="3333" android:width="24.036058dp">
<path android:fillColor="#FF000000" android:fillType="evenOdd" android:pathData="M5,0h1211v1206L5,1206L5,0zM3025,3025h309v304h-309v-304zM2432,3025h309v290h-618v-592h299v-300h304v-598h309v294h294v304h-294v304h-603v299zM1515,2417h299v-304h-289v-304h289v-304h-294v304h-309v-304h304L1515,603h309v902h299v304h294v-304h309v304h-294v304h-309v598h-299v603h-309v-897zM3020,1505h309v304h-309v-304zM608,1505h309v304L608,1809v-304zM5,1505h309v304L5,1809v-304zM1515,0h309v304h-309L1515,0zM0,2118h1211v1206L0,3324L0,2118zM294,2411h623v620L294,3031v-620zM2118,0h1211v1206L2118,1206L2118,0zM2412,293h623v620h-623L2412,293zM299,293h623v620L299,913L299,293z"/>
</vector>
</item>
</layer-list>
@@ -0,0 +1,12 @@
<vector android:height="60dp" android:tint="#F7F7F7"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:scaleX="0.5"
android:scaleY="0.5"
android:pivotX="12"
android:pivotY="12"
>
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</group>
</vector>
+78 -47
View File
@@ -7,37 +7,44 @@
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.ui.AuthActivity">
<LinearLayout
android:orientation="vertical"
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
tools:layout_editor_absoluteY="8dp"
tools:layout_editor_absoluteX="8dp">
android:layout_height="match_parent"
android:layout_margin="48dp"
android:orientation="vertical"
tools:layout_editor_absoluteX="8dp"
tools:layout_editor_absoluteY="8dp">
<ImageView
android:id="@+id/imageView2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="20dp"
app:srcCompat="@drawable/ic_aegis_iconx" />
<TextView
android:text="@string/authentication"
android:textColor="?attr/primaryText"
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:id="@+id/textView2" />
android:fontFamily="sans-serif-light"
android:singleLine="false"
android:text="@string/authentication_multiline"
android:textColor="?attr/authText"
android:textFontWeight="500"
android:textSize="46sp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp">
<TextView
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_enter_password"
android:textColor="?attr/secondaryText" />
android:layout_marginTop="12dp"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingBottom="25dp"
app:hintEnabled="false"
app:passwordToggleEnabled="true"
app:passwordToggleTint="#949494">
@@ -46,48 +53,72 @@
android:id="@+id/text_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
android:hint="@string/password"
android:inputType="textPassword"
android:textColor="?attr/primaryText"
android:textColorHint="?attr/authText"
app:backgroundTint="?attr/authText" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="25dp">
<Button
android:id="@+id/button_decrypt"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_rounded_corners"
android:minWidth="125dp"
android:text="@string/unlock" />
<Button
android:id="@+id/button_biometrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="125dp"
android:text="@string/biometrics"
android:visibility="gone" />
android:text="@string/unlock"
android:textColor="@color/primary_text_inverted"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/box_biometric_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="12dp"
android:visibility="gone">
<ImageView
<TextView
android:id="@+id/button_biometrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp"
android:layout_marginEnd="15dp"/>
android:layout_gravity="center"
android:minWidth="125dp"
android:text="@string/auth_use_biometrics"
android:textColor="?attr/authText"
android:visibility="gone" />
<TextView
<LinearLayout
android:id="@+id/box_biometric_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/invalidated_biometrics"/>
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_info_outline_black_24dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/invalidated_biometrics" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:textStyle="bold"
android:textAllCaps="true"
android:text="@string/app_name_full"
android:paddingBottom="50dp"
android:textColor="@color/divider" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+36 -5
View File
@@ -8,12 +8,43 @@
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.ui.MainActivity">
<fragment
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="match_parent"
<LinearLayout
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/btn_backup_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp"
android:background="@color/colorAccent"
android:foreground="?android:selectableItemBackground"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/icon_primary_inverted"
android:src="@drawable/ic_alert_black_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/backup_error_bar_message"
android:textColor="@color/primary_text_inverted"
android:layout_marginStart="5dp" />
</LinearLayout>
<fragment
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="fill_parent"
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</LinearLayout>
<!-- note: the fab should always be the last element to be sure it's displayed on top -->
<com.getbase.floatingactionbutton.FloatingActionsMenu
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/cardBackground"
tools:context="com.beemdevelopment.aegis.ui.TransferEntriesActivity">
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="250dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.496"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.399" />
<TextView
android:id="@+id/tvTransfer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/transfer_entry"
android:textColor="?attr/primaryText"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/tvDescription"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:text="@string/transfer_entry_description"
app:layout_constraintBottom_toTopOf="@+id/ivQrCode"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/tvIssuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Issuer"
android:textColor="?attr/primaryText"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivQrCode" />
<TextView
android:id="@+id/tvAccountName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Accountname"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvIssuer" />
<Button
android:id="@+id/btnNext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/transparent"
android:text="@string/next"
android:textAllCaps="false"
android:textColor="@color/code_primary_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnPrevious"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/transparent"
android:text="@string/previous"
android:textAllCaps="false"
android:visibility="invisible"
android:textColor="@color/code_primary_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tvEntriesCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/entries_count"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/btnNext"
app:layout_constraintHorizontal_bias="0.506"
app:layout_constraintStart_toStartOf="@+id/btnPrevious" />
</androidx.constraintlayout.widget.ConstraintLayout>
+52 -20
View File
@@ -2,11 +2,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:orientation="vertical">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
@@ -30,6 +31,17 @@
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/item_selected"
android:visibility="gone"
app:civ_circle_background_color="@color/colorSecondary" />
</RelativeLayout>
<RelativeLayout
@@ -42,41 +54,61 @@
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="12dp"
android:paddingTop="18dp"
android:paddingBottom="16dp"
android:paddingEnd="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingStart="16dp">
<TextView
android:id="@+id/profile_copied"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/secondaryText"
android:includeFontPadding="false"
android:textSize="15sp"
android:layout_below="@+id/profile_code"
android:maxLines="1"
android:text="@string/copied"
android:layout_above="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:ellipsize="end"
android:maxLines="1"/>
android:textColor="?attr/secondaryText"
android:textSize="16sp"
android:visibility="invisible" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textStyle="bold"
android:textColor="?attr/primaryText"
android:includeFontPadding="false"
android:textSize="16sp"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_code"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/extra_info_text"
android:textSize="15sp"
android:textColor="@color/secondary_text"
android:textSize="16sp"
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
@@ -85,12 +117,12 @@
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:textSize="34sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
@@ -120,12 +152,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="4dp"
android:id="@+id/progressBar"
android:max="1000"
android:max="5000"
android:layout_weight="1"/>
</LinearLayout>
+39 -8
View File
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
@@ -30,6 +31,17 @@
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/item_selected"
android:visibility="gone"
app:civ_circle_background_color="@color/colorSecondary" />
</RelativeLayout>
<RelativeLayout
@@ -42,25 +54,44 @@
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="6dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp">
<TextView
android:id="@+id/profile_copied"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:maxLines="1"
android:text="@string/copied"
android:layout_above="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:textColor="?attr/secondaryText"
android:textSize="13sp"
android:visibility="invisible" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/secondaryText"
android:textColor="?attr/primaryText"
android:textStyle="bold"
android:includeFontPadding="false"
android:textSize="13sp"
android:layout_below="@+id/profile_code"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:ellipsize="end"
android:maxLines="1"/>
@@ -68,7 +99,6 @@
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_code"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
@@ -76,6 +106,7 @@
android:textColor="@color/extra_info_text"
android:textSize="13sp"
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
@@ -85,12 +116,12 @@
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:textSize="26sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
@@ -120,7 +151,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="3dp"
+39 -9
View File
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
@@ -30,6 +31,17 @@
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/item_selected"
android:visibility="gone"
app:civ_circle_background_color="@color/colorSecondary" />
</RelativeLayout>
<RelativeLayout
@@ -42,25 +54,44 @@
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="6dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp">
<TextView
android:id="@+id/profile_copied"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:maxLines="1"
android:text="@string/copied"
android:layout_above="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:textColor="?attr/secondaryText"
android:textSize="13sp"
android:visibility="invisible" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/secondaryText"
android:textStyle="bold"
android:textColor="?attr/primaryText"
android:includeFontPadding="false"
android:textSize="13sp"
android:layout_below="@+id/profile_code"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:ellipsize="end"
android:maxLines="1"/>
@@ -68,7 +99,6 @@
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_code"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
@@ -76,7 +106,7 @@
android:textColor="@color/extra_info_text"
android:textSize="13sp"
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
@@ -85,12 +115,12 @@
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:textSize="26sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
@@ -120,7 +150,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="3dp"
+11 -11
View File
@@ -62,6 +62,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-info-outline"
app:ico_size="16dp" />
@@ -71,7 +72,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -97,7 +97,7 @@
android:id="@+id/btn_changelog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -108,6 +108,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-time-restore"
app:ico_size="16dp" />
@@ -116,7 +117,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -150,6 +150,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-github"
app:ico_size="16dp" />
@@ -158,7 +159,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -192,6 +192,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-file-text"
app:ico_size="16dp" />
@@ -200,7 +201,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -267,6 +267,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
@@ -275,7 +276,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -312,6 +312,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
@@ -320,7 +321,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -357,6 +357,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-email"
app:ico_size="16dp" />
@@ -365,7 +366,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/email_us"
@@ -389,6 +389,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-globe"
app:ico_size="16dp" />
@@ -396,7 +397,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/visit_website"
@@ -438,7 +438,7 @@
android:id="@+id/btn_rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
@@ -450,6 +450,7 @@
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-star"
app:ico_size="16dp" />
@@ -458,7 +459,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="25dp"
android:paddingBottom="10dp"
android:paddingEnd="25dp"
android:paddingTop="10dp">
<TextView
android:id="@+id/error_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/error_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="10dp"
android:textStyle="italic"/>
</LinearLayout>
@@ -7,6 +7,14 @@
android:paddingBottom="10dp"
android:paddingEnd="20dp"
android:paddingTop="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_password_explanation"
android:textColor="#FF0000"
android:textStyle="bold"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"/>
<EditText
android:id="@+id/text_password"
android:hint="@string/password"
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="10dp"
android:paddingTop="10dp">
<TextView
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/time_sync_warning_message"/>
<CheckBox
android:id="@+id/check_warning_disable"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/time_sync_warning_disable"/>
</LinearLayout>
@@ -6,14 +6,14 @@
android:background="?attr/background"
android:orientation="vertical">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:progressDrawable="@drawable/progress_horizontal"
android:id="@+id/progressBar"
android:visibility="gone"
android:max="1000"/>
android:max="5000"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
@@ -20,13 +20,19 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp"
android:orientation="horizontal">
android:layout_marginTop="24dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_explanation"/>
<RadioGroup
android:id="@+id/rg_authenticationMethod"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginTop="24dp">
<RadioButton
android:id="@+id/rb_none"
@@ -14,11 +14,19 @@
android:textColor="@color/primary_text_inverted"
android:id="@+id/textView2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_password_explanation"
android:textColor="#FFFF00"
android:textStyle="bold"
android:layout_marginTop="24dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
android:layout_marginTop="24dp">
<TextView
android:layout_width="match_parent"
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/cardBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="@string/password_reminder"/>
</LinearLayout>
@@ -22,4 +22,12 @@
android:icon="@drawable/ic_delete_white"
android:tint="?attr/iconColorPrimary"
app:showAsAction="always"/>
<item
android:id="@+id/action_share_qr"
android:title="@string/action_transfer"
android:orderInCategory="100"
android:icon="@drawable/ic_qr_code_full"
android:tint="?attr/iconColorPrimary"
app:showAsAction="always"/>
</menu>
+6 -3
View File
@@ -8,12 +8,15 @@
app:showAsAction="ifRoom"
android:title="@string/save"/>
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
android:id="@+id/action_edit_icon"
android:title="@string/action_edit_icon"
app:showAsAction="never"/>
<item
android:id="@+id/action_default_icon"
android:title="@string/action_default_icon"
app:showAsAction="never"/>
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
app:showAsAction="never"/>
</menu>
+9 -4
View File
@@ -31,10 +31,10 @@
<license>MIT License</license>
</notice>
<notice>
<name>Spongycastle</name>
<url>https://github.com/rtyley/spongycastle/</url>
<copyright>Copyright (c) 2000-2017 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)</copyright>
<license>Apache Software License 2.0</license>
<name>Bouncy Castle</name>
<url>https://www.bouncycastle.org/</url>
<copyright>Copyright (c) 2000-2019 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)</copyright>
<license>MIT License</license>
</notice>
<notice>
<name>CircleImageView</name>
@@ -70,4 +70,9 @@
<copyright>Copyright (C) 2016 The Android Open Source Project</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>SQLCipher for Android</name>
<url>https://github.com/sqlcipher/android-database-sqlcipher</url>
<license>Apache Software License 2.0</license>
</notice>
</notices>
+65 -46
View File
@@ -11,7 +11,7 @@
<string name="settings">Předvolby</string>
<string name="pref_appearance_group_title">Vzhled</string>
<string name="pref_general_group_title">Obecné</string>
<string name="pref_security_group_title">Bezpečnost</string>
<string name="pref_security_group_title">Zabezpečení</string>
<string name="pref_tools_group_title">Nástroje</string>
<string name="pref_select_theme_title">Vzhled</string>
<string name="pref_view_mode_title">Režim zobrazení</string>
@@ -21,37 +21,47 @@
<string name="pref_timeout_title">Časový limit</string>
<string name="pref_timeout_summary">Automaticky zamknout trezor po %1$s sekundách nečinnosti</string>
<string name="pref_slots_title">Sloty klíčů</string>
<string name="pref_slots_summary">Správa seznamu klíčů, které mohou dešifrovat databázi</string>
<string name="pref_slots_summary">Správa seznamu klíčů, které mohou dešifrovat trezor</string>
<string name="pref_import_file_title">Importovat ze souboru</string>
<string name="pref_import_file_summary">Importovat databázi ze souboru</string>
<string name="pref_import_file_summary">Importovat tokeny ze souboru</string>
<string name="pref_import_app_title">Importovat z aplikace</string>
<string name="pref_import_app_summary">Importovat databázi z aplikace (vyžaduje root přístup)</string>
<string name="pref_import_app_summary">Importovat tokeny z aplikace (vyžaduje root přístup)</string>
<string name="pref_export_title">Exportovat</string>
<string name="pref_export_summary">Exportovat databázi</string>
<string name="pref_export_summary">Exportovat trezor</string>
<string name="pref_password_reminder_title">Připomenutí hesla</string>
<string name="pref_password_reminder_summary">Jednou za čas zobrazit výzvu pro zadání hesla, abyste ho nezapomněli.</string>
<string name="pref_secure_screen_title">Zabezpečení obrazovky</string>
<string name="pref_secure_screen_summary">Blokovat snímky obrazovky a další pokusy o zachycení obrazovky v rámci aplikace</string>
<string name="pref_tap_to_reveal_title">Zobrazit po klepnutí</string>
<string name="pref_tap_to_reveal_summary">Ve výchozím nastavení budou tokeny skryté. Kód zobrazíte klepnutím na token.</string>
<string name="pref_tap_to_reveal_time_title">Čas do skrytí zobrazeného kódu</string>
<string name="pref_tap_to_reveal_title">Zobrazit klepnutím</string>
<string name="pref_tap_to_reveal_summary">Ve výchozím stavu budou kódy skryté. Kód zobrazíte klepnutím na token.</string>
<string name="pref_tap_to_reveal_time_title">Prodleva zobrazení kódu</string>
<string name="pref_auto_lock_title">Automatické uzamknutí</string>
<string name="pref_auto_lock_summary">Automaticky zamknout trezor po zavření aplikace, nebo uzamknutí zařízení.</string>
<string name="pref_auto_lock_summary">Automaticky uzamknout trezor při zavření aplikace, nebo uzamčení zařízení.</string>
<string name="pref_encryption_title">Šifrování</string>
<string name="pref_encryption_summary">Šifrovat databázi a odemknout ji pomocí hesla nebo otisku prstu</string>
<string name="pref_encryption_summary">Šifrovat trezor a odemykat jej pomocí hesla, nebo biometrie</string>
<string name="pref_biometrics_title">Biometrické odemknutí</string>
<string name="pref_biometrics_summary">Povolit biometrické ověření pro odemčení trezoru</string>
<string name="pref_set_password_title">Změnit heslo</string>
<string name="pref_set_password_summary">Nastavení nového hesla potřebného pro odemknutí trezoru</string>
<string name="pref_set_password_summary">Nastavení nového hesla potřebného pro odemčení trezoru</string>
<string name="choose_authentication_method">Zabezpečení</string>
<string name="authentication_method_explanation">Aegis je 2FA aplikace zaměřená na bezpečnost. Tokeny jsou uloženy v trezoru, který může být zašifrovaný vámi zvoleným heslem. Získá-li útočník váš zašifrovaný trezor, bez hesla se do něj nedostane.\n\nPředvybrali jsme možnost, která by podle nás měla nejvíce vyhovovat vašemu zařízení.</string>
<string name="authentication_method_none">Žádné</string>
<string name="authentication_method_none_description">K odemknutí trezoru není potřeba heslo a trezor nebude šifrován. Tato volba není doporučena.</string>
<string name="authentication_method_none_description">Trezor nebude šifrován a nebude potřeba heslo pro jeho odemčení. Tuto možnost <b>nedoporučujeme</b>.</string>
<string name="authentication_method_password">Heslo</string>
<string name="authentication_method_password_description">K odemknutí trezoru potřebujete heslo.</string>
<string name="authentication_method_password_description">K odemčení trezoru je potřeba heslo.</string>
<string name="authentication_method_password_explanation">Upozornění: Pokud zapomenete své heslo, trvale ztratíte přístup ke svým tokenům. Bez hesla neexistuje způsob, jak je obnovit.</string>
<string name="authentication_method_biometrics">Biometrie</string>
<string name="authentication_method_biometrics_description">Kromě hesla půjde k odemčení trezoru použít také biometrické prvky registrované v tomto zařízení jako je otisk prstu nebo tvář.</string>
<string name="authentication_method_set_password">Heslo</string>
<string name="authentication_enter_password">Zadejte své heslo</string>
<string name="authentication">Odemknout trezor</string>
<string name="set_password">Zadejte heslo</string>
<string name="set_group">Zadejte název skupiny</string>
<string name="set_number">Zadejte číslo</string>
<string name="set_password_confirm">Potvrďte heslo</string>
<string name="invalidated_biometrics">Zjištěna změna nastavení zabezpečení zařízení. Přejděte do „Aegis“ → „Nastavení“ → „Biometrie“ a povolte biometrické odemknutí znovu.</string>
<string name="password_reminder">Od posledního zadání vašeho hesla uplynulo už mnoho vody. Stále si ho pamatujete?</string>
<string name="unlock">Odemknout</string>
<string name="biometrics">Biometrie</string>
<string name="advanced">Pokročilé</string>
<string name="seconds">sekund</string>
<string name="counter">Počítadlo</string>
@@ -60,70 +70,78 @@
<string name="scan">Naskenovat QR kód</string>
<string name="scan_image">Skenovat obrázek</string>
<string name="enter_manually">Zadat ručně</string>
<string name="add_biometric">Přidat biometriku</string>
<string name="add_biometric_slot">Přidat biometrický slot</string>
<string name="set_up_biometric">Nastavení biometrického odemčení</string>
<string name="add_password">Přidat heslo</string>
<string name="slots_warning">Trezor je bezpečný jen jako nejslabší místo. Pokud bude do zařízení přidán nový otisk prstu, budete muset v Aegis reaktivovat ověření otiskem prstu.</string>
<string name="slots_warning">Trezor je stejně bezpečný jako jeho nejslabší místo. Změníte-li nastavení biometrického ověření ve vašem zařízení, budete muset v Aegis znovu aktivovat biometrické odemknutí.</string>
<string name="copy">Kopírovat</string>
<string name="edit">Upravit</string>
<string name="delete">Odstranit</string>
<string name="password">Heslo</string>
<string name="confirm_password">Potvrzení hesla</string>
<string name="new_profile">Nový profil</string>
<string name="add_new_profile">Přidat nový profil</string>
<string name="confirm_password">Potvrdit heslo</string>
<string name="show_password">Zobrazit heslo</string>
<string name="unlock_vault_error">Trezor nelze odemknout</string>
<string name="unlock_vault_error_description">Nesprávné heslo. Ujistěte se, že je heslo správně zadané.</string>
<string name="unlock_vault_error_description">Nesprávné heslo. Ujistěte se, že je zadané správně.</string>
<string name="password_equality_error">Hesla musí být stejná a nesmí být prázdná</string>
<string name="snackbar_authentication_method">Zvolte způsob ověření</string>
<string name="snackbar_authentication_method">Zvolte prosím způsob ověření</string>
<string name="encrypting_vault">Šifrování trezoru</string>
<string name="delete_entry">Odstranit položku</string>
<string name="delete_entry_description">Opravdu chcete odstranit tento záznam?</string>
<string name="delete_entry_description">Opravdu chcete odstranit tuto položku?</string>
<string name="delete_entries">Odstranit položky</string>
<string name="delete_entries_description">Opravdu chcete odstranit %d položek?</string>
<string name="discard_changes">Zahodit změny?</string>
<string name="discard_changes_description">Změny nebyly uloženy</string>
<string name="folder">Složka</string>
<string name="tap_to_select">Klepnutím vyberte</string>
<string name="tap_to_select">Vyberte klepnutím</string>
<string name="saving_profile_error">Chyba při ukládání profilu</string>
<string name="welcome">Vítejte</string>
<string name="app_description">Aegis je bezpečná open source 2FA aplikace. Aegis je zdarma.</string>
<string name="app_description">Aegis je bezpečná, bezplatná a open source 2FA aplikace</string>
<string name="setup_completed">Nastavení dokončeno</string>
<string name="setup_completed_description">Nastavení Aegis dokončeno. Můžeme začít.</string>
<string name="vault_not_found">Trezor nenalezen, spouštění nastavení…</string>
<string name="code_copied">Kód zkopírován do schránky</string>
<string name="setup_completed_description">Aegis je nastaven a připraven k používání.</string>
<string name="vault_not_found">Trezor nebyl nenalezen, spouštím nastavení…</string>
<string name="errors_copied">Chyby zkopírovány do schránky</string>
<string name="version_copied">Verze zkopírována do schránky</string>
<string name="decryption_error">Při odemykání trezoru došlo k chybě</string>
<string name="saving_error">Při pokusu o uložení trezoru došlo k chybě</string>
<string name="disable_encryption">Vypnout šifrování</string>
<string name="disable_encryption_description">Opravdu chcete vypnout šifrování úložiště? Obsah trezoru bude uložen jako prostý text.</string>
<string name="disable_encryption_description">Opravdu chcete vypnout šifrování úložiště? Obsah trezoru pak bude uložen jako prostý text.</string>
<string name="enable_encryption_error">Při zapínání šifrování došlo k chybě</string>
<string name="disable_encryption_error">Při vypínání šifrování došlo k chybě</string>
<string name="permission_denied">Přístup zamítnut</string>
<string name="choose_application">Zvolte aplikaci, ze které chcete importovat databázi</string>
<string name="andotp_new_format">Nový formát (v0.6.3 nebo novější) </string>
<string name="andotp_old_format">Starý formát (v0.6.2 a starší) </string>
<string name="choose_andotp_importer">Jaký formát má záložní andOTP soubor?</string>
<string name="choose_application">Zvolte aplikaci, ze které chcete importovat údaje</string>
<string name="choose_theme">Vyberte si vzhled rozhraní</string>
<string name="choose_view_mode">Vyberte si režim zobrazení</string>
<string name="parsing_file_error">Při zpracování souboru došlo k chybě</string>
<string name="file_not_found">Chyba: Soubor nenalezen</string>
<string name="reading_file_error">Při čtení souboru došlo k chybě</string>
<string name="reading_file_error">Při pokusu o čtení souboru došlo k chybě</string>
<string name="app_lookup_error">Chyba: Aplikace není nainstalována</string>
<string name="root_error">Chyba: Nelze získat root přístup</string>
<string name="imported_entries_count">Počet importovaných záznamů: %d</string>
<string name="read_entries_count">Čtení %d záznamů. %d chyby.</string>
<string name="imported_entries_count">Naimportováno %d záznamů</string>
<string name="read_entries_count">Čtení %d záznamů. %d chyb.</string>
<string name="import_error_title">Při importu došlo k jedné nebo více chybám</string>
<string name="export_warning">Databáze bude exportována mimo soukromé úložiště aplikace Aegis.</string>
<string name="encryption_set_password_error">Při pokusu o nastavení hesla došlo k chybě: </string>
<string name="no_cameras_available">Fotoaparát je nedostupný</string>
<string name="exporting_vault_error">Při pokusu o export trezoru došlo k chybě</string>
<string name="exported_vault">Trezor byl exportován</string>
<string name="export_warning">Trezor bude exportován mimo soukromé úložiště aplikace Aegis.</string>
<string name="no_cameras_available">Fotoaparát není dostupný</string>
<string name="read_qr_error">Při pokusu o přečtení QR kódu došlo k chybě</string>
<string name="authentication_method_raw">Prosté</string>
<string name="unlocking_vault">Odemykání trezoru</string>
<string name="unlocking_vault_repair">Odemykání trezoru (oprava)</string>
<string name="unlocking_vault_repair">Odemykání trezoru (opravuji)</string>
<string name="password_slot_error">Je potřeba alespoň jeden slot hesla</string>
<string name="remove_slot">Odstranit slot</string>
<string name="remove_slot_description">Opravdu chcete tento slot odstranit?</string>
<string name="remove_group">Odstranit skupinu</string>
<string name="remove_group_description">Opravdu chcete odstranit tuto skupinu? Záznamy v této skupině budou automaticky přeřazeny do „Žádná skupina“.</string>
<string name="remove_group_description">Opravdu chcete odstranit tuto skupinu? Záznamy z ní budou automaticky přemístěny do „Žádná skupina“.</string>
<string name="adding_new_slot_error">Při pokusu o přidání nového slotu došlo k chybě:</string>
<string name="progressbar_error">Měřítko trvání animace nelze resetovat. Ukazatele průběhu budou neviditelné.</string>
<string name="progressbar_error">Měřítko trvání animace nelze resetovat. Ukazatele průběhu nebudou viditelné.</string>
<string name="details">Podrobnosti</string>
<string name="filter">Filtr</string>
<string name="lock">Zamknout</string>
<string name="filter_ungrouped">Bez skupiny</string>
<string name="lock">Uzamknout</string>
<string name="all">Vše</string>
<string name="name">Název</string>
<string name="no_group">Žádná skupina</string>
@@ -139,24 +157,25 @@
<string name="preference_manage_groups_summary">Správa a odstranění skupin</string>
<string name="pref_search_name_title">Hledat v názvech účtů</string>
<string name="pref_search_name_summary">Zahrnout shody názvů účtů do výsledků hledání</string>
<string name="tap_to_reveal">Skryto</string>
<string name="pref_highlight_entry_title">Zvýraznit tokeny po klepnutí</string>
<string name="pref_highlight_entry_summary">Usnadnit vzájemné rozlišení tokenů dočasným zvýrazněním po kleputí</string>
<string name="tap_to_reveal"> skryto </string>
<string name="selected">Vybráno</string>
<string name="dark_theme_title">Tmavý vzhled</string>
<string name="light_theme_title">Světlý vzhled</string>
<string name="amoled_theme_title">Amoled vzhled</string>
<string name="normal_viewmode_title">Normální</string>
<string name="compact_mode_title">Kompaktní</string>
<string name="small_mode_title">Malý</string>
<string name="unknown_issuer">Neznámý poskytovatel</string>
<string name="unknown_account_name">Neznámý název účtu</string>
<string name="import_error_dialog">Aegis nedokáže importovat některé tokeny (%d). Tyto tokeny budou přeskočeny. Klepněte na „Podrobnosti“ pro více informací o chybách.</string>
<string name="import_error_dialog">Nepovedlo se naimportovat %d tokenů a byly přeskočeny. Klepněte na „podrobnosti“ pro více informací o chybách.</string>
<string name="unable_to_read_qrcode">QR kód nelze přečíst a zpracovat</string>
<string name="select_picture">Vyberte obrázek</string>
<string name="toggle_checkboxes">Přepnout zaškrtávací políčka</string>
<string name="search">Hledat</string>
<string name="channel_name_lock_status">Stav zámku</string>
<string name="channel_description_lock_status">Aegis může vytvořit trvalé oznámení, které vás upozorní, když je trezor uzamčen</string>
<string name="vault_unlocked_state">Trezor odemknut. Klepnutím ho zamknete.</string>
<string name="vault_unlocked_state">Trezor je odemčený. Klepnutím uzamkněte.</string>
<string name="title_activity_about">O aplikaci</string>
<string name="version">Verze</string>
<string name="changelog">Seznam změn</string>
@@ -169,9 +188,9 @@
<string name="visit_website">Navštivte náš web</string>
<string name="about_support">Podpora</string>
<string name="support_rate">Ohodnoťte aplikaci</string>
<string name="support_rate_description">Podpořte nás zanecháním recenze v obchodě Google Play</string>
<string name="webview_error">Vaše zařízení nepodporuje WebView, které je nutné pro zobrazení seznamu změn. Tato komponenta v systému zřejmě chybí.</string>
<string name="support_rate_description">Podpořte nás napsáním recenze v obchodě Google Play</string>
<string name="webview_error">Vaše zařízení nepodporuje WebView, které je nutné pro zobrazení seznamu změn. Tato komponenta zřejmě chybí v systému.</string>
<string name="email">E-mail</string>
<string name="empty_list">Žádné kódy. Začněte přidáním položek pomocí tlačítka plus v pravém dolním rohu.</string>
<string name="empty_list_title">Nenalezeny žádné záznamy</string>
<string name="empty_list">Žádné kódy. Začněte přidáním položek pomocí tlačítka + v pravém dolním rohu.</string>
<string name="empty_list_title">Žádné záznamy nenalezeny</string>
</resources>
@@ -67,8 +67,6 @@
<string name="delete">Löschen</string>
<string name="password">Passwort</string>
<string name="confirm_password">Passwort bestätigen</string>
<string name="new_profile">Neues Profil</string>
<string name="add_new_profile">Neues Profil hinzufügen</string>
<string name="unlock_vault_error">Der Tresor konnte nicht entsperrt werden.</string>
<string name="unlock_vault_error_description">Falsches Passwort. Achte darauf, dass du dein Passwort nicht falsch eingibst.</string>
<string name="password_equality_error">Passwörter sollten identisch und nicht leer sein.</string>
@@ -86,7 +84,6 @@
<string name="setup_completed">Einrichten abgeschlossen</string>
<string name="setup_completed_description">Aegis ist eingerichtet und einsatzbereit.</string>
<string name="vault_not_found">Tresor nicht gefunden, Starten der Einrichtung…</string>
<string name="code_copied">Code in die Zwischenablage kopiert</string>
<string name="errors_copied">Fehlermeldungen in die Zwischenablage kopiert</string>
<string name="version_copied">Version in die Zwischenablage kopiert</string>
<string name="decryption_error">Beim Versuch, den Tresor zu entsperren, ist ein Fehler aufgetreten.</string>
+196
View File
@@ -0,0 +1,196 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">Ρυθμίσεις</string>
<string name="action_about">Σχετικά</string>
<string name="action_import">Εισαγωγή</string>
<string name="action_delete">Διαγραφή</string>
<string name="action_default_icon">Επαναφορά εικονιδίου</string>
<string name="discard">Απόρριψη</string>
<string name="save">Αποθήκευση</string>
<string name="issuer">Εκδότης</string>
<string name="settings">Προτιμήσεις</string>
<string name="pref_appearance_group_title">Εμφάνιση</string>
<string name="pref_general_group_title">Γενικές</string>
<string name="pref_security_group_title">Ασφάλεια</string>
<string name="pref_tools_group_title">Εργαλεία</string>
<string name="pref_select_theme_title">Θέμα</string>
<string name="pref_view_mode_title">Λειτουργία προβολής</string>
<string name="pref_lang_title">Γλώσσα</string>
<string name="pref_account_name_title">Προβολή ονόματος λογαριασμού</string>
<string name="pref_account_name_summary">Ενεργοποίηση προβολής ονόματος λογαριασμού δίπλα στον εκδότη</string>
<string name="pref_timeout_title">Χρονικό όριο</string>
<string name="pref_timeout_summary">Αυτόματο κλείδωμα λίστας μετά από %1$s δευτερόλεπτα αδράνειας</string>
<string name="pref_slots_title">Υποδοχή κλειδιών</string>
<string name="pref_slots_summary">Διαχείριση λίστας κλειδιών που μπορούν να αποκρυπτογραφήσουν τη λίστα</string>
<string name="pref_import_file_title">Εισαγωγή από αρχείο</string>
<string name="pref_import_file_summary">Εισαγωγή αναγνωριστικών από αρχείο</string>
<string name="pref_import_app_title">Εισαγωγή από εφαρμογή</string>
<string name="pref_import_app_summary">Εισαγωγή αναγνωριστικών από εφαρμογή (απαιτείται πρόσβαση root)</string>
<string name="pref_export_title">Εξαγωγή</string>
<string name="pref_export_summary">Εξαγωγή λίστας</string>
<string name="pref_password_reminder_title">Υπενθύμιση κωδικού</string>
<string name="pref_password_reminder_summary">Προβολή υπενθύμισης για εισαγωγή κωδικού περιστασιακά, έτσι ώστε να μην τον ξεχάσετε.</string>
<string name="pref_secure_screen_title">Ασφάλεια οθόνης</string>
<string name="pref_secure_screen_summary">Αποκλεισμός στιγμιότυπων οθόνης και άλλων προσπαθειών για καταγραφή της οθόνης μέσα στην εφαρμογή</string>
<string name="pref_tap_to_reveal_title">Πατήστε για προβολή</string>
<string name="pref_tap_to_reveal_summary">Απόκρυψη αναγνωριστικών από προεπιλογή. Πατήστε στα αναγνωριστικά για προβολή κωδικού.</string>
<string name="pref_tap_to_reveal_time_title">Χρονικό όριο πατήματος προβολής</string>
<string name="pref_auto_lock_title">Αυτόματο κλείδωμα</string>
<string name="pref_auto_lock_summary">Αυτόματο κλείδωμα στο κλείσιμο της εφαρμογής ή στο κλείδωμα της συσκευή σας.</string>
<string name="pref_encryption_title">Κρυπτογράφηση</string>
<string name="pref_encryption_summary">Κρυπτογράφηση λίστας και ξεκλείδωμα με κωδικό ή βιομετρική μέθοδο</string>
<string name="pref_biometrics_title">Βιομετρικό ξεκλείδωμα</string>
<string name="pref_biometrics_summary">Χορήγηση βιομετρικής πιστοποίησης για ξεκλείδωμα λίστας</string>
<string name="pref_set_password_title">Αλλαγή κωδικού</string>
<string name="pref_set_password_summary">Ορισμός νέου κωδικού που θα χρειαστεί για να ξεκλειδώσετε τη λίστα σας</string>
<string name="choose_authentication_method">Ασφάλεια</string>
<string name="authentication_method_explanation">Το Aegis είναι μια εφαρμογή 2FA που εστιάζει στην ασφάλεια. Τα αναγνωριστικά αποθηκεύονται σε μία λίστα, η οποία μπορεί προαιρετικά να κρυπτογραφηθεί με έναν κωδικό της επιλογής σας. Αν κάποιος εισβολέας αποκτήσει το κρυπτογραφημένο αρχείο της λίστας, δεν θα έχει πρόσβαση στα περιεχόμενα χωρίς να γνωρίζει τον κωδικό.\n\nΈχουμε προεπιλέξει την επιλογή που νομίζουμε ότι θα ταιριάζει καλύτερα στη συσκευή σας.</string>
<string name="authentication_method_none">Χωρίς</string>
<string name="authentication_method_none_description">Χωρίς απαίτηση κωδικού για ξεκλείδωμα λίστας η οποία δεν θα κρυπτογραφηθεί. <b>Αυτή η επιλογή δεν συνιστάται</b>.</string>
<string name="authentication_method_password">Κωδικός</string>
<string name="authentication_method_password_description">Απαίτηση κωδικού για ξεκλείδωμα λίστας.</string>
<string name="authentication_method_password_explanation">Προειδοποίηση: Εάν ξεχάσετε τον κωδικό σας, θα χάσετε μόνιμα πρόσβαση στα αναγνωριστικά σας. Δεν υπάρχει τρόπος να τα ανακτήσετε χωρίς τον κωδικό.</string>
<string name="authentication_method_biometrics">Βιομετρική σύνδεση</string>
<string name="authentication_method_biometrics_description">Εκτός από έναν κωδικό, τα βιομετρικά στοιχεία που έχουν καταχωρηθεί σε αυτήν τη συσκευή, όπως ένα δακτυλικό αποτύπωμα ή το πρόσωπο σας, μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε τη λίστα σας.</string>
<string name="authentication_method_set_password">Κωδικός</string>
<string name="authentication_enter_password">Εισάγετε τον κωδικό σας</string>
<string name="set_password">Παρακαλώ εισάγετε κωδικό</string>
<string name="set_group">Παρακαλώ εισάγετε όνομα ομάδας</string>
<string name="set_number">Παρακαλώ εισάγετε αριθμό</string>
<string name="set_password_confirm">Παρακαλώ επιβεβαιώστε τον κωδικό</string>
<string name="invalidated_biometrics">Έχει εντοπιστεί μια αλλαγή στις ρυθμίσεις ασφαλείας της συσκευής σας. Παρακαλώ πηγαίνετε στο \"Aegis -&gt; Ρυθμίσεις -&gt; Βιομετρική σύνδεση και ενεργοποιήστε ξανά το βιομετρικό ξεκλείδωμα.</string>
<string name="password_reminder">Έχει περάσει λίγος καιρός από τότε που εισάγατε τον κωδικό σας. Τον θυμάστε ακόμα;</string>
<string name="unlock">Ξεκλείδωμα</string>
<string name="biometrics">Βιομετρική σύνδεση</string>
<string name="advanced">Σύνθετες</string>
<string name="seconds">δευτερόλεπτα</string>
<string name="counter">Μετρητής</string>
<string name="digits">Ψηφία</string>
<string name="secret">Μυστικός κωδικός</string>
<string name="scan">Σάρωση κώδικα QR</string>
<string name="scan_image">Σάρωση εικόνας</string>
<string name="enter_manually">Χειροκίνητη εισαγωγή</string>
<string name="add_biometric">Προσθήκη βιομετρίας</string>
<string name="add_biometric_slot">Προσθήκη βιομετρικής υποδοχής</string>
<string name="set_up_biometric">Ρύθμιση βιομετρικού ξεκλειδώματος</string>
<string name="add_password">Προσθήκη κωδικού</string>
<string name="slots_warning">Η λίστα σας είναι τόσο ασφαλής όσο με το πιο αδύναμο μυστικό σας. Εάν αλλάξετε τις ρυθμίσεις βιομετρικού ελέγχου ταυτότητας της συσκευής σας, θα χρειαστεί να ενεργοποιήσετε ξανά το βιομετρικό ξεκλείδωμα μέσα από το Aegis.</string>
<string name="copy">Αντιγραφή</string>
<string name="edit">Επεξεργασία</string>
<string name="delete">Διαγραφή</string>
<string name="password">Κωδικός</string>
<string name="confirm_password">Επιβεβαίωση κωδικού</string>
<string name="show_password">Προβολή κωδικού</string>
<string name="unlock_vault_error">Αδυναμία ξεκλειδώματος λίστας</string>
<string name="unlock_vault_error_description">Εσφαλμένος κωδικός. Βεβαιωθείτε ότι δεν τον πληκτρολογήσατε λάθος.</string>
<string name="password_equality_error">Οι κωδικοί πρέπει να είναι ίδιοι και να μην είναι κενοί</string>
<string name="snackbar_authentication_method">Παρακαλώ επιλέξτε μέθοδο ελέγχου ταυτότητας</string>
<string name="encrypting_vault">Κρυπτογράφηση λίστας</string>
<string name="delete_entry">Διαγραφή καταχώρησης</string>
<string name="delete_entry_description">Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή την καταχώρηση;</string>
<string name="delete_entries">Διαγραφή καταχωρήσεων</string>
<string name="delete_entries_description">Είστε βέβαιοι ότι θέλετε να διαγράψετε %d καταχωρήσεις;</string>
<string name="discard_changes">Απόρριψη αλλαγών;</string>
<string name="discard_changes_description">Οι αλλαγές σας δεν έχουν αποθηκευτεί</string>
<string name="folder">Φάκελος</string>
<string name="tap_to_select">Πατήστε για επιλογή</string>
<string name="saving_profile_error">Σφάλμα κατά την αποθήκευση προφίλ</string>
<string name="welcome">Καλώς ήρθατε</string>
<string name="app_description">Το Aegis είναι μια δωρεάν εφαρμογή 2FA, ασφαλής και ανοικτού κώδικα</string>
<string name="setup_completed">Η εγκατάσταση ολοκληρώθηκε</string>
<string name="setup_completed_description">Το Aegis έχει ρυθμιστεί και είναι έτοιμο προς χρήση.</string>
<string name="vault_not_found">Η λίστα δεν βρέθηκε, έναρξη ρύθμισης…</string>
<string name="errors_copied">Σφάλμα αντιγραφής στο πρόχειρο</string>
<string name="version_copied">Η έκδοση αντιγράφτηκε στο πρόχειρο</string>
<string name="decryption_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ξεκλειδώματος της λίστας</string>
<string name="saving_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια αποθήκευσης της λίστας</string>
<string name="disable_encryption">Απενεργοποίηση κρυπτογράφησης</string>
<string name="disable_encryption_description">Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε την κρυπτογράφηση; Αυτό θα έχει ως αποτέλεσμα την αποθήκευση της λίστας σε απλό κείμενο.</string>
<string name="enable_encryption_error">Παρουσιάστηκε σφάλμα κατά την ενεργοποίηση της κρυπτογράφησης</string>
<string name="disable_encryption_error">Παρουσιάστηκε σφάλμα κατά την απενεργοποίηση της κρυπτογράφησης</string>
<string name="permission_denied">Η άδεια απορρίφθηκε</string>
<string name="andotp_new_format">Νέα μορφή (v0.6.3 ή νεότερη) </string>
<string name="andotp_old_format">Παλιά μορφή (v0.6.2 ή παλαιότερη) </string>
<string name="choose_andotp_importer">Ποια μορφή έχει to αρχείο αντιγράφου ασφαλείας andOTP;</string>
<string name="choose_application">Επιλέξτε εφαρμογή εισαγωγής</string>
<string name="choose_theme">Επιλέξτε επιθυμητό θέμα</string>
<string name="choose_view_mode">Επιλέξτε επιθυμητή λειτουργία προβολής</string>
<string name="parsing_file_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάλυσης του αρχείου</string>
<string name="file_not_found">Σφάλμα: Το αρχείο δεν βρέθηκε</string>
<string name="reading_file_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάγνωσης του αρχείου</string>
<string name="app_lookup_error">Σφάλμα: Η εφαρμογή δεν έχει εγκατασταθεί</string>
<string name="root_error">Σφάλμα: δεν είναι δυνατή η απόκτηση πρόσβασης root</string>
<string name="imported_entries_count">Εισήχθησαν %d καταχωρήσεις</string>
<string name="read_entries_count">Αναγνώστηκαν %d καταχωρήσεις. %d σφάλματα.</string>
<string name="import_error_title">Παρουσιάστηκαν ένα ή περισσότερα σφάλματα κατά την εισαγωγή</string>
<string name="exporting_vault_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια εξαγωγής της λίστας</string>
<string name="exported_vault">Η λίστα έχει εξαχθεί</string>
<string name="export_warning">Αυτή η ενέργεια θα εξάγει τη λίστα από την ιδιωτική αποθήκη του Aegis.</string>
<string name="no_cameras_available">Δεν υπάρχουν διαθέσιμες κάμερες</string>
<string name="read_qr_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάγνωσης του κώδικα QR</string>
<string name="authentication_method_raw">Ακατέργαστη</string>
<string name="unlocking_vault">Ξεκλείδωμα λίστας</string>
<string name="unlocking_vault_repair">Ξεκλείδωμα λίστας (επισκευή)</string>
<string name="password_slot_error">Πρέπει να έχετε τουλάχιστον μία υποδοχή κωδικού</string>
<string name="remove_slot">Κατάργηση υποδοχής</string>
<string name="remove_slot_description">Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτή την υποδοχή;</string>
<string name="remove_group">Κατάργηση ομάδας</string>
<string name="remove_group_description">Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτήν την ομάδα; Οι καταχωρίσεις σε αυτήν θα μεταβούν αυτόματα στο \"Χωρίς ομάδα\".</string>
<string name="adding_new_slot_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια προσθήκης νέας υποδοχής:</string>
<string name="progressbar_error">Δεν είναι δυνατή η επαναφορά της κλίμακας. Οι γραμμές προόδου θα είναι αόρατες.</string>
<string name="details">Λεπτομέρειες</string>
<string name="filter">Φίλτρο</string>
<string name="filter_ungrouped">Χωρίς ομάδα</string>
<string name="lock">Κλείδωμα</string>
<string name="all">Όλα</string>
<string name="name">Όνομα</string>
<string name="no_group">Χωρίς ομάδα</string>
<string name="sort_alphabetically">Εκδότης (Α προς Ζ)</string>
<string name="sort_alphabetically_reverse">Εκδότης (Ζ προς Α)</string>
<string name="sort_alphabetically_name">Λογαριασμός (Α προς Ζ)</string>
<string name="sort_alphabetically_name_reverse">Λογαριασμός (Z προς A)</string>
<string name="sort_custom">Προσαρμογή</string>
<string name="new_group">Νέα ομάδα…</string>
<string name="enter_group_name">Εισαγωγή ονόματος ομάδας</string>
<string name="group_name_hint">Όνομα ομάδας</string>
<string name="preference_manage_groups">Επεξεργασία ομάδων</string>
<string name="preference_manage_groups_summary">Διαχείριση και διαγραφή ομάδων</string>
<string name="pref_search_name_title">Αναζήτηση στα ονόματα λογαριασμών</string>
<string name="pref_search_name_summary">Συμπερίληψη αντιστοίχισης ονόματος λογαριασμού στα αποτελέσματα αναζήτησης</string>
<string name="pref_highlight_entry_title">Επισήμανση αναγνωριστικών όταν πατηθεί</string>
<string name="pref_highlight_entry_summary">Κάνετε τα αναγνωριστικά να ξεχωρίζουν ευκολότερα το ένα από το άλλο, επισημαίνοντας τα προσωρινά όταν τα πατήσετε</string>
<string name="tap_to_reveal">Κρυφό</string>
<string name="selected">Επιλεγμένο</string>
<string name="dark_theme_title">Σκούρο θέμα</string>
<string name="light_theme_title">Φωτεινό θέμα</string>
<string name="normal_viewmode_title">Κανονική</string>
<string name="compact_mode_title">Συμπαγής</string>
<string name="small_mode_title">Μικρή</string>
<string name="unknown_issuer">Άγνωστος εκδότης</string>
<string name="unknown_account_name">Άγνωστο όνομα λογαριασμού</string>
<string name="import_error_dialog">Το Aegis δεν μπόρεσε να εισαγάγει %d αναγνωριστικά. Αυτά θα παραληφθούν. Πατήστε \'Λεπτομέρειες\' για να δείτε περισσότερες πληροφορίες σχετικά με τα σφάλματα.</string>
<string name="unable_to_read_qrcode">Αδυναμία ανάγνωσης και επεξεργασίας κώδικα QR</string>
<string name="select_picture">Επιλέξτε εικόνα</string>
<string name="toggle_checkboxes">Εναλλαγή πλαισίων ελέγχου</string>
<string name="search">Αναζήτηση</string>
<string name="channel_name_lock_status">Κατάσταση κλειδώματος</string>
<string name="channel_description_lock_status">To Aegis μπορεί να δημιουργήσει μια επίμονη ειδοποίηση για να σας ειδοποιήσει όταν η λίστα είναι κλειδωμένη</string>
<string name="vault_unlocked_state">Λίστα ξεκλείδωτη. Πατήστε για κλείδωμα.</string>
<string name="title_activity_about">Σχετικά</string>
<string name="version">Έκδοση</string>
<string name="changelog">Αρχείο καταγραφής αλλαγών</string>
<string name="whats_new">Τι νέο υπάρχει</string>
<string name="github_description">Πηγαίος κώδικας, προβλήματα και πληροφορίες</string>
<string name="licenses">Άδειες</string>
<string name="licenses_description">Άδειες χρήσης βιβλιοθηκών του Aegis</string>
<string name="country_netherlands">Ολλανδία</string>
<string name="email_us">Γράψτε ένα email</string>
<string name="visit_website">Επισκεφτείτε την ιστοσελίδα μας</string>
<string name="about_support">Υποστήριξη</string>
<string name="support_rate">Αξιολόγηση</string>
<string name="support_rate_description">Υποστηρίξτε μας αφήνοντας μια κριτική στο Google Play Store</string>
<string name="webview_error">Αυτή η συσκευή δεν υποστηρίζει προβολή ιστού, η οποία είναι απαραίτητη για την προβολή του αρχείου καταγραφής αλλαγών. Λείπει ένα στοιχείο συστήματος.</string>
<string name="email">Email</string>
<string name="empty_list">Δεν υπάρχουν κωδικοί προς εμφάνιση. Προσθήκη καταχωρήσεων πατώντας στο σύμβολο συν στην κάτω δεξιά γωνία</string>
<string name="empty_list_title">Δεν βρέθηκε καμία καταχώρηση</string>
</resources>

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