Compare commits
367
Commits
v1.0.2
...
v1.4-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
735c086726 | ||
|
|
d94be2420c | ||
|
|
0b6da5a93c | ||
|
|
d24e1471a7 | ||
|
|
7ca072a29c | ||
|
|
d5ac447332 | ||
|
|
0067c79dfa | ||
|
|
4e198d2556 | ||
|
|
f15a0018ef | ||
|
|
d121139676 | ||
|
|
98b246ad20 | ||
|
|
bce0d2c044 | ||
|
|
bf91917745 | ||
|
|
a91339de73 | ||
|
|
5d9de7ac32 | ||
|
|
5f2529ea33 | ||
|
|
61364b5542 | ||
|
|
69196ddbcc | ||
|
|
b21b6dc750 | ||
|
|
e14a61dbf6 | ||
|
|
e8f94968c9 | ||
|
|
8b7e5d265b | ||
|
|
22a70ac626 | ||
|
|
dd83b74251 | ||
|
|
5a9da45a8e | ||
|
|
cac13b082e | ||
|
|
86c3c401ae | ||
|
|
33ff7da7af | ||
|
|
0af48f8bfa | ||
|
|
01885e1fc6 | ||
|
|
52d2713482 | ||
|
|
d875cb6baa | ||
|
|
0cdb882991 | ||
|
|
31d382d6ad | ||
|
|
141f491ca7 | ||
|
|
c0bbe50996 | ||
|
|
875c752f78 | ||
|
|
1d00a2d8d6 | ||
|
|
14407412bf | ||
|
|
bb2716f640 | ||
|
|
0f6b0dddbe | ||
|
|
d43e5e04c7 | ||
|
|
4c86e154c1 | ||
|
|
a45c834b9c | ||
|
|
b746ca0c6e | ||
|
|
7d38bc9b71 | ||
|
|
9557b60c93 | ||
|
|
227b5fd87c | ||
|
|
7d063104dd | ||
|
|
c03d00695d | ||
|
|
171c774b21 | ||
|
|
97944aac88 | ||
|
|
ca21e0bdd1 | ||
|
|
83c1914ddc | ||
|
|
d660fbc6d1 | ||
|
|
35b5ce9d17 | ||
|
|
762fb9bccd | ||
|
|
c3d38c18a3 | ||
|
|
7a3ad11902 | ||
|
|
28fcd68b72 | ||
|
|
c2ed4b4849 | ||
|
|
9bbc2886c2 | ||
|
|
5886462d2c | ||
|
|
98e8a0d97e | ||
|
|
0368a992de | ||
|
|
233eeb6aa1 | ||
|
|
e5d162be9b | ||
|
|
718d575a59 | ||
|
|
59e8c23a01 | ||
|
|
847009d733 | ||
|
|
ce3d674da8 | ||
|
|
9e4313e138 | ||
|
|
02e01723b2 | ||
|
|
874f5e9058 | ||
|
|
7518408554 | ||
|
|
6d68625ee2 | ||
|
|
0e78fd9652 | ||
|
|
9d44d6abb2 | ||
|
|
42c308bc8f | ||
|
|
20fb531f91 | ||
|
|
b8b3da4f85 | ||
|
|
7e403c2af6 | ||
|
|
be0529a0c8 | ||
|
|
9c5f136f5b | ||
|
|
8c34ce8f38 | ||
|
|
3cdf31f940 | ||
|
|
7059f3be74 | ||
|
|
0c0e8c0d66 | ||
|
|
06fd77b5f0 | ||
|
|
9121922ef2 | ||
|
|
2347a322b5 | ||
|
|
fcd3623eef | ||
|
|
121ba06f5a | ||
|
|
6611aa851c | ||
|
|
c0b8b2c7ee | ||
|
|
34888767ed | ||
|
|
61f2e6e020 | ||
|
|
ba3e6203ec | ||
|
|
906d1b453a | ||
|
|
a9ccf412e7 | ||
|
|
21b588dc8b | ||
|
|
90e5003418 | ||
|
|
f8cf87aae9 | ||
|
|
a77b308896 | ||
|
|
a25dbc5cdd | ||
|
|
2aa88a2921 | ||
|
|
7fedb5ab83 | ||
|
|
0eb9a25687 | ||
|
|
330e88c270 | ||
|
|
407d059076 | ||
|
|
de3d0d8515 | ||
|
|
57ef52d6ff | ||
|
|
189bd0d749 | ||
|
|
4ef8d9e826 | ||
|
|
7bb8d28b3b | ||
|
|
94ebc08097 | ||
|
|
a3e14c57e4 | ||
|
|
b55bdf8f04 | ||
|
|
3e0a97dbe0 | ||
|
|
9b13a56002 | ||
|
|
32c145b02d | ||
|
|
25b359a136 | ||
|
|
0eb338a0bf | ||
|
|
b59a29cff4 | ||
|
|
c252e77c5b | ||
|
|
44ca712186 | ||
|
|
93cc945a2c | ||
|
|
68d6c39dd4 | ||
|
|
3a5a185940 | ||
|
|
3d351ece05 | ||
|
|
c65ed16790 | ||
|
|
7c147befcb | ||
|
|
c73c29d2ec | ||
|
|
df0c6b5fa8 | ||
|
|
2ae7ec33f1 | ||
|
|
65beeffa93 | ||
|
|
b5fa6192b3 | ||
|
|
721a3d2d27 | ||
|
|
bf9173301c | ||
|
|
e2cf6a40cb | ||
|
|
db681273e6 | ||
|
|
291fd5427b | ||
|
|
362f1da11f | ||
|
|
cc33c63501 | ||
|
|
6e54497492 | ||
|
|
9fde453ead | ||
|
|
7cd48f1c09 | ||
|
|
4c3d277fce | ||
|
|
0111626d80 | ||
|
|
0b83546db7 | ||
|
|
08ab8237e7 | ||
|
|
39ecfba3e4 | ||
|
|
ae5502b650 | ||
|
|
821d0b5147 | ||
|
|
b4ed1f35ce | ||
|
|
a468342187 | ||
|
|
4f6035dc49 | ||
|
|
506adcf81f | ||
|
|
49c8c2d57d | ||
|
|
2ab698c2d3 | ||
|
|
770f5f9724 | ||
|
|
992dcd651b | ||
|
|
98a38b03e4 | ||
|
|
626995ec91 | ||
|
|
00b0c8deaf | ||
|
|
8e5b51c42f | ||
|
|
b81ec0f073 | ||
|
|
d49d42254b | ||
|
|
95d5b4afa1 | ||
|
|
cca8c9bdf7 | ||
|
|
4e00191039 | ||
|
|
dd09595866 | ||
|
|
b27bfc3fa9 | ||
|
|
c6a9716d16 | ||
|
|
acd7a54ee9 | ||
|
|
e7d240549e | ||
|
|
70fedc53b9 | ||
|
|
bed8c9172d | ||
|
|
ef1ac3e5e9 | ||
|
|
cc163f952f | ||
|
|
95180ba963 | ||
|
|
56d7f0ef7b | ||
|
|
fbd6a5931e | ||
|
|
66dd3561cf | ||
|
|
58db3fdaa2 | ||
|
|
92b6738ff4 | ||
|
|
63f57667ba | ||
|
|
34718a7b89 | ||
|
|
6ac91e6cf3 | ||
|
|
a39d6ab346 | ||
|
|
91d89670d2 | ||
|
|
6a2992530e | ||
|
|
1f839a886d | ||
|
|
91f9a6756a | ||
|
|
89137c0873 | ||
|
|
ffa31c7aa3 | ||
|
|
e12bfdbe58 | ||
|
|
3c887d8392 | ||
|
|
b4d2b6b113 | ||
|
|
fe40c4b952 | ||
|
|
9319c74932 | ||
|
|
4c60b04057 | ||
|
|
1a24f62074 | ||
|
|
1824d38f75 | ||
|
|
e41f200bcd | ||
|
|
ccc990f854 | ||
|
|
56bde0e19b | ||
|
|
6b650e777f | ||
|
|
49a7fda932 | ||
|
|
8ff817856e | ||
|
|
3a85a05078 | ||
|
|
cda78c56c5 | ||
|
|
499f930086 | ||
|
|
393b815409 | ||
|
|
984010dc7a | ||
|
|
0bbc413423 | ||
|
|
98a90e3e1e | ||
|
|
3ccbece822 | ||
|
|
0f81233d4f | ||
|
|
f6b1f02d11 | ||
|
|
301476ff5c | ||
|
|
b84f44bf1e | ||
|
|
842afe6745 | ||
|
|
bc5fa68de8 | ||
|
|
fec2f3ff94 | ||
|
|
77718809ef | ||
|
|
9b328893f7 | ||
|
|
45378b8128 | ||
|
|
b830c6db1b | ||
|
|
7773a51f2a | ||
|
|
f68badb659 | ||
|
|
4ea8501fa9 | ||
|
|
1f86697d2d | ||
|
|
faf24e15bd | ||
|
|
592d482a9b | ||
|
|
ad05e375f1 | ||
|
|
8b30d451a4 | ||
|
|
abe78370ea | ||
|
|
00e2e90aa7 | ||
|
|
f37d70b58e | ||
|
|
09065c705b | ||
|
|
ad58960831 | ||
|
|
7ce74d378f | ||
|
|
8c0ba2dd1b | ||
|
|
016c64610e | ||
|
|
c578543e18 | ||
|
|
d247750e62 | ||
|
|
b8896ec1df | ||
|
|
4a69e9efb4 | ||
|
|
b43bac37db | ||
|
|
0c7b41747e | ||
|
|
8fda6937a5 | ||
|
|
0a4a933a2a | ||
|
|
866466d158 | ||
|
|
d89d5eac9c | ||
|
|
6885463b11 | ||
|
|
a273fdaa1d | ||
|
|
ec60e98b2e | ||
|
|
dc0f7c4668 | ||
|
|
ba7fe15a57 | ||
|
|
3d51b84188 | ||
|
|
f66cebb727 | ||
|
|
9e355c8e52 | ||
|
|
9ce0982191 | ||
|
|
f543802f9e | ||
|
|
32aecadf75 | ||
|
|
d39e3d7b90 | ||
|
|
bd66659b92 | ||
|
|
1a7a794dc9 | ||
|
|
2519633265 | ||
|
|
e38121efee | ||
|
|
261b036f75 | ||
|
|
7e857b53a7 | ||
|
|
73e1d72403 | ||
|
|
5731cccddc | ||
|
|
48d22fc06d | ||
|
|
964ed294a5 | ||
|
|
2a4bb902df | ||
|
|
d3463b82bc | ||
|
|
ef536a0c07 | ||
|
|
7e7c5317b1 | ||
|
|
2bf3546971 | ||
|
|
cd42b0dc65 | ||
|
|
4ba3caeaf4 | ||
|
|
10ac1af6b0 | ||
|
|
833e75ade1 | ||
|
|
7bbd80ed21 | ||
|
|
dbedc9d1d3 | ||
|
|
3bdcc25ebe | ||
|
|
d0108e9859 | ||
|
|
025c89d78c | ||
|
|
c52e60b410 | ||
|
|
3cc644193f | ||
|
|
8020792024 | ||
|
|
837a27f9d7 | ||
|
|
5c71f6254b | ||
|
|
bbea6c2ae4 | ||
|
|
fa799e9542 | ||
|
|
7f1ce1e645 | ||
|
|
dc6bf3795a | ||
|
|
a3538f95e2 | ||
|
|
85c358043d | ||
|
|
ab8f01284a | ||
|
|
97d824d779 | ||
|
|
eedf9d2935 | ||
|
|
1ede56e137 | ||
|
|
f8c106bcb9 | ||
|
|
10c206270a | ||
|
|
f7a0ee7174 | ||
|
|
f5e8ba96e6 | ||
|
|
3506fdaad9 | ||
|
|
8824b779f5 | ||
|
|
4cab92d615 | ||
|
|
35a14d3198 | ||
|
|
e46041ece0 | ||
|
|
28ebaf48c8 | ||
|
|
fc96e7919a | ||
|
|
08b80d2e72 | ||
|
|
051cf9370b | ||
|
|
3d13296890 | ||
|
|
93f32c2903 | ||
|
|
f168768033 | ||
|
|
5ab4307963 | ||
|
|
d0baeef064 | ||
|
|
44b83c888c | ||
|
|
da45fa1969 | ||
|
|
5ad3a10251 | ||
|
|
9ec94a472f | ||
|
|
4e4c39eea4 | ||
|
|
3be9aecb88 | ||
|
|
a6a5af781e | ||
|
|
a93ced6e34 | ||
|
|
82135c8b9d | ||
|
|
9f248e0802 | ||
|
|
65aa95504b | ||
|
|
def7c67614 | ||
|
|
be3f30e666 | ||
|
|
b920fe4b8c | ||
|
|
4b6b10526d | ||
|
|
74b2f8f398 | ||
|
|
34ef4b3fb4 | ||
|
|
63b0f6bb99 | ||
|
|
36973669ea | ||
|
|
2e301c5389 | ||
|
|
6a4b0f7bc5 | ||
|
|
8b063815ab | ||
|
|
75b79d3d4a | ||
|
|
b61b73685f | ||
|
|
864e3bd6f4 | ||
|
|
013e4ae28b | ||
|
|
51a0a16afb | ||
|
|
a5165e2345 | ||
|
|
c41a337791 | ||
|
|
e58dec35c0 | ||
|
|
5e7697039a | ||
|
|
45de9cea3a | ||
|
|
9f075e7bc6 | ||
|
|
a88d1cf11e | ||
|
|
a6eec88dcb | ||
|
|
3c4afe36e8 | ||
|
|
5b71df37ec | ||
|
|
d7d3d52a30 | ||
|
|
ac222c4946 | ||
|
|
0200287203 | ||
|
|
5677bd4112 | ||
|
|
cbe3697d68 | ||
|
|
445410fcd7 |
@@ -0,0 +1 @@
|
||||
custom: ['https://www.buymeacoffee.com/beemdevelopment']
|
||||
@@ -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
|
||||
|
||||
@@ -39,3 +39,4 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
+23
-6
@@ -1,7 +1,24 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- android-28
|
||||
- build-tools-28.0.3
|
||||
language: shell
|
||||
os: linux
|
||||
dist: bionic
|
||||
env:
|
||||
global:
|
||||
- ANDROID_HOME=${HOME}/android
|
||||
- ANDROID_SDK_ROOT=${ANDROID_HOME}
|
||||
- TOOLS=${ANDROID_HOME}/cmdline-tools
|
||||
- PATH=${ANDROID_HOME}/emulator:${TOOLS}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
||||
before_install:
|
||||
- mkdir -p ${ANDROID_HOME}/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
install:
|
||||
- mkdir -p ${TOOLS}
|
||||
- travis_retry wget https://dl.google.com/android/repository/commandlinetools-linux-6514223_latest.zip -O ${TOOLS}/tools.zip
|
||||
- unzip ${TOOLS}/tools.zip -d ${TOOLS}
|
||||
- yes | travis_retry sdkmanager "platform-tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "build-tools;30.0.1" > /dev/null
|
||||
- yes | travis_retry sdkmanager "platforms;android-30" > /dev/null
|
||||
script:
|
||||
- ./gradlew build test
|
||||
- ./gradlew assemble check
|
||||
|
||||
+26
-11
@@ -1,25 +1,40 @@
|
||||
# Contributing
|
||||
|
||||
There are a couple of ways to contribute to Aegis. This document contains some
|
||||
general guidelines for each type of contribution.
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. This document contains some general guidelines for each type of
|
||||
contribution.
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
|
||||
translations of Aegis for lots of different languages. __Pull requests that
|
||||
add/update a translation are not longer accepted.__ Crowdin is our single source
|
||||
of truth for translations, to keep things easy to maintain.
|
||||
|
||||
The top 30 languages are available for translation. It's possible that yours is
|
||||
not in that list. If that's the case, feel free to send us an email and we'll
|
||||
add it.
|
||||
|
||||
## Pull requests
|
||||
|
||||
If you're planning on adding a new feature or making other large changes, please
|
||||
discuss it with us first through a proposal on GitHub. Discussing your idea with
|
||||
us first ensures that everyone is on the same page before you start working on
|
||||
your change. We don't like rejecting pull requests.
|
||||
discuss it with us first through [a
|
||||
proposal](https://github.com/beemdevelopment/Aegis/issues/new?labels=proposal&template=feature.md)
|
||||
on GitHub. Discussing your idea with us first ensures that everyone is on the
|
||||
same page before you start working on your change. We don't like rejecting pull
|
||||
requests.
|
||||
|
||||
## Bug reports
|
||||
|
||||
We use GitHub's issue tracker to track bugs. Bug reports must follow [the
|
||||
We use GitHub's issue tracker to track bugs. To make bug reports easier to
|
||||
follow up on for us, they must follow [the
|
||||
template](.github/ISSUE_TEMPLATE/bug.md). If a bug report does not follow the
|
||||
template and does not contain enough information, it will be closed without a
|
||||
response. Duplicate bug reports receive the same treatment.
|
||||
template and does not contain enough information, it will be closed. Duplicate
|
||||
bug reports receive the same treatment.
|
||||
|
||||
Please consider trying to find the root cause yourself first and perhaps even
|
||||
send us a patch that fixes the issue! We're happy to help you if you get stuck
|
||||
along the way.
|
||||
Please consider trying to find the root cause yourself first and include your
|
||||
analysis of the issue in your report. Perhaps even send us a patch that fixes
|
||||
it! We're happy to help you if you get stuck along the way.
|
||||
|
||||
### Capturing a log with ADB
|
||||
|
||||
|
||||
@@ -1,58 +1,65 @@
|
||||
# Aegis
|
||||
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
|
||||
alt="App icon">
|
||||
|
||||
Aegis is a free, secure and open source 2FA app for Android.
|
||||
# Aegis Authenticator [](https://travis-ci.org/github/beemdevelopment/aegis) [](https://crowdin.com/project/aegis-authenticator) [](https://www.buymeacoffee.com/beemdevelopment) [](https://matrix.to/#/#aegis:matrix.org)
|
||||
|
||||
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
|
||||
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
|
||||
It aims to provide a secure authenticator for your online services, while also
|
||||
including some features missing in existing authenticator apps, like proper
|
||||
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
|
||||
thousands of services.
|
||||
|
||||
# Features
|
||||
The security design of the app and the vault format is described in detail in
|
||||
[this document](docs/vault.md).
|
||||
|
||||
## Features
|
||||
|
||||
- Free and open source
|
||||
- Secure
|
||||
- Encryption (AES-256)
|
||||
- The vault is encrypted (AES-256-GCM), and can be unlocked with:
|
||||
- Password (scrypt)
|
||||
- Fingerprint (Android Keystore)
|
||||
- Biometrics (Android Keystore)
|
||||
- Screen capture prevention
|
||||
- Tap to reveal ability
|
||||
- Multiple ways to add new entries
|
||||
- Scan QR code
|
||||
- Enter details manually
|
||||
- Import from files
|
||||
- andOTP
|
||||
- FreeOTP
|
||||
- Aegis
|
||||
- Import from apps (requires root):
|
||||
- Google Authenticator
|
||||
- Steam
|
||||
- Supported algorithms:
|
||||
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
|
||||
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
|
||||
- Steam ([RFC 6238](https://tools.ietf.org/html/rfc6238) with custom
|
||||
encoding)
|
||||
- Tap to reveal
|
||||
- Compatible with Google Authenticator
|
||||
- Supports industry standard algorithms:
|
||||
[HOTP](https://tools.ietf.org/html/rfc4226) and
|
||||
[TOTP](https://tools.ietf.org/html/rfc6238)
|
||||
- Lots of ways to add new entries
|
||||
- Scan a QR code or an image of one
|
||||
- Enter details manually
|
||||
- Import from other authenticator apps: Authenticator Plus, Authy, andOTP,
|
||||
FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Plain
|
||||
text, Steam, TOTP Authenticator and WinAuth (root access is required for
|
||||
some of these)
|
||||
- Organization
|
||||
- Custom or default icons
|
||||
- Drag and drop
|
||||
- Custom groups
|
||||
- Alphabetic/custom sorting
|
||||
- Custom or automatically generated icons
|
||||
- Group entries together
|
||||
- Advanced entry editing
|
||||
- Material design with multiple themes:
|
||||
- Light theme
|
||||
- Dark theme
|
||||
- Amoled / true dark theme
|
||||
- Search by name/issuer
|
||||
- Material design with multiple themes: Light, Dark, AMOLED
|
||||
- Export (plaintext or encrypted)
|
||||
- Automatic backups of the vault to a location of your choosing
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<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,25 +103,27 @@ 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
|
||||
```
|
||||
|
||||
## Libraries
|
||||
### Community
|
||||
|
||||
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
|
||||
- [FloatingActionButton](https://github.com/Clans/FloatingActionButton) by
|
||||
Dmytro Tarianyk
|
||||
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
|
||||
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
|
||||
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
|
||||
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
|
||||
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
|
||||
Dodenhof
|
||||
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
|
||||
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
|
||||
- [aegis-icons](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
While we're still [working on better icon
|
||||
support](https://github.com/beemdevelopment/Aegis/issues/509) in Aegis, krisu5
|
||||
has started a third-party repository for icons that you may want to check out.
|
||||
|
||||
[<img width=500 alt="Aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
## Contributing
|
||||
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. Translations, bug reports and pull requests are all greatly appreciated.
|
||||
Please refer to our [contributing guidelines](CONTRIBUTING.md) to get started.
|
||||
|
||||
Swing by our Matrix room to interact with other contributors:
|
||||
[#aegis:matrix.org](https://matrix.to/#/#aegis:matrix.org).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0. See the
|
||||
[LICENSE](LICENSE) file for details.
|
||||
|
||||
## Attribution
|
||||
|
||||
The icon is largely based on a design by Freepik from Flaticon.
|
||||
|
||||
+127
-37
@@ -1,14 +1,37 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine cmd
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
def packageName = "com.beemdevelopment.aegis"
|
||||
def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 21
|
||||
versionName "1.0.2"
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 42
|
||||
versionName "1.4-beta2"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -17,6 +40,8 @@ android {
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
@@ -29,56 +54,121 @@ android {
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
|
||||
manifestPlaceholders = [
|
||||
title: "AegisDev",
|
||||
iconName: "ic_launcher_debug",
|
||||
fileProviderAuthority: "${fileProviderAuthorityDebug}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode false
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
|
||||
manifestPlaceholders = [
|
||||
title: "Aegis",
|
||||
iconName: "ic_launcher",
|
||||
fileProviderAuthority: "${fileProviderAuthority}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode true
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def libsuVersion = '2.3.2'
|
||||
def cameraxVersion = '1.0.0-beta11'
|
||||
def glideVersion = '4.11.0'
|
||||
def guavaVersion = '30.0'
|
||||
def junitVersion = '5.7.0'
|
||||
def libsuVersion = '2.5.1'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.2.0"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.2'
|
||||
implementation 'com.github.avito-tech:krop: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 "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.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.13.0'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.3.0'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.4'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
|
||||
}
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
|
||||
androidTestImplementation 'junit:junit:4.13.1'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.3.0'
|
||||
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
</lint>
|
||||
Vendored
+7
@@ -15,3 +15,10 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public abstract class AegisTest {
|
||||
public static final String VAULT_PASSWORD = "test";
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
|
||||
}
|
||||
|
||||
protected VaultManager getVault() {
|
||||
return getApp().getVaultManager();
|
||||
}
|
||||
|
||||
protected VaultManager initVault() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SCryptParameters scryptParams = new SCryptParameters(
|
||||
CryptoUtils.CRYPTO_SCRYPT_N,
|
||||
CryptoUtils.CRYPTO_SCRYPT_r,
|
||||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
try {
|
||||
SecretKey key = slot.deriveKey(VAULT_PASSWORD.toCharArray(), scryptParams);
|
||||
slot.setKey(creds.getKey(), CryptoUtils.createEncryptCipher(key));
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| NoSuchPaddingException
|
||||
| SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
|
||||
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
|
||||
try {
|
||||
vault.save(false);
|
||||
} catch (VaultManagerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
getApp().getPreferences().setIntroDone(true);
|
||||
return vault;
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
byte[] secret = CryptoUtils.generateRandomBytes(20);
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
info = type.getConstructor(byte[].class).newInstance(secret);
|
||||
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/a/30338665
|
||||
protected static ViewAction clickChildViewWithId(final int id) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Click on a child view with specified id.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View v = view.findViewById(id);
|
||||
v.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
public class AegisTestRunner extends AndroidJUnitRunner {
|
||||
@Override
|
||||
public void callApplicationOnCreate(Application app) {
|
||||
Context context = app.getApplicationContext();
|
||||
|
||||
// clear internal storage so that there is no vault file
|
||||
IOUtils.clearDirectory(context.getFilesDir(), false);
|
||||
|
||||
// clear preferences so that the intro is started from MainActivity
|
||||
ApplicationProvider.getApplicationContext().getFilesDir();
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply();
|
||||
|
||||
super.callApplicationOnCreate(app);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntentTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent() {
|
||||
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
launch(info.getUri());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
|
||||
VaultEntry createdEntry = (VaultEntry) getVault().getEntries().toArray()[0];
|
||||
assertTrue(createdEntry.equivalates(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Empty() {
|
||||
launch(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Bad() {
|
||||
launch(Uri.parse("otpauth://bad"));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void launch(Uri uri) {
|
||||
Intent intent = new Intent(getApp(), MainActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
|
||||
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
|
||||
ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
|
||||
rule.launchActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntroTest extends AegisTest {
|
||||
@Rule
|
||||
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
|
||||
|
||||
@Test
|
||||
public void doIntro_None() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_none)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(getVault().getCredentials());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doIntro_Password() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD + "1"), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password_confirm)).perform(replaceText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
prev.perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
SlotList slots = getVault().getCredentials().getSlots();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(slots.has(PasswordSlot.class));
|
||||
assertFalse(slots.has(BiometricSlot.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.AmbiguousViewMatcherException;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class OverallTest extends AegisTest {
|
||||
private static final String _groupName = "Test";
|
||||
|
||||
@Rule
|
||||
public final ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
|
||||
|
||||
@Test
|
||||
public void doOverallTest() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
|
||||
List<VaultEntry> entries = Arrays.asList(
|
||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||
generateEntry(SteamInfo.class, "Gaben", "Steam")
|
||||
);
|
||||
for (VaultEntry entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.spinner_group)).perform(click());
|
||||
onData(anything()).atPosition(1).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
changeSort(R.string.sort_alphabetically_name);
|
||||
changeSort(R.string.sort_alphabetically_name_reverse);
|
||||
changeSort(R.string.sort_alphabetically);
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeFilter(_groupName);
|
||||
changeFilter(R.string.filter_ungrouped);
|
||||
changeFilter(R.string.all);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.lock)).perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.button_decrypt)).perform(click());
|
||||
vault = getVault();
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
}
|
||||
|
||||
private void changeSort(@IdRes int resId) {
|
||||
onView(withId(R.id.action_sort)).perform(click());
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(String text) {
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.filter)).perform(click());
|
||||
onView(withText(text)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(@IdRes int resId) {
|
||||
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab_expand_menu_button)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
|
||||
try {
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
} catch (AmbiguousViewMatcherException e) {
|
||||
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
}
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
|
||||
}
|
||||
if (entry.getInfo() instanceof SteamInfo) {
|
||||
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
|
||||
}
|
||||
}
|
||||
|
||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,14 @@
|
||||
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"
|
||||
@@ -17,16 +22,17 @@
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about"></activity>
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="Select entries" />
|
||||
android:label="@string/title_activity_select_entries" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="${title}"
|
||||
android:launchMode="singleTask">
|
||||
android:label="${title}">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -44,24 +50,51 @@
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="Scan a QR code"
|
||||
android:label="@string/title_activity_scan_qr"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="Edit profile" />
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro" />
|
||||
<activity android:name=".ui.AuthActivity" />
|
||||
<activity android:name=".ui.PreferencesActivity" />
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.AuthActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="Manage key slots" />
|
||||
android:label="@string/title_activity_manage_slots" />
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="Manage groups" />
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.ExitActivity" />
|
||||
|
||||
<service android:name=".services.NotificationService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${fileProviderAuthority}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,65 +1,205 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
* {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
background-color: #%1$s;
|
||||
color: #%2$s;
|
||||
}
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding: 0;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
list-style-position: outside;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 1.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>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
* {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
background-color: %1$s;
|
||||
color: %2$s;
|
||||
}
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding: 0;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
list-style-position: outside;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 1.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
|
||||
<li>More customizable auto-lock</li>
|
||||
<li>More flexible export options
|
||||
<ul>
|
||||
<li>Share mechanism</li>
|
||||
<li>Offer to encrypt even if this feature is disabled in the app</li>
|
||||
<li>Export to a Google Authenticator URI file</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
|
||||
<li>Color improvements to the dark theme (slightly darker)</li>
|
||||
<li>Offer more locations to select an image/icon from</li>
|
||||
<li>Display some helpful information when importing from a different app</li>
|
||||
<li>Minimum tap to reveal timeout changed to 1 second</li>
|
||||
<li>After an entry is added, scroll to it and highlight it</li>
|
||||
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning large images for QR codes would fail</li>
|
||||
<li>The FAB would remain hidden under certain circumstances</li>
|
||||
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
|
||||
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
|
||||
<li>The PIN keyboard would show even if a new non-digit password was set</li>
|
||||
</ul>
|
||||
<h3>Version 1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Completely rewritten intro/onboarding</li>
|
||||
<li>Option to show a PIN keyboard when unlocking Aegis</li>
|
||||
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
|
||||
<li>RTL support</li>
|
||||
<li>Arabic and Portuguese translations</li>
|
||||
<li>Updates to existing translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Better lifecycle handling of the biometric authentication prompt</li>
|
||||
<li>The filename of exported vaults had a double .json extension</li>
|
||||
<li>The navigation bar color was incorrect on devices pre API 27</li>
|
||||
<li>QR code scanner performance and stability improvements</li>
|
||||
<li>Various other small usability and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.2.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix a rare issue where the intro could end up in a bad state</li>
|
||||
</ul>
|
||||
<h3>Version 1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Add navigation bar color to themes</li>
|
||||
<li>Add support for importing from TOTP Authenticator</li>
|
||||
<li>Add support for importing from Microsoft Authenticator</li>
|
||||
<li>Add support for importing from Authenticator Plus</li>
|
||||
<li>Add support for importing a plain text Google Authenticator URI file</li>
|
||||
<li>Add support for importing from the new Google Authenticator export QR codes</li>
|
||||
<li>Add support for otpauth://steam URI's</li>
|
||||
<li>Add an option to copy tokens on tap (and disable it by default)</li>
|
||||
<li>Improve method to notify users on copy</li>
|
||||
<li>Add support for backups</li>
|
||||
<li>Improve multiselect flow</li>
|
||||
<li>Automatically adapt to system theme</li>
|
||||
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
|
||||
<li>Use most frequent period to show progress</li>
|
||||
<li>Append a timestamp to the filename of exported vaults</li>
|
||||
<li>Add Hungarian translation</li>
|
||||
<li>Add Turkish translation</li>
|
||||
<li>Display a warning if automatic time sync is not enabled</li>
|
||||
<li>Minor card entry layout overhaul</li>
|
||||
<li>Ability to transfer tokens with qr codes</li>
|
||||
<li>Lockscreen overhaul</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Improve overall exception handling and error feedback to the user</li>
|
||||
<li>Improve icon editing flow</li>
|
||||
<li>Protect writes of the vault file against corruption with AtomicFile</li>
|
||||
<li>Make the parsing logic of the QR code URI more robust</li>
|
||||
<li>Importing from Authy now asks for password if needed</li>
|
||||
<li>Update Russian localization</li>
|
||||
<li>Increase password reminder period to 30 days</li>
|
||||
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
|
||||
<li>Respect the global animator duration scale setting</li>
|
||||
</ul>
|
||||
<p>Various other minor improvements</p>
|
||||
<h3>Version 1.1.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The export filename was missing the ".json" extension in some cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Password reminder for users who use biometric unlock</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Tokens would not refresh in some rare cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to select multiple entries</li>
|
||||
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
|
||||
<li>Explanation and warning for the security options</li>
|
||||
<li>Removed external storage permissions</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Exporting the vault did not work on Android 10</li>
|
||||
</ul>
|
||||
<h3>Version 1.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
|
||||
<li>Support for importing from WinAuth</li>
|
||||
<li>Support for Chromebooks</li>
|
||||
<li>Option to highlight entries when tapped</li>
|
||||
<li>Filter for ungrouped tokens</li>
|
||||
<li>Ability to search for token account names</li>
|
||||
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
|
||||
<li>Updated translations (thanks to all Crowdin contributers!)</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The behavior of highlighting and revealing entries was inconsistent</li>
|
||||
<li>The changelog dialog didn't work</li>
|
||||
<li>The persistent notification was shown even after the app was killed</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for andOTP's new backup file format</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Disabled automatic backups through the Google Play Store</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.1</h3>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Temporarily disabled search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h3>Version 1.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>New icon</li>
|
||||
<li>Overhaul of interaction with the entry list</li>
|
||||
<li>Persistent notification while the vault is unlocked</li>
|
||||
<li>Language override option</li>
|
||||
<li>Support for importing from FreeOTP+</li>
|
||||
<li>Ability to toggle password visibility during unlock</li>
|
||||
<li>Support for deeplinking otpauth URIs</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Bad overall performance and high battery usage</li>
|
||||
<li>Codes with an uneven number of digits are displayed incorrectly</li>
|
||||
<li>Crash when entering a large value for OTP period</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,38 +12,59 @@ import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class AegisApplication extends Application {
|
||||
private DatabaseManager _manager;
|
||||
private VaultFile _vaultFile;
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_DATABASE_ACTION = "lock_database";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_manager = new DatabaseManager(this);
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
// listen for SCREEN_OFF events
|
||||
ScreenOffReceiver receiver = new ScreenOffReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
intentFilter.addAction(CODE_LOCK_DATABASE_ACTION);
|
||||
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
|
||||
registerReceiver(receiver, intentFilter);
|
||||
|
||||
// lock the app if the user moves the application to the background
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
|
||||
|
||||
// clear the cache directory on startup, to make sure no temporary vault export files remain
|
||||
IOUtils.clearDirectory(getCacheDir(), false);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
initAppShortcuts();
|
||||
}
|
||||
@@ -53,7 +74,48 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
public DatabaseManager getDatabaseManager() {
|
||||
public boolean isVaultLocked() {
|
||||
return _manager == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the vault file from disk at the default location, stores an internal
|
||||
* reference to it for future use and returns it. This must only be called before
|
||||
* initVaultManager() or after lock().
|
||||
*/
|
||||
public VaultFile loadVaultFile() throws VaultManagerException {
|
||||
if (!isVaultLocked()) {
|
||||
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
|
||||
}
|
||||
|
||||
if (_vaultFile == null) {
|
||||
_vaultFile = VaultManager.readFile(this);
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager by decrypting the given vaultFile with the given
|
||||
* creds. This removes the internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
|
||||
_vaultFile = null;
|
||||
_manager = VaultManager.init(this, vaultFile, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager with the given vault and creds. This removes the
|
||||
* internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
|
||||
_vaultFile = null;
|
||||
_manager = new VaultManager(this, vault, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public VaultManager getVaultManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
@@ -61,8 +123,8 @@ public class AegisApplication extends Application {
|
||||
return _prefs;
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
|
||||
public boolean isAutoLockEnabled(int autoLockType) {
|
||||
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
@@ -73,10 +135,16 @@ public class AegisApplication extends Application {
|
||||
_lockListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
_manager.lock();
|
||||
/**
|
||||
* Locks the vault and the app.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
public void lock(boolean userInitiated) {
|
||||
_manager.destroy();
|
||||
_manager = null;
|
||||
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked();
|
||||
listener.onLocked(userInitiated);
|
||||
}
|
||||
|
||||
stopService(new Intent(AegisApplication.this, NotificationService.class));
|
||||
@@ -91,11 +159,12 @@ public class AegisApplication extends Application {
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("action", "scan");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
|
||||
.setShortLabel(getString(R.string.new_profile))
|
||||
.setLongLabel(getString(R.string.add_new_profile))
|
||||
.setShortLabel(getString(R.string.new_entry))
|
||||
.setLongLabel(getString(R.string.add_new_entry))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
@@ -117,16 +186,29 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
public class ScreenOffReceiver extends BroadcastReceiver {
|
||||
private class AppLifecycleObserver implements LifecycleEventObserver {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_STOP && isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScreenOffReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (isAutoLockEnabled()) {
|
||||
lock();
|
||||
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface LockListener {
|
||||
void onLocked();
|
||||
/**
|
||||
* When called, the app/vault has been locked and the listener should perform its cleanup operations.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
void onLocked(boolean userInitiated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum CancelAction {
|
||||
KILL,
|
||||
CLOSE
|
||||
}
|
||||
|
||||
@@ -3,37 +3,110 @@ package com.beemdevelopment.aegis;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Preferences {
|
||||
public static final int AUTO_LOCK_OFF = 1 << 0;
|
||||
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
|
||||
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
|
||||
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
|
||||
|
||||
public static final int[] AUTO_LOCK_SETTINGS = {
|
||||
AUTO_LOCK_ON_BACK_BUTTON,
|
||||
AUTO_LOCK_ON_MINIMIZE,
|
||||
AUTO_LOCK_ON_DEVICE_LOCK
|
||||
};
|
||||
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
if (getPasswordReminderTimestamp().getTime() == 0) {
|
||||
resetPasswordReminderTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTapToRevealEnabled() {
|
||||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isSearchAccountNameEnabled() {
|
||||
return _prefs.getBoolean("pref_search_names", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isPanicTriggerEnabled() {
|
||||
return _prefs.getBoolean("pref_panic_trigger", false);
|
||||
}
|
||||
|
||||
public boolean isSecureScreenEnabled() {
|
||||
// screen security should be enabled by default, but not for debug builds
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderEnabled() {
|
||||
return _prefs.getBoolean("pref_password_reminder", true);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderNeeded() {
|
||||
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
|
||||
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
|
||||
return isPasswordReminderEnabled() && days >= 30;
|
||||
}
|
||||
|
||||
public Date getPasswordReminderTimestamp() {
|
||||
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
|
||||
}
|
||||
|
||||
public void resetPasswordReminderTimestamp() {
|
||||
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
|
||||
}
|
||||
|
||||
public boolean isAccountNameVisible() {
|
||||
return _prefs.getBoolean("pref_account_name", false);
|
||||
return _prefs.getBoolean("pref_account_name", true);
|
||||
}
|
||||
|
||||
public int getCodeGroupSize() {
|
||||
if (_prefs.getBoolean("pref_code_group_size", false)) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
|
||||
private int getAutoLockMask() {
|
||||
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
|
||||
if (!_prefs.contains("pref_auto_lock_mask")) {
|
||||
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
return _prefs.getInt("pref_auto_lock_mask", def);
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.getBoolean("pref_auto_lock", true);
|
||||
return getAutoLockMask() != AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
public boolean isAutoLockTypeEnabled(int autoLockType) {
|
||||
return (getAutoLockMask() & autoLockType) == autoLockType;
|
||||
}
|
||||
|
||||
public void setAutoLockMask(int autoLock) {
|
||||
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
|
||||
}
|
||||
|
||||
public void setIntroDone(boolean done) {
|
||||
@@ -57,7 +130,7 @@ public class Preferences {
|
||||
}
|
||||
|
||||
public Theme getCurrentTheme() {
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
|
||||
}
|
||||
|
||||
public void setCurrentTheme(Theme theme) {
|
||||
@@ -87,6 +160,64 @@ public class Preferences {
|
||||
}
|
||||
}
|
||||
|
||||
return new Locale(lang);
|
||||
String[] parts = lang.split("_");
|
||||
if (parts.length == 1) {
|
||||
return new Locale(parts[0]);
|
||||
}
|
||||
|
||||
return new Locale(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
public void setIsBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public Uri getBackupsLocation() {
|
||||
String str = _prefs.getString("pref_backups_location", null);
|
||||
if (str != null) {
|
||||
return Uri.parse(str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBackupsLocation(Uri location) {
|
||||
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
|
||||
}
|
||||
|
||||
public int getBackupsVersionCount() {
|
||||
return _prefs.getInt("pref_backups_versions", 5);
|
||||
}
|
||||
|
||||
public void setBackupsVersionCount(int versions) {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
|
||||
public void setBackupsError(Exception e) {
|
||||
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
|
||||
}
|
||||
|
||||
public String getBackupsError() {
|
||||
return _prefs.getString("pref_backups_error", null);
|
||||
}
|
||||
|
||||
public boolean isPinKeyboardEnabled() {
|
||||
return _prefs.getBoolean("pref_pin_keyboard", false);
|
||||
}
|
||||
|
||||
public boolean isTimeSyncWarningEnabled() {
|
||||
return _prefs.getBoolean("pref_warn_time_sync", true);
|
||||
}
|
||||
|
||||
public void setIsTimeSyncWarningEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isCopyOnTapEnabled() {
|
||||
return _prefs.getBoolean("pref_copy_on_tap", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.beemdevelopment.aegis;
|
||||
public enum Theme {
|
||||
LIGHT,
|
||||
DARK,
|
||||
AMOLED;
|
||||
AMOLED,
|
||||
SYSTEM,
|
||||
SYSTEM_AMOLED;
|
||||
|
||||
private static Theme[] _values;
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeMap {
|
||||
private ThemeMap() {
|
||||
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme,
|
||||
Theme.DARK, R.style.AppTheme_Dark,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme_Light_NoActionBar,
|
||||
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
|
||||
byte[] nonce = Hex.decode(obj.getString("nonce"));
|
||||
byte[] tag = Hex.decode(obj.getString("tag"));
|
||||
return new CryptParameters(nonce, tag);
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import org.spongycastle.crypto.generators.SCrypt;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -12,6 +13,7 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
@@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class KeyStoreHandle {
|
||||
private final KeyStore _keyStore;
|
||||
private static final String STORE_NAME = "AndroidKeyStore";
|
||||
@@ -61,6 +61,14 @@ public class KeyStoreHandle {
|
||||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
@@ -88,18 +96,14 @@ public class KeyStoreHandle {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher
|
||||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
|
||||
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
|
||||
// so instead, check for it here
|
||||
if (e instanceof KeyPermanentlyInvalidatedException) {
|
||||
return true;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
|
||||
|
||||
public MasterKey(SecretKey key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException();
|
||||
throw new IllegalArgumentException("Key cannot be null");
|
||||
}
|
||||
_key = key;
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA2048() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA4096() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +1,21 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// modified for use in Aegis
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
/* (PD) 2001 The Bitzi Corporation
|
||||
* Please see http://bitzi.com/publicdomain for more info.
|
||||
*
|
||||
* As modified by Patrick Woodworth:
|
||||
*
|
||||
* Copyright 2011 Patrick Woodworth
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Base32 - encodes and decodes RFC3548 Base32
|
||||
* (see http://www.faqs.org/rfcs/rfc3548.html )
|
||||
*
|
||||
* @author Robert Kaye
|
||||
* @author Gordon Mohr
|
||||
*/
|
||||
public class Base32 {
|
||||
private static final String base32Chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
private static final int[] base32Lookup =
|
||||
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
|
||||
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
|
||||
};
|
||||
private Base32() {
|
||||
|
||||
/**
|
||||
* Encodes byte array to Base32 String.
|
||||
*
|
||||
* @param bytes Bytes to encode.
|
||||
* @return Encoded byte array <code>bytes</code> as a String.
|
||||
*
|
||||
*/
|
||||
public static char[] encode(final byte[] bytes) {
|
||||
int i = 0, index = 0, digit = 0, j = 0;
|
||||
int currByte, nextByte;
|
||||
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
|
||||
|
||||
while (i < bytes.length) {
|
||||
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
|
||||
|
||||
/* Is the current digit going to span a byte boundary? */
|
||||
if (index > 3) {
|
||||
if ((i + 1) < bytes.length) {
|
||||
nextByte =
|
||||
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
|
||||
} else {
|
||||
nextByte = 0;
|
||||
}
|
||||
|
||||
digit = currByte & (0xFF >> index);
|
||||
index = (index + 5) % 8;
|
||||
digit <<= index;
|
||||
digit |= nextByte >> (8 - index);
|
||||
i++;
|
||||
} else {
|
||||
digit = (currByte >> (8 - (index + 5))) & 0x1F;
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0)
|
||||
i++;
|
||||
}
|
||||
base32[j++] = base32Chars.charAt(digit);
|
||||
}
|
||||
|
||||
return Arrays.copyOf(base32, j);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given Base32 String to a raw byte array.
|
||||
*
|
||||
* @param base32
|
||||
* @return Decoded <code>base32</code> String as a raw byte array.
|
||||
*/
|
||||
public static byte[] decode(final char[] base32) throws Base32Exception {
|
||||
int i, index, lookup, offset, digit;
|
||||
byte[] bytes = new byte[base32.length * 5 / 8];
|
||||
|
||||
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
|
||||
// stop decoding when a padding char is encountered
|
||||
if (base32[i] == '=') {
|
||||
// make sure the rest is also padding, but don't bother verifying the length
|
||||
for (int j = i + 1; j < base32.length; j++) {
|
||||
if (base32[j] != '=') {
|
||||
throw new Base32Exception("bad padding");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
lookup = base32[i] - '0';
|
||||
digit = decodeDigit(lookup);
|
||||
|
||||
if (index <= 3) {
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0) {
|
||||
bytes[offset] |= digit;
|
||||
offset++;
|
||||
if (offset >= bytes.length)
|
||||
break;
|
||||
} else {
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
} else {
|
||||
index = (index + 5) % 8;
|
||||
bytes[offset] |= (digit >>> index);
|
||||
offset++;
|
||||
|
||||
if (offset >= bytes.length) {
|
||||
break;
|
||||
}
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base32().decode(s.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int decodeDigit(int c) throws Base32Exception {
|
||||
/* Skip chars outside the lookup table */
|
||||
if (c < 0 || c >= base32Lookup.length) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
int digit = base32Lookup[c];
|
||||
|
||||
/* If this digit is not in the table, ignore it */
|
||||
if (digit == 0xFF) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
return digit;
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base32().omitPadding().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base32Exception extends Exception {
|
||||
public Base32Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class Base64 {
|
||||
private static final int _flags = android.util.Base64.NO_WRAP;
|
||||
|
||||
private Base64() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws Base64Exception {
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return android.util.Base64.decode(s, _flags);
|
||||
return BaseEncoding.base64().decode(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new Base64Exception(e);
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] s) throws EncodingException {
|
||||
return decode(new String(s, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
byte[] encoded = android.util.Base64.encode(data, _flags);
|
||||
return new String(encoded, StandardCharsets.UTF_8);
|
||||
return BaseEncoding.base64().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base64Exception extends Exception {
|
||||
public Base64Exception(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EncodingException extends IOException {
|
||||
public EncodingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,21 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
|
||||
// It is licensed under GPLv2 with a classpath exception.
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
public class Hex {
|
||||
private Hex() {
|
||||
|
||||
}
|
||||
|
||||
private static int hexToBin(char ch) {
|
||||
if ('0' <= ch && ch <= '9') return ch - '0';
|
||||
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
|
||||
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static final char[] hexCode = "0123456789abcdef".toCharArray();
|
||||
|
||||
public static byte[] decode(String s) throws HexException {
|
||||
final int len = s.length();
|
||||
|
||||
if (len % 2 != 0)
|
||||
throw new HexException("hexBinary needs to be even-length: " + s);
|
||||
|
||||
byte[] out = new byte[len / 2];
|
||||
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
int h = hexToBin(s.charAt(i));
|
||||
int l = hexToBin(s.charAt(i + 1));
|
||||
if (h == -1 || l == -1)
|
||||
throw new HexException("contains illegal character for hexBinary: " + s);
|
||||
|
||||
out[i / 2] = (byte) (h * 16 + l);
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base16().decode(s.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
StringBuilder r = new StringBuilder(data.length * 2);
|
||||
for (byte b : data) {
|
||||
r.append(hexCode[(b >> 4) & 0xF]);
|
||||
r.append(hexCode[(b & 0xF)]);
|
||||
}
|
||||
return r.toString();
|
||||
return BaseEncoding.base16().lowerCase().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class HexException extends Exception {
|
||||
public HexException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* A class that can prepare initialization of a BiometricSlot by generating a new
|
||||
* key in the Android KeyStore and authenticating a cipher for it through a
|
||||
* BiometricPrompt.
|
||||
*/
|
||||
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
|
||||
private BiometricSlot _slot;
|
||||
private Listener _listener;
|
||||
private BiometricPrompt _prompt;
|
||||
|
||||
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new key in the Android KeyStore for the new BiometricSlot,
|
||||
* initializes a cipher with it and shows a BiometricPrompt to the user for
|
||||
* authentication. If authentication is successful, the new slot will be
|
||||
* initialized and delivered back through the listener.
|
||||
*/
|
||||
public void authenticate(BiometricPrompt.PromptInfo info) {
|
||||
if (_slot != null) {
|
||||
throw new IllegalStateException("Biometric authentication already in progress");
|
||||
}
|
||||
|
||||
KeyStoreHandle keyStore;
|
||||
try {
|
||||
keyStore = new KeyStoreHandle();
|
||||
} catch (KeyStoreHandleException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// generate a new Android KeyStore key
|
||||
// and assign it the UUID of the new slot as an alias
|
||||
Cipher cipher;
|
||||
BiometricSlot slot = new BiometricSlot();
|
||||
try {
|
||||
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_slot = slot;
|
||||
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the BiometricPrompt and resets the state of the initializer. It will
|
||||
* also attempt to delete the previously generated Android KeyStore key.
|
||||
*/
|
||||
public void cancelAuthentication() {
|
||||
if (_slot == null) {
|
||||
throw new IllegalStateException("Biometric authentication not in progress");
|
||||
}
|
||||
|
||||
reset();
|
||||
_prompt.cancelAuthentication();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
if (_slot != null) {
|
||||
try {
|
||||
// clean up the unused KeyStore key
|
||||
// this is non-critical, so just fail silently if an error occurs
|
||||
String uuid = _slot.getUUID().toString();
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
if (keyStore.containsKey(uuid)) {
|
||||
keyStore.deleteKey(uuid);
|
||||
}
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
_slot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(int errorCode, CharSequence errString) {
|
||||
reset();
|
||||
_listener.onSlotInitializationFailed(errorCode, errString);
|
||||
}
|
||||
|
||||
private void fail(Exception e) {
|
||||
e.printStackTrace();
|
||||
fail(0, e.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
fail(errorCode, errString.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
|
||||
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.biometric.BiometricConstants;
|
||||
import androidx.biometric.BiometricManager;
|
||||
|
||||
public class BiometricsHelper {
|
||||
private BiometricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static BiometricManager getManager(Context context) {
|
||||
BiometricManager manager = BiometricManager.from(context);
|
||||
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isCanceled(int errorCode) {
|
||||
return errorCode == BiometricConstants.ERROR_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
|
||||
*/
|
||||
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
|
||||
if (maxHeight <= 0 || maxWidth <= 0) {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
float maxRatio = (float) maxWidth / maxHeight;
|
||||
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
|
||||
|
||||
int width = maxWidth;
|
||||
int height = maxHeight;
|
||||
if (maxRatio > 1) {
|
||||
width = (int) ((float) maxHeight * ratio);
|
||||
} else {
|
||||
height = (int) ((float) maxWidth / ratio);
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
|
||||
*/
|
||||
public class ContextHelper {
|
||||
private ContextHelper() {
|
||||
|
||||
}
|
||||
|
||||
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
|
||||
@Nullable
|
||||
public static ComponentActivity getActivity(@NonNull Context context) {
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof ComponentActivity) {
|
||||
return (ComponentActivity) context;
|
||||
}
|
||||
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Lifecycle getLifecycle(@NonNull Context context) {
|
||||
ComponentActivity activity = getActivity(context);
|
||||
return activity == null ? null : activity.getLifecycle();
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,10 @@ public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
||||
public static void clearEditText(EditText text) {
|
||||
text.getText().clear();
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
return getEditTextChars(text, false);
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text, boolean removeSpaces) {
|
||||
String editTextString = text.getText().toString();
|
||||
if (removeSpaces) {
|
||||
editTextString = editTextString.replaceAll("\\s","");
|
||||
}
|
||||
|
||||
char[] chars = new char[editTextString.length()];
|
||||
editTextString.getChars(0, editTextString.length(), chars, 0);
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
editable.getChars(0, editable.length(), chars, 0);
|
||||
return chars;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,26 @@ public class FabScrollHelper {
|
||||
|
||||
public void onScroll(int dx, int dy) {
|
||||
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
|
||||
setVisible(false);
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
_isAnimating = true;
|
||||
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
|
||||
int fabBottomMargin = lp.bottomMargin;
|
||||
@@ -32,18 +52,7 @@ public class FabScrollHelper {
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class FingerprintHelper {
|
||||
private FingerprintHelper() {
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static FingerprintManager getManager(Context context) {
|
||||
if (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;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class PasswordStrengthHelper {
|
||||
// Material design color palette
|
||||
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
|
||||
public static String getString(int score, Context context) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
String[] strings = context.getResources().getStringArray(R.array.password_strength);
|
||||
return strings[score];
|
||||
}
|
||||
|
||||
public static String getColor(int score) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
return COLORS[score];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static android.graphics.ImageFormat.YUV_420_888;
|
||||
import static android.graphics.ImageFormat.YUV_422_888;
|
||||
import static android.graphics.ImageFormat.YUV_444_888;
|
||||
|
||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||
|
||||
private final QrCodeAnalyzer.Listener _listener;
|
||||
|
||||
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(@NonNull ImageProxy image) {
|
||||
int format = image.getFormat();
|
||||
if (format != YUV_420_888 && format != YUV_422_888 && format != YUV_444_888) {
|
||||
Log.e(TAG, String.format("Expected YUV format, got %d instead", format));
|
||||
image.close();
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer buf = image.getPlanes()[0].getBuffer();
|
||||
byte[] data = new byte[buf.remaining()];
|
||||
buf.get(data);
|
||||
buf.rewind();
|
||||
|
||||
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
|
||||
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
|
||||
);
|
||||
|
||||
QRCodeReader reader = new QRCodeReader();
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
try {
|
||||
Result result = reader.decode(bitmap);
|
||||
if (_listener != null) {
|
||||
_listener.onQrCodeDetected(result);
|
||||
}
|
||||
} catch (ChecksumException | FormatException | NotFoundException ignored) {
|
||||
|
||||
} finally {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onQrCodeDetected(Result result);
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -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;
|
||||
}
|
||||
|
||||
@@ -45,7 +43,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
|
||||
if (adapter.getEntryAt(position) != _selectedEntry)
|
||||
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
|
||||
{
|
||||
dragFlags = 0;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import me.dm7.barcodescanner.core.ViewFinderView;
|
||||
|
||||
public class SquareFinderView extends ViewFinderView {
|
||||
|
||||
public SquareFinderView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public SquareFinderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setSquareViewFinder(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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,19 +2,22 @@ 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.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
@@ -23,33 +26,28 @@ public class AegisImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
DatabaseFile file = DatabaseFile.fromBytes(bytes);
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
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 +56,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 +84,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,10 +97,10 @@ 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);
|
||||
} catch (JSONException | OtpInfoException | Base64Exception e) {
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,41 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
@@ -32,29 +44,31 @@ import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AndOtpImporter extends DatabaseImporter {
|
||||
private static final int INT_SIZE = 4;
|
||||
private static final int NONCE_SIZE = 12;
|
||||
private static final int TAG_SIZE = 16;
|
||||
private static final int SALT_SIZE = 12;
|
||||
private static final int KEY_SIZE = 256; // bits
|
||||
|
||||
public AndOtpImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = reader.readAll();
|
||||
bytes = IOUtils.readAll(stream);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
@@ -81,24 +95,15 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
try {
|
||||
// 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");
|
||||
|
||||
// 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);
|
||||
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);
|
||||
@@ -111,16 +116,66 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
DecryptedState state = decryptData(key, 0);
|
||||
listener.onStateDecrypted(state);
|
||||
} else {
|
||||
int offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
|
||||
try {
|
||||
DecryptedState state = decryptData(key1, offset);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
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 {
|
||||
decrypt(context, password, i != 0, listener);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.create());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +194,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,12 +206,12 @@ 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");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
@@ -176,18 +231,83 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
String name;
|
||||
String issuer = "";
|
||||
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
if (obj.has("issuer")) {
|
||||
name = obj.getString("label");
|
||||
issuer = obj.getString("issuer");
|
||||
} else {
|
||||
name = parts[0];
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
||||
public AndOtpKeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
AndOtpKeyDerivationTask.Params params = args[0];
|
||||
SecretKey key;
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private char[] _password;
|
||||
private byte[] _salt;
|
||||
private int _iterations;
|
||||
|
||||
public Params(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.LocalFileHeader;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
private static final String FILENAME = "Accounts.txt";
|
||||
|
||||
public AuthenticatorPlusImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
return new EncryptedState(IOUtils.readAll(stream));
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
private EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
|
||||
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
|
||||
LocalFileHeader header;
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
|
||||
DatabaseImporter.State state = importer.read(zipStream);
|
||||
listener.onStateDecrypted(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(FILENAME);
|
||||
} catch (IOException | DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -18,52 +26,184 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
|
||||
private static final String _subPath = "shared_prefs";
|
||||
private static final String _pkgName = "com.authy.authy";
|
||||
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
|
||||
private static final String _authyFilename = "com.authy.storage.tokens.authy";
|
||||
|
||||
private static final int ITERATIONS = 1000;
|
||||
private static final int KEY_SIZE = 256;
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
public AuthyImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
|
||||
JSONArray array;
|
||||
JSONArray authyArray;
|
||||
try {
|
||||
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < authyArray.length(); i++) {
|
||||
array.put(authyArray.getJSONObject(i));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return read(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
|
||||
if (entry.Name.equals(String.format("%s.key", _authFilename))
|
||||
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
|
||||
array = new JSONArray(entry.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new State(array);
|
||||
return read(array);
|
||||
} catch (XmlPullParserException | JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private JSONArray _obj;
|
||||
private State read(JSONArray array) throws DatabaseImporterException {
|
||||
try {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject obj = array.getJSONObject(i);
|
||||
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
|
||||
return new EncryptedState(array);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
private State(JSONArray obj) {
|
||||
return new DecryptedState(array);
|
||||
}
|
||||
|
||||
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(inStream, null);
|
||||
parser.nextTag();
|
||||
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(key)) {
|
||||
return new JSONArray(entry.Value);
|
||||
}
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
|
||||
}
|
||||
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private EncryptedState(JSONArray array) {
|
||||
super(true);
|
||||
_array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = JsonUtils.optString(obj, "encryptedSecret");
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] encryptedSecret = Base64.decode(secretString);
|
||||
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
|
||||
byte[] secret = cipher.doFinal(encryptedSecret);
|
||||
obj.remove("encryptedSecret");
|
||||
obj.remove("salt");
|
||||
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
DecryptedState state = new DecryptedState(_array);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (JSONException
|
||||
| EncodingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private DecryptedState(JSONArray array) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
_array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,10 +211,10 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
JSONObject entryObj = _obj.getJSONObject(i);
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject entryObj = _array.getJSONObject(i);
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(entryObj);
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -87,45 +227,59 @@ 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");
|
||||
authyEntryInfo.AccountType = entry.getString("accountType");
|
||||
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
|
||||
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
|
||||
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
sanitizeEntryInfo(authyEntryInfo);
|
||||
boolean isAuthy = !entry.optString("accountType", "authy").equals("authenticator");
|
||||
sanitizeEntryInfo(authyEntryInfo, isAuthy);
|
||||
|
||||
byte[] secret;
|
||||
if (isAuthy) {
|
||||
secret = Hex.decode(entry.getString("secretSeed"));
|
||||
} else {
|
||||
secret = Base32.decode(entry.getString("decryptedSecret"));
|
||||
}
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
|
||||
return new DatabaseEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | Base32Exception e) {
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, digits == 7 ? 10 : 30);
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
|
||||
String seperator = "";
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
|
||||
if (!isAuthy) {
|
||||
String separator = "";
|
||||
|
||||
if (info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
seperator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
seperator = " - ";
|
||||
if (info.OriginalIssuer != null) {
|
||||
info.Issuer = info.OriginalIssuer;
|
||||
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
separator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
separator = " - ";
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + separator, "");
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
info.Issuer = info.Name;
|
||||
info.Name = "";
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + seperator, "");
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthyEntryInfo {
|
||||
String OriginalName;
|
||||
String OriginalIssuer;
|
||||
String AccountType;
|
||||
String Issuer;
|
||||
String Name;
|
||||
|
||||
@@ -2,49 +2,43 @@ 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.util.ByteInputStream;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class DatabaseImporter {
|
||||
private Context _context;
|
||||
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _importers;
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
|
||||
private static List<Definition> _importers;
|
||||
|
||||
static {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new LinkedHashMap<>();
|
||||
_importers.put("Aegis", AegisImporter.class);
|
||||
_importers.put("Authy", AuthyImporter.class);
|
||||
_importers.put("andOTP", AndOtpImporter.class);
|
||||
_importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_importers.put("Steam", SteamImporter.class);
|
||||
|
||||
_appImporters = new LinkedHashMap<>();
|
||||
_appImporters.put("Authy", AuthyImporter.class);
|
||||
_appImporters.put("FreeOTP", FreeOtpImporter.class);
|
||||
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_appImporters.put("Steam", SteamImporter.class);
|
||||
_importers = new ArrayList<>();
|
||||
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
|
||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
|
||||
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
|
||||
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
|
||||
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
|
||||
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
|
||||
}
|
||||
|
||||
public DatabaseImporter(Context context) {
|
||||
@@ -55,20 +49,27 @@ public abstract class DatabaseImporter {
|
||||
return _context;
|
||||
}
|
||||
|
||||
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
return getAppPath(getAppPkgName(), getAppSubPath());
|
||||
}
|
||||
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
|
||||
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
|
||||
PackageManager man = getContext().getPackageManager();
|
||||
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
|
||||
}
|
||||
|
||||
protected abstract String getAppPkgName();
|
||||
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
|
||||
|
||||
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
public State read(InputStream stream) throws DatabaseImporterException {
|
||||
return read(stream, false);
|
||||
}
|
||||
|
||||
public abstract State read(FileReader reader) throws DatabaseImporterException;
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile file = getAppPath();
|
||||
try (SuFileInputStream stream = new SuFileInputStream(file)) {
|
||||
return read(stream, true);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
|
||||
try {
|
||||
@@ -79,12 +80,42 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
|
||||
return Collections.unmodifiableMap(_importers);
|
||||
public static List<Definition> getImporters(boolean isDirect) {
|
||||
if (isDirect) {
|
||||
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(_importers);
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
|
||||
return Collections.unmodifiableMap(_appImporters);
|
||||
public static class Definition {
|
||||
private final String _name;
|
||||
private final Class<? extends DatabaseImporter> _type;
|
||||
private final @StringRes int _help;
|
||||
private final boolean _supportsDirect;
|
||||
|
||||
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
|
||||
_name = name;
|
||||
_type = type;
|
||||
_help = help;
|
||||
_supportsDirect = supportsDirect;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public Class<? extends DatabaseImporter> getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public @StringRes int getHelp() {
|
||||
return _help;
|
||||
}
|
||||
|
||||
public boolean supportsDirect() {
|
||||
return _supportsDirect;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class State {
|
||||
@@ -116,10 +147,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 +158,7 @@ public abstract class DatabaseImporter {
|
||||
_errors.add(error);
|
||||
}
|
||||
|
||||
public UUIDMap<DatabaseEntry> getEntries() {
|
||||
public UUIDMap<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
@@ -136,38 +167,6 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileReader {
|
||||
private InputStream _stream;
|
||||
private boolean _internal;
|
||||
|
||||
public FileReader(InputStream stream) {
|
||||
this(stream, false);
|
||||
}
|
||||
|
||||
public FileReader(InputStream stream, boolean internal) {
|
||||
_stream = stream;
|
||||
_internal = internal;
|
||||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
|
||||
return stream.getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return _stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this reader reads the internal state of an app.
|
||||
* @return true if reading from internal file, false if reading from external file
|
||||
*/
|
||||
public boolean isInternal() {
|
||||
return _internal;
|
||||
}
|
||||
}
|
||||
|
||||
public interface DecryptListener {
|
||||
void onStateDecrypted(State state);
|
||||
void onError(Exception e);
|
||||
|
||||
+5
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
||||
public class DatabaseImporterEntryException extends Exception {
|
||||
private String _text;
|
||||
|
||||
public DatabaseImporterEntryException(String message, String text) {
|
||||
super(message);
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public DatabaseImporterEntryException(Throwable cause, String text) {
|
||||
super(cause);
|
||||
_text = text;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -18,6 +20,7 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -30,21 +33,16 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
@@ -73,7 +71,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 +81,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 +108,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());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,24 +25,19 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
State state;
|
||||
|
||||
if (reader.isInternal()) {
|
||||
state = new FreeOtpImporter(getContext()).read(reader);
|
||||
if (isInternal) {
|
||||
state = new FreeOtpImporter(getContext()).read(stream);
|
||||
} else {
|
||||
try {
|
||||
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
|
||||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
JSONArray array = obj.getJSONArray("tokens");
|
||||
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_HOTP = 1;
|
||||
@@ -34,48 +28,23 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
File file;
|
||||
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
ShellUtils.pump(reader.getStream(), out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Entry entry = new Entry(cursor);
|
||||
entries.add(entry);
|
||||
} while(cursor.moveToNext());
|
||||
}
|
||||
|
||||
return new State(entries);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
@@ -92,7 +61,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,9 +71,9 @@ 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());
|
||||
byte[] secret = Base32.decode(entry.getSecret());
|
||||
|
||||
OtpInfo info;
|
||||
switch (entry.getType()) {
|
||||
@@ -124,34 +93,14 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
name = parts[1];
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, entry.getIssuer());
|
||||
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
|
||||
return new VaultEntry(info, name, entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName) {
|
||||
return getString(cursor, columnName, null);
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _email;
|
||||
@@ -159,11 +108,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private long _counter;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
_type = getInt(cursor, "type");
|
||||
_secret = getString(cursor, "secret");
|
||||
_email = getString(cursor, "email", "");
|
||||
_issuer = getString(cursor, "issuer", "");
|
||||
_counter = getLong(cursor, "counter");
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "secret");
|
||||
_email = SqlImporterHelper.getString(cursor, "email", "");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
|
||||
_counter = SqlImporterHelper.getLong(cursor, "counter");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GoogleAuthUriImporter extends DatabaseImporter {
|
||||
public GoogleAuthUriImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try (InputStreamReader streamReader = new InputStreamReader(stream);
|
||||
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new GoogleAuthUriImporter.State(lines);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private ArrayList<String> _lines;
|
||||
|
||||
private State(ArrayList<String> lines) {
|
||||
super(false);
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.Result convert() {
|
||||
DatabaseImporter.Result result = new DatabaseImporter.Result();
|
||||
|
||||
for (String line : _lines) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(line);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
|
||||
return new VaultEntry(info);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "databases/PhoneFactor";
|
||||
private static final String _pkgName = "com.azure.authenticator";
|
||||
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_MICROSOFT = 1;
|
||||
|
||||
public MicrosoftAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<Entry> _entries;
|
||||
|
||||
private State(List<Entry> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (Entry sqlEntry : _entries) {
|
||||
try {
|
||||
int type = sqlEntry.getType();
|
||||
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
|
||||
VaultEntry entry = convertEntry(sqlEntry);
|
||||
result.addEntry(entry);
|
||||
}
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret;
|
||||
int digits = 6;
|
||||
|
||||
switch (entry.getType()) {
|
||||
case TYPE_TOTP:
|
||||
secret = Base32.decode(entry.getSecret());
|
||||
break;
|
||||
case TYPE_MICROSOFT:
|
||||
digits = 8;
|
||||
secret = Base64.decode(entry.getSecret());
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
|
||||
}
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _issuer;
|
||||
private String _userName;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "account_type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "name");
|
||||
_userName = SqlImporterHelper.getString(cursor, "username");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return _secret;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return _userName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.google.common.io.Files;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class SqlImporterHelper {
|
||||
private Context _context;
|
||||
|
||||
public SqlImporterHelper(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
|
||||
File dir = Files.createTempDir();
|
||||
File mainFile = new File(dir, path.getName());
|
||||
|
||||
List<File> fileCopies = new ArrayList<>();
|
||||
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
|
||||
// create temporary copies of the database files so that SQLiteDatabase can open them
|
||||
File fileCopy = null;
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
fileCopy = new File(dir, file.getName());
|
||||
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
fileCopies.add(fileCopy);
|
||||
} catch (IOException e) {
|
||||
if (fileCopy != null) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
|
||||
for (File fileCopy2 : fileCopies) {
|
||||
fileCopy2.delete();
|
||||
}
|
||||
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, mainFile, table);
|
||||
} finally {
|
||||
for (File fileCopy : fileCopies) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
|
||||
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
|
||||
File file = null;
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("db-import-", "", _context.getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, file, table);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
|
||||
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
|
||||
List<T> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
|
||||
entries.add(entry);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
|
||||
return entries;
|
||||
} catch (InstantiationException | IllegalAccessException
|
||||
| NoSuchMethodException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getString(Cursor cursor, String columnName) {
|
||||
return cursor.getString(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static abstract class Entry {
|
||||
public Entry(Cursor cursor) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,19 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SteamImporter extends DatabaseImporter {
|
||||
@@ -26,27 +27,23 @@ public class SteamImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
// NOTE: this assumes that a global root shell has already been obtained by the caller
|
||||
SuFile path = getAppPath(getAppPkgName(), _subDir);
|
||||
SuFile path = getAppPath(_pkgName, _subDir);
|
||||
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
// TODO: handle multiple files (can this even occur?)
|
||||
return new SuFile(_subDir, files[0].getName()).getPath();
|
||||
return files[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
|
||||
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
@@ -66,7 +63,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,14 +72,14 @@ 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");
|
||||
} catch (JSONException | Base64Exception | OtpInfoException e) {
|
||||
return new VaultEntry(info, account, "Steam");
|
||||
} catch (JSONException | EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
|
||||
private static final String _pkgName = "com.authenticator.authservice2";
|
||||
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
|
||||
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
|
||||
|
||||
public TotpAuthenticatorImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
if (isInternal) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
String data = null;
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(PREF_KEY)) {
|
||||
data = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
|
||||
}
|
||||
|
||||
List<JSONObject> entries = parse(data);
|
||||
return new DecryptedState(entries);
|
||||
} else {
|
||||
byte[] base64 = IOUtils.readAll(stream);
|
||||
byte[] cipherText = Base64.decode(base64);
|
||||
return new EncryptedState(cipherText);
|
||||
}
|
||||
} catch (IOException | XmlPullParserException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<JSONObject> parse(String data) throws JSONException {
|
||||
JSONArray array = new JSONArray(data);
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
String s = array.getString(i);
|
||||
entries.add(new JSONObject(s));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
public EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
IvParameterSpec spec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
|
||||
byte[] bytes = cipher.doFinal(_data);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
JSONArray keys = obj.names();
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
if (keys != null && keys.length() > 0) {
|
||||
entries = parse((String) keys.get(0));
|
||||
}
|
||||
|
||||
return new DecryptedState(entries);
|
||||
} catch (NoSuchAlgorithmException
|
||||
| NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException
|
||||
| JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.choose_totpauth_importer)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
decrypt(password, listener);
|
||||
});
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
decrypt(PASSWORD, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void decrypt(char[] password, DecryptListener listener) {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private List<JSONObject> _objs;
|
||||
|
||||
private DecryptedState(List<JSONObject> objs) {
|
||||
super(false);
|
||||
_objs = objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (JSONObject obj : _objs) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
int base = obj.getInt("base");
|
||||
String secretString = obj.getString("key");
|
||||
|
||||
byte[] secret;
|
||||
switch (base) {
|
||||
case 16:
|
||||
secret = Hex.decode(secretString);
|
||||
break;
|
||||
case 32:
|
||||
secret = Base32.decode(secretString);
|
||||
break;
|
||||
case 64:
|
||||
secret = Base64.decode(secretString);
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
|
||||
}
|
||||
|
||||
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
|
||||
String name = obj.optString("name");
|
||||
String issuer = obj.optString("issuer");
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WinAuthImporter extends DatabaseImporter {
|
||||
public WinAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
|
||||
DatabaseImporter.State state = importer.read(stream);
|
||||
return new State(state);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private DatabaseImporter.State _state;
|
||||
|
||||
private State(DatabaseImporter.State state) {
|
||||
super(false);
|
||||
_state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = _state.convert();
|
||||
|
||||
for (VaultEntry entry : result.getEntries()) {
|
||||
entry.setIssuer(entry.getName());
|
||||
entry.setName("WinAuth");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class ProtobufLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Protocol Buffers License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,20 @@ package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.GoogleAuthProtos;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthInfo implements Serializable {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
public class GoogleAuthInfo {
|
||||
private OtpInfo _info;
|
||||
private String _accountName;
|
||||
private String _issuer;
|
||||
@@ -22,21 +32,25 @@ public class GoogleAuthInfo {
|
||||
|
||||
public Uri getUri() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("otpauth");
|
||||
builder.scheme(SCHEME);
|
||||
|
||||
if (_info instanceof TotpInfo) {
|
||||
builder.authority("totp");
|
||||
if (_info instanceof SteamInfo) {
|
||||
builder.authority("steam");
|
||||
} else {
|
||||
builder.authority("totp");
|
||||
}
|
||||
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
|
||||
} else if (_info instanceof HotpInfo) {
|
||||
builder.authority("hotp");
|
||||
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
||||
}
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
||||
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
|
||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
||||
|
||||
if (_issuer != null && !_issuer.equals("")) {
|
||||
builder.path(String.format("%s:%s", _issuer, _accountName));
|
||||
@@ -51,35 +65,37 @@ public class GoogleAuthInfo {
|
||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("bad uri format");
|
||||
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals("otpauth")) {
|
||||
throw new GoogleAuthInfoException("unsupported protocol");
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("'secret' is not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
// decode secret
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(encodedSecret.toCharArray());
|
||||
} catch (Base32Exception e) {
|
||||
throw new GoogleAuthInfoException("bad secret", e);
|
||||
secret = parseSecret(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException("Bad secret", e);
|
||||
}
|
||||
|
||||
// check the otp type
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "totp":
|
||||
TotpInfo totpInfo = new TotpInfo(secret);
|
||||
@@ -89,19 +105,27 @@ public class GoogleAuthInfo {
|
||||
}
|
||||
info = totpInfo;
|
||||
break;
|
||||
case "steam":
|
||||
SteamInfo steamInfo = new SteamInfo(secret);
|
||||
period = uri.getQueryParameter("period");
|
||||
if (period != null) {
|
||||
steamInfo.setPeriod(Integer.parseInt(period));
|
||||
}
|
||||
info = steamInfo;
|
||||
break;
|
||||
case "hotp":
|
||||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("'counter' was not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
@@ -141,13 +165,87 @@ public class GoogleAuthInfo {
|
||||
if (digits != null) {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
|
||||
*/
|
||||
public static byte[] parseSecret(String s) throws EncodingException {
|
||||
s = s.trim().replace("-", "").replace(" ", "");
|
||||
return Base32.decode(s);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException("Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
try {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_HOTP:
|
||||
otp = new HotpInfo(secret, params.getCounter());
|
||||
break;
|
||||
case OTP_TOTP:
|
||||
otp = new TotpInfo(secret);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
String issuer = params.getIssuer();
|
||||
int colonI = name.indexOf(':');
|
||||
if (issuer.isEmpty() && colonI != -1) {
|
||||
issuer = name.substring(0, colonI);
|
||||
name = name.substring(colonI + 1);
|
||||
}
|
||||
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
|
||||
infos.add(info);
|
||||
}
|
||||
|
||||
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
@@ -155,4 +253,34 @@ public class GoogleAuthInfo {
|
||||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public static class Export {
|
||||
private int _batchId;
|
||||
private int _batchIndex;
|
||||
private int _batchSize;
|
||||
private List<GoogleAuthInfo> _entries;
|
||||
|
||||
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
|
||||
_batchId = batchId;
|
||||
_batchIndex = batchIndex;
|
||||
_batchSize = batchSize;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public List<GoogleAuthInfo> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return _batchSize;
|
||||
}
|
||||
|
||||
public int getBatchIndex() {
|
||||
return _batchIndex;
|
||||
}
|
||||
|
||||
public int getBatchId() {
|
||||
return _batchId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class HotpInfo extends OtpInfo {
|
||||
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -93,7 +93,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
OtpInfo info;
|
||||
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
|
||||
@@ -110,7 +110,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
default:
|
||||
throw new OtpInfoException("unsupported otp type: " + type);
|
||||
}
|
||||
} catch (Base32Exception | JSONException e) {
|
||||
} catch (EncodingException | JSONException e) {
|
||||
throw new OtpInfoException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import androidx.annotation.AttrRes;
|
||||
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.Iconics;
|
||||
import com.beemdevelopment.aegis.licenses.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
import de.psdev.licensesdialog.LicensesDialog;
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
|
||||
@@ -41,14 +43,21 @@ public class AboutActivity extends AegisActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Iconics.init(getApplicationContext());
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentVersion());
|
||||
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));
|
||||
@@ -70,17 +79,16 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
ChangelogDialog.create().setTheme(getConfiguredTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
});
|
||||
}
|
||||
|
||||
private String getCurrentVersion() {
|
||||
try {
|
||||
return getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
private static String getCurrentAppVersion() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
|
||||
}
|
||||
return "Unknown version";
|
||||
|
||||
return BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
private void openUrl(String url) {
|
||||
@@ -91,6 +99,13 @@ public class AboutActivity extends AegisActivity {
|
||||
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));
|
||||
@@ -102,14 +117,17 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
private void showLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
|
||||
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
|
||||
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
|
||||
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
String textColor = getThemeColorAsHex(R.attr.primaryText);
|
||||
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
|
||||
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
|
||||
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
LicenseResolver.registerLicense(new ProtobufLicense());
|
||||
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.licenses)
|
||||
@@ -118,4 +136,21 @@ public class AboutActivity extends AegisActivity {
|
||||
.build()
|
||||
.show();
|
||||
}
|
||||
|
||||
private String getThemeColorAsHex(@AttrRes int attributeId) {
|
||||
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
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;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
|
||||
private boolean _resumed;
|
||||
private AegisApplication _app;
|
||||
private Theme _currentTheme;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -30,15 +27,16 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
|
||||
// set the theme and locale before creating the activity
|
||||
Preferences prefs = getPreferences();
|
||||
setPreferredTheme(prefs.getCurrentTheme());
|
||||
onSetTheme();
|
||||
setLocale(prefs.getLocale());
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// if the app was killed, relaunch MainActivity and close everything else
|
||||
if (savedInstanceState != null && isOrphan()) {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,9 +45,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
// apply a dirty hack to make progress bars work even if animations are disabled
|
||||
setGlobalAnimationDurationScale();
|
||||
|
||||
// register a callback to listen for lock events
|
||||
_app.registerLockListener(this);
|
||||
}
|
||||
@@ -61,24 +56,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
_resumed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
_resumed = false;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onLocked() {
|
||||
if (isOrphan()) {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finish();
|
||||
}
|
||||
public void onLocked(boolean userInitiated) {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
@@ -89,20 +69,38 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
return _app.getPreferences();
|
||||
}
|
||||
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
_currentTheme = theme;
|
||||
/**
|
||||
* Called when the activity is expected to set its theme.
|
||||
*/
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme);
|
||||
break;
|
||||
case DARK:
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
break;
|
||||
case AMOLED:
|
||||
setTheme(R.style.AppTheme_TrueBlack);
|
||||
break;
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
protected void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
protected Theme getConfiguredTheme() {
|
||||
Theme theme = getPreferences().getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (currentNightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
theme = Theme.LIGHT;
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
protected void setLocale(Locale locale) {
|
||||
@@ -111,36 +109,24 @@ 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity has been resumed. (i.e. onResume was called)
|
||||
*/
|
||||
protected boolean isOpen() {
|
||||
return _resumed;
|
||||
protected boolean saveVault(boolean backup) {
|
||||
try {
|
||||
getApp().getVaultManager().save(backup);
|
||||
return true;
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity instance has become an orphan. This can happen if
|
||||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
*/
|
||||
private boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && _app.getDatabaseManager().isLocked();
|
||||
}
|
||||
|
||||
private void setGlobalAnimationDurationScale() {
|
||||
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
|
||||
if (durationScale != 1) {
|
||||
try {
|
||||
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
|
||||
} catch (Throwable t) {
|
||||
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Theme getCurrentTheme() {
|
||||
return _currentTheme;
|
||||
protected boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -15,47 +14,66 @@ import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
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.crypto.MasterKey;
|
||||
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.ui.tasks.SlotListTask;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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 {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
private FingerprintUiHelper _fingerHelper;
|
||||
private FingerprintManager.CryptoObject _fingerCryptoObj;
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
|
||||
private int _failedUnlockAttempts;
|
||||
|
||||
// the first time this activity is resumed after creation, it's possible to inhibit showing the
|
||||
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
|
||||
private boolean _inhibitBioPrompt;
|
||||
|
||||
private Preferences _prefs;
|
||||
private boolean _stateless;
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
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);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
@@ -64,25 +82,38 @@ 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));
|
||||
if (_prefs.isPinKeyboardEnabled()) {
|
||||
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (savedInstanceState == null) {
|
||||
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
|
||||
} else {
|
||||
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
|
||||
}
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
|
||||
_stateless = _slots != null;
|
||||
if (!_stateless) {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
vaultFile = getApp().loadVaultFile();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (_slots.has(FingerprintSlot.class) && FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
_slots = vaultFile.getHeader().getSlots();
|
||||
}
|
||||
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
boolean invalidated = false;
|
||||
FingerprintManager manager = FingerprintHelper.getManager(this);
|
||||
|
||||
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)) {
|
||||
@@ -92,122 +123,244 @@ 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);
|
||||
|
||||
_bioSlot = slot;
|
||||
_bioKey = key;
|
||||
biometricsButton.setVisibility(View.VISIBLE);
|
||||
invalidated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
}
|
||||
|
||||
// display a help message if a matching invalidated keystore entry was found
|
||||
if (invalidated) {
|
||||
boxFingerprintInfo.setVisibility(View.VISIBLE);
|
||||
boxBiometricInfo.setVisibility(View.VISIBLE);
|
||||
biometricsButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
decryptButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
decryptButton.setOnClickListener(v -> {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
trySlots(PasswordSlot.class, password);
|
||||
}
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
});
|
||||
|
||||
if (_fingerHelper == null) {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
showBiometricPrompt();
|
||||
});
|
||||
}
|
||||
|
||||
private void showError() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
|
||||
}
|
||||
|
||||
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
|
||||
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
|
||||
new SlotListTask<>(type, this, this).execute(params);
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
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() {
|
||||
switch (_cancelAction) {
|
||||
case KILL:
|
||||
finishAffinity();
|
||||
|
||||
case CLOSE:
|
||||
Intent intent = new Intent();
|
||||
setResult(RESULT_CANCELED, intent);
|
||||
finish();
|
||||
if (_stateless) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
finishAffinity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_fingerHelper != null) {
|
||||
_fingerHelper.startListening(_fingerCryptoObj);
|
||||
boolean remindPassword = _prefs.isPasswordReminderNeeded();
|
||||
if (_bioKey == null || remindPassword) {
|
||||
focusPasswordField();
|
||||
}
|
||||
|
||||
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
|
||||
_bioPrompt = showBiometricPrompt();
|
||||
}
|
||||
|
||||
_inhibitBioPrompt = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!isChangingConfigurations() && _bioPrompt != null) {
|
||||
_bioPrompt.cancelAuthentication();
|
||||
_bioPrompt = null;
|
||||
}
|
||||
|
||||
if (_fingerHelper != null) {
|
||||
_fingerHelper.stopListening();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
|
||||
showPasswordReminder();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onAuthenticated() {
|
||||
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
|
||||
protected boolean isOrphan() {
|
||||
return _stateless && super.isOrphan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
|
||||
private void focusPasswordField() {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
private void showPasswordReminder() {
|
||||
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
|
||||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
|
||||
_textPassword.postDelayed(popup::dismiss, 5000);
|
||||
}
|
||||
|
||||
public BiometricPrompt showBiometricPrompt() {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0);
|
||||
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = _bioSlot.createDecryptCipher(_bioKey);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
BiometricPrompt prompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.authentication))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
prompt.authenticate(info, cryptoObj);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
if (_stateless) {
|
||||
// send the master key back to the calling activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
saveVault(true);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new DatabaseFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} else {
|
||||
showError();
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onInvalidPassword() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
|
||||
_failedUnlockAttempts ++;
|
||||
|
||||
if (_failedUnlockAttempts >= 3) {
|
||||
_textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
}
|
||||
|
||||
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
|
||||
_prefs.resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
onInvalidPassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
_bioPrompt = null;
|
||||
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_bioPrompt = null;
|
||||
|
||||
MasterKey key;
|
||||
BiometricSlot slot = _slots.find(BiometricSlot.class);
|
||||
|
||||
try {
|
||||
key = slot.getKey(result.getCryptoObject().getCipher());
|
||||
} catch (SlotException | SlotIntegrityException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
finish(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,16 @@ public class ChangelogDialog extends DialogFragment {
|
||||
|
||||
private String replaceStylesheet(String changelog) {
|
||||
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme())));
|
||||
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme())));
|
||||
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;
|
||||
|
||||
|
||||
@@ -2,43 +2,50 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
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.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.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.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 com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class Dialogs {
|
||||
private Dialogs() {
|
||||
@@ -65,6 +72,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 = activity.getResources().getQuantityString(R.plurals.delete_entries_description, totalEntries, totalEntries);
|
||||
} else {
|
||||
title = activity.getString(R.string.delete_entry);
|
||||
message = activity.getString(R.string.delete_entry_description);
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -74,10 +99,27 @@ public class Dialogs {
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
|
||||
public static void showSetPasswordDialog(ComponentActivity activity, Dialogs.SlotListener listener) {
|
||||
Zxcvbn zxcvbn = new Zxcvbn();
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
|
||||
EditText textPassword = view.findViewById(R.id.text_password);
|
||||
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
ProgressBar barPasswordStrength = view.findViewById(R.id.progressBar);
|
||||
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
|
||||
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
|
||||
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
|
||||
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
textPassword.setTransformationMethod(null);
|
||||
textPasswordConfirm.setTransformationMethod(null);
|
||||
textPassword.clearFocus();
|
||||
textPasswordConfirm.clearFocus();
|
||||
} else {
|
||||
textPassword.setTransformationMethod(new PasswordTransformationMethod());
|
||||
textPasswordConfirm.setTransformationMethod(new PasswordTransformationMethod());
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.set_password)
|
||||
@@ -85,6 +127,7 @@ public class Dialogs {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
|
||||
final AtomicReference<Button> buttonOK = new AtomicReference<>();
|
||||
dialog.setOnShowListener(d -> {
|
||||
@@ -100,7 +143,7 @@ public class Dialogs {
|
||||
|
||||
char[] password = EditTextHelper.getEditTextChars(textPassword);
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
DerivationTask task = new DerivationTask(activity, key -> {
|
||||
KeyDerivationTask task = new KeyDerivationTask(activity, (passSlot, key) -> {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
@@ -112,19 +155,30 @@ public class Dialogs {
|
||||
listener.onSlotResult(slot, cipher);
|
||||
dialog.dismiss();
|
||||
});
|
||||
task.execute(new DerivationTask.Params(slot, password));
|
||||
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, password);
|
||||
task.execute(activity.getLifecycle(), params);
|
||||
});
|
||||
});
|
||||
|
||||
TextWatcher watcher = new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence c, int start, int before, int count) {
|
||||
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
|
||||
buttonOK.get().setEnabled(equal);
|
||||
|
||||
Strength strength = zxcvbn.measure(textPassword.getText());
|
||||
barPasswordStrength.setProgress(strength.getScore());
|
||||
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
|
||||
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
|
||||
textPasswordWrapper.setError(strength.getFeedback().getWarning());
|
||||
strength.wipe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable c) {
|
||||
}
|
||||
};
|
||||
@@ -134,7 +188,7 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
|
||||
EditText input = view.findViewById(R.id.text_input);
|
||||
if (isSecret) {
|
||||
@@ -142,18 +196,31 @@ public class Dialogs {
|
||||
}
|
||||
input.setHint(hintId);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
char[] text = EditTextHelper.getEditTextChars(input);
|
||||
listener.onTextInputResult(text);
|
||||
})
|
||||
.create();
|
||||
char[] text = EditTextHelper.getEditTextChars(input);
|
||||
listener.onTextInputResult(text);
|
||||
});
|
||||
|
||||
if (dismissListener != null) {
|
||||
builder.setOnDismissListener(dismissListener);
|
||||
}
|
||||
|
||||
if (messageId != 0) {
|
||||
builder.setMessage(messageId);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
showTextInputDialog(context, titleId, hintId, listener, false);
|
||||
}
|
||||
@@ -162,10 +229,49 @@ public class Dialogs {
|
||||
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) {
|
||||
showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true);
|
||||
}
|
||||
|
||||
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_checkbox, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
||||
checkBox.setText(checkboxMessageId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.no, (dialog1, which) ->
|
||||
listener.onCheckboxInputResult(false))
|
||||
.setPositiveButton(R.string.yes, (dialog1, which) ->
|
||||
listener.onCheckboxInputResult(checkBox.isChecked()));
|
||||
|
||||
if (messageId != 0) {
|
||||
builder.setMessage(messageId);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
final AtomicReference<Button> buttonOK = new AtomicReference<>();
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setEnabled(false);
|
||||
buttonOK.set(button);
|
||||
});
|
||||
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> buttonOK.get().setEnabled(isChecked));
|
||||
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setMinValue(3);
|
||||
numberPicker.setMinValue(1);
|
||||
numberPicker.setMaxValue(60);
|
||||
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
|
||||
numberPicker.setWrapSelectorWheel(true);
|
||||
@@ -180,50 +286,140 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
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);
|
||||
public static void showBackupVersionsPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
final int max = 30;
|
||||
String[] numbers = new String[max / 5];
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
numbers[i] = Integer.toString(i * 5 + 5);
|
||||
}
|
||||
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setDisplayedValues(numbers);
|
||||
numberPicker.setMaxValue(numbers.length - 1);
|
||||
numberPicker.setMinValue(0);
|
||||
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getBackupsVersionCount() / 5 - 1);
|
||||
numberPicker.setWrapSelectorWheel(false);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.register_fingerprint)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setOnDismissListener(d -> {
|
||||
helper.get().stopListening();
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
listener.onNumberInputResult(numberPicker.getValue()))
|
||||
.create();
|
||||
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e) {
|
||||
showErrorDialog(context, message, e, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error) {
|
||||
showErrorDialog(context, message, error, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e, DialogInterface.OnClickListener listener) {
|
||||
showErrorDialog(context, message, e.toString(), listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error, DialogInterface.OnClickListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null);
|
||||
TextView textDetails = view.findViewById(R.id.error_details);
|
||||
textDetails.setText(error);
|
||||
TextView textMessage = view.findViewById(R.id.error_message);
|
||||
textMessage.setText(message);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(R.string.details, (dialog1, which) -> {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
})
|
||||
.create();
|
||||
|
||||
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
listener.onSlotResult(slot, obj.getCipher());
|
||||
dialog.dismiss();
|
||||
}
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(v -> {
|
||||
if (textDetails.getVisibility() == View.GONE) {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
button.setText(R.string.copy);
|
||||
} else {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard != null) {
|
||||
ClipData clip = ClipData.newPlainText("text/plain", error);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
}
|
||||
}));
|
||||
public static void showTimeSyncWarningDialog(Context context, Dialog.OnClickListener listener) {
|
||||
Preferences prefs = new Preferences(context);
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
|
||||
|
||||
helper.get().startListening(obj);
|
||||
showSecureDialog(dialog);
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.time_sync_warning_title)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog, which);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
}
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
|
||||
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
|
||||
String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new);
|
||||
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
|
||||
TextView helpText = view.findViewById(R.id.text_importer_help);
|
||||
setImporterHelpText(helpText, importers.get(0), isDirect);
|
||||
ListView listView = view.findViewById(R.id.list_importers);
|
||||
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
|
||||
listView.setItemChecked(0, true);
|
||||
listView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
setImporterHelpText(helpText, importers.get(position), isDirect);
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.choose_application)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition()));
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
|
||||
if (isDirect) {
|
||||
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
|
||||
} else {
|
||||
view.setText(definition.getHelp());
|
||||
}
|
||||
}
|
||||
|
||||
public interface CheckboxInputListener {
|
||||
void onCheckboxInputResult(boolean checkbox);
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
@@ -238,4 +434,8 @@ public class Dialogs {
|
||||
void onSlotResult(Slot slot, Cipher cipher);
|
||||
void onException(Exception e);
|
||||
}
|
||||
|
||||
public interface ImporterListener {
|
||||
void onImporterSelectionResult(DatabaseImporter.Definition definition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -22,49 +23,53 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
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.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
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
|
||||
private boolean _hasChangedIcon = false;
|
||||
private boolean _isEditingIcon;
|
||||
private CircleImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
@@ -88,23 +93,29 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private RelativeLayout _advancedSettingsHeader;
|
||||
private RelativeLayout _advancedSettings;
|
||||
|
||||
private VaultManager _vault;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
|
||||
_vault = getApp().getVaultManager();
|
||||
_groups = _vault.getGroups();
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// retrieve info from the calling activity
|
||||
Intent intent = getIntent();
|
||||
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
|
||||
_isNew = intent.getBooleanExtra("isNew", false);
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
if (_isNew) {
|
||||
setTitle(R.string.add_new_profile);
|
||||
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
|
||||
if (entryUUID != null) {
|
||||
_origEntry = _vault.getEntryByUUID(entryUUID);
|
||||
} else {
|
||||
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
|
||||
_isNew = true;
|
||||
setTitle(R.string.add_new_entry);
|
||||
}
|
||||
|
||||
String selectedGroup = intent.getStringExtra("selectedGroup");
|
||||
@@ -157,14 +168,14 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
|
||||
}
|
||||
_textDigits.setText(Integer.toString(info.getDigits()));
|
||||
|
||||
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
String secretString = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretString);
|
||||
}
|
||||
|
||||
String type = _origEntry.getInfo().getType();
|
||||
@@ -184,9 +195,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String type = _spinnerType.getSelectedItem().toString();
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
|
||||
switch (type) {
|
||||
case TotpInfo.ID:
|
||||
case SteamInfo.ID:
|
||||
_rowCounter.setVisibility(View.GONE);
|
||||
@@ -197,7 +207,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,11 +248,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
});
|
||||
|
||||
_iconView.setOnClickListener(v -> {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
|
||||
startIconSelectionActivity();
|
||||
});
|
||||
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
@@ -250,7 +256,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if (_isNew) {
|
||||
openAdvancedSettings();
|
||||
|
||||
setGroup(selectedGroup);
|
||||
}
|
||||
}
|
||||
@@ -263,6 +268,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
|
||||
_spinnerGroup.setSelection(pos + 1, false);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
@@ -319,8 +325,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(false);
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicReference<String> msg = new AtomicReference<>();
|
||||
AtomicReference<DatabaseEntry> entry = new AtomicReference<>();
|
||||
AtomicReference<VaultEntry> entry = new AtomicReference<>();
|
||||
|
||||
try {
|
||||
entry.set(parseEntry());
|
||||
@@ -343,7 +354,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
finish(entry.get(), false);
|
||||
addAndFinish(entry.get());
|
||||
},
|
||||
(dialog, which) -> super.onBackPressed()
|
||||
);
|
||||
@@ -360,9 +371,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
break;
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
|
||||
finish(_origEntry, true);
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelectionActivity();
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
@@ -375,6 +389,56 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startIconSelectionActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
fileIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(v -> {
|
||||
stopEditingIcon(true);
|
||||
});
|
||||
|
||||
_isEditingIcon = true;
|
||||
}
|
||||
|
||||
private void stopEditingIcon(boolean save) {
|
||||
if (save) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
}
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = _hasCustomIcon || save;
|
||||
_hasChangedIcon = save;
|
||||
_isEditingIcon = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_edit, menu);
|
||||
@@ -388,46 +452,40 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void finish(DatabaseEntry entry, boolean delete) {
|
||||
private void addAndFinish(VaultEntry entry) {
|
||||
// It's possible that the new entry was already added to the vault, but writing the
|
||||
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
|
||||
// again would cause a crash in that case, so the isEntryDuplicate check prevents
|
||||
// that.
|
||||
if (_isNew && !_vault.isEntryDuplicate(entry)) {
|
||||
_vault.addEntry(entry);
|
||||
} else {
|
||||
_vault.replaceEntry(entry);
|
||||
}
|
||||
|
||||
saveAndFinish(entry, false);
|
||||
}
|
||||
|
||||
private void deleteAndFinish(VaultEntry entry) {
|
||||
_vault.removeEntry(entry);
|
||||
saveAndFinish(entry, true);
|
||||
}
|
||||
|
||||
private void saveAndFinish(VaultEntry entry, boolean delete) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
intent.putExtra("delete", delete);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
||||
if (saveVault(true)) {
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data.getData())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
}
|
||||
});
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -441,7 +499,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.");
|
||||
}
|
||||
@@ -458,8 +516,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
|
||||
} catch (Base32Exception e) {
|
||||
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
|
||||
secret = GoogleAuthInfo.parseSecret(secretString);
|
||||
if (secret.length == 0) {
|
||||
throw new ParseException("Secret cannot be empty");
|
||||
}
|
||||
} catch (EncodingException e) {
|
||||
throw new ParseException("Secret is not valid base32.");
|
||||
}
|
||||
|
||||
@@ -482,7 +544,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
info = new HotpInfo(secret, algo, digits, counter);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
|
||||
info.setDigits(digits);
|
||||
@@ -491,7 +553,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
throw new ParseException("The entered info is incorrect: " + e.getMessage());
|
||||
}
|
||||
|
||||
DatabaseEntry entry = Cloner.clone(_origEntry);
|
||||
VaultEntry entry = Cloner.clone(_origEntry);
|
||||
entry.setInfo(info);
|
||||
entry.setIssuer(_textIssuer.getText().toString());
|
||||
entry.setName(_textName.getText().toString());
|
||||
@@ -528,7 +590,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private boolean onSave() {
|
||||
DatabaseEntry entry;
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(true);
|
||||
}
|
||||
|
||||
VaultEntry entry;
|
||||
try {
|
||||
entry = parseEntry();
|
||||
} catch (ParseException e) {
|
||||
@@ -536,7 +602,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
finish(entry, false);
|
||||
addAndFinish(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
public static void exitAppAndRemoveFromRecents(Context context) {
|
||||
Intent intent = new Intent(context, ExitActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK |
|
||||
Intent.FLAG_ACTIVITY_NO_ANIMATION |
|
||||
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
@@ -11,7 +12,6 @@ import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -19,6 +19,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private TreeSet<String> _groups;
|
||||
private RecyclerView _slotsView;
|
||||
private View _emptyStateView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -29,21 +31,35 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
// set up the recycler view
|
||||
_adapter = new GroupAdapter(this);
|
||||
RecyclerView slotsView = findViewById(R.id.list_slots);
|
||||
_slotsView= findViewById(R.id.list_slots);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
slotsView.setLayoutManager(layoutManager);
|
||||
slotsView.setAdapter(_adapter);
|
||||
slotsView.setNestedScrollingEnabled(false);
|
||||
_slotsView.setLayoutManager(layoutManager);
|
||||
_slotsView.setAdapter(_adapter);
|
||||
_slotsView.setNestedScrollingEnabled(false);
|
||||
|
||||
for (String group : _groups) {
|
||||
_adapter.addGroup(group);
|
||||
}
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_slotsView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_slotsView.setVisibility(View.GONE);
|
||||
_emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +70,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
_groups.remove(group);
|
||||
_adapter.removeGroup(group);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
|
||||
@@ -1,203 +1,99 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
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.ui.slides.CustomAuthenticatedSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
import com.github.paolorotolo.appintro.AppIntro2;
|
||||
import com.github.paolorotolo.appintro.AppIntroFragment;
|
||||
import com.github.paolorotolo.appintro.model.SliderPage;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
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;
|
||||
|
||||
private CustomAuthenticatedSlide _authenticatedSlide;
|
||||
private CustomAuthenticationSlide _authenticationSlide;
|
||||
private Fragment _endSlide;
|
||||
|
||||
private Database _database;
|
||||
private DatabaseFile _databaseFile;
|
||||
private PasswordSlot _passwordSlot;
|
||||
private Cipher _passwordCipher;
|
||||
|
||||
private Preferences _prefs;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
|
||||
|
||||
public class IntroActivity extends IntroBaseActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// set FLAG_SECURE on the window of every IntroActivity
|
||||
_prefs = new Preferences(this);
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
setWizardMode(true);
|
||||
showSkipButton(false);
|
||||
pager.setPagingEnabled(false);
|
||||
//showPagerIndicator(false);
|
||||
setGoBackLock(true);
|
||||
setBarColor(getResources().getColor(R.color.colorPrimary));
|
||||
|
||||
SliderPage homeSliderPage = new SliderPage();
|
||||
homeSliderPage.setTitle(getString(R.string.welcome));
|
||||
homeSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setDescription(getString(R.string.app_description));
|
||||
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(AppIntroFragment.newInstance(homeSliderPage));
|
||||
|
||||
_authenticationSlide = new CustomAuthenticationSlide();
|
||||
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
//_authenticationSlide.setDescColor(getResources().getColor(R.color.primary_text_dark));
|
||||
addSlide(_authenticationSlide);
|
||||
_authenticatedSlide = new CustomAuthenticatedSlide();
|
||||
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(_authenticatedSlide);
|
||||
|
||||
SliderPage endSliderPage = new SliderPage();
|
||||
endSliderPage.setTitle(getString(R.string.setup_completed));
|
||||
endSliderPage.setDescription(getString(R.string.setup_completed_description));
|
||||
endSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
_endSlide = AppIntroFragment.newInstance(endSliderPage);
|
||||
addSlide(_endSlide);
|
||||
|
||||
// create the database and database file
|
||||
_database = new Database();
|
||||
_databaseFile = new DatabaseFile();
|
||||
}
|
||||
|
||||
private void setException(Exception e) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("exception", e);
|
||||
setResult(RESULT_EXCEPTION, result);
|
||||
finish();
|
||||
addSlide(WelcomeSlide.class);
|
||||
addSlide(SecurityPickerSlide.class);
|
||||
addSlide(SecuritySetupSlide.class);
|
||||
addSlide(DoneSlide.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
|
||||
Intent intent = getIntent();
|
||||
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_passwordSlot = new PasswordSlot();
|
||||
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
|
||||
new DerivationTask(this, this).execute(params);
|
||||
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
|
||||
// skip to the last slide if no encryption will be used
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// TODO: no magic indices
|
||||
getPager().setCurrentItem(4);
|
||||
}
|
||||
}
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDonePressed(Fragment currentFragment) {
|
||||
super.onDonePressed(currentFragment);
|
||||
|
||||
int cryptType = _authenticatedSlide.getCryptType();
|
||||
// wait for the key derivation background task
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
|
||||
(_passwordSlot == null || _passwordCipher == null)) {
|
||||
return;
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
if (oldSlide == SecurityPickerSlide.class
|
||||
&& newSlide == SecuritySetupSlide.class
|
||||
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
|
||||
skipToSlide(DoneSlide.class);
|
||||
return true;
|
||||
}
|
||||
|
||||
// generate the master key
|
||||
DatabaseFileCredentials creds = null;
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
creds = new DatabaseFileCredentials();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDonePressed() {
|
||||
Bundle state = getState();
|
||||
|
||||
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
|
||||
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
|
||||
if (cryptType == CRYPT_TYPE_INVALID
|
||||
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|
||||
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|
||||
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
|
||||
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
|
||||
}
|
||||
|
||||
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
|
||||
if (_passwordSlot == null || _passwordCipher == null) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
try {
|
||||
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
|
||||
creds.getSlots().add(_passwordSlot);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
|
||||
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);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// finally, save the database
|
||||
Vault vault = new Vault();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
try {
|
||||
JSONObject obj = _database.toJson();
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_databaseFile.setContent(obj);
|
||||
JSONObject obj = vault.toJson();
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
vaultFile.setContent(obj);
|
||||
} else {
|
||||
_databaseFile.setContent(obj, creds);
|
||||
vaultFile.setContent(obj, creds);
|
||||
}
|
||||
DatabaseManager.save(getApplicationContext(), _databaseFile);
|
||||
} catch (DatabaseManagerException | DatabaseFileException e) {
|
||||
setException(e);
|
||||
|
||||
VaultManager.save(getApplicationContext(), vaultFile);
|
||||
} catch (VaultManagerException | VaultFileException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent result = new Intent();
|
||||
result.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, result);
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
getApp().initVaultManager(vault, null);
|
||||
} else {
|
||||
getApp().initVaultManager(vault, creds);
|
||||
}
|
||||
|
||||
// skip the intro from now on
|
||||
_prefs.setIntroDone(true);
|
||||
getPreferences().setIntroDone(true);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SecretKey key) {
|
||||
if (key != null) {
|
||||
try {
|
||||
_passwordCipher = Slot.createEncryptCipher(key);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
} else {
|
||||
setException(new NullPointerException());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,77 +10,85 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
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.BitmapHelper;
|
||||
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.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Reader;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.mikepenz.iconics.context.IconicsContextWrapper;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
private static final int CODE_SCAN = 0;
|
||||
private static final int CODE_ADD_ENTRY = 1;
|
||||
private static final int CODE_EDIT_ENTRY = 2;
|
||||
private static final int CODE_ENTER_ENTRY = 3;
|
||||
private static final int CODE_DO_INTRO = 4;
|
||||
private static final int CODE_DECRYPT = 5;
|
||||
private static final int CODE_PREFERENCES = 6;
|
||||
private static final int CODE_SCAN_IMAGE = 7;
|
||||
private static final int CODE_DO_INTRO = 3;
|
||||
private static final int CODE_DECRYPT = 4;
|
||||
private static final int CODE_PREFERENCES = 5;
|
||||
private static final int CODE_SCAN_IMAGE = 6;
|
||||
|
||||
// permission request codes
|
||||
private static final int CODE_PERM_CAMERA = 0;
|
||||
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 boolean _isAuthenticating;
|
||||
private boolean _isDoingIntro;
|
||||
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private ActionMode _actionMode;
|
||||
|
||||
private Menu _menu;
|
||||
private SearchView _searchView;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
private EntryListView _entryListView;
|
||||
private LinearLayout _btnBackupError;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
@@ -89,28 +97,35 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
_db = _app.getDatabaseManager();
|
||||
_vault = _app.getVaultManager();
|
||||
_loaded = false;
|
||||
|
||||
// set up the main view
|
||||
setContentView(R.layout.activity_main);
|
||||
if (savedInstanceState != null) {
|
||||
_isAuthenticating = savedInstanceState.getBoolean("isAuthenticating");
|
||||
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
|
||||
}
|
||||
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
|
||||
_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);
|
||||
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
|
||||
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
|
||||
});
|
||||
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
@@ -121,7 +136,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startScanActivity();
|
||||
});
|
||||
|
||||
_btnBackupError = findViewById(R.id.btn_backup_error);
|
||||
_btnBackupError.setOnClickListener(view -> {
|
||||
startPreferencesActivity("pref_backups");
|
||||
});
|
||||
|
||||
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("isAuthenticating", _isAuthenticating);
|
||||
outState.putBoolean("isDoingIntro", _isDoingIntro);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -147,50 +175,39 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return super.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
|
||||
doShortcutActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
_isAuthenticating = false;
|
||||
_isDoingIntro = false;
|
||||
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _db.isLocked()) {
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN:
|
||||
onScanResult(resultCode, data);
|
||||
onScanResult(data);
|
||||
break;
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(resultCode, data);
|
||||
onAddEntryResult(data);
|
||||
break;
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ENTER_ENTRY:
|
||||
onEnterEntryResult(resultCode, data);
|
||||
onEditEntryResult(data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
onDoIntroResult();
|
||||
break;
|
||||
case CODE_DECRYPT:
|
||||
onDecryptResult(resultCode, data);
|
||||
onDecryptResult();
|
||||
break;
|
||||
case CODE_PREFERENCES:
|
||||
onPreferencesResult(resultCode, data);
|
||||
onPreferencesResult(data);
|
||||
break;
|
||||
case CODE_SCAN_IMAGE:
|
||||
onScanImageResult(resultCode, data);
|
||||
onScanImageResult(data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,99 +227,109 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult(int resultCode, Intent data) {
|
||||
private void onPreferencesResult(Intent data) {
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.refresh(true);
|
||||
if (_loaded) {
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
int codeGroupSize = getPreferences().getCodeGroupSize();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
|
||||
_entryListView.refresh(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
|
||||
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("entry", entry != null ? entry : DatabaseEntry.getDefault());
|
||||
intent.putExtra("isNew", isNew);
|
||||
if (isNew) {
|
||||
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
|
||||
} else {
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
}
|
||||
intent.putExtra("selectedGroup", _selectedGroup);
|
||||
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) 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");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) 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);
|
||||
_entryListView.replaceEntry(oldEntry, entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnterEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(int resultCode, Intent intent) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
private void onScanResult(Intent data) {
|
||||
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vault.addEntry(entry);
|
||||
if (_loaded) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new MultiFormatReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
DatabaseEntry entry = new DatabaseEntry(info);
|
||||
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
saveVault(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(Intent data) {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(Intent intent) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
bitmap = BitmapHelper.resize(bitmap, 640, 480);
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new QRCodeReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +344,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);
|
||||
@@ -330,6 +357,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);
|
||||
}
|
||||
|
||||
@@ -344,33 +375,26 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setGroupFilter(group, true);
|
||||
}
|
||||
|
||||
private void addEntry(DatabaseEntry entry) {
|
||||
_db.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
private void onDoIntroResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void onDoIntroResult(int resultCode, Intent data) {
|
||||
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
|
||||
// TODO: user feedback
|
||||
Exception e = (Exception) data.getSerializableExtra("exception");
|
||||
throw new RuntimeException(e);
|
||||
private void checkTimeSyncSetting() {
|
||||
boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1;
|
||||
if (!autoTime && getPreferences().isTimeSyncWarningEnabled()) {
|
||||
Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> {
|
||||
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockDatabase(creds);
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
|
||||
boolean unlocked = unlockDatabase(creds);
|
||||
|
||||
// save the database in case a slot was repaired
|
||||
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
|
||||
_db.setCredentials(creds);
|
||||
saveDatabase();
|
||||
}
|
||||
|
||||
doShortcutActions();
|
||||
private void onDecryptResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
@@ -383,21 +407,27 @@ 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/*");
|
||||
|
||||
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
fileIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
startActivityForResult(chooserIntent, CODE_SCAN_IMAGE);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity(String preference) {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
intent.putExtra("pref", preference);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
}
|
||||
|
||||
private void doShortcutActions() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getStringExtra("action");
|
||||
if (action == null || _db.isLocked()) {
|
||||
if (action == null || _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,49 +441,63 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void handleDeeplink() {
|
||||
if (_db.isLocked()) {
|
||||
if (_app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
Uri uri = intent.getData();
|
||||
getIntent().setData(null);
|
||||
getIntent().setAction(null);
|
||||
Uri uri = intent.getData();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction()) && uri != null) {
|
||||
intent.setData(null);
|
||||
intent.setAction(null);
|
||||
|
||||
GoogleAuthInfo info = null;
|
||||
try {
|
||||
info = GoogleAuthInfo.parseUri(uri);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
DatabaseEntry entry = new DatabaseEntry(info);
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
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 == null) {
|
||||
// start the intro if the vault file doesn't exist
|
||||
if (!_isDoingIntro && !VaultManager.fileExists(this)) {
|
||||
if (getPreferences().isIntroDone()) {
|
||||
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
_isDoingIntro = true;
|
||||
return;
|
||||
} else {
|
||||
unlockDatabase(null);
|
||||
}
|
||||
|
||||
// read the vault from disk
|
||||
// if this fails, show the error to the user and close the app
|
||||
try {
|
||||
VaultFile vaultFile = _app.loadVaultFile();
|
||||
if (!vaultFile.isEncrypted()) {
|
||||
_vault = _app.initVaultManager(vaultFile, null);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog1, which) -> finish());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_app.isVaultLocked()) {
|
||||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
if (_menu != null) {
|
||||
@@ -464,11 +508,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.refresh(false);
|
||||
} else {
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
handleDeeplink();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
updateBackupErrorBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -483,24 +529,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return;
|
||||
}
|
||||
|
||||
if (_app.isAutoLockEnabled()) {
|
||||
_app.lock();
|
||||
if (_app.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
|
||||
_app.lock(false);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(IconicsContextWrapper.wrap(newBase));
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -550,8 +593,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
startPreferencesActivity(null);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
@@ -560,7 +602,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return true;
|
||||
}
|
||||
case R.id.action_lock:
|
||||
_app.lock();
|
||||
_app.lock(true);
|
||||
return true;
|
||||
default:
|
||||
if (item.getGroupId() == R.id.action_filter_group) {
|
||||
@@ -608,101 +650,97 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private boolean unlockDatabase(DatabaseFileCredentials creds) {
|
||||
try {
|
||||
if (!_db.isLoaded()) {
|
||||
_db.load();
|
||||
}
|
||||
if (_db.isLocked()) {
|
||||
if (creds == null) {
|
||||
startAuthActivity();
|
||||
return false;
|
||||
} else {
|
||||
_db.unlock(creds);
|
||||
}
|
||||
}
|
||||
} catch (DatabaseManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
|
||||
startAuthActivity();
|
||||
return false;
|
||||
}
|
||||
|
||||
loadEntries();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
// load all entries
|
||||
List<DatabaseEntry> entries = new ArrayList<DatabaseEntry>(_db.getEntries());
|
||||
_entryListView.addEntries(entries);
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
if (!_loaded) {
|
||||
_entryListView.addEntries(_vault.getEntries());
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void startAuthActivity() {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("slots", _db.getFileHeader().getSlots());
|
||||
intent.putExtra("requiresUnlock", false);
|
||||
intent.putExtra("cancelAction", CancelAction.KILL);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
}
|
||||
|
||||
private void saveDatabase() {
|
||||
try {
|
||||
_db.save();
|
||||
} catch (DatabaseManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
private void startAuthActivity(boolean inhibitBioPrompt) {
|
||||
if (!_isAuthenticating) {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
_isAuthenticating = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockIcon() {
|
||||
// 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 && !_app.isVaultLocked()) {
|
||||
MenuItem item = _menu.findItem(R.id.action_lock);
|
||||
item.setVisible(_db.isEncryptionEnabled());
|
||||
item.setVisible(_vault.isEncryptionEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupErrorBar() {
|
||||
String error = null;
|
||||
if (_app.getPreferences().isBackupsEnabled()) {
|
||||
error = _app.getPreferences().getBackupsError();
|
||||
}
|
||||
|
||||
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(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();
|
||||
}
|
||||
|
||||
@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(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(DatabaseEntry entry) {
|
||||
saveDatabase();
|
||||
public void onEntryChange(VaultEntry entry) {
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
public void onEntryCopy(VaultEntry entry) {
|
||||
copyEntryCode(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -711,7 +749,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocked() {
|
||||
public void onListChange() { _fabScrollHelper.setVisible(true); }
|
||||
|
||||
@Override
|
||||
public void onLocked(boolean userInitiated) {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
@@ -719,11 +760,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
|
||||
if (isOpen()) {
|
||||
startAuthActivity();
|
||||
if (userInitiated) {
|
||||
startAuthActivity(true);
|
||||
} else {
|
||||
super.onLocked(userInitiated);
|
||||
}
|
||||
}
|
||||
|
||||
super.onLocked();
|
||||
private void copyEntryCode(VaultEntry entry) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
|
||||
private class ActionModeCallbacks implements ActionMode.Callback {
|
||||
@@ -742,22 +789,44 @@ 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);
|
||||
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_share_qr:
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
authInfos.add(authInfo);
|
||||
}
|
||||
|
||||
intent.putExtra("authInfos", authInfos);
|
||||
startActivity(intent);
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntry(_selectedEntry);
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
|
||||
if (_selectedEntry.getGroup() != null) {
|
||||
if (!_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;
|
||||
@@ -767,7 +836,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.pins.GuardianProjectFDroidRSA2048;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import info.guardianproject.GuardianProjectRSA4096;
|
||||
import info.guardianproject.trustedintents.TrustedIntents;
|
||||
|
||||
public class PanicResponderActivity extends AegisActivity {
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Preferences prefs = getPreferences();
|
||||
|
||||
if(!prefs.isPanicTriggerEnabled()) {
|
||||
Toast.makeText(this, R.string.panic_trigger_ignore_toast, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
TrustedIntents trustedIntents = TrustedIntents.get(this);
|
||||
trustedIntents.addTrustedSigner(GuardianProjectRSA4096.class);
|
||||
trustedIntents.addTrustedSigner(GuardianProjectFDroidRSA2048.class);
|
||||
|
||||
Intent intent = trustedIntents.getIntentFromTrustedSender(this);
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
getApp().lock(false);
|
||||
VaultManager.deleteFile(this);
|
||||
finishApp();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void finishApp() {
|
||||
ExitActivity.exitAppAndRemoveFromRecents(this);
|
||||
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity {
|
||||
private PreferencesFragment _fragment;
|
||||
@@ -12,9 +11,30 @@ public class PreferencesActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
|
||||
} else {
|
||||
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
Intent intent = getIntent();
|
||||
String preference = intent.getStringExtra("pref");
|
||||
if (preference != null) {
|
||||
_fragment.scrollToPreference(preference);
|
||||
intent.removeExtra("pref");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,19 +61,15 @@ public class PreferencesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme);
|
||||
break;
|
||||
|
||||
case DARK:
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
break;
|
||||
|
||||
case AMOLED:
|
||||
setTheme(R.style.AppTheme_TrueBlack_Preferences);
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,82 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SquareFinderView;
|
||||
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
|
||||
private ProcessCameraProvider _cameraProvider;
|
||||
private ListenableFuture<ProcessCameraProvider> _cameraProviderFuture;
|
||||
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
private List<Integer> _lenses;
|
||||
private int _currentLens;
|
||||
|
||||
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
|
||||
private ZXingScannerView _scannerView;
|
||||
private Menu _menu;
|
||||
private int _facing = CAMERA_FACING_BACK;
|
||||
private PreviewView _previewView;
|
||||
|
||||
private int _batchId = 0;
|
||||
private int _batchIndex = -1;
|
||||
private List<VaultEntry> _entries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_scanner);
|
||||
|
||||
_scannerView = new ZXingScannerView(this) {
|
||||
@Override
|
||||
protected IViewFinder createViewFinderView(Context context) {
|
||||
return new SquareFinderView(context);
|
||||
_entries = new ArrayList<>();
|
||||
_lenses = new ArrayList<>();
|
||||
_previewView = findViewById(R.id.preview_view);
|
||||
|
||||
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
_cameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
_cameraProvider = _cameraProviderFuture.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
// if we're to believe the Android documentation, this should never happen
|
||||
// https://developer.android.com/training/camerax/preview#check-provider
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
_scannerView.setResultHandler(this);
|
||||
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
|
||||
|
||||
int camera = getRearCameraId();
|
||||
if (camera == -1) {
|
||||
camera = getFrontCameraId();
|
||||
if (camera == -1) {
|
||||
addCamera(CameraSelector.LENS_FACING_BACK);
|
||||
addCamera(CameraSelector.LENS_FACING_FRONT);
|
||||
if (_lenses.size() == 0) {
|
||||
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
_facing = CAMERA_FACING_FRONT;
|
||||
}
|
||||
_scannerView.startCamera(camera);
|
||||
_currentLens = _lenses.get(0);
|
||||
updateCameraIcon();
|
||||
|
||||
setContentView(_scannerView);
|
||||
bindPreview(_cameraProvider);
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
protected void onSetTheme() {
|
||||
setTheme(R.style.AppTheme_Fullscreen);
|
||||
}
|
||||
|
||||
@@ -66,72 +84,43 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
getMenuInflater().inflate(R.menu.menu_scanner, menu);
|
||||
updateCameraIcon();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_camera:
|
||||
_scannerView.stopCamera();
|
||||
switch (_facing) {
|
||||
case CAMERA_FACING_BACK:
|
||||
_facing = CAMERA_FACING_FRONT;
|
||||
break;
|
||||
case CAMERA_FACING_FRONT:
|
||||
_facing = CAMERA_FACING_BACK;
|
||||
break;
|
||||
}
|
||||
updateCameraIcon();
|
||||
_scannerView.startCamera(getCameraId(_facing));
|
||||
return true;
|
||||
case R.id.action_lock:
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.action_camera) {
|
||||
_cameraProvider.unbindAll();
|
||||
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
|
||||
bindPreview(_cameraProvider);
|
||||
updateCameraIcon();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
_scannerView.startCamera(getCameraId(_facing));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
_scannerView.stopCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
private void addCamera(int lens) {
|
||||
try {
|
||||
// parse google auth uri
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
|
||||
DatabaseEntry entry = new DatabaseEntry(info);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
|
||||
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
|
||||
if (_cameraProvider.hasCamera(camera)) {
|
||||
_lenses.add(lens);
|
||||
}
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
}
|
||||
|
||||
private void updateCameraIcon() {
|
||||
if (_menu != null) {
|
||||
MenuItem item = _menu.findItem(R.id.action_camera);
|
||||
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
|
||||
boolean dual = _lenses.size() > 1;
|
||||
if (dual) {
|
||||
switch (_facing) {
|
||||
case CAMERA_FACING_BACK:
|
||||
switch (_currentLens) {
|
||||
case CameraSelector.LENS_FACING_BACK:
|
||||
item.setIcon(R.drawable.ic_camera_front_24dp);
|
||||
break;
|
||||
case CAMERA_FACING_FRONT:
|
||||
case CameraSelector.LENS_FACING_FRONT:
|
||||
item.setIcon(R.drawable.ic_camera_rear_24dp);
|
||||
break;
|
||||
}
|
||||
@@ -140,23 +129,76 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
}
|
||||
}
|
||||
|
||||
private static int getCameraId(int facing) {
|
||||
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
|
||||
Camera.getCameraInfo(i, info);
|
||||
if (info.facing == facing) {
|
||||
return i;
|
||||
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
||||
Preview preview = new Preview.Builder().build();
|
||||
preview.setSurfaceProvider(_previewView.getSurfaceProvider());
|
||||
|
||||
CameraSelector selector = new CameraSelector.Builder()
|
||||
.requireLensFacing(_currentLens)
|
||||
.build();
|
||||
|
||||
ImageAnalysis analysis = new ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QrCodeAnalyzer(this));
|
||||
|
||||
cameraProvider.bindToLifecycle(this, selector, preview, analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrCodeDetected(Result result) {
|
||||
try {
|
||||
Uri uri = Uri.parse(result.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
}
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, ((dialog, which) -> bindPreview(_cameraProvider)));
|
||||
_cameraProvider.unbindAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
List<VaultEntry> entries = new ArrayList<>();
|
||||
entries.add(new VaultEntry(info));
|
||||
finish(entries);
|
||||
}
|
||||
|
||||
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
|
||||
|
||||
if (_batchId == 0) {
|
||||
_batchId = export.getBatchId();
|
||||
}
|
||||
|
||||
return -1;
|
||||
int batchIndex = export.getBatchIndex();
|
||||
if (_batchId != export.getBatchId()) {
|
||||
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
|
||||
for (GoogleAuthInfo info : export.getEntries()) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
_batchIndex = batchIndex;
|
||||
if (_batchIndex + 1 == export.getBatchSize()) {
|
||||
finish(_entries);
|
||||
}
|
||||
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.google_qr_export_scanned, export.getBatchSize(), _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex != batchIndex) {
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static int getRearCameraId() {
|
||||
return getCameraId(CAMERA_FACING_BACK);
|
||||
}
|
||||
|
||||
private static int getFrontCameraId() {
|
||||
return getCameraId(CAMERA_FACING_FRONT);
|
||||
private void finish(List<VaultEntry> entries) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entries", (ArrayList<VaultEntry>) entries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -16,6 +10,12 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
@@ -25,10 +25,12 @@ import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class SelectEntriesActivity extends AegisActivity {
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
private boolean _vaultContainsEntries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -57,6 +59,7 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
Intent intent = getIntent();
|
||||
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
|
||||
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
|
||||
_vaultContainsEntries = intent.getBooleanExtra("vaultContainsEntries", false);
|
||||
|
||||
for (ImportEntry entry : entries) {
|
||||
_adapter.addEntry(entry);
|
||||
@@ -67,14 +70,20 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
FloatingActionButton fabMenu = findViewById(R.id.fab);
|
||||
fabMenu.setOnClickListener(v -> returnSelectedEntries());
|
||||
fabMenu.setOnClickListener(v -> {
|
||||
if (_vaultContainsEntries) {
|
||||
showWipeEntriesDialog();
|
||||
} else {
|
||||
returnSelectedEntries(false);
|
||||
}
|
||||
});
|
||||
_fabScrollHelper = new FabScrollHelper(fabMenu);
|
||||
}
|
||||
|
||||
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(getString(R.string.import_error_dialog, errors.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
|
||||
.create());
|
||||
@@ -100,10 +109,19 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
.create());
|
||||
}
|
||||
|
||||
private void returnSelectedEntries() {
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
private void showWipeEntriesDialog() {
|
||||
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
|
||||
R.string.dialog_wipe_entries_message,
|
||||
R.string.dialog_wipe_entries_checkbox,
|
||||
this::returnSelectedEntries
|
||||
);
|
||||
}
|
||||
|
||||
private void returnSelectedEntries(boolean wipeEntries) {
|
||||
Intent intent = new Intent();
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
intent.putExtra("wipeEntries", wipeEntries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -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,14 +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 -> {
|
||||
if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
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
|
||||
@@ -61,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.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
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;
|
||||
@@ -88,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() {
|
||||
@@ -164,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<GoogleAuthInfo> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
private TextView _entriesCount;
|
||||
private Button _nextButton;
|
||||
private Button _previousButton;
|
||||
|
||||
private VaultManager _vault;
|
||||
private int _currentEntryCount = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
_vault = getApp().getVaultManager();
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_issuer = findViewById(R.id.tvIssuer);
|
||||
_accountName = findViewById(R.id.tvAccountName);
|
||||
_entriesCount = findViewById(R.id.tvEntriesCount);
|
||||
_nextButton = findViewById(R.id.btnNext);
|
||||
_previousButton = findViewById(R.id.btnPrevious);
|
||||
|
||||
if (getSupportActionBar() != null){
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
|
||||
|
||||
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
|
||||
_nextButton.setVisibility(controlVisibility);
|
||||
|
||||
_nextButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount < _authInfos.size()) {
|
||||
_previousButton.setVisibility(View.VISIBLE);
|
||||
_currentEntryCount++;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == _authInfos.size()) {
|
||||
_nextButton.setText(R.string.done);
|
||||
}
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
_previousButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount > 1 ) {
|
||||
_nextButton.setText(R.string.next);
|
||||
_currentEntryCount--;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == 1) {
|
||||
_previousButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
generateQR();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void generateQR() {
|
||||
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||
_issuer.setText(selectedEntry.getIssuer());
|
||||
_accountName.setText(selectedEntry.getAccountName());
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
QRCodeWriter writer = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = null;
|
||||
try {
|
||||
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
|
||||
} catch (WriterException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = bitMatrix.getWidth();
|
||||
int height = bitMatrix.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
_qrImage.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface IntroActivityInterface {
|
||||
/**
|
||||
* Navigate to the next slide.
|
||||
*/
|
||||
void goToNextSlide();
|
||||
|
||||
/**
|
||||
* Navigate to the previous slide.
|
||||
*/
|
||||
void goToPreviousSlide();
|
||||
|
||||
/**
|
||||
* Navigate to the slide of the given type.
|
||||
*/
|
||||
void skipToSlide(Class<? extends SlideFragment> type);
|
||||
|
||||
/**
|
||||
* Retrieves the state of the intro. The state is shared among all slides and is
|
||||
* properly restored after a configuration change. This method may only be called
|
||||
* after onAttach has been called.
|
||||
*/
|
||||
@NonNull
|
||||
Bundle getState();
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ui.AegisActivity;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class IntroBaseActivity extends AegisActivity implements IntroActivityInterface {
|
||||
private Bundle _state;
|
||||
private ViewPager2 _pager;
|
||||
private ScreenSlidePagerAdapter _adapter;
|
||||
private List<Class<? extends SlideFragment>> _slides;
|
||||
private WeakReference<SlideFragment> _currentSlide;
|
||||
|
||||
private ImageButton _btnPrevious;
|
||||
private ImageButton _btnNext;
|
||||
private SlideIndicator _slideIndicator;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_intro);
|
||||
|
||||
_slides = new ArrayList<>();
|
||||
_state = new Bundle();
|
||||
|
||||
_btnPrevious = findViewById(R.id.btnPrevious);
|
||||
_btnPrevious.setOnClickListener(v -> goToPreviousSlide());
|
||||
_btnNext = findViewById(R.id.btnNext);
|
||||
_btnNext.setOnClickListener(v -> goToNextSlide());
|
||||
_slideIndicator = findViewById(R.id.slideIndicator);
|
||||
|
||||
_adapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
|
||||
_pager = findViewById(R.id.pager);
|
||||
_pager.setAdapter(_adapter);
|
||||
_pager.setUserInputEnabled(false);
|
||||
_pager.registerOnPageChangeCallback(new SlideSkipBlocker());
|
||||
|
||||
View pagerChild = _pager.getChildAt(0);
|
||||
if (pagerChild instanceof RecyclerView) {
|
||||
pagerChild.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
_state = savedInstanceState.getBundle("introState");
|
||||
updatePagerControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBundle("introState", _state);
|
||||
}
|
||||
|
||||
void setCurrentSlide(SlideFragment slide) {
|
||||
_currentSlide = new WeakReference<>(slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToNextSlide() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
if (pos != _slides.size() - 1) {
|
||||
SlideFragment currentSlide = _currentSlide.get();
|
||||
if (currentSlide.isFinished()) {
|
||||
currentSlide.onSaveIntroState(_state);
|
||||
setPagerPosition(pos, 1);
|
||||
} else {
|
||||
currentSlide.onNotFinishedError();
|
||||
}
|
||||
} else {
|
||||
onDonePressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToPreviousSlide() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
if (pos != 0 && pos != _slides.size() - 1) {
|
||||
setPagerPosition(pos, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipToSlide(Class<? extends SlideFragment> type) {
|
||||
int i = _slides.indexOf(type);
|
||||
if (i == -1) {
|
||||
throw new IllegalStateException(String.format("Cannot skip to slide of type %s because it is not in the slide list", type.getName()));
|
||||
}
|
||||
|
||||
setPagerPosition(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a slide change is made. Overriding gives implementers the
|
||||
* opportunity to block a slide change. onSaveIntroState is guaranteed to have been
|
||||
* called on oldSlide before onBeforeSlideChanged is called.
|
||||
* @param oldSlide the slide that is currently shown.
|
||||
* @param newSlide the next slide that will be shown.
|
||||
* @return whether to block the transition.
|
||||
*/
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a slide change was made.
|
||||
* @param oldSlide the slide that was previously shown.
|
||||
* @param newSlide the slide that is now shown.
|
||||
*/
|
||||
protected void onAfterSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
|
||||
}
|
||||
|
||||
private void setPagerPosition(int pos) {
|
||||
Class<? extends SlideFragment> oldSlide = _currentSlide.get().getClass();
|
||||
Class<? extends SlideFragment> newSlide = _slides.get(pos);
|
||||
|
||||
if (!onBeforeSlideChanged(oldSlide, newSlide)) {
|
||||
_pager.setCurrentItem(pos);
|
||||
}
|
||||
onAfterSlideChanged(oldSlide, newSlide);
|
||||
|
||||
updatePagerControls();
|
||||
}
|
||||
|
||||
private void setPagerPosition(int pos, int delta) {
|
||||
pos += delta;
|
||||
setPagerPosition(pos);
|
||||
}
|
||||
|
||||
private void updatePagerControls() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
_btnPrevious.setVisibility(
|
||||
pos != 0 && pos != _slides.size() - 1
|
||||
? View.VISIBLE
|
||||
: View.INVISIBLE);
|
||||
if (pos == _slides.size() - 1) {
|
||||
_btnNext.setImageResource(R.drawable.circular_button_done);
|
||||
}
|
||||
_slideIndicator.setSlideCount(_slides.size());
|
||||
_slideIndicator.setCurrentSlide(pos);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Bundle getState() {
|
||||
return _state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
goToPreviousSlide();
|
||||
}
|
||||
|
||||
protected abstract void onDonePressed();
|
||||
|
||||
public void addSlide(Class<? extends SlideFragment> type) {
|
||||
if (_slides.contains(type)) {
|
||||
throw new IllegalStateException(String.format("Only one slide of type %s may be added to the intro", type.getName()));
|
||||
}
|
||||
|
||||
_slides.add(type);
|
||||
_slideIndicator.setSlideCount(_slides.size());
|
||||
}
|
||||
|
||||
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
|
||||
public ScreenSlidePagerAdapter(FragmentManager fm) {
|
||||
super(fm, getLifecycle());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
Class<? extends SlideFragment> type = _slides.get(position);
|
||||
|
||||
try {
|
||||
return type.newInstance();
|
||||
} catch (IllegalAccessException | InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _slides.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class SlideSkipBlocker extends ViewPager2.OnPageChangeCallback {
|
||||
@Override
|
||||
public void onPageScrollStateChanged(@ViewPager2.ScrollState int state) {
|
||||
// disable the buttons while scrolling to prevent disallowed skipping of slides
|
||||
boolean enabled = state == ViewPager2.SCROLL_STATE_IDLE;
|
||||
_btnNext.setEnabled(enabled);
|
||||
_btnPrevious.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class SlideFragment extends Fragment implements IntroActivityInterface {
|
||||
private WeakReference<IntroBaseActivity> _parent;
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(context instanceof IntroBaseActivity)) {
|
||||
throw new ClassCastException("Parent context is expected to be of type IntroBaseActivity");
|
||||
}
|
||||
|
||||
_parent = new WeakReference<>((IntroBaseActivity) context);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
getParent().setCurrentSlide(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether or not all required user actions are finished on this slide,
|
||||
* indicating that we're ready to move to the next slide.
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the user tried to move to the next slide, but isFinished returned false.
|
||||
*/
|
||||
protected void onNotFinishedError() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the SlideFragment is expected to write its state to the given shared
|
||||
* introState. This is only called if the user navigates to the next slide, not
|
||||
* when a previous slide is next to be shown.
|
||||
*/
|
||||
protected void onSaveIntroState(@NonNull Bundle introState) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToNextSlide() {
|
||||
getParent().goToNextSlide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToPreviousSlide() {
|
||||
getParent().goToPreviousSlide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipToSlide(Class<? extends SlideFragment> type) {
|
||||
getParent().skipToSlide(type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Bundle getState() {
|
||||
return getParent().getState();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private IntroBaseActivity getParent() {
|
||||
if (_parent == null || _parent.get() == null) {
|
||||
throw new IllegalStateException("This method must not be called before onAttach()");
|
||||
}
|
||||
|
||||
return _parent.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class SlideIndicator extends View {
|
||||
private Paint _paint;
|
||||
private int _slideCount;
|
||||
private int _slideIndex;
|
||||
|
||||
private float _dotRadius;
|
||||
private float _dotSeparator;
|
||||
private int _dotColor;
|
||||
private int _dotColorSelected;
|
||||
|
||||
public SlideIndicator(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
_paint = new Paint();
|
||||
_paint.setAntiAlias(true);
|
||||
_paint.setStyle(Paint.Style.FILL);
|
||||
|
||||
TypedArray array = null;
|
||||
try {
|
||||
array = context.obtainStyledAttributes(attrs, R.styleable.SlideIndicator);
|
||||
_dotRadius = array.getDimension(R.styleable.SlideIndicator_dot_radius, 5f);
|
||||
_dotSeparator = array.getDimension(R.styleable.SlideIndicator_dot_separation, 5f);
|
||||
_dotColor = array.getColor(R.styleable.SlideIndicator_dot_color, Color.GRAY);
|
||||
_dotColorSelected = array.getColor(R.styleable.SlideIndicator_dot_color_selected, Color.BLACK);
|
||||
} finally {
|
||||
if (array != null) {
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSlideCount(int slideCount) {
|
||||
if (slideCount < 0) {
|
||||
throw new IllegalArgumentException("Slide count cannot be negative");
|
||||
}
|
||||
|
||||
_slideCount = slideCount;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setCurrentSlide(int index) {
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Slide index cannot be negative");
|
||||
}
|
||||
|
||||
if (index + 1 > _slideCount) {
|
||||
throw new IllegalStateException(String.format("Slide index out of range, slides: %d, index: %d", _slideCount, index));
|
||||
}
|
||||
|
||||
_slideIndex = index;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (_slideCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
float dotDp = density * _dotRadius * 2;
|
||||
float spaceDp = density * _dotSeparator;
|
||||
|
||||
float offset;
|
||||
if (_slideCount % 2 == 0) {
|
||||
offset = (spaceDp / 2) + (dotDp / 2) + dotDp * (_slideCount / 2f - 1) + spaceDp * (_slideCount / 2f - 1);
|
||||
} else {
|
||||
int spaces = _slideCount > 1 ? _slideCount - 2 : 0;
|
||||
offset = (_slideCount - 1) * (dotDp / 2) + spaces * spaceDp;
|
||||
}
|
||||
|
||||
canvas.translate((getWidth() / 2f) - offset,getHeight() / 2f);
|
||||
|
||||
for (int i = 0; i < _slideCount; i++) {
|
||||
int slideIndex = isRtl() ? (_slideCount - 1) - _slideIndex : _slideIndex;
|
||||
_paint.setColor(i == slideIndex ? _dotColorSelected : _dotColor);
|
||||
canvas.drawCircle(0,0, dotDp / 2, _paint);
|
||||
canvas.translate(dotDp + spaceDp,0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRtl() {
|
||||
return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user