Compare commits

...
103 Commits
Author SHA1 Message Date
Alexander Bakker dbedc9d1d3 Release v1.1.4 2020-01-22 20:42:22 +01:00
Michael SchättgenandGitHub 3bdcc25ebe Merge pull request #298 from alexbakker/saf-json
Append ".json" extension to export filenames
2020-01-22 00:40:03 -08:00
Alexander Bakker d0108e9859 Append ".json" extension to export filenames
Apparently not all SAF providers append a file extension based on the specified
MIME type.
2020-01-21 09:34:06 +01:00
Alexander Bakker 025c89d78c Release v1.1.3 2020-01-20 21:36:15 +01:00
Alexander BakkerandGitHub c52e60b410 Merge pull request #288 from michaelschattgen/set-locale
Fix setting locale on 7.0
2020-01-20 21:35:04 +01:00
Michael Schättgen 3cc644193f Update languages and gitignore 2020-01-20 21:34:20 +01:00
Michael Schättgen 8020792024 Fix setting locale on 7.0 2020-01-20 21:01:17 +01:00
Michael SchättgenandGitHub 837a27f9d7 Merge pull request #293 from alexbakker/password-reminder
Remind users who use biometrics to enter their password periodically
2020-01-20 11:56:49 -08:00
Michael SchättgenandGitHub 5c71f6254b Merge pull request #296 from alexbakker/fix-295
Fix #295
2020-01-20 11:56:32 -08:00
Alexander Bakker bbea6c2ae4 Fix #295 2020-01-19 23:00:03 +01:00
Alexander Bakker fa799e9542 Remind users who use biometrics to enter their password periodically
Instead of showing the reminder after x unlocks, I decided to show the reminder
2 weeks after the vault was last unlocked with the password. Let me know if you
agree with that.

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

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

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

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

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

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

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

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

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

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

Add review fixes

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

* Update arrays.xml
2019-09-03 10:10:22 +02:00
129 changed files with 4625 additions and 1799 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with fingerprint unlock)/Yes/No
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
###### Steps to reproduce
+1
View File
@@ -39,3 +39,4 @@ captures/
# Keystore files
*.jks
crowdin.properties
+15
View File
@@ -0,0 +1,15 @@
language: android
env:
global:
- ANDROID_HOME=/usr/local/android-sdk
android:
components:
- android-29
- build-tools-29.0.2
before_install:
- mkdir -p $ANDROID_HOME/licenses
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
script:
- ./gradlew build test
+28 -17
View File
@@ -2,7 +2,8 @@
Aegis is a free, secure and open source 2FA app for Android.
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
Aegis' security design and vault format is described in detail in [this
document](docs/vault.md).
# Features
@@ -10,7 +11,7 @@ Aegis' security design and vault format is described in detail in [this document
- Secure
- Encryption (AES-256)
- Password (scrypt)
- Fingerprint (Android Keystore)
- Biometrics (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
@@ -42,17 +43,22 @@ Aegis' security design and vault format is described in detail in [this document
## Screenshots
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
[<img width=200 alt="Screenshot 1"
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
[<img width=200 alt="Screenshot 2"
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
[<img width=200 alt="Screenshot 3"
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Screenshot 4"
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
[<img width=200 alt="Screenshot 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
## Downloads
@@ -96,6 +102,16 @@ Certificate fingerprints:
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
```
### Community
- [aegis-icons](https://github.com/krisu5/aegis-icons)
While we're still working on better icon support in Aegis, krisu5 has started
a third-party repository for icons that you may want to check out.
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
## Libraries
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
@@ -104,7 +120,6 @@ Certificate fingerprints:
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
Dodenhof
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
@@ -114,7 +129,3 @@ Certificate fingerprints:
This project is licensed under the GNU General Public License v3.0. See the
[LICENSE](LICENSE) file for details.
## Attribution
The icon is largely based on a design by Freepik from Flaticon.
+45 -20
View File
@@ -1,19 +1,43 @@
apply plugin: 'com.android.application'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine cmd
standardOutput = stdout
}
return stdout.toString().trim()
}
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 18
versionName "1.0-beta1"
targetSdkVersion 29
versionCode 28
versionName "1.1.4"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
}
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
unitTests.all {
useJUnitPlatform()
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
}
@@ -39,32 +63,33 @@ android {
}
dependencies {
def libsuVersion = '2.3.2'
def libsuVersion = '2.5.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:v4.2.2'
implementation 'com.github.apl-devs:appintro:5.1.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'me.dm7.barcodescanner:zxing:1.9'
implementation '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.9.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
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") {
transitive = false
}
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
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'
}
@@ -1,13 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
+11 -4
View File
@@ -4,19 +4,23 @@
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application
android:name=".AegisApplication"
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about" />
<activity
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
@@ -26,12 +30,15 @@
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
</intent-filter>
</activity>
+116
View File
@@ -0,0 +1,116 @@
<html>
<head>
<style type="text/css">
* {
word-wrap: break-word;
}
body {
background-color: %1$s;
color: %2$s;
}
ul {
list-style-position: inside;
padding: 0;
padding-left: 5px;
margin-bottom: 5px;
}
li {
padding-bottom: 8px;
list-style-position: outside;
margin-left: 1em;
}
h4 {
margin-top: 0px;
margin-bottom: 0px;
}
h3 {
margin-bottom: 7px;
padding-top: 7px;
}
</style>
</head>
<body>
<div></div>
<h3>Version 1.1.4</h3>
<h4>Fixes</h4>
<ul>
<li>The export filename was missing the ".json" extension in some cases</li>
</ul>
<h3>Version 1.1.3</h3>
<h4>New</h4>
<ul>
<li>Password reminder for users who use biometric unlock</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Tokens would not refresh in some rare cases</li>
</ul>
<h3>Version 1.1.2</h3>
<h4>New</h4>
<ul>
<li>Ability to select multiple entries</li>
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
<li>Explanation and warning for the security options</li>
<li>Removed external storage permissions</li>
</ul>
<h3>Version 1.1.1</h3>
<h4>Fixes</h4>
<ul>
<li>Exporting the vault did not work on Android 10</li>
</ul>
<h3>Version 1.1</h3>
<h4>New</h4>
<ul>
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
<li>Support for importing from WinAuth</li>
<li>Support for Chromebooks</li>
<li>Option to highlight entries when tapped</li>
<li>Filter for ungrouped tokens</li>
<li>Ability to search for token account names</li>
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
<li>Updated translations (thanks to all Crowdin contributers!)</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The behavior of highlighting and revealing entries was inconsistent</li>
<li>The changelog dialog didn't work</li>
<li>The persistent notification was shown even after the app was killed</li>
</ul>
<h3>Version 1.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for andOTP's new backup file format</li>
</ul>
<h3>Version 1.0.2</h3>
<h4>Fixes</h4>
<ul>
<li>Search feature on Huawei devices</li>
</ul>
<h4>Notes</h4>
<ul>
<li>Disabled automatic backups through the Google Play Store</li>
</ul>
<h3>Version 1.0.1</h3>
<h4>Notes</h4>
<ul>
<li>Temporarily disabled search feature on Huawei devices</li>
</ul>
<h3>Version 1.0</h3>
<h4>New</h4>
<ul>
<li>New icon</li>
<li>Overhaul of interaction with the entry list</li>
<li>Persistent notification while the vault is unlocked</li>
<li>Language override option</li>
<li>Support for importing from FreeOTP+</li>
<li>Ability to toggle password visibility during unlock</li>
<li>Support for deeplinking otpauth URIs</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Bad overall performance and high battery usage</li>
<li>Codes with an uneven number of digits are displayed incorrectly</li>
<li>Crash when entering a large value for OTP period</li>
</ul>
</body>
</html>
@@ -12,36 +12,41 @@ import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.RequiresApi;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_DATABASE_ACTION = "lock_database";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_manager = new VaultManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(CODE_LOCK_DATABASE_ACTION);
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
registerReceiver(receiver, intentFilter);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
@@ -53,7 +58,7 @@ public class AegisApplication extends Application {
}
}
public DatabaseManager getDatabaseManager() {
public VaultManager getVaultManager() {
return _manager;
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -6,26 +6,58 @@ import android.content.res.Resources;
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() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isPasswordReminderEnabled() {
return _prefs.getBoolean("pref_password_reminder", true);
}
public boolean isPasswordReminderNeeded() {
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return isPasswordReminderEnabled() && days >= 7;
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
public void resetPasswordReminderTimestamp() {
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
return _prefs.getBoolean("pref_account_name", true);
}
public boolean isIntroDone() {
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
@@ -24,8 +24,8 @@ public enum SortCategory {
return _values[x];
}
public Comparator<DatabaseEntry> getComparator() {
Comparator<DatabaseEntry> comparator = null;
public Comparator<VaultEntry> getComparator() {
Comparator<VaultEntry> comparator = null;
switch (this) {
case ACCOUNT:
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseException extends Exception {
public DatabaseException(Throwable cause) {
super(cause);
}
public DatabaseException(String message) {
super(message);
}
}
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseFileException extends Exception {
public DatabaseFileException(Throwable cause) {
super(cause);
}
public DatabaseFileException(String message) {
super(message);
}
}
@@ -1,239 +0,0 @@
package com.beemdevelopment.aegis.db;
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 org.json.JSONObject;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.Collection;
import java.util.TreeSet;
import java.util.UUID;
public class DatabaseManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
private Database _db;
private DatabaseFile _file;
private DatabaseFileCredentials _creds;
private boolean _encrypt;
private Context _context;
public DatabaseManager(Context context) {
_context = context;
}
public boolean fileExists() {
File file = new File(_context.getFilesDir(), FILENAME);
return file.exists() && file.isFile();
}
public void load() throws DatabaseManagerException {
assertState(true, false);
try (FileInputStream file = _context.openFileInput(FILENAME)) {
byte[] fileBytes = new byte[(int) file.getChannel().size()];
DataInputStream stream = new DataInputStream(file);
stream.readFully(fileBytes);
stream.close();
_file = DatabaseFile.fromBytes(fileBytes);
_encrypt = _file.isEncrypted();
if (!isEncryptionEnabled()) {
JSONObject obj = _file.getContent();
_db = Database.fromJson(obj);
}
} catch (IOException | DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public void lock() {
assertState(false, true);
_creds = null;
_db = null;
}
public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(true, true);
try {
JSONObject obj = _file.getContent(creds);
_db = Database.fromJson(obj);
_creds = creds;
_context.startService(new Intent(_context, NotificationService.class));
} catch (DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public static void save(Context context, DatabaseFile file) throws DatabaseManagerException {
byte[] bytes = file.toBytes();
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
stream.write(bytes);
} catch (IOException e) {
throw new DatabaseManagerException(e);
}
}
public void save() throws DatabaseManagerException {
assertState(false, true);
try {
JSONObject obj = _db.toJson();
if (isEncryptionEnabled()) {
_file.setContent(obj, _creds);
} else {
_file.setContent(obj);
}
save(_context, _file);
} catch (DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public String export(boolean encrypt) throws DatabaseManagerException {
assertState(false, true);
try {
DatabaseFile dbFile = new DatabaseFile();
if (encrypt && isEncryptionEnabled()) {
dbFile.setContent(_db.toJson(), _creds);
} else {
dbFile.setContent(_db.toJson());
}
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("error creating external storage directory");
}
byte[] bytes = dbFile.toBytes();
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
try (FileOutputStream stream = new FileOutputStream(file)) {
stream.write(bytes);
}
return file.getAbsolutePath();
} catch (IOException | DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public void addEntry(DatabaseEntry entry) {
assertState(false, true);
_db.getEntries().add(entry);
}
public DatabaseEntry removeEntry(DatabaseEntry entry) {
assertState(false, true);
return _db.getEntries().remove(entry);
}
public DatabaseEntry replaceEntry(DatabaseEntry entry) {
assertState(false, true);
return _db.getEntries().replace(entry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
assertState(false, true);
_db.getEntries().swap(entry1, entry2);
}
public boolean isEntryDuplicate(DatabaseEntry entry) {
assertState(false, true);
return _db.getEntries().has(entry);
}
public Collection<DatabaseEntry> getEntries() {
assertState(false, true);
return _db.getEntries().getValues();
}
public TreeSet<String> getGroups() {
assertState(false, true);
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
for (DatabaseEntry entry : getEntries()) {
String group = entry.getGroup();
if (group != null) {
groups.add(group);
}
}
return groups;
}
public DatabaseFileCredentials getCredentials() {
assertState(false, true);
return _creds;
}
public void setCredentials(DatabaseFileCredentials creds) {
assertState(false, true);
_creds = creds;
}
public DatabaseFile.Header getFileHeader() {
assertLoaded(true);
return _file.getHeader();
}
public boolean isEncryptionEnabled() {
assertLoaded(true);
return _encrypt;
}
public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(false, true);
_creds = creds;
_encrypt = true;
save();
}
public void disableEncryption() throws DatabaseManagerException {
assertState(false, true);
_creds = null;
_encrypt = false;
save();
}
public boolean isLoaded() {
return _file != null;
}
public boolean isLocked() {
return _db == null;
}
private void assertState(boolean locked, boolean loaded) {
assertLoaded(loaded);
if (isLocked() && !locked) {
throw new AssertionError("database file has not been unlocked yet");
} else if (!isLocked() && locked) {
throw new AssertionError("database file has already been unlocked");
}
}
private void assertLoaded(boolean loaded) {
if (isLoaded() && !loaded) {
throw new AssertionError("database file has already been loaded");
} else if (!isLoaded() && loaded) {
throw new AssertionError("database file has not been loaded yet");
}
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseManagerException extends Exception {
public DatabaseManagerException(Throwable cause) {
super(cause);
}
}
@@ -1,20 +0,0 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
super();
}
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_FINGERPRINT;
}
}
@@ -0,0 +1,136 @@
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricConstants.ERROR_CANCELED
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.Manifest;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class FingerprintHelper {
private FingerprintHelper() {
}
public static FingerprintManager getManager(Context context) {
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
@@ -1,156 +0,0 @@
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
// It has been modified to suit Aegis' needs
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.mattprecious.swirl.SwirlView;
import androidx.annotation.RequiresApi;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 100;
private final FingerprintManager mFingerprintManager;
private final SwirlView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
private boolean mSelfCancelled;
/**
* Constructor for {@link FingerprintUiHelper}.
*/
public FingerprintUiHelper(FingerprintManager fingerprintManager,
SwirlView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setState(SwirlView.State.ON);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setState(SwirlView.State.OFF);
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mIcon.setState(SwirlView.State.ON);
}
}, 500);
}
private void showError(CharSequence error) {
mIcon.setState(SwirlView.State.ERROR);
mErrorTextView.setText(error);
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setState(SwirlView.State.ON);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}
@@ -3,14 +3,12 @@ package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import java.util.Map;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private DatabaseEntry _selectedEntry;
private VaultEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
@@ -29,7 +27,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(DatabaseEntry entry) {
public void setSelectedEntry(VaultEntry entry) {
_selectedEntry = entry;
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}
@@ -1,12 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<DatabaseEntry> {
public class AccountNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
public int compare(VaultEntry a, VaultEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
@@ -1,12 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<DatabaseEntry> {
public class IssuerNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
public int compare(VaultEntry a, VaultEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
@@ -2,11 +2,11 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.slots.SlotList;
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;
@@ -36,20 +36,20 @@ public class AegisImporter extends DatabaseImporter {
public State read(FileReader reader) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
DatabaseFile file = DatabaseFile.fromBytes(bytes);
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (DatabaseFileException | IOException e) {
} catch (VaultFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private DatabaseFile _file;
private VaultFile _file;
private EncryptedState(DatabaseFile file) {
private EncryptedState(VaultFile file) {
super(true);
_file = file;
}
@@ -58,7 +58,7 @@ public class AegisImporter extends DatabaseImporter {
return _file.getHeader().getSlots();
}
public State decrypt(DatabaseFileCredentials creds) throws DatabaseFileException {
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
JSONObject obj = _file.getContent(creds);
return new DecryptedState(obj);
}
@@ -86,7 +86,7 @@ public class AegisImporter extends DatabaseImporter {
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -99,9 +99,9 @@ public class AegisImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return DatabaseEntry.fromJson(obj);
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
@@ -2,10 +2,13 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
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.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
@@ -20,11 +23,14 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
@@ -32,9 +38,18 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
private static final int MAX_ITERATIONS = 10000;
public AndOtpImporter(Context context) {
super(context);
@@ -81,30 +96,48 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support encrypted andOTP backups
// 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));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
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, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length);
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 offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE;
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
@@ -113,14 +146,26 @@ public class AndOtpImporter extends DatabaseImporter {
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
})
.create());
}
}
@@ -139,7 +184,7 @@ public class AndOtpImporter extends DatabaseImporter {
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
@@ -151,7 +196,7 @@ public class AndOtpImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
@@ -184,7 +229,7 @@ public class AndOtpImporter extends DatabaseImporter {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.OtpInfo;
@@ -74,7 +74,7 @@ public class AuthyImporter extends DatabaseImporter {
for (int i = 0; i < _obj.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -87,7 +87,7 @@ public class AuthyImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName");
@@ -101,7 +101,7 @@ public class AuthyImporter extends DatabaseImporter {
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new DatabaseEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | Base32Exception e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
@@ -2,17 +2,12 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
@@ -38,6 +33,7 @@ public abstract class DatabaseImporter {
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
@@ -116,10 +112,10 @@ public abstract class DatabaseImporter {
}
public static class Result {
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
@@ -127,7 +123,7 @@ public abstract class DatabaseImporter {
_errors.add(error);
}
public UUIDMap<DatabaseEntry> getEntries() {
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -73,7 +73,7 @@ public class FreeOtpImporter extends DatabaseImporter {
for (JSONObject obj : _entries) {
try {
DatabaseEntry entry = convertEntry(obj);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -83,7 +83,7 @@ public class FreeOtpImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
@@ -110,7 +110,7 @@ public class FreeOtpImporter extends DatabaseImporter {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
return new DatabaseEntry(info, name, issuer);
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
@@ -5,7 +5,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
@@ -92,7 +92,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
for (Entry sqlEntry : _entries) {
try {
DatabaseEntry entry = convertEntry(sqlEntry);
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -102,7 +102,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
@@ -124,7 +124,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
name = parts[1];
}
return new DatabaseEntry(info, name, entry.getIssuer());
return new VaultEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -66,7 +66,7 @@ public class SteamImporter extends DatabaseImporter {
Result result = new Result();
try {
DatabaseEntry entry = convertEntry(_obj);
VaultEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -75,13 +75,13 @@ public class SteamImporter extends DatabaseImporter {
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new DatabaseEntry(info, account, "Steam");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
@@ -0,0 +1,83 @@
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;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return null;
}
@Override
protected String getAppSubPath() {
return null;
}
@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);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public Result convert() {
Result result = new 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);
VaultEntry vaultEntry = new VaultEntry(info);
vaultEntry.setIssuer(vaultEntry.getName());
vaultEntry.setName("WinAuth");
return vaultEntry;
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -9,31 +9,25 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
public class NotificationService extends Service {
public static final int DATABASE_UNLOCKED_ID = 1;
public static final int VAULT_UNLOCKED_ID = 1;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_DATABASE_ACTION = "lock_database";
public NotificationService() {
}
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@Override
public int onStartCommand(Intent intent,int flags, int startId){
super.onStartCommand(intent, flags, startId);
serviceMethod();
return Service.START_STICKY;
}
public void serviceMethod() {
Intent intentAction = new Intent(CODE_LOCK_DATABASE_ACTION);
PendingIntent lockDatabaseIntent = PendingIntent.getBroadcast(this,1,intentAction, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CODE_LOCK_STATUS_ID)
Intent intentAction = new Intent(CODE_LOCK_VAULT_ACTION);
PendingIntent lockDatabaseIntent = PendingIntent.getBroadcast(this, 1, intentAction, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CODE_LOCK_STATUS_ID)
.setSmallIcon(R.drawable.ic_fingerprint_black_24dp)
.setContentTitle(getString(R.string.app_name_full))
.setContentText(getString(R.string.vault_unlocked_state))
@@ -42,15 +36,20 @@ public class NotificationService extends Service {
.setContentIntent(lockDatabaseIntent);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(DATABASE_UNLOCKED_ID, builder.build());
notificationManager.notify(VAULT_UNLOCKED_ID, builder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(DATABASE_UNLOCKED_ID);
notificationManager.cancel(VAULT_UNLOCKED_ID);
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
stopSelf();
}
@Nullable
@@ -0,0 +1,128 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.ui.glide.GlideLicense;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
import de.psdev.licensesdialog.LicensesDialog;
public class AboutActivity extends AegisActivity {
private static String GITHUB = "https://github.com/beemdevelopment/Aegis";
private static String WEBSITE_ALEXANDER = "https://alexbakker.me";
private static String GITHUB_MICHAEL = "https://github.com/michaelschattgen";
private static String MAIL_BEEMDEVELOPMENT = "beemdevelopment@gmail.com";
private static String WEBSITE_BEEMDEVELOPMENT = "https://beem.dev/";
private static String PLAYSTORE_BEEMDEVELOPMENT = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
View btnAppVersion = findViewById(R.id.btn_app_version);
btnAppVersion.setOnClickListener(v -> {
copyToClipboard(getCurrentAppVersion(), R.string.version_copied);
});
View btnGithub = findViewById(R.id.btn_github);
btnGithub.setOnClickListener(v -> openUrl(GITHUB));
View btnAlexander = findViewById(R.id.btn_alexander);
btnAlexander.setOnClickListener(v -> openUrl(WEBSITE_ALEXANDER));
View btnMichael = findViewById(R.id.btn_michael);
btnMichael.setOnClickListener(v -> openUrl(GITHUB_MICHAEL));
View btnMail = findViewById(R.id.btn_email);
btnMail.setOnClickListener(v -> openMail(MAIL_BEEMDEVELOPMENT));
View btnWebsite = findViewById(R.id.btn_website);
btnWebsite.setOnClickListener(v -> openUrl(WEBSITE_BEEMDEVELOPMENT));
View btnRate = findViewById(R.id.btn_rate);
btnRate.setOnClickListener(v -> openUrl(PLAYSTORE_BEEMDEVELOPMENT ));
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
});
}
private static String getCurrentAppVersion() {
if (BuildConfig.DEBUG) {
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
}
return BuildConfig.VERSION_NAME;
}
private void openUrl(String url) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW);
browserIntent.setData(Uri.parse(url));
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(browserIntent);
}
private void copyToClipboard(String text, @StringRes int messageId) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData data = ClipData.newPlainText("text/plain", text);
clipboard.setPrimaryClip(data);
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
}
private void openMail(String mailaddress) {
Intent mailIntent = new Intent(Intent.ACTION_SENDTO);
mailIntent.setData(Uri.parse("mailto:" + mailaddress));
mailIntent.putExtra(Intent.EXTRA_EMAIL, mailaddress);
mailIntent.putExtra(Intent.EXTRA_SUBJECT, R.string.app_name_full);
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
}
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())));
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
LicenseResolver.registerLicense(new GlideLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
.show();
}
}
@@ -3,7 +3,6 @@ package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.WindowManager;
@@ -22,6 +21,7 @@ import java.util.Locale;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -89,6 +89,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
protected void setPreferredTheme(Theme theme) {
_currentTheme = theme;
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
@@ -108,7 +110,7 @@ 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());
}
/**
@@ -123,7 +125,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.getDatabaseManager().isLocked();
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
}
private void setGlobalAnimationDurationScale() {
@@ -136,4 +138,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
}
protected Theme getCurrentTheme() {
return _currentTheme;
}
}
@@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
@@ -14,44 +13,51 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.PopupWindow;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import com.mattprecious.swirl.SwirlView;
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.beemdevelopment.aegis.vault.slots.SlotList;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.appcompat.app.AlertDialog;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private BiometricPrompt.CryptoObject _bioCryptoObj;
private BiometricPrompt _bioPrompt;
private Preferences _prefs;
@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 boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button 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,23 +66,17 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
return false;
});
SwirlView imgFingerprint = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert);
imgFingerprint = new SwirlView(this);
insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
// 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)) {
boolean invalidated = false;
try {
// find a fingerprint slot with an id that matches an alias in the keystore
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
// find a biometric slot with an id that matches an alias in the keystore
for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) {
String id = slot.getUUID().toString();
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.containsKey(id)) {
@@ -86,10 +86,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
invalidated = true;
continue;
}
Cipher cipher = slot.createDecryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
@@ -100,7 +101,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxFingerprintInfo.setVisibility(View.VISIBLE);
boxBiometricInfo.setVisibility(View.VISIBLE);
}
}
@@ -115,10 +116,9 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
}
});
if (_fingerHelper == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
});
}
private void showError() {
@@ -138,43 +138,72 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
private void selectPassword() {
_textPassword.selectAll();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
@Override
public void onBackPressed() {
// ignore back button presses
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
Intent intent = new Intent();
setResult(RESULT_CANCELED, intent);
finish();
}
}
@Override
public void onResume() {
super.onResume();
if (_fingerHelper != null) {
_fingerHelper.startListening(_fingerCryptoObj);
if (_bioPrompt != null) {
if (_prefs.isPasswordReminderNeeded()) {
focusPasswordField();
showPasswordReminder();
} else {
showBiometricPrompt();
}
} else {
focusPasswordField();
}
}
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(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() {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
_bioPrompt.authenticate(info, _bioCryptoObj);
}
@Override
public void onPause() {
super.onPause();
if (_fingerHelper != null) {
_fingerHelper.stopListening();
if (_bioPrompt != null) {
_bioPrompt.cancelAuthentication();
}
}
@Override
public void onAuthenticated() {
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
}
@Override
public void onError() {
}
@Override
public void onTaskFinished(SlotListTask.Result result) {
if (result != null) {
@@ -183,9 +212,13 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
_slots.replace(result.getSlot());
}
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
_prefs.resetPasswordReminderTimestamp();
}
// send the master key back to the main activity
Intent intent = new Intent();
intent.putExtra("creds", new DatabaseFileCredentials(result.getKey(), _slots));
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
intent.putExtra("repairedSlot", result.isSlotRepaired());
setResult(RESULT_OK, intent);
finish();
@@ -193,4 +226,25 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
showError();
}
}
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
}
}
@@ -0,0 +1,94 @@
package com.beemdevelopment.aegis.ui;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ChangelogDialog extends DialogFragment {
private Theme _themeStyle;
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View customView;
try {
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("Changelog")
.setView(customView)
.setPositiveButton(android.R.string.ok, null)
.show();
final WebView webView = customView.findViewById(R.id.web_view);
StringBuilder buf = new StringBuilder();
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
String str;
while ((str = in.readLine()) != null)
buf.append(str);
in.close();
String changelog = buf.toString();
changelog = replaceStylesheet(changelog);
webView.loadData(changelog, "text/html", "UTF-8");
} catch (IOException e) {
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
}
return dialog;
}
private String replaceStylesheet(String changelog) {
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
String textColor = colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
return String.format(changelog, backgroundColor, textColor);
}
private static String colorToCSS(int color) {
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
}
public ChangelogDialog setTheme(Theme theme) {
_themeStyle = theme;
return this;
}
}
@@ -4,39 +4,33 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
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.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
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.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.mattprecious.swirl.SwirlView;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
public class Dialogs {
private Dialogs() {
@@ -63,6 +57,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))
@@ -76,6 +88,18 @@ public class Dialogs {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
if (isChecked) {
textPassword.setTransformationMethod(null);
textPassword.clearFocus();
textPasswordConfirm.setEnabled(false);
} else {
textPassword.setTransformationMethod(new PasswordTransformationMethod());
textPasswordConfirm.setEnabled(true);
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
@@ -92,7 +116,7 @@ public class Dialogs {
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
return;
}
@@ -117,7 +141,7 @@ public class Dialogs {
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);
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
@@ -178,51 +202,6 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
FingerprintManager.CryptoObject obj;
FingerprintSlot slot;
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
FingerprintManager manager = FingerprintHelper.getManager(activity);
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
Cipher cipher = Slot.createEncryptCipher(key);
obj = new FingerprintManager.CryptoObject(cipher);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.register_fingerprint)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(d -> {
helper.get().stopListening();
})
.create();
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, obj.getCipher());
dialog.dismiss();
}
@Override
public void onError() {
}
}));
helper.get().startListening(obj);
showSecureDialog(dialog);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
@@ -25,7 +25,7 @@ import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@@ -60,7 +60,7 @@ public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
private boolean _isNew = false;
private DatabaseEntry _origEntry;
private VaultEntry _origEntry;
private TreeSet<String> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
@@ -99,7 +99,7 @@ public class EditEntryActivity extends AegisActivity {
// retrieve info from the calling activity
Intent intent = getIntent();
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
@@ -133,51 +133,49 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry != null) {
if (_origEntry.hasIcon()) {
Glide.with(this)
.asDrawable()
.load(_origEntry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
_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 type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String group = _origEntry.getGroup();
setGroup(group);
if (_origEntry.hasIcon()) {
Glide.with(this)
.asDrawable()
.load(_origEntry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
_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 type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String group = _origEntry.getGroup();
setGroup(group);
// update the icon if the text changed
_textIssuer.addTextChangedListener(_iconChangeListener);
_textName.addTextChangedListener(_iconChangeListener);
@@ -322,7 +320,7 @@ public class EditEntryActivity extends AegisActivity {
@Override
public void onBackPressed() {
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<DatabaseEntry> entry = new AtomicReference<>();
AtomicReference<VaultEntry> entry = new AtomicReference<>();
try {
entry.set(parseEntry());
@@ -331,7 +329,7 @@ public class EditEntryActivity extends AegisActivity {
}
// close the activity if the entry has not been changed
if (_origEntry != null && !_hasChangedIcon && _origEntry.equals(entry.get())) {
if (!_hasChangedIcon && _origEntry.equals(entry.get())) {
super.onBackPressed();
return;
}
@@ -390,7 +388,7 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void finish(DatabaseEntry entry, boolean delete) {
private void finish(VaultEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entry", entry);
intent.putExtra("delete", delete);
@@ -443,7 +441,7 @@ public class EditEntryActivity extends AegisActivity {
}
}
private DatabaseEntry parseEntry() throws ParseException {
private VaultEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
}
@@ -465,7 +463,6 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("Secret is not valid base32.");
}
// set otp info
OtpInfo info;
try {
switch (type.toLowerCase()) {
@@ -494,14 +491,8 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("The entered info is incorrect: " + e.getMessage());
}
// set database entry info
DatabaseEntry entry;
if (_origEntry == null) {
entry = new DatabaseEntry(info);
} else {
entry = Cloner.clone(_origEntry);
entry.setInfo(info);
}
VaultEntry entry = Cloner.clone(_origEntry);
entry.setInfo(info);
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
@@ -537,7 +528,7 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
DatabaseEntry entry;
VaultEntry entry;
try {
entry = parseEntry();
} catch (ParseException e) {
@@ -4,19 +4,20 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.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;
@@ -29,8 +30,6 @@ import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.fragment.app.Fragment;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
@@ -39,8 +38,8 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private Database _database;
private DatabaseFile _databaseFile;
private Vault _vault;
private VaultFile _vaultFile;
private PasswordSlot _passwordSlot;
private Cipher _passwordCipher;
@@ -65,7 +64,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
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));
@@ -83,14 +82,13 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
// create the database and database file
_database = new Database();
_databaseFile = new DatabaseFile();
_vault = new Vault();
_vaultFile = new VaultFile();
}
private void setException(Exception e) {
@@ -130,12 +128,11 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
// generate the master key
DatabaseFileCredentials creds = null;
VaultFileCredentials creds = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
creds = new DatabaseFileCredentials();
creds = new VaultFileCredentials();
}
SlotList slots = null;
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
@@ -150,30 +147,26 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
try {
// encrypt the master key with the fingerprint key
// and add it to the list of slots
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
} catch (SlotException e) {
setException(e);
return;
}
creds.getSlots().add(slot);
}
// finally, save the database
// finally, save the vault
try {
JSONObject obj = _database.toJson();
JSONObject obj = _vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_databaseFile.setContent(obj);
_vaultFile.setContent(obj);
} else {
_databaseFile.setContent(obj, creds);
_vaultFile.setContent(obj, creds);
}
DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (DatabaseManagerException | DatabaseFileException e) {
VaultManager.save(getApplicationContext(), _vaultFile);
} catch (VaultManagerException | VaultFileException e) {
setException(e);
return;
}
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -16,29 +15,26 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.widget.LinearLayout;
import android.widget.SearchView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.core.view.MenuItemCompat;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
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.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
@@ -72,12 +68,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private static final int CODE_PERM_READ_STORAGE = 1;
private AegisApplication _app;
private DatabaseManager _db;
private VaultManager _vault;
private boolean _loaded;
private String _selectedGroup;
private boolean _searchSubmitted;
private DatabaseEntry _selectedEntry;
private List<VaultEntry> _selectedEntries;
private ActionMode _actionMode;
private Menu _menu;
@@ -92,8 +88,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
_db = _app.getDatabaseManager();
_vault = _app.getVaultManager();
_loaded = false;
// set up the main view
@@ -103,6 +100,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
@@ -124,6 +123,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
_selectedEntries = new ArrayList<>();
}
@Override
@@ -164,7 +164,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _db.isLocked()) {
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
return;
}
@@ -193,6 +193,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -218,10 +220,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
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);
@@ -229,52 +235,50 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
private void startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (entry != null) {
intent.putExtra("entry", entry);
}
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
intent.putExtra("isNew", isNew);
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, requestCode);
}
private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
saveVault();
}
}
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
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
DatabaseEntry oldEntry = _db.replaceEntry(entry);
VaultEntry oldEntry = _vault.replaceEntry(entry);
_entryListView.replaceEntry(oldEntry, entry);
saveDatabase();
saveVault();
}
}
}
private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
saveVault();
}
}
@@ -300,7 +304,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
DatabaseEntry entry = new DatabaseEntry(info);
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
@@ -321,7 +325,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _db.getGroups();
TreeSet<String> groups = _vault.getGroups();
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
@@ -334,6 +338,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
if (groups.size() > 0) {
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
@@ -348,8 +356,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setGroupFilter(group, true);
}
private void addEntry(DatabaseEntry entry) {
_db.addEntry(entry);
private void addEntry(VaultEntry entry) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
}
@@ -360,18 +368,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
throw new RuntimeException(e);
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
unlockDatabase(creds);
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
unlockVault(creds);
}
private void onDecryptResult(int resultCode, Intent intent) {
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
boolean unlocked = unlockDatabase(creds);
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
boolean unlocked = unlockVault(creds);
// save the database in case a slot was repaired
// save the vault in case a slot was repaired
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
_db.setCredentials(creds);
saveDatabase();
_vault.setCredentials(creds);
saveVault();
}
doShortcutActions();
@@ -387,10 +395,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/*");
@@ -401,7 +405,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void doShortcutActions() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null || _db.isLocked()) {
if (action == null || _vault.isLocked()) {
return;
}
@@ -415,10 +419,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void handleDeeplink() {
if (_db.isLocked()) {
if (_vault.isLocked()) {
return;
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
@@ -434,7 +438,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
if (info != null) {
DatabaseEntry entry = new DatabaseEntry(info);
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
@@ -445,10 +449,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
protected void onResume() {
super.onResume();
if (_db.isLocked()) {
// start the intro if the database file doesn't exist
if (!_db.isLoaded() && !_db.fileExists()) {
// the db doesn't exist, start the intro
if (_vault.isLocked()) {
// start the intro if the vault file doesn't exist
if (!_vault.isLoaded() && !_vault.fileExists()) {
// the vault doesn't exist, start the intro
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
@@ -456,7 +460,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(intro, CODE_DO_INTRO);
return;
} else {
unlockDatabase(null);
unlockVault(null);
}
} else if (_loaded) {
// update the list of groups in the filter menu
@@ -477,7 +481,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onBackPressed() {
if (!_searchView.isIconified() || _searchSubmitted ) {
if (!_searchView.isIconified() || _searchSubmitted) {
_searchSubmitted = false;
_entryListView.setSearchFilter(null);
@@ -495,9 +499,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onBackPressed();
}
private void deleteEntry(DatabaseEntry entry) {
DatabaseEntry oldEntry = _db.removeEntry(entry);
saveDatabase();
private void deleteEntries(List<VaultEntry> entries) {
for (VaultEntry entry: entries) {
VaultEntry oldEntry = _vault.removeEntry(entry);
_entryListView.removeEntry(oldEntry);
}
saveVault();
}
private void deleteEntry(VaultEntry entry) {
VaultEntry oldEntry = _vault.removeEntry(entry);
saveVault();
_entryListView.removeEntry(oldEntry);
}
@@ -548,10 +561,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
return true;
}
case R.id.action_about: {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
return true;
}
case R.id.action_lock:
_app.lock();
return true;
@@ -601,20 +620,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_searchView.setIconified(true);
}
private boolean unlockDatabase(DatabaseFileCredentials creds) {
private boolean unlockVault(VaultFileCredentials creds) {
try {
if (!_db.isLoaded()) {
_db.load();
if (!_vault.isLoaded()) {
_vault.load();
}
if (_db.isLocked()) {
if (_vault.isLocked()) {
if (creds == null) {
startAuthActivity();
return false;
} else {
_db.unlock(creds);
_vault.unlock(creds);
}
}
} catch (DatabaseManagerException e) {
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
startAuthActivity();
return false;
@@ -626,7 +645,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void loadEntries() {
// load all entries
List<DatabaseEntry> entries = new ArrayList<DatabaseEntry>(_db.getEntries());
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.getEntries());
_entryListView.addEntries(entries);
_entryListView.runEntriesAnimation();
_loaded = true;
@@ -634,67 +653,83 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("slots", _db.getFileHeader().getSlots());
intent.putExtra("slots", _vault.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
}
private void saveDatabase() {
private void saveVault() {
try {
_db.save();
} catch (DatabaseManagerException e) {
_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 database is not unlocked
if (_menu != null && !_db.isLocked()) {
// hide the lock icon if the vault is not unlocked
if (_menu != null && !_vault.isLocked()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_db.isEncryptionEnabled());
item.setVisible(_vault.isEncryptionEnabled());
}
}
@Override
public void onEntryClick(DatabaseEntry entry) {
if (_selectedEntry != null) {
if (_selectedEntry == entry) {
public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) {
if (_selectedEntries.isEmpty()) {
_actionMode.finish();
} else {
setIsMultipleSelected(_selectedEntries.size() > 1);
}
return;
}
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();
copyEntryCode(entry);
}
@Override
public void onLongEntryClick(DatabaseEntry entry) {
if (_selectedEntry != null) {
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 (!_selectedEntries.isEmpty()) {
return;
}
_selectedEntry = entry;
_selectedEntries.add(entry);
_entryListView.setActionModeState(true, entry);
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_db.swapEntries(entry1, entry2);
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vault.swapEntries(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
saveDatabase();
public void onEntryDrop(VaultEntry entry) {
saveVault();
}
@Override
public void onEntryChange(DatabaseEntry entry) {
saveDatabase();
public void onEntryChange(VaultEntry entry) {
saveVault();
}
@Override
@@ -718,6 +753,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onLocked();
}
private void copyEntryCode(VaultEntry entry) {
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 {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
@@ -734,22 +776,30 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_copy:
copyEntryCode(_selectedEntries.get(0));
mode.finish();
return true;
case R.id.action_edit:
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
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 (!_db.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;
@@ -759,7 +809,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,36 +1,32 @@
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;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
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.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
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;
@@ -39,7 +35,16 @@ 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.takisoft.preferencex.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.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.SlotList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@@ -47,6 +52,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;
@@ -56,9 +62,6 @@ import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
@@ -66,32 +69,31 @@ 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 Intent _result;
private DatabaseManager _db;
private VaultManager _vault;
// keep a reference to the type of database converter the user selected
private Class<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private UUIDMap<DatabaseEntry> _importerEntries;
private UUIDMap<VaultEntry> _importerEntries;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
private SwitchPreference _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
private Preference _passwordReminderPreference;
@Override
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_db = app.getDatabaseManager();
_vault = app.getVaultManager();
// set the result intent in advance
setResult(new Intent());
@@ -115,7 +117,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
@@ -123,11 +125,16 @@ 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();
Preference viewModePreference = findPreference("pref_view_mode");
@@ -146,7 +153,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRefresh", true);
dialog.dismiss();
})
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
@@ -157,7 +164,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
startImport();
return true;
}
});
@@ -171,12 +178,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;
}
});
@@ -200,6 +206,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference searchAccountNamePreference = findPreference("pref_search_names");
searchAccountNamePreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@@ -242,7 +260,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (!_db.isEncryptionEnabled()) {
if (!_vault.isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
@@ -251,8 +269,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
_db.disableEncryption();
} catch (DatabaseManagerException e) {
_vault.disableEncryption();
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
return;
}
@@ -276,20 +294,27 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
_biometricsPreference = (SwitchPreference) findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
DatabaseFileCredentials creds = _db.getCredentials();
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(FingerprintSlot.class)) {
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
if (!slots.has(BiometricSlot.class)) {
if (BiometricsHelper.isAvailable(getContext())) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
} else {
// remove the fingerprint slot
FingerprintSlot slot = slots.find(FingerprintSlot.class);
// remove the biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
_db.setCredentials(creds);
_vault.setCredentials(creds);
// remove the KeyStore key
try {
@@ -299,7 +324,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
e.printStackTrace();
}
saveDatabase();
saveVault();
updateEncryptionPreferences();
}
@@ -318,7 +343,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", _db.getCredentials());
intent.putExtra("creds", _vault.getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
}
@@ -329,38 +354,22 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
}
});
_autoLockPreference = findPreference("pref_auto_lock");
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
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;
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data == null) {
@@ -383,6 +392,11 @@ 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;
}
}
@@ -395,11 +409,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]);
@@ -462,12 +472,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
try {
DatabaseImporter.State state = importer.read(reader);
if (state.isEncrypted()) {
// temporary special case for encrypted Aegis databases
// temporary special case for encrypted Aegis vaults
if (state instanceof AegisImporter.EncryptedState) {
_importerState = state;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
intent.putExtra("cancelAction", CancelAction.CLOSE);
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
} else {
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
@@ -498,11 +509,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
DatabaseImporter.State state;
try {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (DatabaseFileException e) {
} catch (VaultFileException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
return;
@@ -543,7 +554,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_importerEntries = result.getEntries();
List<ImportEntry> entries = new ArrayList<>();
for (DatabaseEntry entry : _importerEntries) {
for (VaultEntry entry : _importerEntries) {
entries.add(new ImportEntry(entry));
}
@@ -553,32 +564,22 @@ 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("Export the database")
.setTitle(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename;
try {
filename = _db.export(checked.get());
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), R.string.exporting_database_error, Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN);
// make sure the new file is visible
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
Toast.makeText(getActivity(), getString(R.string.export_database_location) + filename, Toast.LENGTH_SHORT).show();
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
})
.setNegativeButton(android.R.string.cancel, null);
if (_db.isEncryptionEnabled()) {
final String[] items = {"Keep the database encrypted"};
if (_vault.isEncryptionEnabled()) {
final String[] items = {"Keep the vault encrypted"};
final boolean[] checkedItems = {true};
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
@@ -597,9 +598,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
_db.setCredentials(creds);
saveDatabase();
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
_vault.setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
@@ -610,7 +611,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (DatabaseEntry entry : _db.getEntries()) {
for (VaultEntry entry : _vault.getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
@@ -624,18 +625,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
for (ImportEntry selectedEntry : selectedEntries) {
DatabaseEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.isEntryDuplicate(savedEntry)) {
if (_vault.isEntryDuplicate(savedEntry)) {
savedEntry.resetUUID();
}
_db.addEntry(savedEntry);
_vault.addEntry(savedEntry);
}
_importerEntries = null;
if (!saveDatabase()) {
if (!saveVault()) {
return;
}
@@ -645,10 +646,26 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRecreate", true);
}
private boolean saveDatabase() {
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) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
private boolean saveVault() {
try {
_db.save();
} catch (DatabaseManagerException e) {
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
return false;
}
@@ -657,34 +674,37 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
private void updateEncryptionPreferences() {
boolean encrypted = _db.isEncryptionEnabled();
boolean encrypted = _vault.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_fingerprintPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = _db.getCredentials().getSlots();
SlotList slots = _vault.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
} else {
_setPasswordPreference.setEnabled(false);
_fingerprintPreference.setEnabled(false);
_fingerprintPreference.setChecked(false, true);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
_passwordReminderPreference.setVisible(false);
}
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
try {
@@ -702,8 +722,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
_db.setCredentials(creds);
saveDatabase();
_vault.setCredentials(creds);
saveVault();
}
@Override
@@ -713,42 +733,41 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private class RegisterFingerprintListener implements Dialogs.SlotListener {
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
VaultFileCredentials creds = _vault.getCredentials();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
onSlotInitializationFailed(0, e.toString());
return;
}
creds.getSlots().add(slot);
_vault.setCredentials(creds);
slots.add(slot);
_db.setCredentials(creds);
saveDatabase();
saveVault();
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
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();
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = new DatabaseFileCredentials();
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_db.enableEncryption(creds);
} catch (DatabaseManagerException | SlotException e) {
_vault.enableEncryption(creds);
} catch (VaultManagerException | SlotException e) {
onException(e);
return;
}
@@ -10,7 +10,7 @@ import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
@@ -109,7 +109,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
try {
// parse google auth uri
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
DatabaseEntry entry = new DatabaseEntry(info);
VaultEntry entry = new VaultEntry(info);
Intent intent = new Intent();
intent.putExtra("entry", entry);
@@ -7,27 +7,30 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
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.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
import javax.crypto.Cipher;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener {
private VaultFileCredentials _creds;
private SlotAdapter _adapter;
private boolean _edited;
@@ -42,12 +45,19 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
Dialogs.showFingerprintDialog(this ,this);
findViewById(R.id.button_add_biometric).setOnClickListener(view -> {
if (BiometricsHelper.isAvailable(this)) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SlotManagerActivity.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.add_biometric_slot))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
});
findViewById(R.id.button_add_password).setOnClickListener(view -> {
Dialogs.showSetPasswordDialog(this, this);
Dialogs.showSetPasswordDialog(this, new PasswordListener());
});
// set up the recycler view
@@ -59,22 +69,22 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
slotsView.setNestedScrollingEnabled(false);
// load the slots and masterKey
_creds = (DatabaseFileCredentials) getIntent().getSerializableExtra("creds");
_creds = (VaultFileCredentials) getIntent().getSerializableExtra("creds");
for (Slot slot : _creds.getSlots()) {
_adapter.addSlot(slot);
}
updateFingerprintButton();
updateBiometricsButton();
}
private void updateFingerprintButton() {
// only show the fingerprint option if we can get an instance of the fingerprint manager
private void updateBiometricsButton() {
// only show the biometrics option if we can get an instance of the biometrics manager
// and if none of the slots in the collection has a matching alias in the keystore
int visibility = View.VISIBLE;
if (FingerprintHelper.getManager(this) != null) {
if (BiometricsHelper.isAvailable(this)) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
for (BiometricSlot slot : _creds.getSlots().findAll(BiometricSlot.class)) {
if (keyStore.containsKey(slot.getUUID().toString())) {
visibility = View.GONE;
break;
@@ -86,7 +96,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
} else {
visibility = View.GONE;
}
findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
findViewById(R.id.button_add_biometric).setVisibility(visibility);
}
private void onSave() {
@@ -162,29 +172,60 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
slots.remove(slot);
_adapter.removeSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
private void addSlot(Slot slot) {
_creds.getSlots().add(slot);
_adapter.addSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
}
@Override
public void onException(Exception e) {
Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
private void showSlotError(String error) {
Toast.makeText(SlotManagerActivity.this, getString(R.string.adding_new_slot_error) + error, Toast.LENGTH_SHORT).show();
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
addSlot(slot);
} catch (SlotException e) {
onSlotInitializationFailed(0, e.toString());
}
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
showSlotError(errString.toString());
}
}
}
private class PasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
addSlot(slot);
}
@Override
public void onException(Exception e) {
showSlotError(e.toString());
}
}
}
@@ -4,7 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
@@ -16,6 +16,6 @@ import java.nio.ByteBuffer;
public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(DatabaseEntry.class, ByteBuffer.class, new IconLoader.Factory());
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
}
}
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class GlideLicense extends License {
@Override
public String getName() {
return "Glide License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://github.com/bumptech/glide/blob/master/LICENSE";
}
}
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Options;
@@ -13,21 +13,21 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<DatabaseEntry, ByteBuffer> {
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull DatabaseEntry model, int width, int height, @NonNull Options options) {
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
}
@Override
public boolean handles(@NonNull DatabaseEntry model) {
public boolean handles(@NonNull VaultEntry model) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private DatabaseEntry _model;
private VaultEntry _model;
private Fetcher(DatabaseEntry model) {
private Fetcher(VaultEntry model) {
_model = model;
}
@@ -61,10 +61,10 @@ public class IconLoader implements ModelLoader<DatabaseEntry, ByteBuffer> {
}
}
public static class Factory implements ModelLoaderFactory<DatabaseEntry, ByteBuffer> {
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> {
@NonNull
@Override
public ModelLoader<DatabaseEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
}
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.Serializable;
import java.util.UUID;
@@ -13,7 +13,7 @@ public class ImportEntry implements Serializable {
private transient Listener _listener;
private boolean _isChecked = true;
public ImportEntry(DatabaseEntry entry) {
public ImportEntry(VaultEntry entry) {
_uuid = entry.getUUID();
_name = entry.getName();
_issuer = entry.getIssuer();
@@ -1,63 +1,58 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
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.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.fragment.app.Fragment;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
private int _bgColor;
private LinearLayout _boxFingerprint;
private SwirlView _imgFingerprint;
private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private boolean _fingerAuthenticated;
private BiometricSlot _bioSlot;
private Cipher _bioCipher;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_boxFingerprint = view.findViewById(R.id.box_fingerprint);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert);
_imgFingerprint = new SwirlView(getContext());
insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
_textPassword.setTransformationMethod(null);
_textPassword.clearFocus();
_textPasswordConfirm.setEnabled(false);
} else {
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
_textPasswordConfirm.setEnabled(true);
}
});
_textFingerprint = view.findViewById(R.id.text_fingerprint);
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
@@ -66,114 +61,96 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
return _cryptType;
}
public BiometricSlot getBiometricSlot() {
return _bioSlot;
}
public Cipher getBiometriCipher() {
return _bioCipher;
}
public char[] getPassword() {
return EditTextHelper.getEditTextChars(_textPassword);
}
public Cipher getFingerCipher() {
return _fingerCryptoObj.getCipher();
}
public FingerprintSlot getFingerSlot() {
return _fingerSlot;
}
public void setBgColor(int color) {
_bgColor = color;
}
public void showBiometricPrompt() {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
switch(_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
break;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
_boxFingerprint.setVisibility(View.VISIBLE);
SecretKey key;
try {
if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle();
_fingerSlot = new FingerprintSlot();
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new RuntimeException(e);
}
if (_fingerHelper == null) {
FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
_fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this);
}
try {
Cipher cipher = Slot.createEncryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
} catch (Exception e) {
throw new RuntimeException(e);
}
_fingerHelper.startListening(_fingerCryptoObj);
break;
default:
throw new RuntimeException();
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
showBiometricPrompt();
}
}
@Override
public void onSlideDeselected() {
if (_fingerHelper != null) {
_fingerAuthenticated = false;
_boxFingerprint.setVisibility(View.INVISIBLE);
_fingerHelper.stopListening();
}
}
@Override
public boolean isPolicyRespected() {
switch(_cryptType) {
switch (_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
if (!_fingerAuthenticated) {
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
if (_bioSlot == null) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
}
return false;
default:
throw new RuntimeException();
return false;
}
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
message = getString(R.string.password_equality_error);
} else if (!_fingerAuthenticated) {
message = getString(R.string.register_fingerprint);
} else {
return;
}
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
} else if (_bioSlot == null) {
showBiometricPrompt();
}
}
@Override
public void onAuthenticated() {
_fingerAuthenticated = true;
}
private class BiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onError() {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
_bioSlot = slot;
_bioCipher = cipher;
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
}
}
}
}
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -10,18 +9,18 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_FINGER = 3;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
@@ -34,12 +33,12 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
FingerprintManager manager = FingerprintHelper.getManager(getContext());
if (manager != null) {
RadioButton button = view.findViewById(R.id.rb_fingerprint);
TextView text = view.findViewById(R.id.text_rb_fingerprint);
if (BiometricsHelper.isAvailable(getContext())) {
RadioButton button = view.findViewById(R.id.rb_biometrics);
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
_buttonGroup.check(R.id.rb_biometrics);
}
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
@@ -75,8 +74,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
break;
case R.id.rb_fingerprint:
id = CRYPT_TYPE_FINGER;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException();
@@ -5,7 +5,7 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import javax.crypto.SecretKey;
@@ -9,7 +9,7 @@ import com.beemdevelopment.aegis.ui.Dialogs;
import androidx.annotation.CallSuper;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
private ProgressDialog _dialog;
public ProgressDialogTask(Context context, String message) {
@@ -33,6 +33,13 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
}
}
@Override
protected void onProgressUpdate(String... values) {
if (values.length == 1) {
_dialog.setMessage(values[0]);
}
}
protected void setPriority() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
}
@@ -1,17 +1,16 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.app.ProgressDialog;
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.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.db.slots.SlotList;
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 javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -38,8 +37,8 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
if (slot instanceof PasswordSlot) {
char[] password = (char[]) params.getObj();
return decryptPasswordSlot((PasswordSlot) slot, password);
} else if (slot instanceof FingerprintSlot) {
return decryptFingerprintSlot((FingerprintSlot) slot, (Cipher) params.getObj());
} else if (slot instanceof BiometricSlot) {
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
}
} catch (SlotException e) {
throw new RuntimeException(e);
@@ -51,7 +50,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
return null;
}
private Result decryptFingerprintSlot(FingerprintSlot slot, Cipher cipher)
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
throws SlotException, SlotIntegrityException {
MasterKey key = slot.getKey(cipher);
return new Result(key, slot);
@@ -72,8 +71,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
throw e;
}
ProgressDialog dialog = getDialog();
dialog.setMessage(dialog.getContext().getString(R.string.unlocking_vault_repair));
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
// try to decrypt the password slot with the old key
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
@@ -1,12 +1,16 @@
package com.beemdevelopment.aegis.ui.views;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
@@ -18,14 +22,15 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private EntryListView _view;
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private DatabaseEntry _selectedEntry;
private List<VaultEntry> _entries;
private List<VaultEntry> _shownEntries;
private List<VaultEntry> _selectedEntries;
private VaultEntry _focusedEntry;
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private String _groupFilter;
@@ -33,6 +38,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
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
private List<EntryHolder> _holders;
@@ -40,7 +47,9 @@ 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;
}
@@ -63,18 +72,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_tapToRevealTime = number;
}
public DatabaseEntry getEntryAt(int position) {
public void setSearchAccountName(boolean searchAccountName) {
_searchAccountName = searchAccountName;
}
public void setHighlightEntry(boolean highlightEntry) {
_highlightEntry = highlightEntry;
}
public VaultEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
public void addEntry(DatabaseEntry entry) {
public void addEntry(VaultEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
return;
}
boolean added = false;
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
// insert the entry in the correct order
// note: this assumes that _shownEntries has already been sorted
@@ -102,13 +119,13 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
public void addEntries(List<DatabaseEntry> entries) {
public void addEntries(List<VaultEntry> entries) {
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
}
public void removeEntry(DatabaseEntry entry) {
public void removeEntry(VaultEntry entry) {
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
@@ -127,7 +144,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) {
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
@@ -150,15 +167,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
checkPeriodUniformity();
}
private boolean isEntryFiltered(DatabaseEntry entry) {
private boolean isEntryFiltered(VaultEntry entry) {
String group = entry.getGroup();
String issuer = entry.getIssuer().toLowerCase();
String name = entry.getName().toLowerCase();
if (_groupFilter != null && (group == null || !group.equals(_groupFilter))) {
return true;
if (_groupFilter != null) {
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
return false;
}
if (group == null || !group.equals(_groupFilter)) {
return true;
}
}
return _searchFilter != null && !issuer.contains(_searchFilter);
if (_searchFilter == null) {
return false;
}
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
}
public void refresh(boolean hard) {
@@ -203,14 +231,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_shownEntries.clear();
// add entries back that are not filtered out
for (DatabaseEntry entry : _entries) {
for (VaultEntry entry : _entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
// sort the remaining list of entries
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
Collections.sort(_shownEntries, comparator);
}
@@ -244,7 +272,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return;
}
// notify the database first
// notify the vault first
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
@@ -275,22 +303,43 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _shownEntries.get(position);
holder.setFocused(entry == _selectedEntry);
VaultEntry entry = _shownEntries.get(position);
holder.setFocused(_selectedEntries.contains(entry));
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
holder.setTapToRevealTime(_tapToRevealTime);
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
holder.loadIcon(_view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
if (_tapToReveal && holder.isCodeHidden() && _selectedEntry == null) {
holder.revealCode();
boolean handled = false;
if (_selectedEntries.isEmpty()) {
if (_highlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
handled = true;
} else {
focusEntry(entry);
}
}
} else {
_view.onEntryClick(_shownEntries.get(position));
if (_selectedEntries.contains(entry)) {
_view.onDeselect(entry);
removeSelectedEntry(entry);
holder.setFocused(false);
} else {
holder.setFocused(true);
addSelectedEntry(entry);
_view.onSelect(entry);
}
}
if (!handled) {
_view.onEntryClick(entry);
}
}
});
@@ -298,8 +347,7 @@ 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));
if (_selectedEntries.isEmpty()) {
holder.setFocused(true);
}
@@ -317,7 +365,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
// notify the listener that the counter has been incremented
// this gives it a chance to save the database
// this gives it a chance to save the vault
_view.onEntryChange(entry);
// finally, refresh the code in the UI
@@ -333,22 +381,24 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
private void checkPeriodUniformity(boolean force) {
int uniformPeriod = getUniformPeriod();
boolean uniform = isPeriodUniform();
if (!force && uniform == _isPeriodUniform) {
if (!force && uniform == _isPeriodUniform && uniformPeriod == _uniformPeriod) {
return;
}
_isPeriodUniform = uniform;
_uniformPeriod = uniformPeriod;
for (EntryHolder holder : _holders) {
holder.setShowProgress(!_isPeriodUniform);
}
_view.onPeriodUniformityChanged(_isPeriodUniform);
_view.onPeriodUniformityChanged(_isPeriodUniform, _uniformPeriod);
}
public int getUniformPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (DatabaseEntry entry : _shownEntries) {
for (VaultEntry entry : _shownEntries) {
OtpInfo info = entry.getInfo();
if (info instanceof TotpInfo) {
infos.add((TotpInfo) info);
@@ -369,12 +419,61 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return period;
}
public void setSelectedEntry(DatabaseEntry entry) {
if (entry == null) {
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
private void focusEntry(VaultEntry entry) {
_focusedEntry = entry;
_dimHandler.removeCallbacksAndMessages(null);
for (EntryHolder holder : _holders) {
if (holder.getEntry() != _focusedEntry) {
if (_highlightEntry) {
holder.dim();
}
if (_tapToReveal) {
holder.hideCode();
}
} else {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.revealCode();
}
}
}
_selectedEntry = entry;
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
}
private void resetFocus() {
for (EntryHolder holder : _holders) {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.hideCode();
}
}
_focusedEntry = null;
}
public void removeSelectedEntry(VaultEntry entry) {
_selectedEntries.remove(entry);
}
public void addSelectedEntry(VaultEntry entry) {
if (entry == null) {
for (VaultEntry vaultEntry: _selectedEntries) {
notifyItemChanged(_shownEntries.indexOf(vaultEntry));
}
_selectedEntries.clear();
return;
} else if (_highlightEntry) {
resetFocus();
}
_selectedEntries.add(entry);
}
public boolean isDragAndDropAllowed() {
@@ -382,7 +481,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public boolean isPeriodUniform() {
return getUniformPeriod() != -1;
return isPeriodUniform(getUniformPeriod());
}
private static boolean isPeriodUniform(int period) {
return period != -1;
}
@Override
@@ -391,11 +494,13 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
boolean onLongEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onPeriodUniformityChanged(boolean uniform);
void onEntryClick(VaultEntry entry);
boolean onLongEntryClick(VaultEntry entry);
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onPeriodUniformityChanged(boolean uniform, int period);
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
}
}
@@ -1,14 +1,16 @@
package com.beemdevelopment.aegis.ui.views;
import android.graphics.PorterDuff;
import android.os.Handler;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
@@ -19,25 +21,23 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
private static final float DIMMED_ALPHA = 0.2f;
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private VaultEntry _entry;
private ImageView _buttonRefresh;
private boolean _hidden;
private int _tapToRevealTime;
private PeriodProgressBar _progressBar;
private View _view;
private UiRefresher _refresher;
private Handler _hiddenHandler;
public EntryHolder(final View view) {
super(view);
@@ -58,7 +58,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
if (!isCodeHidden()) {
if (!_hidden) {
refreshCode();
}
@@ -70,11 +70,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
}
});
_hiddenHandler = new Handler();
}
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress, boolean hidden) {
public void setData(VaultEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
_entry = entry;
_hidden = hidden;
@@ -90,14 +88,17 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileName.setText(" - " + entry.getName());
}
// cancel any scheduled hideCode calls
_hiddenHandler.removeCallbacksAndMessages(null);
if (_hidden) {
hideCode();
} else {
refreshCode();
}
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
}
public VaultEntry getEntry() {
return _entry;
}
public void loadIcon(Fragment fragment) {
@@ -118,10 +119,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
return _profileDrawable;
}
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
}
public void setOnRefreshClickListener(View.OnClickListener listener) {
_buttonRefresh.setOnClickListener(listener);
}
@@ -162,7 +159,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void refreshCode() {
if (!isCodeHidden()) {
if (!_hidden) {
updateCode();
}
}
@@ -187,16 +184,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void revealCode() {
updateCode();
_hiddenHandler.postDelayed(this::hideCode, _tapToRevealTime * 1000);
_hidden = false;
}
private void hideCode() {
public void hideCode() {
_profileCode.setText(R.string.tap_to_reveal);
_hidden = true;
}
public boolean isCodeHidden() {
return _hidden;
public void dim() {
animateAlphaTo(DIMMED_ALPHA);
}
public void highlight() {
animateAlphaTo(DEFAULT_ALPHA);
}
private void animateAlphaTo(float alpha) {
itemView.animate().alpha(alpha).setDuration(200).start();
}
}
@@ -8,12 +8,21 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.Toast;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
@@ -27,15 +36,6 @@ import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
@@ -43,10 +43,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<DatabaseEntry> _preloadSizeProvider;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
private UiRefresher _refresher;
@@ -81,7 +82,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
// set up icon preloading
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<DatabaseEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
RecyclerViewPreloader<VaultEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
@@ -107,6 +108,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
});
_emptyStateView = view.findViewById(R.id.vEmptyList);
return view;
}
@@ -129,10 +132,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setActionModeState(boolean enabled, DatabaseEntry entry) {
public void setIsLongPressDragEnabled(boolean enabled) {
_touchCallback.setIsLongPressDragEnabled(enabled);
}
public void setActionModeState(boolean enabled, VaultEntry entry) {
_touchCallback.setSelectedEntry(entry);
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
_adapter.setSelectedEntry(entry);
_adapter.addSelectedEntry(entry);
}
public void setSortCategory(SortCategory sortCategory, boolean apply) {
@@ -167,36 +174,43 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
@Override
public void onEntryClick(DatabaseEntry entry) {
public void onEntryClick(VaultEntry entry) {
_listener.onEntryClick(entry);
}
public boolean onLongEntryClick(DatabaseEntry entry) {
public boolean onLongEntryClick(VaultEntry entry) {
_listener.onLongEntryClick(entry);
return true;
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_listener.onEntryMove(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
public void onEntryDrop(VaultEntry entry) {
_listener.onEntryDrop(entry);
}
@Override
public void onEntryChange(DatabaseEntry entry) {
public void onEntryChange(VaultEntry entry) {
_listener.onEntryChange(entry);
}
@Override
public void onPeriodUniformityChanged(boolean isUniform) {
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) {
_refresher.stop();
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
_progressBar.setPeriod(period);
_refresher.start();
} else {
_progressBar.setVisibility(View.GONE);
@@ -208,6 +222,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setShowAccountName(showAccountName);
}
public void setSearchAccountName(boolean searchAccountName) {
_adapter.setSearchAccountName(searchAccountName);
}
public void setHighlightEntry(boolean highlightEntry) {
_adapter.setHighlightEntry(highlightEntry);
}
public void setTapToReveal(boolean tapToReveal) {
_adapter.setTapToReveal(tapToReveal);
}
@@ -216,23 +238,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setTapToRevealTime(number);
}
public void addEntry(DatabaseEntry entry) {
public void addEntry(VaultEntry entry) {
_adapter.addEntry(entry);
updateEmptyState();
}
public void addEntries(List<DatabaseEntry> entries) {
public void addEntries(List<VaultEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
}
public void removeEntry(DatabaseEntry entry) {
public void removeEntry(VaultEntry entry) {
_adapter.removeEntry(entry);
updateEmptyState();
}
public void clearEntries() {
_adapter.clearEntries();
}
public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) {
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
_adapter.replaceEntry(oldEntry, newEntry);
}
@@ -267,13 +292,25 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.addItemDecoration(_dividerDecoration);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_recyclerView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_recyclerView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onLongEntryClick(DatabaseEntry entry);
void onEntryClick(VaultEntry entry);
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(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 {
@@ -294,11 +331,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<DatabaseEntry> {
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override
public List<DatabaseEntry> getPreloadItems(int position) {
DatabaseEntry entry = _adapter.getEntryAt(position);
public List<VaultEntry> getPreloadItems(int position) {
VaultEntry entry = _adapter.getEntryAt(position);
if (!entry.hasIcon()) {
return Collections.emptyList();
}
@@ -307,7 +344,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull DatabaseEntry entry) {
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(EntryListView.this)
.asDrawable()
.load(entry)
@@ -5,7 +5,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import java.util.ArrayList;
@@ -6,7 +6,6 @@ import android.widget.CheckBox;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import androidx.recyclerview.widget.RecyclerView;
@@ -5,7 +5,7 @@ import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import java.util.ArrayList;
@@ -5,16 +5,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.RawSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.RawSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
public class SlotHolder extends RecyclerView.ViewHolder {
private TextView _slotUsed;
@@ -36,10 +36,10 @@ public class SlotHolder extends RecyclerView.ViewHolder {
if (slot instanceof PasswordSlot) {
_slotName.setText(R.string.password);
_slotImg.setImageResource(R.drawable.ic_create_black_24dp);
} else if (slot instanceof FingerprintSlot) {
_slotName.setText(R.string.authentication_method_fingerprint);
} else if (slot instanceof BiometricSlot) {
_slotName.setText(R.string.authentication_method_biometrics);
_slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
if (FingerprintHelper.isSupported()) {
if (BiometricsHelper.isAvailable(itemView.getContext())) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(slot.getUUID().toString())) {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db;
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -8,14 +8,14 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Database {
public class Vault {
private static final int VERSION = 1;
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
public JSONObject toJson() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
for (VaultEntry e : _entries) {
array.put(e.toJson());
}
@@ -28,29 +28,29 @@ public class Database {
}
}
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
UUIDMap<DatabaseEntry> entries = db.getEntries();
public static Vault fromJson(JSONObject obj) throws VaultException {
Vault vault = new Vault();
UUIDMap<VaultEntry> entries = vault.getEntries();
try {
int ver = obj.getInt("version");
if (ver != VERSION) {
throw new DatabaseException("Unsupported version");
throw new VaultException("Unsupported version");
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
entries.add(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
throw new VaultException(e);
}
return db;
return vault;
}
public UUIDMap<DatabaseEntry> getEntries() {
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
}
@@ -1,10 +1,11 @@
package com.beemdevelopment.aegis.db;
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
@@ -14,30 +15,30 @@ import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
public class DatabaseEntry extends UUIDMap.Value {
public class VaultEntry extends UUIDMap.Value {
private String _name = "";
private String _issuer = "";
private String _group;
private OtpInfo _info;
private byte[] _icon;
private DatabaseEntry(UUID uuid, OtpInfo info) {
private VaultEntry(UUID uuid, OtpInfo info) {
super(uuid);
_info = info;
}
public DatabaseEntry(OtpInfo info) {
public VaultEntry(OtpInfo info) {
super();
_info = info;
}
public DatabaseEntry(OtpInfo info, String name, String issuer) {
public VaultEntry(OtpInfo info, String name, String issuer) {
this(info);
setName(name);
setIssuer(issuer);
}
public DatabaseEntry(GoogleAuthInfo info) {
public VaultEntry(GoogleAuthInfo info) {
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
}
@@ -59,7 +60,7 @@ public class DatabaseEntry extends UUIDMap.Value {
return obj;
}
public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
// if there is no uuid, generate a new one
UUID uuid;
if (!obj.has("uuid")) {
@@ -69,7 +70,7 @@ public class DatabaseEntry extends UUIDMap.Value {
}
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
DatabaseEntry entry = new DatabaseEntry(uuid, info);
VaultEntry entry = new VaultEntry(uuid, info);
entry.setName(obj.getString("name"));
entry.setIssuer(obj.getString("issuer"));
entry.setGroup(obj.optString("group", null));
@@ -128,16 +129,39 @@ public class DatabaseEntry extends UUIDMap.Value {
@Override
public boolean equals(Object o) {
if (!(o instanceof DatabaseEntry)) {
if (!(o instanceof VaultEntry)) {
return false;
}
DatabaseEntry entry = (DatabaseEntry) o;
return super.equals(entry)
&& getName().equals(entry.getName())
VaultEntry entry = (VaultEntry) o;
return super.equals(entry) && equivalates(entry);
}
/**
* Reports whether this entry is equivalent to the given entry. The UUIDs of these
* entries are ignored during the comparison, so they are not necessarily the same
* instance.
*/
public boolean equivalates(VaultEntry entry) {
return getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon());
}
/**
* Reports whether this entry has its values set to the defaults.
*/
public boolean isDefault() {
return equivalates(getDefault());
}
public static VaultEntry getDefault() {
try {
return new VaultEntry(new TotpInfo(null));
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.vault;
public class VaultException extends Exception {
public VaultException(Throwable cause) {
super(cause);
}
public VaultException(String message) {
super(message);
}
}
@@ -1,10 +1,10 @@
package com.beemdevelopment.aegis.db;
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.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotListException;
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;
@@ -14,17 +14,17 @@ import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public class DatabaseFile {
public class VaultFile {
public static final byte VERSION = 1;
private Object _content;
private Header _header;
public DatabaseFile() {
public VaultFile() {
}
private DatabaseFile(Object content, Header header) {
private VaultFile(Object content, Header header) {
_content = content;
_header = header;
}
@@ -60,29 +60,29 @@ public class DatabaseFile {
}
}
public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException {
public static VaultFile fromJson(JSONObject obj) throws VaultFileException {
try {
if (obj.getInt("version") > VERSION) {
throw new DatabaseFileException("unsupported version");
throw new VaultFileException("unsupported version");
}
Header header = Header.fromJson(obj.getJSONObject("header"));
if (!header.isEmpty()) {
return new DatabaseFile(obj.getString("db"), header);
return new VaultFile(obj.getString("db"), header);
}
return new DatabaseFile(obj.getJSONObject("db"), header);
return new VaultFile(obj.getJSONObject("db"), header);
} catch (JSONException e) {
throw new DatabaseFileException(e);
throw new VaultFileException(e);
}
}
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
public static VaultFile fromBytes(byte[] data) throws VaultFileException {
try {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return DatabaseFile.fromJson(obj);
return VaultFile.fromJson(obj);
} catch (JSONException e) {
throw new DatabaseFileException(e);
throw new VaultFileException(e);
}
}
@@ -90,13 +90,13 @@ public class DatabaseFile {
return (JSONObject) _content;
}
public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException {
public JSONObject getContent(VaultFileCredentials creds) throws VaultFileException {
try {
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) {
throw new DatabaseFileException(e);
throw new VaultFileException(e);
}
}
@@ -105,16 +105,16 @@ public class DatabaseFile {
_header = new Header(null, null);
}
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
public void setContent(JSONObject obj, VaultFileCredentials creds) throws VaultFileException {
try {
String string = obj.toString(4);
byte[] dbBytes = string.getBytes(StandardCharsets.UTF_8);
byte[] vaultBytes = string.getBytes(StandardCharsets.UTF_8);
CryptResult result = creds.encrypt(dbBytes);
CryptResult result = creds.encrypt(vaultBytes);
_content = Base64.encode(result.getData());
_header = new Header(creds.getSlots(), result.getParams());
} catch (MasterKeyException | JSONException e) {
throw new DatabaseFileException(e);
throw new VaultFileException(e);
}
}
@@ -127,7 +127,7 @@ public class DatabaseFile {
_params = params;
}
public static Header fromJson(JSONObject obj) throws DatabaseFileException {
public static Header fromJson(JSONObject obj) throws VaultFileException {
if (obj.isNull("slots") && obj.isNull("params")) {
return new Header(null, null);
}
@@ -137,7 +137,7 @@ public class DatabaseFile {
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
return new Header(slots, params);
} catch (SlotListException | JSONException | HexException e) {
throw new DatabaseFileException(e);
throw new VaultFileException(e);
}
}
@@ -1,23 +1,23 @@
package com.beemdevelopment.aegis.db;
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import java.io.Serializable;
public class DatabaseFileCredentials implements Serializable {
public class VaultFileCredentials implements Serializable {
private MasterKey _key;
private SlotList _slots;
public DatabaseFileCredentials() {
public VaultFileCredentials() {
_key = MasterKey.generate();
_slots = new SlotList();
}
public DatabaseFileCredentials(MasterKey key, SlotList slots) {
public VaultFileCredentials(MasterKey key, SlotList slots) {
_key = key;
_slots = slots;
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.vault;
public class VaultFileException extends Exception {
public VaultFileException(Throwable cause) {
super(cause);
}
public VaultFileException(String message) {
super(message);
}
}
@@ -0,0 +1,225 @@
package com.beemdevelopment.aegis.vault;
import android.content.Context;
import android.content.Intent;
import com.beemdevelopment.aegis.services.NotificationService;
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;
public class VaultManager {
private 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;
public VaultManager(Context context) {
_context = context;
}
public boolean fileExists() {
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);
try {
JSONObject obj = _file.getContent(creds);
_vault = Vault.fromJson(obj);
_creds = creds;
_context.startService(new Intent(_context, NotificationService.class));
} catch (VaultFileException | VaultException 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)) {
stream.write(bytes);
} catch (IOException e) {
throw new VaultManagerException(e);
}
}
public void save() throws VaultManagerException {
assertState(false, true);
try {
JSONObject obj = _vault.toJson();
if (isEncryptionEnabled()) {
_file.setContent(obj, _creds);
} else {
_file.setContent(obj);
}
save(_context, _file);
} catch (VaultFileException e) {
throw new VaultManagerException(e);
}
}
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
assertState(false, true);
try {
VaultFile vaultFile = new VaultFile();
if (encrypt && isEncryptionEnabled()) {
vaultFile.setContent(_vault.toJson(), _creds);
} else {
vaultFile.setContent(_vault.toJson());
}
byte[] bytes = vaultFile.toBytes();
stream.write(bytes);
} catch (IOException | VaultFileException e) {
throw new VaultManagerException(e);
}
}
public void addEntry(VaultEntry entry) {
assertState(false, true);
_vault.getEntries().add(entry);
}
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();
if (group != null) {
groups.add(group);
}
}
return groups;
}
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;
}
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
assertState(false, true);
_creds = creds;
_encrypt = true;
save();
}
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");
}
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.vault;
public class VaultManagerException extends Exception {
public VaultManagerException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,20 @@
package com.beemdevelopment.aegis.vault.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class BiometricSlot extends RawSlot {
public BiometricSlot() {
super();
}
BiometricSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_BIOMETRIC;
}
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
@@ -28,7 +28,7 @@ import javax.crypto.spec.SecretKeySpec;
public abstract class Slot extends UUIDMap.Value {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
public final static byte TYPE_BIOMETRIC = 0x02;
private byte[] _encryptedMasterKey;
private CryptParameters _encryptedMasterKeyParams;
@@ -138,8 +138,8 @@ public abstract class Slot extends UUIDMap.Value {
boolean repaired = obj.optBoolean("repaired", false);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired);
break;
case Slot.TYPE_FINGERPRINT:
slot = new FingerprintSlot(uuid, key, keyParams);
case Slot.TYPE_BIOMETRIC:
slot = new BiometricSlot(uuid, key, keyParams);
break;
default:
throw new SlotException("unrecognized slot type");
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
public class SlotException extends Exception {
public SlotException(Throwable cause) {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
public class SlotIntegrityException extends Exception {
public SlotIntegrityException() {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
import com.beemdevelopment.aegis.util.UUIDMap;
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.db.slots;
package com.beemdevelopment.aegis.vault.slots;
public class SlotListException extends Exception {
public SlotListException(Throwable cause) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@@ -4,5 +4,5 @@
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" />
<path android:fillColor="#FFFFFF" android:pathData="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" />
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.ui.AboutActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:isScrollContainer="true">
<include layout="@layout/content_about" />
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+20 -31
View File
@@ -49,40 +49,29 @@
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_decrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="125dp"
android:text="@string/unlock" />
</LinearLayout>
<LinearLayout
android:id="@+id/box_fingerprint"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp"
android:visibility="gone">
<LinearLayout
android:id="@+id/img_fingerprint_insert"
android:layout_width="60dp"
android:layout_height="60dp"
android:orientation="horizontal"/>
<TextView
android:id="@+id/text_fingerprint"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp"
android:text="@string/fingerprint_hint"
android:textColor="?attr/secondaryText"/>
android:layout_height="wrap_content">
<Button
android:id="@+id/button_decrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/box_fingerprint_info"
android:id="@+id/box_biometric_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@@ -98,7 +87,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/invalidated_fingerprint"/>
android:text="@string/invalidated_biometrics"/>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+2 -2
View File
@@ -53,7 +53,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/button_add_fingerprint"
android:id="@+id/button_add_biometric"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@@ -75,7 +75,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_fingerprint"
android:text="@string/add_biometric"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
+485
View File
@@ -0,0 +1,485 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:cardUseCompatPadding="true">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:background="?attr/cardBackground"
android:layout_marginBottom="8dp"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground"
android:padding="16dp">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="@string/app_name_full"
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-info-outline"
app:ico_size="16dp" />
<LinearLayout
android:id="@+id/btn_app_version"
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/version"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0.0 (0)"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_changelog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-time-restore"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/changelog"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/whats_new"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_github"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-github"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/github"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/github_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-file-text"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/licenses"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/licenses_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:background="?attr/cardBackground"
android:layout_marginBottom="8dp"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:text="@string/org_name_full"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorSecondary" />
<LinearLayout
android:id="@+id/btn_alexander"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/author_alex"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/country_netherlands"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_michael"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/author_michael"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/country_netherlands"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-email"
app:ico_size="16dp" />
<TextView
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"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:id="@+id/btn_website"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-globe"
app:ico_size="16dp" />
<TextView
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"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:layout_marginBottom="8dp"
android:background="?attr/cardBackground"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:text="@string/about_support"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorSecondary" />
<LinearLayout
android:id="@+id/btn_rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-star"
app:ico_size="16dp" />
<LinearLayout
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_rate"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_rate_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp">
<com.mattprecious.swirl.SwirlView
android:id="@+id/img_fingerprint"
android:layout_width="60dp"
android:layout_height="60dp"/>
<TextView
android:id="@+id/text_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="@string/fingerprint_hint"
android:textColor="?attr/secondaryText"
android:layout_gravity="center_vertical"/>
</LinearLayout>
+17 -1
View File
@@ -4,8 +4,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:paddingBottom="10dp"
android:paddingEnd="20dp"
android:paddingTop="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"
@@ -18,4 +27,11 @@
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<CheckBox
android:id="@+id/check_toggle_visibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/show_password" />
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none" />
</FrameLayout>
@@ -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"
@@ -59,44 +67,12 @@
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"/>
</LinearLayout>
<LinearLayout
android:id="@+id/box_fingerprint"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:visibility="invisible">
<TextView
android:text="@string/authentication_method_fingerprint"
<CheckBox
android:id="@+id/check_toggle_visibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textColor="@color/primary_text_inverted"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp">
<LinearLayout
android:id="@+id/img_fingerprint_insert"
android:layout_width="60dp"
android:layout_height="60dp"
android:orientation="horizontal"/>
<TextView
android:id="@+id/text_fingerprint"
android:textColor="@color/primary_text_inverted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp"
android:text="@string/fingerprint_hint"/>
</LinearLayout>
android:layout_marginTop="5dp"
android:text="@string/show_password" />
</LinearLayout>
@@ -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"
@@ -60,22 +66,22 @@
android:textColor="@color/secondary_text_inverted" />
<RadioButton
android:id="@+id/rb_fingerprint"
android:id="@+id/rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_fingerprint"
android:text="@string/authentication_method_biometrics"
android:textSize="16sp" />
<TextView
android:id="@+id/text_rb_fingerprint"
android:id="@+id/text_rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_fingerprint_description"
android:text="@string/authentication_method_biometrics_description"
android:textColor="@color/disabled_textview_colors" />
</RadioGroup>
@@ -22,4 +22,47 @@
android:id="@+id/rvKeyProfiles"
android:layoutAnimation="@anim/layout_animation_fall_down"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|fill_vertical"
android:id="@+id/vEmptyList"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="150dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_qrcode_scan"
android:tint="?attr/primaryText" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_list_title"
android:paddingTop="17dp"
android:textColor="?attr/primaryText"
android:textSize="18sp" />
<TextView
android:id="@+id/textView5"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:lineSpacingExtra="5dp"
android:paddingTop="7dp"
android:text="@string/empty_list"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -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>
@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_copy"
android:orderInCategory="80"
android:icon="@drawable/ic_content_copy_white_24dp"
android:title="@string/copy"
app:showAsAction="always" />
<item
android:id="@+id/action_edit"
android:orderInCategory="90"
+6 -1
View File
@@ -6,7 +6,7 @@
android:id="@+id/mi_search"
android:title="@string/search"
app:showAsAction="always"
app:actionViewClass="android.widget.SearchView"
app:actionViewClass="androidx.appcompat.widget.SearchView"
/>
<item
android:id="@+id/action_lock"
@@ -63,4 +63,9 @@
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:orderInCategory="110"
android:title="@string/action_about"
app:showAsAction="never" />
</menu>
+94
View File
@@ -0,0 +1,94 @@
License for everything not in third_party and not otherwise marked:
Copyright 2014 Google, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Google, Inc.
---------------------------------------------------------------------------------------------
License for third_party/disklrucache:
Copyright 2012 Jake Wharton
Copyright 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---------------------------------------------------------------------------------------------
License for third_party/gif_decoder:
Copyright (c) 2013 Xcellent Creations, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------------------------------------------------------------
License for third_party/gif_encoder/AnimatedGifEncoder.java and
third_party/gif_encoder/LZWEncoder.java:
No copyright asserted on the source code of this class. May be used for any
purpose, however, refer to the Unisys LZW patent for restrictions on use of
the associated LZWEncoder class. Please forward any corrections to
kweiner@fmsware.com.
-----------------------------------------------------------------------------
License for third_party/gif_encoder/NeuQuant.java
Copyright (c) 1994 Anthony Dekker
NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
"Kohonen neural networks for optimal colour quantization" in "Network:
Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
the algorithm.
Any party obtaining a copy of these files from the author, directly or
indirectly, is granted, free of charge, a full and unrestricted irrevocable,
world-wide, paid up, royalty-free, nonexclusive right and license to deal in
this software and documentation files (the "Software"), including without
limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons who
receive copies from any such party to do so, with the only requirement being
that this copyright notice remain intact.
+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<notices>
<notice>
<name>TextDrawable</name>
<url>https://github.com/amulyakhare/TextDrawable</url>
<copyright>Copyright (C) 2014 Amulya Khare</copyright>
<license>MIT License</license>
</notice>
<notice>
<name>Preferencex</name>
<url>https://github.com/takisoft/preferencex-android</url>
<copyright />
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>FloatingActionButton</name>
<url>https://github.com/futuresimple/android-floating-action-button</url>
<copyright>Copyright 2014 Jerzy Chalupski</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>AppIntro</name>
<url>https://github.com/AppIntro/AppIntro</url>
<copyright/>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Krop</name>
<url>https://github.com/avito-tech/krop</url>
<copyright>Copyright (c) 2017 Avito Technology</copyright>
<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>
</notice>
<notice>
<name>CircleImageView</name>
<url>https://github.com/hdodenhof/CircleImageView</url>
<copyright>Copyright 2014 - 2019 Henning Dodenhof</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Barcodescanner</name>
<url>https://github.com/dm77/barcodescanner</url>
<copyright>Copyright (c) 2014 Dushyanth Maguluru</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>libsu</name>
<url>https://github.com/topjohnwu/libsu</url>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Glide</name>
<url>https://github.com/bumptech/glide</url>
<license>Glide License</license>
</notice>
<notice>
<name>Iconics</name>
<url>https://github.com/mikepenz/Android-Iconics</url>
<copyright/>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Android Support Libraries</name>
<url>http://developer.android.com/tools/support-library/index.html</url>
<copyright>Copyright (C) 2016 The Android Open Source Project</copyright>
<license>Apache Software License 2.0</license>
</notice>
</notices>
+193
View File
@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">Nastavení</string>
<string name="action_about">O aplikaci</string>
<string name="action_import">Importovat</string>
<string name="action_delete">Odstranit</string>
<string name="action_default_icon">Obnovit původní ikonu</string>
<string name="discard">Zahodit</string>
<string name="save">Uložit</string>
<string name="issuer">Poskytovatel</string>
<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">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>
<string name="pref_lang_title">Jazyk</string>
<string name="pref_account_name_title">Zobrazit název účtu</string>
<string name="pref_account_name_summary">Zobrazit název účtu vedle poskytovatele</string>
<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 trezor</string>
<string name="pref_import_file_title">Importovat 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 tokeny z aplikace (vyžaduje root přístup)</string>
<string name="pref_export_title">Exportovat</string>
<string name="pref_export_summary">Exportovat trezor</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 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 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 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 odemčení trezoru</string>
<string name="choose_authentication_method">Zabezpečení</string>
<string name="authentication_method_none">Žádné</string>
<string name="authentication_method_password">Heslo</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="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>
<string name="digits">Číslice</string>
<string name="secret">Tajný klíč</string>
<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 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">Potvrdit heslo</string>
<string name="show_password">Zobrazit heslo</string>
<string name="new_profile">Nový profil</string>
<string name="add_new_profile">Přidat nový profil</string>
<string name="unlock_vault_error">Trezor nelze odemknout</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 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 tuto položku?</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">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á, bezplatná a open source 2FA aplikace</string>
<string name="setup_completed">Nastavení dokončeno</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="code_copied">Kód zkopírován do schránky</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 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="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 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">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="exporting_vault_error">Při pokusu o export trezoru došlo k chybě</string>
<string name="export_warning">Trezor bude exportován 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="encryption_enable_biometrics_error">Při pokusu o zapnutí biometrického odemykání došlo k chybě</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 (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 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 nebudou viditelné.</string>
<string name="details">Podrobnosti</string>
<string name="filter">Filtr</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>
<string name="sort_alphabetically">Poskytovatel (A → Z)</string>
<string name="sort_alphabetically_reverse">Poskytovatel (Z → A)</string>
<string name="sort_alphabetically_name">Účet (A → Z)</string>
<string name="sort_alphabetically_name_reverse">Účet (Z → A)</string>
<string name="sort_custom">Vlastní</string>
<string name="new_group">Nová skupina…</string>
<string name="enter_group_name">Zadejte název skupiny</string>
<string name="group_name_hint">Název skupiny</string>
<string name="preference_manage_groups">Upravit skupiny</string>
<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="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">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 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>
<string name="whats_new">Co je nového?</string>
<string name="github_description">Zdrojový kód, problémy a informace</string>
<string name="licenses">Licence</string>
<string name="licenses_description">Licence knihoven použitých v Aegis</string>
<string name="country_netherlands">Nizozemsko</string>
<string name="email_us">Pošlete nám e-mail</string>
<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 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 + v pravém dolním rohu.</string>
<string name="empty_list_title">Žádné záznamy nenalezeny</string>
</resources>
+177
View File
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">Einstellungen</string>
<string name="action_about">Über uns</string>
<string name="action_import">Importieren</string>
<string name="action_delete">Löschen</string>
<string name="action_default_icon">Standard-Symbol wiederherstellen</string>
<string name="discard">Verwerfen</string>
<string name="save">Speichern</string>
<string name="issuer">Herausgeber</string>
<string name="settings">Einstellungen</string>
<string name="pref_appearance_group_title">Aussehen</string>
<string name="pref_general_group_title">Allgemein</string>
<string name="pref_security_group_title">Sicherheit</string>
<string name="pref_tools_group_title">Hilfsmittel</string>
<string name="pref_select_theme_title">Thema</string>
<string name="pref_view_mode_title">Ansichtsmodus</string>
<string name="pref_lang_title">Sprache</string>
<string name="pref_account_name_title">Anzeigen des Kontonamens</string>
<string name="pref_account_name_summary">Aktiviere dies, um den Kontonamen neben dem Herausgeber anzuzeigen.</string>
<string name="pref_timeout_title">Zeitüberschreitung</string>
<string name="pref_timeout_summary">Sperrt die Datenbank automatisch nach %1$s Sekunden Inaktivität.</string>
<string name="pref_slots_title">Schlüssel Steckplätze</string>
<string name="pref_slots_summary">Verwalte die Liste der Schlüssel, die die Datenbank entschlüsseln können.</string>
<string name="pref_import_file_title">Aus Datei importieren</string>
<string name="pref_import_file_summary">Importieren einer Datenbank aus einer Datei</string>
<string name="pref_import_app_title">Aus App importieren</string>
<string name="pref_import_app_summary">Importieren einer Datenbank aus einer App (erfordert Root-Zugriff)</string>
<string name="pref_export_title">Exportieren</string>
<string name="pref_export_summary">Exportieren der Datenbank</string>
<string name="pref_secure_screen_title">Bildschirmsicherheit</string>
<string name="pref_secure_screen_summary">Blockiere Screenshots und andere Versuche, den Bildschirm innerhalb der App aufzunehmen.</string>
<string name="pref_tap_to_reveal_title">Zum Aufdecken tippen</string>
<string name="pref_tap_to_reveal_summary">Schlüssel werden standardmäßig ausgeblendet. Tippe auf die Schlüssel, um den Code anzuzeigen.</string>
<string name="pref_tap_to_reveal_time_title">Zeitüberschreitung für Tippen zum Anzeigen</string>
<string name="pref_auto_lock_title">Automatische Sperre</string>
<string name="pref_auto_lock_summary">Automatische Sperre, wenn die App geschlossen oder dein Gerät gesperrt wird.</string>
<string name="pref_encryption_title">Verschlüsselung</string>
<string name="pref_encryption_summary">Verschlüssele die Datenbank und entsperre mit einem Passwort oder Fingerabdruck.</string>
<string name="pref_set_password_title">Passwort ändern</string>
<string name="pref_set_password_summary">Legen ein neues Passwort fest, das erforderlich ist, um deinen Tresor freizuschalten.</string>
<string name="choose_authentication_method">Sicherheit</string>
<string name="authentication_method_none">Keine</string>
<string name="authentication_method_none_description">Du brauchst kein Passwort, um den Tresor freizuschalten, und es wird nicht verschlüsselt. Diese Option wird nicht empfohlen.</string>
<string name="authentication_method_password">Passwort</string>
<string name="authentication_method_password_description">Du benötigst ein Passwort, um den Tresor freizuschalten.</string>
<string name="authentication_method_set_password">Passwort</string>
<string name="authentication_enter_password">Gib dein Passwort ein</string>
<string name="authentication">Entsperre den Tresor.</string>
<string name="set_password">Bitte gib ein Passwort ein</string>
<string name="set_group">Bitte gib einen Gruppennamen ein</string>
<string name="set_number">Bitte gib eine Zahl ein</string>
<string name="set_password_confirm">Bitte bestätige das Passwort</string>
<string name="unlock">Entsperren</string>
<string name="advanced">Fortgeschritten</string>
<string name="seconds">Sekunden</string>
<string name="counter">Zähler</string>
<string name="digits">Ziffern</string>
<string name="secret">Schlüssel</string>
<string name="scan">QR-Code scannen</string>
<string name="scan_image">Bild scannen</string>
<string name="enter_manually">Manuell eingeben</string>
<string name="add_password">Passwort hinzufügen</string>
<string name="slots_warning">Der Tresor ist nur so sicher wie dein schwächster Schlüssel. Wenn ein neuer Fingerabdruck zu deinem Gerät hinzugefügt wird, musst du die Fingerabdruck-Authentifizierung in Aegis reaktivieren.</string>
<string name="copy">Kopieren</string>
<string name="edit">Bearbeiten</string>
<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>
<string name="snackbar_authentication_method">Bitte wähle eine Authentifizierungsmethode aus</string>
<string name="encrypting_vault">Verschlüsseln des Tresors</string>
<string name="delete_entry">Eintrag löschen</string>
<string name="delete_entry_description">Bist du sicher, dass du diesen Eintrag löschen möchtest?</string>
<string name="discard_changes">Änderungen verwerfen?</string>
<string name="discard_changes_description">Deine Änderungen wurden nicht gespeichert.</string>
<string name="folder">Ordner</string>
<string name="tap_to_select">Zum Auswählen tippen</string>
<string name="saving_profile_error">Fehler beim Speichern des Profils</string>
<string name="welcome">Willkommen</string>
<string name="app_description">Aegis ist eine kostenlose, sichere und Open Source 2FA App.</string>
<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>
<string name="saving_error">Beim Versuch, den Tresor zu speichern, ist ein Fehler aufgetreten.</string>
<string name="disable_encryption">Verschlüsselung deaktivieren</string>
<string name="disable_encryption_description">Bist du sicher, dass du die Verschlüsselung deaktivieren willst? Dadurch wird der Tresor im Klartext gespeichert.</string>
<string name="enable_encryption_error">Bei der Aktivierung der Verschlüsselung ist ein Fehler aufgetreten.</string>
<string name="disable_encryption_error">Bei der Deaktivierung der Verschlüsselung ist ein Fehler aufgetreten.</string>
<string name="permission_denied">Zugriff verweigert</string>
<string name="choose_application">Wähle die Anwendung aus, aus der du eine Datenbank importieren möchtest.</string>
<string name="choose_theme">Wähle das gewünschte Thema aus</string>
<string name="choose_view_mode">Wähle den gewünschten Ansichtsmodus aus</string>
<string name="parsing_file_error">Beim Versuch, die Datei zu analysieren, ist ein Fehler aufgetreten.</string>
<string name="file_not_found">Fehler: Datei nicht gefunden</string>
<string name="reading_file_error">Beim Versuch, die Datei zu lesen, ist ein Fehler aufgetreten.</string>
<string name="app_lookup_error">Fehler: App ist nicht installiert</string>
<string name="root_error">Fehler: Es konnte kein Root-Zugriff gewährt werden.</string>
<string name="imported_entries_count">Importierte %d Einträge</string>
<string name="read_entries_count">%d Einträge gelesen. %d Fehler.</string>
<string name="import_error_title">Beim Import sind ein oder mehrere Fehler aufgetreten.</string>
<string name="export_warning">Diese Aktion exportiert die Datenbank aus dem privaten Speicher von Aegis.</string>
<string name="encryption_set_password_error">Beim Versuch, das Passwort festzulegen, ist ein Fehler aufgetreten: </string>
<string name="no_cameras_available">Keine Kameras verfügbar</string>
<string name="read_qr_error">Beim Versuch, den QR-Code zu lesen, ist ein Fehler aufgetreten.</string>
<string name="authentication_method_raw">Raw</string>
<string name="unlocking_vault">Entsperren des Tresors</string>
<string name="unlocking_vault_repair">Entsperren des Tresors (Reparieren)</string>
<string name="password_slot_error">Du musst mindestens einen Passwort-Slot haben.</string>
<string name="remove_slot">Slot entfernen</string>
<string name="remove_slot_description">Bist du sicher, dass du diesen Slot entfernen willst?</string>
<string name="remove_group">Gruppe entfernen</string>
<string name="remove_group_description">Bist du sicher, dass du diese Gruppe entfernen möchtest? Einträge in dieser Gruppe wechseln automatisch zu \'Keine Gruppe\'.</string>
<string name="adding_new_slot_error">Beim Versuch, einen neuen Slot hinzuzufügen, ist ein Fehler aufgetreten:</string>
<string name="progressbar_error">Die Skala der Animationsdauer kann nicht zurückgesetzt werden. Fortschrittsbalken werden unsichtbar sein.</string>
<string name="details">Details</string>
<string name="filter">Filter</string>
<string name="lock">Sperren</string>
<string name="all">Alle</string>
<string name="name">Name</string>
<string name="no_group">Keine Gruppe</string>
<string name="sort_alphabetically">Herausgeber (A bis Z)</string>
<string name="sort_alphabetically_reverse">Herausgeber (Z bis A)</string>
<string name="sort_alphabetically_name">Konto (A bis Z)</string>
<string name="sort_alphabetically_name_reverse">Konto (Z bis A)</string>
<string name="sort_custom">Benutzerdefiniert</string>
<string name="new_group">Neue Gruppe…</string>
<string name="enter_group_name">Gib einen Gruppennamen ein</string>
<string name="group_name_hint">Gruppenname</string>
<string name="preference_manage_groups">Gruppen bearbeiten</string>
<string name="preference_manage_groups_summary">Verwalte und lösche deine Gruppen hier</string>
<string name="pref_search_name_title">In Kontonamen suchen</string>
<string name="pref_search_name_summary">Entsprechende Kontonamen in den Suchergebnissen miteinbeziehen</string>
<string name="tap_to_reveal">Versteckt</string>
<string name="selected">Ausgewählt</string>
<string name="dark_theme_title">Dunkles Thema</string>
<string name="light_theme_title">Helles Thema</string>
<string name="amoled_theme_title">Amoled Thema</string>
<string name="normal_viewmode_title">Normal</string>
<string name="compact_mode_title">Kompakt</string>
<string name="small_mode_title">Klein</string>
<string name="unknown_issuer">Unbekannter Herausgeber</string>
<string name="unknown_account_name">Unbekannter Kontoname</string>
<string name="import_error_dialog">Aegis konnte %d Tokens nicht importieren. Diese Tokens werden übersprungen. Drücke \'details\', um mehr Informationen über die Fehler zu erhalten.</string>
<string name="unable_to_read_qrcode">QR-Code kann nicht gelesen und verarbeitet werden.</string>
<string name="select_picture">Bild auswählen</string>
<string name="toggle_checkboxes">Markierungen umschalten</string>
<string name="search">Suche</string>
<string name="channel_name_lock_status">Sperrstatus</string>
<string name="channel_description_lock_status">Aegis kann eine dauerhafte Benachrichtigung erstellen, um dich zu benachrichtigen, wenn der Tresor gesperrt ist.</string>
<string name="vault_unlocked_state">Der Tresor ist freigeschaltet. Hier tippen, um zu sperren.</string>
<string name="title_activity_about">Über uns</string>
<string name="version">Version</string>
<string name="changelog">Liste der Änderungen</string>
<string name="whats_new">Was ist neu</string>
<string name="github_description">Quellcode, Probleme und Informationen</string>
<string name="licenses">Lizenzen</string>
<string name="licenses_description">Lizenzen der Bibliotheken, die Aegis verwendet</string>
<string name="country_netherlands">Niederlande</string>
<string name="email_us">Eine E-Mail schreiben</string>
<string name="visit_website">Besuchen Sie unsere Webseite</string>
<string name="about_support">Support</string>
<string name="support_rate">Bewerten</string>
<string name="support_rate_description">Unterstützen Sie uns, indem Sie eine Bewertung im Google Play Store hinterlassen</string>
<string name="webview_error">Dieses Gerät unterstützt keine Web-Ansicht, die notwendig ist, um das Changelog anzuzeigen. Es fehlt eine Systemkomponente.</string>
<string name="email">E-Mail</string>
<string name="empty_list">Es gibt keine zu zeigenden Codes. Fügen Sie Einträge hinzu, indem Sie das Plus-Zeichen in der unteren rechten Ecke antippen</string>
<string name="empty_list_title">Keine Einträge gefunden</string>
</resources>
+194
View File
@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">Configuración</string>
<string name="action_about">Acerca de</string>
<string name="action_import">Importar</string>
<string name="action_delete">Eliminar</string>
<string name="action_default_icon">Restaurar icono predeterminado</string>
<string name="discard">Descartar</string>
<string name="save">Guardar</string>
<string name="issuer">Nombre</string>
<string name="settings">Preferencias</string>
<string name="pref_appearance_group_title">Apariencia</string>
<string name="pref_general_group_title">General</string>
<string name="pref_security_group_title">Seguridad</string>
<string name="pref_tools_group_title">Herramientas</string>
<string name="pref_select_theme_title">Tema</string>
<string name="pref_view_mode_title">Modo de visualización</string>
<string name="pref_lang_title">Idioma</string>
<string name="pref_account_name_title">Mostrar el nombre de la cuenta</string>
<string name="pref_account_name_summary">Active esto para mostrar el nombre de la cuenta junto al nombre</string>
<string name="pref_timeout_title">Tiempo de espera</string>
<string name="pref_timeout_summary">Bloquear automáticamente la caja fuerte después de %1$s segundos de inactividad</string>
<string name="pref_slots_title">Claves</string>
<string name="pref_slots_summary">Administrar la lista de claves que puede descifrar la caja fuerte</string>
<string name="pref_import_file_title">Importar desde un archivo</string>
<string name="pref_import_file_summary">Importar tokens desde un archivo</string>
<string name="pref_import_app_title">Importar desde una aplicación</string>
<string name="pref_import_app_summary">Importar tokens desde una aplicación (requiere acceso root)</string>
<string name="pref_export_title">Exportar</string>
<string name="pref_export_summary">Exportar la caja fuerte</string>
<string name="pref_secure_screen_title">Seguridad en pantalla</string>
<string name="pref_secure_screen_summary">Bloquea las capturas de pantalla y otros intentos de captura dentro de la aplicación</string>
<string name="pref_tap_to_reveal_title">Pulsar para mostrar</string>
<string name="pref_tap_to_reveal_summary">Los tokens se ocultarán por defecto. Pulse sobre ellos para revelar el código.</string>
<string name="pref_tap_to_reveal_time_title">Tiempo de espera de pulsar para mostrar</string>
<string name="pref_auto_lock_title">Bloqueo automático</string>
<string name="pref_auto_lock_summary">Bloquear automáticamente cuando cierre la aplicación o bloquee su dispositivo.</string>
<string name="pref_encryption_title">Cifrado</string>
<string name="pref_encryption_summary">Cifra la base de datos y la desbloquea mediante una contraseña o huella biométrica</string>
<string name="pref_biometrics_title">Desbloqueo biométrico</string>
<string name="pref_biometrics_summary">Permite la autenticación biométrica para desbloquear la caja fuerte</string>
<string name="pref_set_password_title">Cambiar contraseña</string>
<string name="pref_set_password_summary">Establece una nueva contraseña que necesitará para desbloquear su caja fuerte</string>
<string name="choose_authentication_method">Seguridad</string>
<string name="authentication_method_none">Ninguno</string>
<string name="authentication_method_none_description">No necesitará una contraseña para desbloquear la caja fuerte y no se cifrará. No se recomienda esta opción.</string>
<string name="authentication_method_password">Contraseña</string>
<string name="authentication_method_biometrics">Biometría</string>
<string name="authentication_method_biometrics_description">Además de una contraseña, la biometría registrada en este dispositivo, como una huella digital o su cara, pueden ser usados para desbloquear la caja fuerte.</string>
<string name="authentication_method_set_password">Contraseña</string>
<string name="authentication_enter_password">Introduzca su contraseña</string>
<string name="authentication">Desbloquear la caja fuerte</string>
<string name="set_password">Por favor introduzca una contraseña</string>
<string name="set_group">Por favor introduzca un nombre de grupo</string>
<string name="set_number">Por favor introduzca un número</string>
<string name="set_password_confirm">Por favor confirme la contraseña</string>
<string name="invalidated_biometrics">Se ha detectado un cambio en la configuración de seguridad de su dispositivo. Por favor, vaya a \"Aegis -&gt; Configuración -&gt; Biometría\" y vuelva a activar el desbloqueo biométrico.</string>
<string name="unlock">Desbloquear</string>
<string name="biometrics">Biometría</string>
<string name="advanced">Avanzado</string>
<string name="seconds">segundos</string>
<string name="counter">Contador</string>
<string name="digits">Dígitos</string>
<string name="secret">Código secreto</string>
<string name="scan">Escanear código QR</string>
<string name="scan_image">Escanear imagen</string>
<string name="enter_manually">Introducir manualmente</string>
<string name="add_biometric">Añadir biometría</string>
<string name="add_biometric_slot">Añadir clave biométrica</string>
<string name="set_up_biometric">Configurar desbloqueo biométrico</string>
<string name="add_password">Añadir contraseña</string>
<string name="slots_warning">La caja fuerte es tan segura como su código secreto más débil. Si cambia la configuración de autenticación biométrica de su dispositivo, tendrá que reactivar el desbloqueo biométrico dentro de Aegis.</string>
<string name="copy">Copiar</string>
<string name="edit">Editar</string>
<string name="delete">Eliminar</string>
<string name="password">Contraseña</string>
<string name="confirm_password">Confirmar contraseña</string>
<string name="show_password">Mostrar contraseña</string>
<string name="new_profile">Nuevo perfil</string>
<string name="add_new_profile">Añadir nuevo perfil</string>
<string name="unlock_vault_error">No se pudo desbloquear la caja fuerte</string>
<string name="unlock_vault_error_description">Contraseña incorrecta. Asegúrese de no haberla introducido erróneamente.</string>
<string name="password_equality_error">Las contraseñas deben ser idénticas y no estar en blanco</string>
<string name="snackbar_authentication_method">Por favor seleccione un método de autentificación</string>
<string name="encrypting_vault">Cifrando la caja fuerte</string>
<string name="delete_entry">Eliminar entrada</string>
<string name="delete_entry_description">¿Está seguro de que desea eliminar esta entrada?</string>
<string name="discard_changes">¿Descartar cambios?</string>
<string name="discard_changes_description">Sus cambios no se han guardado</string>
<string name="folder">Carpeta</string>
<string name="tap_to_select">Pulsar para seleccionar</string>
<string name="saving_profile_error">Error al guardar el perfil</string>
<string name="welcome">Bienvenido</string>
<string name="app_description">Aegis es una aplicación de 2FA gratuita, segura y de código abierto</string>
<string name="setup_completed">Configuración completada</string>
<string name="setup_completed_description">Aegis se ha configurado y está listo para funcionar.</string>
<string name="vault_not_found">Caja fuerte no encontrada, iniciando la configuración…</string>
<string name="code_copied">Código copiado al portapapeles</string>
<string name="errors_copied">Errores copiados al portapapeles</string>
<string name="version_copied">Versión copiada al portapapeles</string>
<string name="decryption_error">Se ha producido un error tratando de desbloquear la caja fuerte</string>
<string name="saving_error">Se ha producido un error tratando de guardar la caja fuerte</string>
<string name="disable_encryption">Desactivar cifrado</string>
<string name="disable_encryption_description">¿Está seguro de que desea desactivar el cifrado? Esto provocará que la caja fuerte se almacene en texto plano.</string>
<string name="enable_encryption_error">Se ha producido un error tratando de activar el cifrado</string>
<string name="disable_encryption_error">Se ha producido un error tratando de desactivar el cifrado</string>
<string name="permission_denied">Permiso denegado</string>
<string name="andotp_new_format">Formato nuevo (v0.6.3 o superior) </string>
<string name="andotp_old_format">Formato antiguo (v0.6.2 o anterior) </string>
<string name="choose_andotp_importer">¿Qué formato tiene el archivo de copia de seguridad de andOTP?</string>
<string name="choose_application">Seleccione la aplicación desde la que desea importar</string>
<string name="choose_theme">Seleccione su tema deseado</string>
<string name="choose_view_mode">Seleccione su modo de visualización deseado</string>
<string name="parsing_file_error">Se ha producido un error procesando el archivo</string>
<string name="file_not_found">Error: Archivo no encontrado</string>
<string name="reading_file_error">Se ha producido un error tratando de leer el archivo</string>
<string name="app_lookup_error">Error: La aplicación no está instalada</string>
<string name="root_error">Error: No se puede obtener acceso root</string>
<string name="imported_entries_count">Importadas %d entradas</string>
<string name="read_entries_count">Leídas %d entradas. %d errores.</string>
<string name="import_error_title">Se han producido uno o más errores durante la importación</string>
<string name="exporting_vault_error">Se ha producido un error tratando de exportar la caja fuerte</string>
<string name="export_warning">Esta acción exportará la caja fuerte fuera del almacenamiento privado de Aegis.</string>
<string name="encryption_set_password_error">Se ha producido un error tratando de establecer la contraseña: </string>
<string name="encryption_enable_biometrics_error">Se ha producido un error tratando de activar el desbloqueo biométrico</string>
<string name="no_cameras_available">No hay ninguna cámara disponible</string>
<string name="read_qr_error">Se ha producido un error tratando de leer el código QR</string>
<string name="authentication_method_raw">Raw</string>
<string name="unlocking_vault">Desbloqueando la caja fuerte</string>
<string name="unlocking_vault_repair">Desbloqueando la caja fuerte (reparando)</string>
<string name="password_slot_error">Debe tener al menos una contraseña</string>
<string name="remove_slot">Eliminar clave</string>
<string name="remove_slot_description">¿Está seguro de que desea eliminar esta clave?</string>
<string name="remove_group">Eliminar grupo</string>
<string name="remove_group_description">¿Está seguro de que desea eliminar este grupo? Las entradas se moverán automáticamente a \'Sin grupo\'.</string>
<string name="adding_new_slot_error">Se ha producido un error tratando de añadir una nueva clave:</string>
<string name="progressbar_error">No se puede restablecer la escala de duración de la animación. Las barras de progreso serán invisibles.</string>
<string name="details">Detalles</string>
<string name="filter">Filtrar</string>
<string name="filter_ungrouped">Sin agrupar</string>
<string name="lock">Bloquear</string>
<string name="all">Todo</string>
<string name="name">Cuenta</string>
<string name="no_group">Sin grupo</string>
<string name="sort_alphabetically">Nombre (A a Z)</string>
<string name="sort_alphabetically_reverse">Nombre (Z a A)</string>
<string name="sort_alphabetically_name">Cuenta (A a Z)</string>
<string name="sort_alphabetically_name_reverse">Cuenta (Z a A)</string>
<string name="sort_custom">Personalizado</string>
<string name="new_group">Nuevo grupo…</string>
<string name="enter_group_name">Introduzca un nombre de grupo</string>
<string name="group_name_hint">Nombre del grupo</string>
<string name="preference_manage_groups">Editar grupos</string>
<string name="preference_manage_groups_summary">Gestione y elimine sus grupos aquí</string>
<string name="pref_search_name_title">Buscar en los nombres de cuenta</string>
<string name="pref_search_name_summary">Incluir coincidencias del nombre de la cuenta en los resultados de búsqueda</string>
<string name="pref_highlight_entry_title">Resaltar tokens al pulsarlos</string>
<string name="pref_highlight_entry_summary">Hace que los tokens sean más fáciles de distinguir entre ellos resaltándolos temporalmente tras ser pulsados</string>
<string name="tap_to_reveal">Oculto</string>
<string name="selected">Seleccionado</string>
<string name="dark_theme_title">Tema oscuro</string>
<string name="light_theme_title">Tema claro</string>
<string name="amoled_theme_title">Tema Amoled</string>
<string name="normal_viewmode_title">Normal</string>
<string name="compact_mode_title">Compacto</string>
<string name="small_mode_title">Pequeño</string>
<string name="unknown_issuer">Nombre desconocido</string>
<string name="unknown_account_name">Nombre de cuenta desconocido</string>
<string name="import_error_dialog">Aegis no pudo importar %d tokens. Estos tokens se omitirán. Pulse \'detalles\' para ver más información sobre los errores.</string>
<string name="unable_to_read_qrcode">No se puede leer y procesar el código QR</string>
<string name="select_picture">Seleccionar imagen</string>
<string name="toggle_checkboxes">Activar casillas de verificación</string>
<string name="search">Buscar</string>
<string name="channel_name_lock_status">Estado de bloqueo</string>
<string name="channel_description_lock_status">Aegis puede crear una notificación persistente para notificar cuando la caja fuerte está bloqueada</string>
<string name="vault_unlocked_state">La caja fuerte está desbloqueada. Pulse aquí para bloquear.</string>
<string name="title_activity_about">Acerca de</string>
<string name="version">Versión</string>
<string name="changelog">Historial de cambios</string>
<string name="whats_new">Novedades</string>
<string name="github_description">Código fuente, problemas e información</string>
<string name="licenses">Licencias</string>
<string name="licenses_description">Licencias de las librerías usadas por Aegis</string>
<string name="country_netherlands">Países Bajos</string>
<string name="email_us">Escríbanos un correo electrónico</string>
<string name="visit_website">Visite nuestra página web</string>
<string name="about_support">Soporte</string>
<string name="support_rate">Valorar</string>
<string name="support_rate_description">Ayúdenos dejando una reseña en Google Play Store</string>
<string name="webview_error">Este dispositivo no soporta web view, lo cual es necesario para visualizar el registro de cambios. Falta un componente del sistema.</string>
<string name="email">Correo electrónico</string>
<string name="empty_list">No hay códigos que mostrar. Comience añadiendo entradas pulsando el signo + en la esquina inferior derecha</string>
<string name="empty_list_title">No hay entradas</string>
</resources>
@@ -1,20 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aegis</string>
<string name="app_name_dev">AegisDev</string>
<string name="action_settings">Paramètres</string>
<string name="action_about">À propos</string>
<string name="action_import">Importer</string>
<string name="action_delete">Supprimer</string>
<string name="action_default_icon">Restaurer l\'icône par défaut</string>
<string name="discard">Annuler</string>
<string name="save">Enregistrer</string>
<string name="issuer">Émetteur</string>
<string name="settings">Préférences</string>
<string name="pref_appearance_group_title">Apparence</string>
<string name="pref_general_group_title">Général</string>
<string name="pref_security_group_title">Sécurité</string>
<string name="pref_tools_group_title">Outils</string>
<string name="pref_select_theme_title">Thème</string>
<string name="pref_view_mode_title">Mode d\'affichage</string>
<string name="pref_lang_title">Langue</string>
<string name="pref_account_name_title">Afficher le nom du compte</string>
<string name="pref_account_name_summary">Activer pour afficher le nom de compte près de l\'émetteur</string>
<string name="pref_timeout_title">Délai</string>
@@ -36,22 +37,13 @@
<string name="pref_auto_lock_summary">Verrouiller automatiquement lorsque vous fermez l\'application ou verrouillez votre appareil.</string>
<string name="pref_encryption_title">Chiffrement</string>
<string name="pref_encryption_summary">Chiffrer la base de données et la déverrouiller avec un mot de passe ou une empreinte digitale</string>
<string name="pref_fingerprint_title">Empreinte digitale</string>
<string name="pref_fingerprint_summary">Autoriser les empreintes digitales enregistrées sur cet appareil à déverrouiller le coffre-fort</string>
<string name="pref_set_password_title">Changer le mot de passe</string>
<string name="pref_set_password_summary">Définir un mot de passe dont vous aurez besoin pour déverrouiller votre coffre-fort</string>
<string name="fingerprint_hint">Appuyez sur le capteur</string>
<string name="fingerprint_not_recognized">Empreinte digitale non reconnue. Réessayez.</string>
<string name="fingerprint_success">Empreinte digitale reconnue</string>
<string name="choose_authentication_method">Sécurité</string>
<string name="authentication_method_none">Aucun</string>
<string name="authentication_method_none_description">Vous n\'avez pas besoin d\'un mot de passe pour déverrouiller le coffre-fort et il ne sera pas chiffré. Cette option n\'est pas recommandée.</string>
<string name="authentication_method_password">Mot de passe</string>
<string name="authentication_method_password_description">Vous avez besoin d\'un mot de passe pour déverrouiller le coffre-fort.</string>
<string name="authentication_method_fingerprint">Empreinte digitale</string>
<string name="authentication_method_fingerprint_description">En plus d\'un mot de passe, les empreintes digitales enregistrées sur cet appareil peuvent être utilisées pour déverrouiller le coffre-fort.</string>
<string name="authentication_method_set_password">Mot de passe</string>
<string name="authentication_enter_password">Saisissez votre mot de passe</string>
<string name="authentication">Déverrouiller le coffre-fort</string>
@@ -59,8 +51,6 @@
<string name="set_group">Veuillez saisir un nom de groupe</string>
<string name="set_number">Veuillez saisir un nombre</string>
<string name="set_password_confirm">Veuillez confirmer le mot de passe</string>
<string name="invalidated_fingerprint">Un changement dans les paramètres de sécurité de votre appareil a été détecté. Veuillez aller dans \"Paramètres -> Empreinte digitale\" et ajouter à nouveau votre empreinte digitale.</string>
<string name="unlock">Déverrouiller</string>
<string name="advanced">Avancé</string>
<string name="seconds">secondes</string>
@@ -70,7 +60,6 @@
<string name="scan">Scanner code QR</string>
<string name="scan_image">Scanner image</string>
<string name="enter_manually">Saisir manuellement</string>
<string name="add_fingerprint">Ajouter une empreinte digitale</string>
<string name="add_password">Ajouter un mot de passe</string>
<string name="slots_warning">Le coffre-fort est seulement aussi sécurisé que votre code secret le plus faible. Quand une nouvelle empreinte digitale est ajoutée à votre appareil, vous devrez réactiver l\'authentification par empreinte digitale dans Aegis.</string>
<string name="copy">Copier</string>
@@ -83,7 +72,6 @@
<string name="unlock_vault_error">Impossible de déverrouiller le coffre-fort</string>
<string name="unlock_vault_error_description">Mot de passe incorrect. Assurez-vous de ne pas avoir fait de faute de frappe lors de la saisie de votre mot de passe.</string>
<string name="password_equality_error">Les mots de passe doivent être identiques et non vides</string>
<string name="register_fingerprint">Enregistrer votre empreinte digitale</string>
<string name="snackbar_authentication_method">Veuillez sélectionner une méthode d\'authentification</string>
<string name="encrypting_vault">Chiffrement du coffre-fort</string>
<string name="delete_entry">Supprimer entrée</string>
@@ -100,6 +88,7 @@
<string name="vault_not_found">Coffre-fort introuvable, lancement de la configuration…</string>
<string name="code_copied">Code copié dans le presse-papier</string>
<string name="errors_copied">Erreurs copiées dans le presse-papier</string>
<string name="version_copied">Version copiée dans le presse-papier</string>
<string name="decryption_error">Une erreur est survenue en essayant de déverrouiller le coffre-fort</string>
<string name="saving_error">Une erreur est survenue en essayant d\'enregistrer le coffre-fort</string>
<string name="disable_encryption">Désactiver le chiffrement</string>
@@ -118,15 +107,13 @@
<string name="imported_entries_count">%d entrées importées</string>
<string name="read_entries_count">%d entrées lues. %d erreurs.</string>
<string name="import_error_title">Une ou plusieurs erreurs sont survenues lors de l\'importation</string>
<string name="exporting_database_error">Une erreur est survenue en essayant d\'exporter la base de données</string>
<string name="export_database_location">La base de données a été exportée vers :</string>
<string name="export_warning">Cette action va exporter la base de données en dehors du stockage privé d\'Aegis.</string>
<string name="encryption_set_password_error">Une erreur est survenue en essayant de définir le mot de passe : </string>
<string name="encryption_enable_fingerprint_error">Une erreur est survenue en essayant d\'activer le déverrouillage par empreinte digitale : </string>
<string name="no_cameras_available">Aucun appareil photo disponible</string>
<string name="read_qr_error">Une erreur est survenue en essayant de lire le code QR</string>
<string name="authentication_method_raw">Brut</string>
<string name="unlocking_vault">Déverrouillage du coffre-fort</string>
<string name="unlocking_vault_repair">Déverrouillage du coffre-fort (réparation)</string>
<string name="password_slot_error">Vous devez avoir au moins un emplacement de mot de passe</string>
<string name="remove_slot">Supprimer emplacement</string>
<string name="remove_slot_description">Êtes-vous sûr de vouloir supprimer cet emplacement ?</string>
@@ -150,6 +137,8 @@
<string name="group_name_hint">Nom de groupe</string>
<string name="preference_manage_groups">Modifier les groupes</string>
<string name="preference_manage_groups_summary">Gérez et supprimez vos groupes ici</string>
<string name="pref_search_name_title">Rechercher dans les noms de comptes</string>
<string name="pref_search_name_summary">Inclure les correspondances de noms de comptes dans les résultats de recherche</string>
<string name="tap_to_reveal">Masqué</string>
<string name="selected">Sélectionné</string>
<string name="dark_theme_title">Thème sombre</string>
@@ -164,4 +153,25 @@
<string name="unable_to_read_qrcode">Impossible de lire et traiter le code QR</string>
<string name="select_picture">Sélectionner image</string>
<string name="toggle_checkboxes">Basculer les cases</string>
<string name="search">Recherche</string>
<string name="channel_name_lock_status">Statut du verrouillage</string>
<string name="channel_description_lock_status">Aegis peut créer une notification persistante pour vous notifier quand le coffre-fort est verrouillé</string>
<string name="vault_unlocked_state">Le coffre-fort est déverrouillé. Appuyer ici pour verrouiller.</string>
<string name="title_activity_about">À propos</string>
<string name="version">Version</string>
<string name="changelog">Historique des modifications</string>
<string name="whats_new">Les nouveautés</string>
<string name="github_description">Code source, problèmes et informations</string>
<string name="licenses">Licences</string>
<string name="licenses_description">Licences des bibliothèques utilisées par Aegis</string>
<string name="country_netherlands">Pays-Bas</string>
<string name="email_us">Écrire un email</string>
<string name="visit_website">Visiter notre site internet</string>
<string name="about_support">Soutien</string>
<string name="support_rate">Noter</string>
<string name="support_rate_description">Soutenez-nous en laissant un avis sur le Google Play Store</string>
<string name="webview_error">Cet appareil ne supporte pas web view, qui est nécessaire pour voir l\'historique des modifications. C\'est un composant système manquant.</string>
<string name="email">Email</string>
<string name="empty_list">Il n\'y a pas de code à afficher. Commencez à ajouter des entrées en appuyant sur le signe plus dans le coin inférieur droit</string>
<string name="empty_list_title">Aucune entrée trouvée</string>
</resources>
+191
View File
@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">Impostazioni</string>
<string name="action_about">Informazioni su</string>
<string name="action_import">Importare</string>
<string name="action_delete">Elimina</string>
<string name="action_default_icon">Ripristina icona predefinita</string>
<string name="discard">Annulla</string>
<string name="save">Salva</string>
<string name="issuer">Login</string>
<string name="settings">Impostazioni</string>
<string name="pref_appearance_group_title">Aspetto</string>
<string name="pref_general_group_title">Generale</string>
<string name="pref_security_group_title">Sicurezza</string>
<string name="pref_tools_group_title">Strumenti</string>
<string name="pref_select_theme_title">Tema</string>
<string name="pref_view_mode_title">Visualizzazione</string>
<string name="pref_lang_title">Seleziona Lingua</string>
<string name="pref_account_name_title">Mostra nome account</string>
<string name="pref_account_name_summary">Se attivo mostra il nome dell\'account accanto al login</string>
<string name="pref_timeout_title">Timeout</string>
<string name="pref_timeout_summary">Blocca automaticamente la cassaforte dopo %1$s secondi di inattività</string>
<string name="pref_slots_title">Slot chiavi</string>
<string name="pref_slots_summary">Gestisci la lista delle chiavi che possono decriptare il database</string>
<string name="pref_import_file_title">Importa da file</string>
<string name="pref_import_file_summary">Importa token da file</string>
<string name="pref_import_app_title">Importa da un\'app</string>
<string name="pref_import_app_summary">Importa token da app (richiede accesso root)</string>
<string name="pref_export_title">Esporta</string>
<string name="pref_export_summary">Esporta la cassaforte</string>
<string name="pref_secure_screen_title">Sicurezza schermo</string>
<string name="pref_secure_screen_summary">Blocca i tentativi di catturare lo schermo all\'interno dell\'app</string>
<string name="pref_tap_to_reveal_title">Tocca per mostrare</string>
<string name="pref_tap_to_reveal_summary">I token saranno nascosti. Toccandone uno viene mostrato il codice.</string>
<string name="pref_tap_to_reveal_time_title">Timeout visualizzazione codice OTP</string>
<string name="pref_auto_lock_title">Blocco automatico</string>
<string name="pref_auto_lock_summary">Blocca automaticamente quando chiudi l\'app o blocca il dispositivo.</string>
<string name="pref_encryption_title">Crittografia</string>
<string name="pref_encryption_summary">Cripta il database e sbloccalo con password o impronta digitale</string>
<string name="pref_biometrics_title">Sblocco biometrico</string>
<string name="pref_biometrics_summary">Consenti l\'autenticazione biometrica per sbloccare la cassaforte</string>
<string name="pref_set_password_title">Cambia password</string>
<string name="pref_set_password_summary">Modifica la password della tua cassaforte</string>
<string name="choose_authentication_method">Sicurezza</string>
<string name="authentication_method_none">Nessuna</string>
<string name="authentication_method_password">Password</string>
<string name="authentication_method_biometrics">Biometrica</string>
<string name="authentication_method_biometrics_description">Oltre a una password, i dati biometrici registrati su questo dispositivo, come un\'impronta digitale o il tuo volto, possono essere utilizzati per sbloccare la cassaforte.</string>
<string name="authentication_method_set_password">Password</string>
<string name="authentication_enter_password">Inserisci la tua password</string>
<string name="authentication">Sblocca la cassaforte</string>
<string name="set_password">Inserisci una password</string>
<string name="set_group">Inserisci il nome di un gruppo</string>
<string name="set_number">Inserisci un numero</string>
<string name="set_password_confirm">Conferma la tua password</string>
<string name="invalidated_biometrics">È stata rilevata una modifica delle impostazioni di sicurezza del dispositivo. Vai a \"Aegis -&gt; Impostazioni -&gt; Sicurezza\" e riattiva lo sblocco biometrico.</string>
<string name="unlock">Sblocca</string>
<string name="biometrics">Biometrica</string>
<string name="advanced">Avanzate</string>
<string name="seconds">secondi</string>
<string name="counter">Contatore</string>
<string name="digits">Cifre</string>
<string name="secret">Segreto</string>
<string name="scan">Scansiona codice QR</string>
<string name="scan_image">Scansiona immagine</string>
<string name="enter_manually">Inserisci manualmente</string>
<string name="add_biometric">Aggiungi dati biometrici</string>
<string name="set_up_biometric">Imposta sblocco biometrico</string>
<string name="add_password">Aggiungi password</string>
<string name="copy">Copia</string>
<string name="edit">Modifica</string>
<string name="delete">Elimina</string>
<string name="password">Password</string>
<string name="confirm_password">Conferma password</string>
<string name="show_password">Mostra password</string>
<string name="new_profile">Nuovo profilo</string>
<string name="add_new_profile">Aggiungi nuovo profilo</string>
<string name="unlock_vault_error">Impossibile sbloccare la cassaforte</string>
<string name="unlock_vault_error_description">Password errata. Assicurati di non aver sbagliato a digitare.</string>
<string name="password_equality_error">Le password devono essere identiche e non vuote</string>
<string name="snackbar_authentication_method">Seleziona un metodo di autenticazione</string>
<string name="encrypting_vault">Criptando la cassaforte</string>
<string name="delete_entry">Elimina elemento</string>
<string name="delete_entry_description">Sei sicuro di voler eliminare questa voce?</string>
<string name="discard_changes">Annullare le modifiche?</string>
<string name="discard_changes_description">Le modifiche non sono state salvate</string>
<string name="folder">Cartella</string>
<string name="tap_to_select">Tocca per selezionare</string>
<string name="saving_profile_error">Errore nel salvataggio del profilo</string>
<string name="welcome">Benvenuto</string>
<string name="app_description">Aegis è un\'app 2FA gratuita, sicura e open source</string>
<string name="setup_completed">Configurazione completata</string>
<string name="setup_completed_description">Aegis è stata configurata, sei pronto a partire.</string>
<string name="vault_not_found">Cassaforte non trovata, avvio configurazione…</string>
<string name="code_copied">Codice copiato negli appunti</string>
<string name="errors_copied">Codice errore copiato negli appunti</string>
<string name="version_copied">Versione copiata negli appunti</string>
<string name="decryption_error">Si è verificato un errore nello sblocco della cassaforte</string>
<string name="saving_error">Si è verificato un errore durante il salvataggio della cassaforte</string>
<string name="disable_encryption">Disabilita crittografia</string>
<string name="disable_encryption_description">Sei sicuro di voler disabilitare la crittografia? La cassaforte sarà salvata come testo in chiaro.</string>
<string name="enable_encryption_error">Errore durante l\'attivazione della crittografia</string>
<string name="disable_encryption_error">Errore durante la disabilitazione della crittografia</string>
<string name="permission_denied">Autorizzazione negata</string>
<string name="andotp_new_format">Nuovo formato (v0.6.3 o più recente) </string>
<string name="andotp_old_format">Vecchio formato (v0.6.2 o più vecchio) </string>
<string name="choose_andotp_importer">Quale formato ha il file di backup andOTP?</string>
<string name="choose_application">Seleziona l\'applicazione da cui vuoi importare un database</string>
<string name="choose_theme">Seleziona il tema</string>
<string name="choose_view_mode">Seleziona la modalità di visualizzazione</string>
<string name="parsing_file_error">Errore durante l\'analisi del file</string>
<string name="file_not_found">Errore: File non trovato</string>
<string name="reading_file_error">Errore di lettura del file</string>
<string name="app_lookup_error">Errore: App non installata</string>
<string name="root_error">Errore: impossibile ottenere permessi root</string>
<string name="imported_entries_count">Importati %d elementi</string>
<string name="read_entries_count">Letti %d elementi. %d errori.</string>
<string name="import_error_title">Errori durante l\'importazione</string>
<string name="exporting_vault_error">Si è verificato un errore durante il tentativo di esportare la cassaforte</string>
<string name="export_warning">Questa azione esporterà la cassaforte dall\'archivio privato di Aegis.</string>
<string name="encryption_set_password_error">Errore durante l\'impostazione della password: </string>
<string name="encryption_enable_biometrics_error">Si è verificato un errore durante il tentativo di abilitare lo sblocco biometrico</string>
<string name="no_cameras_available">Nessuna fotocamera disponibile</string>
<string name="read_qr_error">Errore di lettura del codice QR</string>
<string name="authentication_method_raw">Raw</string>
<string name="unlocking_vault">Sblocco della cassaforte</string>
<string name="unlocking_vault_repair">Sblocco della cassaforte (riparazione)</string>
<string name="password_slot_error">Devi avere almeno uno slot password</string>
<string name="remove_slot">Rimuovi slot</string>
<string name="remove_slot_description">Sei sicuro di voler rimuovere questo slot?</string>
<string name="remove_group">Rimuovi gruppo</string>
<string name="remove_group_description">Sei sicuro di voler rimuovere questo gruppo? Gli elementi in questo gruppo verranno automaticamente spostati in \'Nessun gruppo\'.</string>
<string name="adding_new_slot_error">Errore nell\'aggiunta di un nuovo slot:</string>
<string name="progressbar_error">Impossibile ripristinare la durata delle animazioni. Le barre di caricamento non saranno visibili.</string>
<string name="details">Dettagli</string>
<string name="filter">Filtra</string>
<string name="filter_ungrouped">Non raggruppati</string>
<string name="lock">Blocca</string>
<string name="all">Tutti</string>
<string name="name">Nome</string>
<string name="no_group">Nessun gruppo</string>
<string name="sort_alphabetically">Login (A-Z)</string>
<string name="sort_alphabetically_reverse">Login (Z-A)</string>
<string name="sort_alphabetically_name">Account (A-Z)</string>
<string name="sort_alphabetically_name_reverse">Account (Z-A)</string>
<string name="sort_custom">Personalizza</string>
<string name="new_group">Nuovo gruppo...</string>
<string name="enter_group_name">Inserisci un nome di gruppo</string>
<string name="group_name_hint">Nome gruppo</string>
<string name="preference_manage_groups">Modifica Gruppi</string>
<string name="preference_manage_groups_summary">Gestisci ed elimina i tuoi gruppi qui</string>
<string name="pref_search_name_title">Cerca nei nomi degli account</string>
<string name="pref_search_name_summary">Includi il nome dell\'account nei risultati di ricerca</string>
<string name="pref_highlight_entry_title">Evidenzia i token quando premuti</string>
<string name="pref_highlight_entry_summary">Rendi i token più facili da distinguere, evidenziandoli temporaneamente quando vengono toccati</string>
<string name="tap_to_reveal">Nascosto</string>
<string name="selected">Selezionato</string>
<string name="dark_theme_title">Tema scuro</string>
<string name="light_theme_title">Tema chiaro</string>
<string name="amoled_theme_title">Tema Amoled (nero)</string>
<string name="normal_viewmode_title">Normale</string>
<string name="compact_mode_title">Compatto</string>
<string name="small_mode_title">Piccolo</string>
<string name="unknown_issuer">Login sconosciuto</string>
<string name="unknown_account_name">Nome account sconosciuto</string>
<string name="import_error_dialog">Aegis non può importare %d token. Questi token verranno saltati. Premi \'dettagli\' per vedere ulteriori informazioni sugli errori.</string>
<string name="unable_to_read_qrcode">Impossibile verificare il codice QR</string>
<string name="select_picture">Seleziona immagine</string>
<string name="toggle_checkboxes">Spunta</string>
<string name="search">Cerca</string>
<string name="channel_name_lock_status">Stato blocco</string>
<string name="channel_description_lock_status">Aegis può mostrare una notifica persistente quando la cassaforte è sbloccata</string>
<string name="vault_unlocked_state">Cassaforte sbloccata. Tocca qui per bloccare.</string>
<string name="title_activity_about">Informazioni su</string>
<string name="version">Versione</string>
<string name="changelog">Changelog</string>
<string name="whats_new">Novità</string>
<string name="github_description">Codice, problemi e informazioni</string>
<string name="licenses">Licenze</string>
<string name="licenses_description">Licenze delle librerie usate da Aegis</string>
<string name="country_netherlands">Olanda</string>
<string name="email_us">Scrivici una email</string>
<string name="visit_website">Visita il nostro sito web</string>
<string name="about_support">Supporto</string>
<string name="support_rate">Vota</string>
<string name="support_rate_description">Supportaci lasciando una recensione nel Google Play Store</string>
<string name="webview_error">Questo dispositivo non supporta la visualizzazione web, necessaria per visualizzare il changelog. Manca un componente di sistema.</string>
<string name="email">Email</string>
<string name="empty_list">Non ci sono codici. Aggiungine uno premendo il \'+\' in basso a destra</string>
<string name="empty_list_title">Nessun elemento trovato</string>
</resources>

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