Compare commits

..
2 Commits
Author SHA1 Message Date
Alexander Bakker 68bb2b7e17 Release v1.0.3 2019-12-05 18:49:03 +01:00
Alexander Bakker b1d9be10f2 Add support for the new andOTP backup format
This patch adds support for the new backup file format of andOTP. andOTP has
improved their security by switching from SHA-256 to PBKDF2 to derive the key
for encrypted backups.

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

See: andOTP/andOTP@d96b037.
(cherry picked from commit a5165e2345)
2019-12-05 18:33:32 +01:00
596 changed files with 9129 additions and 51748 deletions
-10
View File
@@ -1,10 +0,0 @@
project_id: "372633"
api_token: "<api-token-here>"
base_path: "../app/src/main"
base_url: "https://api.crowdin.com"
preserve_hierarchy: true
files:
- source: "res/values/strings.xml"
dest: "strings.xml"
translation: "res/values-%android_code%/%original_file_name%"
-4
View File
@@ -1,4 +0,0 @@
buy_me_a_coffee: beemdevelopment
custom:
- "https://www.blockchain.com/btc/address/bc1q26kyxqjkc6tu477pzy0whagwhs4ypv93qls22n"
- "https://nanocrawler.cc/explorer/account/nano_1aegisc559b1x4p3839egnu579jkd4htpidy14eo9e31gzqmwuafypnj4q94"
+33
View File
@@ -0,0 +1,33 @@
---
name: "Bug report"
about: "Create a report to help us fix a bug"
labels: bug
---
<!-- Please read the [bug reports section of the contribution
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
###### Info
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with fingerprint unlock)/Yes/No
* __Android version and ROM:__
###### Steps to reproduce
A detailed list of reproduction steps.
###### What do you expect to happen?
...
###### What happens instead?
...
###### Log
```
If applicable, paste the debug log here.
```
-89
View File
@@ -1,89 +0,0 @@
name: Bug Report
description: Create a report to help us fix a bug
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please read the [bug reports section of the contribution guidelines](https://github.com/beemdevelopment/Aegis/blob/master/CONTRIBUTING.md#bug-reports) before submitting an issue.
- type: input
id: version
attributes:
label: Version
description: Which version of Aegis are you using?
placeholder: "Example: v2.1"
validations:
required: true
- type: dropdown
id: source
attributes:
label: Source
description: Where did you get Aegis from?
options:
- Google Play
- F-Droid
- GitHub
- Other
validations:
required: true
- type: dropdown
id: encryption
attributes:
label: Vault encryption
description: Do you have encryption enabled for your Aegis vault?
options:
- "Yes (with biometric unlock)"
- "Yes"
- "No"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: Which device are you using Aegis on?
placeholder: "Example: Pixel 5"
validations:
required: true
- type: input
id: android_version
attributes:
label: Android version
description: Which Android version is running on your device?
placeholder: "Example: Android 13"
validations:
required: true
- type: input
id: rom
attributes:
label: ROM
description: Are you using a custom ROM? If so, which one and which version? If you're using the stock OS that came with your device, you can leave this field empty.
placeholder: "Example: GrapheneOS"
validations:
required: false
- type: textarea
id: reproduction_steps
attributes:
label: Steps to reproduce
description: A detailed list of reproduction steps.
validations:
required: true
- type: textarea
id: expectations
attributes:
label: What do you expect to happen?
validations:
required: true
- type: textarea
id: reality
attributes:
label: What happens instead?
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: If applicable, paste the debug log that you captured using ADB here.
validations:
required: false
-63
View File
@@ -1,63 +0,0 @@
name: build
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v4
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Build the app
run: ./gradlew build
- uses: actions/upload-artifact@v4
with:
name: apk
path: app/build/outputs/apk/debug/app-debug.apk
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Tests
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
with:
api-level: 31
arch: x86_64
profile: pixel_3a
heap-size: 512M
ram-size: 4096M
emulator-options: -memory 4096 -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
disk-size: 8G
script: |
mkdir -p artifacts/report
adb logcat -c
adb logcat -G 16M && adb logcat -g
./gradlew connectedCheck || touch tests_failing
adb logcat -d > artifacts/logcat.txt
cp -r app/build/reports/androidTests/connected/* artifacts/report/
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
test ! -f tests_failing
- uses: actions/upload-artifact@v4
if: always()
with:
name: instrumented-test-report
path: |
artifacts/*
if-no-files-found: ignore
-41
View File
@@ -1,41 +0,0 @@
name: codeql
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '25 16 * * 2'
jobs:
analyze:
name: analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Exclude paths
# The importers are excluded from analysis, because some of the apps Aegis
# can import from don't have such great crypto, which will cause false
# positive security alerts.
run: |
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: java
- name: Build
run: ./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
-31
View File
@@ -1,31 +0,0 @@
name: crowdin
on:
push:
branches:
- master
# run sequentially (per branch)
concurrency: "crowdin-upload-${{ github.ref }}"
jobs:
upload-sources:
runs-on: ubuntu-latest
if: github.repository == 'beemdevelopment/Aegis'
steps:
- uses: actions/checkout@v4
- name: Install crowdin-cli
run: |
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
unzip crowdin-cli.zip -d crowdin-cli
- name: Upload to Crowdin
env:
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
run: |
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
--no-progress \
--token "$CROWDIN_TOKEN" \
--project-id 372633 \
--base-path app/src/main \
--source res/values/strings.xml \
--translation "res/values-%android_code%/%original_file_name%" \
--dest strings.xml \
--branch master
-2
View File
@@ -39,5 +39,3 @@ captures/
# Keystore files
*.jks
crowdin.properties
.crowdin/config.yml
+7
View File
@@ -0,0 +1,7 @@
language: android
android:
components:
- android-28
- build-tools-28.0.3
script:
- ./gradlew build test
+14 -32
View File
@@ -1,42 +1,25 @@
# Contributing
Looking to contribute to Aegis? That's great! There are a couple of ways to help
out. This document contains some general guidelines for each type of
contribution.
Please review [the FAQ](FAQ.md) before reporting a bug, asking a question or
requesting a feature.
## Translations
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
translations of Aegis for lots of different languages. __Pull requests that
add/update a translation are no 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.
There are a couple of ways to contribute to Aegis. This document contains some
general guidelines for each type of contribution.
## Pull requests
If you're planning on adding a new feature or making other large changes, please
discuss it with us first through [a
proposal](https://github.com/beemdevelopment/Aegis/issues/new?labels=proposal&template=feature.md)
on GitHub. Discussing your idea with us first ensures that everyone is on the
same page before you start working on your change. We don't like rejecting pull
requests.
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.
## Bug reports
We use GitHub's issue tracker to track bugs. To make bug reports easier to
follow up on for us, please fill out the form as accurately as possible. If a
bug report does not contain enough information, it will be closed. Duplicate bug
reports receive the same treatment.
We use GitHub's issue tracker to track bugs. Bug reports 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.
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.
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.
### Capturing a log with ADB
@@ -98,7 +81,7 @@ __On your PC__:
```
adb logcat -c
adb logcat > debug.log
adb logcat -f debug.log
```
The logcat command captures the full system log by default, which may expose
@@ -108,9 +91,8 @@ __On your PC__:
one below:
```sh
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) > debug.log
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
```
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
``com.beemdevelopment.aegis.debug``._
-101
View File
@@ -1,101 +0,0 @@
# FAQ
## General
### How can I contribute?
There are lots of ways! Please refer to our [contributing
guide](https://github.com/beemdevelopment/Aegis/blob/master/CONTRIBUTING.md).
### Why is the latest version not on F-Droid yet?
We don't release new versions of Aegis on F-Droid ourselves. Once we've released
a new version on GitHub, F-Droid will usually kick off their automatic build
process a day later and publish the app to their repository a couple of days
afterwards. It can sometimes take up to a week for a new version to appear on
F-Droid.
### Can you port Aegis to iOS/Windows/MacOS/Browser Extension?
We don't have plans to port Aegis to other platforms.
### Can you add support for Autofill?
On Android, only one app can be active in the Autofill slot at a time, and since
this is typically occupied by the password manager, we don't see much value in
adding support for this feature in Aegis.
### What is the difference between exporting and backing up?
Exporting is done manually and backups are done automatically. The format of the
vault file is exactly the same for both.
## Security
### I can no longer use biometrics to unlock the app. What should I do?
If you could previously unlock Aegis with biometrics, but suddenly can't do so
anymore, this is probably caused by a change made to the security settings of
your device. The app will tell you when this happened in most cases. To resolve
this, unlock the app with your password, disable biometric unlock in the
settings of Aegis and re-enable it.
### Why does Aegis keep prompting me for my password, even though I have enabled biometric authentication?
You're probably encountering the password reminder. Try entering your password
to unlock the vault once. After that, Aegis will prompt for biometrics by
default again until it's time for another password reminder.
Since forgetting your password will result in loss of access to the contents of
the vault, __we do NOT recommend disabling the password reminder__.
### Aegis uses SHA1 for most/all of my tokens. Isn't that insecure?
The hash algorithm is imposed by the service you're setting up 2FA for (e.g.
Google, Facebook, GitHub, etc). There is nothing we can do about that. If we
were to change this on Aegis' end, the tokens would stop working. Furthermore,
when using SHA1 in an HMAC calculation, the currently known issues in SHA1 are
not of concern.
### Why doesn't Aegis support biometric unlock for my device, even though it works with other apps?
The reason for this is pretty technical. In short, since you're not entering
your password when using biometric unlock, Aegis needs some other way to decrypt
the vault. For this purpose, we generate and use a key in the Android Keystore,
telling it to only allow us to use that key if the user authenticates using
their biometrics first. Some devices have buggy implementations of this feature,
resulting in the error displayed to you by Aegis in an error dialog.
If biometrics works with other apps, but not with Aegis, that means those other
apps probably perform a weaker form of biometric authentication.
## Backups
### How can I back up my Aegis vault to the cloud automatically?
Aegis can only automatically back up to the cloud if the app of your cloud
provider is installed on your device and fully participates in the Android
Storage Access Framework. Aegis doesn't have access to the internet and we don't
have plans to change this, so adding support for specific cloud providers in the
app is not possible.
Cloud providers currently known to be supported:
- Nextcloud
Another common setup is to configure Aegis to back up to a folder on local
storage of your device and then have a separate app (like
[Syncthing](https://syncthing.net/)) sync that folder anywhere you want.
## Importing
### When importing from Authenticator Plus, an error is shown claiming that Accounts.txt is missing
Make sure you supply an Authenticator Plus export file obtained through
__Settings -> Backup & Restore -> Export as Text and HTML__. The ``.db`` format
is not supported.
If it still doesn't work, please report the issue to us. As a temporary
workaround, you can try extracting the ZIP archive on a computer, recreating it
without a password and then importing that into Aegis. Another option is
extracting the ZIP archive on a computer and importing the resulting
Accounts.txt file into Aegis with the "Plain text" import option.
+56 -93
View File
@@ -1,72 +1,58 @@
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
alt="App icon">
# Aegis
# Aegis Authenticator
Aegis is a free, secure and open source 2FA app for Android.
<br>
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
[![Build](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml/badge.svg)](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml?query=branch%3Amaster) [![Crowdin](https://badges.crowdin.net/aegis-authenticator/localized.svg)](https://crowdin.com/project/aegis-authenticator) [![Donate](https://img.shields.io/badge/donate-buy%20us%20a%20beer-%23FF813F)](https://www.buymeacoffee.com/beemdevelopment) [![Matrix](https://img.shields.io/matrix/aegis:matrix.org?color=blue)](https://matrix.to/#/#aegis:matrix.org)
**Aegis Authenticator** is a free, secure and open source 2FA app for Android.
It aims to provide a secure authenticator for your online services, while also
including some features missing in existing authenticator apps, like proper
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
thousands of services.
For a list of frequently asked questions, please check out [the FAQ](FAQ.md).
The security design of the app and the vault format is described in detail in
[this document](docs/vault.md).
## Features
# Features
- Free and open source
- Secure
- The vault is encrypted (AES-256-GCM), and can be unlocked with:
- Encryption (AES-256)
- Password (scrypt)
- Biometrics (Android Keystore)
- Fingerprint (Android Keystore)
- Screen capture prevention
- 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
- Tap to reveal ability
- Multiple ways to add new entries
- Scan QR code
- Enter details manually
- Import from other authenticator apps: 2FAS Authenticator, 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)
- 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)
- Compatible with Google Authenticator
- Organization
- Alphabetic/custom sorting
- Custom or automatically generated icons
- Group entries together
- Custom or default icons
- Drag and drop
- Custom groups
- Advanced entry editing
- Search by name/issuer
- Material design with multiple themes: Light, Dark, AMOLED
- Material design with multiple themes:
- Light theme
- Dark theme
- Amoled / true dark theme
- Export (plaintext or encrypted)
- Automatic backups of the vault to a location of your choosing
## Screenshots
[<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="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="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 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
[<img width=200 alt="Screenshot 8"
src="metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true)
[<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)
## Downloads
@@ -78,7 +64,7 @@ src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
[<img height="80" alt="Get it on F-Droid"
src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
/>](https://f-droid.org/app/com.beemdevelopment.aegis)
### Verification
APK releases on Google Play and GitHub are signed using the same key. They can
@@ -110,48 +96,25 @@ 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
```
### Icon packs
## Libraries
Aegis supports icon packs to make it easier to assign icons to the entries in
your vault. There are no official icon packs, but the community maintains a
number of third-party icon packs you may want to check out. To learn how to
create your own Aegis-compatible icon pack, see [the
documentation](docs/iconpacks.md).
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
Unofficial monochrome-styled 2FA icons.
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/).
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
\* The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
## 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).
- [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
## 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.
+47 -173
View File
@@ -1,210 +1,84 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
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 {
compileSdk 34
namespace packageName
compileSdkVersion 28
defaultConfig {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 34
versionCode 70
versionName "3.1.1"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
buildConfigField "java.util.concurrent.atomic.AtomicBoolean", "TEST", "new java.util.concurrent.atomic.AtomicBoolean(false)"
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 22
versionName "1.0.3"
}
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.all {
useJUnitPlatform()
unitTests {
all {
maxHeapSize "3g"
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
}
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
includeAndroidResources true
}
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
manifestPlaceholders = [
title: "AegisDev",
iconName: "ic_launcher_debug",
fileProviderAuthority: "${fileProviderAuthorityDebug}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
resValue "bool", "pref_secure_screen_default", "false"
}
release {
manifestPlaceholders = [
title: "Aegis",
iconName: "ic_launcher",
fileProviderAuthority: "${fileProviderAuthority}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
resValue "bool", "pref_secure_screen_default", "true"
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
resources {
excludes += ['/org/bouncycastle/pqc/**/*.properties']
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
lint {
abortOnError true
checkDependencies true
disable 'MissingQuantity', 'MissingTranslation'
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.25.1'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
aboutLibraries {
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
prettyPrint = true
configPath = "app/config"
fetchRemoteFunding = false
registerAndroidTasks = false
exclusionPatterns = [~"javax.annotation.*"]
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
}
dependencies {
def cameraxVersion = '1.3.1'
def glideVersion = '4.16.0'
def guavaVersion = '33.0.0'
def hiltVersion = '2.50'
def junitVersion = '4.13.2'
def libsuVersion = '5.2.2'
def roomVersion = "2.6.1"
annotationProcessor 'androidx.annotation:annotation:1.7.1'
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
annotationProcessor "androidx.room:room-compiler:$roomVersion"
def libsuVersion = '2.3.2'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.activity:activity:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation "androidx.core:core:1.12.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation "com.google.dagger:hilt-android:$hiltVersion"
implementation 'com.github.avito-tech:krop:0.52'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
transitive = false
}
implementation 'androidx.appcompat:appcompat:1.0.2'
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 '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.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
implementation 'com.google.zxing:core:3.5.2'
implementation("com.mikepenz:aboutlibraries:11.1.0") {
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
implementation 'com.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") {
transitive = false
}
implementation "com.mikepenz:aboutlibraries-core-android:11.1.0"
implementation 'com.nulab-inc:zxcvbn:1.8.2'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
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'
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'androidx.test:core:1.5.0'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "junit:junit:${junitVersion}"
testImplementation 'org.json:json:20231013'
testImplementation 'org.robolectric:robolectric:4.11.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"uniqueId": "com.github.avito-tech:krop",
"licenses": [
"MIT"
]
}
-6
View File
@@ -1,6 +0,0 @@
{
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
"licenses": [
"Apache-2.0"
]
}
-6
View File
@@ -1,6 +0,0 @@
{
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
"licenses": [
"MIT"
]
}
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
<issue id="NotificationPermission">
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
</issue>
<issue id="ResourceType" severity="Warning" />
</lint>
+15 -8
View File
@@ -1,10 +1,17 @@
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-dontobfuscate
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/alex/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.ui.fragments.preferences.*
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
# Add any project specific keep options here:
-dontwarn javax.naming.**
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
@@ -1,203 +0,0 @@
package com.beemdevelopment.aegis;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.ui.views.EntryHolder;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import java.lang.reflect.InvocationTargetException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.inject.Inject;
import dagger.hilt.android.testing.HiltAndroidRule;
public abstract class AegisTest {
public static final String VAULT_PASSWORD = "test";
public static final String VAULT_PASSWORD_CHANGED = "test2";
public static final String VAULT_BACKUP_PASSWORD = "something";
public static final String VAULT_BACKUP_PASSWORD_CHANGED = "something2";
@Rule
public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@Rule
public final GrantPermissionRule permRule = getGrantPermissionRule();
@Inject
protected VaultManager _vaultManager;
@Inject
protected Preferences _prefs;
@Before
public void init() {
hiltRule.inject();
}
private static GrantPermissionRule getGrantPermissionRule() {
List<String> perms = new ArrayList<>();
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
perms.add(Manifest.permission.POST_NOTIFICATIONS);
}*/
return GrantPermissionRule.grant(perms.toArray(new String[0]));
}
protected AegisApplicationBase getApp() {
return (AegisApplicationBase) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
protected VaultRepository initEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
return initVault(creds, VaultEntries.get());
}
protected VaultRepository initEmptyEncryptedVault() {
VaultFileCredentials creds = generateCredentials();
return initVault(creds, null);
}
protected VaultRepository initPlainVault() {
return initVault(null, VaultEntries.get());
}
protected VaultRepository initEmptyPlainVault() {
return initVault(null, null);
}
private VaultRepository initVault(@Nullable VaultFileCredentials creds, @Nullable List<VaultEntry> entries) {
VaultRepository vault;
try {
vault = _vaultManager.initNew(creds);
} catch (VaultRepositoryException e) {
throw new RuntimeException(e);
}
if (entries != null) {
for (VaultEntry entry : entries) {
_vaultManager.getVault().addEntry(entry);
}
}
try {
_vaultManager.save();
} catch (VaultRepositoryException e) {
throw new RuntimeException(e);
}
_prefs.setIntroDone(true);
return vault;
}
protected VaultFileCredentials generateCredentials() {
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);
return creds;
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
return generateEntry(type, name, issuer, 20);
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer, int secretLength) {
byte[] secret = CryptoUtils.generateRandomBytes(secretLength);
OtpInfo info;
try {
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();
}
};
}
@NonNull
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
@Override
public boolean matchesSafely(EntryHolder holder) {
return holder != null
&& holder.getEntry() != null
&& holder.getEntry().getInfo().getClass().equals(otpClass);
}
@Override
public void describeTo(Description description) {
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
}
};
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis;
import dagger.hilt.android.testing.CustomTestApplication;
@CustomTestApplication(AegisApplicationBase.class)
public interface AegisTestApplication {
}
@@ -1,40 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.Instrumentation;
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 {
static {
BuildConfig.TEST.set(true);
}
@Override
public Application newApplication(ClassLoader cl, String name, Context context)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Instrumentation.newApplication(AegisTestApplication_Application.class, context);
}
@Override
public void callApplicationOnCreate(Application app) {
Context context = app.getApplicationContext();
// clear internal storage so that there is no vault file
IOUtils.clearDirectory(context.getFilesDir(), false);
// clear preferences so that the intro is started from MainActivity
ApplicationProvider.getApplicationContext().getFilesDir();
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.clear()
.apply();
super.callApplicationOnCreate(app);
}
}
@@ -1,400 +0,0 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.GoogleAuthUriImporter;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.PreferencesActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class BackupExportTest extends AegisTest {
private final ActivityScenarioRule<PreferencesActivity> _activityRule = new ActivityScenarioRule<>(PreferencesActivity.class);
@Rule
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Before
public void setUp() {
Intents.init();
}
@After
public void tearDown() {
Intents.release();
}
@Test
public void testPlainVaultExportPlainJson() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testPlainVaultExportPlainTxt() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testPlainVaultExportEncryptedJson() {
initPlainVault();
openExportDialog();
File file = doExport();
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
readVault(file, VAULT_PASSWORD);
}
@Test
public void testEncryptedVaultExportPlainJson() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readVault(file, null);
}
@Test
public void testEncryptedVaultExportPlainTxt() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
File file = doExport();
readTxtExport(file);
}
@Test
public void testEncryptedVaultExportEncryptedJson() {
initEncryptedVault();
openExportDialog();
File file = doExport();
readVault(file, VAULT_PASSWORD);
}
@Test
public void testPlainVaultExportHtml() {
initPlainVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
}
@Test
public void testEncryptedVaultExportHtml() {
initEncryptedVault();
openExportDialog();
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
onView(withId(R.id.dropdown_export_format)).perform(click());
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
}
@Test
public void testSeparateExportPassword() {
initEncryptedVault();
setSeparateBackupExportPassword();
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD);
}
@Test
public void testChangeBackupPassword() throws SlotIntegrityException {
initEncryptedVault();
setSeparateBackupExportPassword();
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_change_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_BACKUP_PASSWORD, VAULT_BACKUP_PASSWORD_CHANGED);
}
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
decryptPasswordSlot(slot, VAULT_PASSWORD);
}
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD_CHANGED);
}
@Test
public void testChangePasswordHavingBackupPassword() throws SlotIntegrityException {
initEncryptedVault();
setSeparateBackupExportPassword();
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_set_password_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_PASSWORD_CHANGED);
}
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
decryptPasswordSlot(slot, VAULT_BACKUP_PASSWORD);
}
openExportDialog();
File file = doExport();
readVault(file, VAULT_BACKUP_PASSWORD);
}
private void setSeparateBackupExportPassword() {
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 0);
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
creds = _vaultManager.getVault().getCredentials();
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_BACKUP_PASSWORD);
}
}
private void verifyPasswordSlotChange(VaultFileCredentials creds, PasswordSlot slot, String oldPassword, String newPassword) {
assertThrows(SlotIntegrityException.class, () -> decryptPasswordSlot(slot, oldPassword));
MasterKey masterKey;
try {
masterKey = decryptPasswordSlot(slot, newPassword);
} catch (SlotIntegrityException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
assertArrayEquals(creds.getKey().getBytes(), masterKey.getBytes());
}
private File doExport() {
File file = getExportFileUri();
Intent resultData = new Intent();
resultData.setData(Uri.fromFile(file));
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
onView(withId(android.R.id.button1)).perform(click());
return file;
}
private void openExportDialog() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_import_export_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_export_title)), click()));
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, String password) throws SlotIntegrityException {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
try {
Cipher cipher = slot.createDecryptCipher(derivedKey);
return slot.getKey(cipher);
} catch (SlotException e) {
throw new RuntimeException("Unable to decrypt password slot", e);
}
}
private File getExportFileUri() {
String dirName = Hex.encode(CryptoUtils.generateRandomBytes(8));
File dir = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), String.format("export-%s", dirName));
if (!dir.mkdirs()) {
throw new RuntimeException(String.format("Unable to create export directory: %s", dir));
}
VaultBackupManager.FileInfo fileInfo = new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT);
return new File(dir, fileInfo.toString());
}
private VaultRepository readVault(File file, @Nullable String password) {
VaultRepository repo;
try (InputStream inStream = new FileInputStream(file)) {
byte[] bytes = IOUtils.readAll(inStream);
VaultFile vaultFile = VaultFile.fromBytes(bytes);
VaultFileCredentials creds = null;
if (password != null) {
SlotList slots = vaultFile.getHeader().getSlots();
for (PasswordSlot slot : slots.findAll(PasswordSlot.class)) {
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
Cipher cipher = slot.createDecryptCipher(derivedKey);
MasterKey masterKey = slot.getKey(cipher);
creds = new VaultFileCredentials(masterKey, slots);
break;
}
}
repo = VaultRepository.fromFile(getInstrumentation().getContext(), vaultFile, creds);
} catch (SlotException | SlotIntegrityException | VaultRepositoryException | VaultFileException | IOException e) {
throw new RuntimeException("Unable to read back vault file", e);
}
checkReadEntries(repo.getEntries());
return repo;
}
private void readTxtExport(File file) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getInstrumentation().getContext());
Collection<VaultEntry> entries;
try (InputStream inStream = new FileInputStream(file)) {
DatabaseImporter.State state = importer.read(inStream);
DatabaseImporter.Result result = state.convert();
entries = result.getEntries().getValues();
} catch (DatabaseImporterException | IOException e) {
throw new RuntimeException("Unable to read txt export file", e);
}
checkReadEntries(entries);
}
private void checkReadEntries(Collection<VaultEntry> entries) {
List<VaultEntry> vectors = VaultEntries.get();
assertEquals(vectors.size(), entries.size());
int i = 0;
for (VaultEntry entry : entries) {
VaultEntry vector = vectors.get(i);
String message = String.format("Entries are not equivalent: (%s) (%s)", vector.toJson().toString(), entry.toJson().toString());
assertTrue(message, vector.equivalates(entry));
try {
assertEquals(message, vector.getInfo().getOtp(), entry.getInfo().getOtp());
} catch (OtpInfoException e) {
throw new RuntimeException("Unable to generate OTP", e);
}
i++;
}
}
}
@@ -1,68 +0,0 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static junit.framework.TestCase.assertTrue;
import android.content.Intent;
import android.net.Uri;
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 dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class DeepLinkTest extends AegisTest {
@Before
public void before() {
initEmptyEncryptedVault();
}
@Test
public void testDeepLinkIntent() {
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
launch(info.getUri());
onView(withId(R.id.action_save)).perform(click());
VaultEntry createdEntry = (VaultEntry) _vaultManager.getVault().getEntries().toArray()[0];
assertTrue(createdEntry.equivalates(entry));
}
@Test
public void testDeepLinkIntent_Empty() {
launch(null);
}
@Test
public void testDeepLinkIntent_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);
}
}
@@ -1,54 +0,0 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class EmptySecretTest extends AegisTest {
private ActivityScenario<MainActivity> _scenario;
@Before
public void before() throws OtpInfoException {
initEmptyPlainVault();
_vaultManager.getVault().addEntry(new VaultEntry(new TotpInfo(new byte[0])));
_scenario = ActivityScenario.launch(MainActivity.class);
}
@After
public void after() {
_scenario.close();
}
@Test
public void testVaultEntryEmptySecret() {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.error_all_caps)), click()));
}
}
@@ -1,228 +0,0 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.not;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.net.Uri;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.intent.Intents;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.viewpager2.widget.ViewPager2;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class IntroTest extends AegisTest {
private final ActivityScenarioRule<IntroActivity> _activityRule = new ActivityScenarioRule<>(IntroActivity.class);
private ViewPager2IdlingResource _viewPager2IdlingResource;
@Rule
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Before
public void setUp() {
Intents.init();
_activityRule.getScenario().onActivity(activity -> {
_viewPager2IdlingResource = new ViewPager2IdlingResource(activity.findViewById(R.id.pager), "viewPagerIdlingResource");
IdlingRegistry.getInstance().register(_viewPager2IdlingResource);
});
}
@After
public void tearDown() {
Intents.release();
IdlingRegistry.getInstance().unregister(_viewPager2IdlingResource);
}
@Test
public void testIntro_None() {
assertFalse(_prefs.isIntroDone());
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());
VaultRepository vault = _vaultManager.getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
assertTrue(_prefs.isIntroDone());
}
@Test
public void testIntro_Password() {
assertFalse(_prefs.isIntroDone());
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());
VaultRepository vault = _vaultManager.getVault();
SlotList slots = vault.getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
assertTrue(_prefs.isIntroDone());
}
@Test
public void testIntro_Import_Plain() {
assertFalse(_prefs.isIntroDone());
Uri uri = getResourceUri("aegis_plain.json");
Intent resultData = new Intent();
resultData.setData(uri);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
ViewInteraction next = onView(withId(R.id.btnNext));
onView(withId(R.id.btnImport)).perform(click());
next.perform(click());
VaultRepository vault = _vaultManager.getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
assertTrue(_prefs.isIntroDone());
}
@Test
public void testIntro_Import_Encrypted() {
assertFalse(_prefs.isIntroDone());
Uri uri = getResourceUri("aegis_encrypted.json");
Intent resultData = new Intent();
resultData.setData(uri);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(not(isInternal())).respondWith(result);
ViewInteraction next = onView(withId(R.id.btnNext));
onView(withId(R.id.btnImport)).perform(click());
onView(withId(R.id.text_input)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
next.perform(click());
VaultRepository vault = _vaultManager.getVault();
SlotList slots = vault.getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
assertTrue(_prefs.isIntroDone());
}
private Uri getResourceUri(String resourceName) {
File targetFile = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), resourceName);
try (InputStream inStream = getClass().getResourceAsStream(resourceName);
FileOutputStream outStream = new FileOutputStream(targetFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Uri.fromFile(targetFile);
}
// Source: https://stackoverflow.com/a/32763454/12972657
private static class ViewPager2IdlingResource implements IdlingResource {
private final String _resName;
private boolean _isIdle = true;
private IdlingResource.ResourceCallback _resourceCallback = null;
public ViewPager2IdlingResource(ViewPager2 viewPager, String resName) {
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrollStateChanged(int state) {
_isIdle = (state == ViewPager2.SCROLL_STATE_IDLE || state == ViewPager2.SCROLL_STATE_DRAGGING);
if (_isIdle && _resourceCallback != null) {
_resourceCallback.onTransitionToIdle();
}
}
});
_resName = resName;
}
@Override
public String getName() {
return _resName;
}
@Override
public boolean isIdleNow() {
return _isIdle;
}
@Override
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
_resourceCallback = resourceCallback;
}
}
}
@@ -1,236 +0,0 @@
package com.beemdevelopment.aegis;
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.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import androidx.annotation.IdRes;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.MotpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@LargeTest
public class OverallTest extends AegisTest {
private static final String _groupName = "Test";
private final ActivityScenarioRule<MainActivity> _activityRule = new ActivityScenarioRule<>(MainActivity.class);
@Rule
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
@Test
public void testOverall() {
ViewInteraction next = onView(withId(R.id.btnNext));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(click()).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.btnNext)).perform(click());
VaultRepository vault = _vaultManager.getVault();
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
assertTrue(_prefs.isIntroDone());
List<VaultEntry> entries = Arrays.asList(
generateEntry(TotpInfo.class, "Frank", "Google"),
generateEntry(HotpInfo.class, "John", "GitHub"),
generateEntry(TotpInfo.class, "Alice", "Office 365"),
generateEntry(SteamInfo.class, "Gaben", "Steam"),
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16),
generateEntry(MotpInfo.class, "Jimmy McGill", "PfSense", 16)
);
for (VaultEntry entry : entries) {
addEntry(entry);
}
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
for (int i = 0; i < realEntries.size(); i++) {
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
}
for (int i = 0; i < 10; i++) {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
}
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
_activityRule.getScenario().onActivity(activity -> {
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
});
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.dropdown_group)).perform(click());
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).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);
changeGroupFilter(_groupName);
changeGroupFilter(null);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, click()));
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
onView(withText(R.string.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
openContextualActionModeOverflowMenu();
onView(withText(R.string.lock)).perform(click());
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.button_decrypt)).perform(click());
vault = _vaultManager.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_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, 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.actionOnItemAtPosition(1, 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 changeGroupFilter(String text) {
onView(withId(R.id.chip_group)).perform(click());
if (text == null) {
onView(withId(R.id.btnClear)).perform(click());
} else {
onView(withText(text)).perform(click());
onView(isRoot()).perform(pressBack());
}
}
private void addEntry(VaultEntry entry) {
onView(withId(R.id.fab)).perform(click());
onView(withId(R.id.fab_enter)).perform(click());
onView(withId(R.id.accordian_header)).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) {
String otpType;
if (entry.getInfo() instanceof HotpInfo) {
otpType = "HOTP";
} else if (entry.getInfo() instanceof SteamInfo) {
otpType = "Steam";
} else if (entry.getInfo() instanceof YandexInfo) {
otpType = "Yandex";
} else if (entry.getInfo() instanceof MotpInfo) {
otpType = "MOTP";
} else if (entry.getInfo() instanceof TotpInfo) {
otpType = "TOTP";
} else {
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
}
onView(withId(R.id.dropdown_type)).perform(click());
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
}
String secret;
if (Objects.equals(entry.getInfo().getTypeId(), MotpInfo.ID)) {
secret = Hex.encode(entry.getInfo().getSecret());
} else {
secret = Base32.encode(entry.getInfo().getSecret());
}
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
if (entry.getInfo() instanceof YandexInfo) {
String pin = "123456";
((YandexInfo) entry.getInfo()).setPin(pin);
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
} else if (entry.getInfo() instanceof MotpInfo) {
String pin = "1234";
((MotpInfo) entry.getInfo()).setPin(pin);
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
}
onView(withId(R.id.action_save)).perform(click());
}
}
@@ -1,58 +0,0 @@
package com.beemdevelopment.aegis;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
import com.beemdevelopment.aegis.ui.PanicResponderActivity;
import com.beemdevelopment.aegis.vault.VaultRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class PanicTriggerTest extends AegisTest {
@Before
public void before() {
initEncryptedVault();
}
@Test
public void testPanicTriggerDisabled() {
assertFalse(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertTrue(_vaultManager.isVaultLoaded());
_vaultManager.getVault();
assertTrue(VaultRepository.fileExists(getApp()));
}
@Test
public void testPanicTriggerEnabled() {
_prefs.setIsPanicTriggerEnabled(true);
assertTrue(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertFalse(_vaultManager.isVaultLoaded());
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
assertFalse(VaultRepository.fileExists(getApp()));
}
private void launchPanic() {
Intent intent = new Intent(PanicResponderActivity.PANIC_TRIGGER_ACTION);
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
ActivityTestRule<PanicResponderActivity> rule = new ActivityTestRule<>(PanicResponderActivity.class);
rule.launchActivity(intent);
}
}
@@ -1,36 +0,0 @@
package com.beemdevelopment.aegis.rules;
import android.graphics.Bitmap;
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
import androidx.test.runner.screenshot.Screenshot;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import java.io.IOException;
import java.util.HashSet;
public class ScreenshotTestRule extends TestWatcher {
@Override
protected void failed(Throwable e, Description description) {
super.failed(e, description);
String filename = description.getTestClass().getSimpleName() + "-" + description.getMethodName();
ScreenCapture capture = Screenshot.capture();
capture.setName(filename);
capture.setFormat(Bitmap.CompressFormat.PNG);
HashSet<ScreenCaptureProcessor> processors = new HashSet<>();
processors.add(new BasicScreenCaptureProcessor());
try {
capture.process(processors);
} catch (IOException e2) {
e.printStackTrace();
}
}
}
@@ -1,43 +0,0 @@
package com.beemdevelopment.aegis.vault;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.AegisTest;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import dagger.hilt.android.testing.HiltAndroidTest;
@RunWith(AndroidJUnit4.class)
@HiltAndroidTest
@SmallTest
public class VaultRepositoryTest extends AegisTest {
@Before
public void before() {
initEncryptedVault();
}
@Test
public void testToggleEncryption() throws VaultRepositoryException {
VaultRepository vault = _vaultManager.getVault();
_vaultManager.disableEncryption();
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
VaultFileCredentials creds = generateCredentials();
_vaultManager.enableEncryption(creds);
assertTrue(vault.isEncryptionEnabled());
assertNotNull(vault.getCredentials());
assertEquals(vault.getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
}
}
@@ -1 +0,0 @@
../../../../../../test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java
@@ -1 +0,0 @@
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_encrypted.json
@@ -1 +0,0 @@
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_plain.json
+23 -120
View File
@@ -1,46 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- NOTE: Disabled for now. See issue: #1047
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<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" />
<application
android:name=".AegisApplication"
android:allowBackup="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules_old"
android:dataExtractionRules="@xml/backup_rules"
android:backupAgent=".AegisBackupAgent"
android:enableOnBackInvokedCallback="true"
android:allowBackup="false"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/Theme.Aegis.Launch"
tools:targetApi="tiramisu">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about" />
android:label="@string/title_activity_about"></activity>
<activity
android:name=".ui.ImportEntriesActivity"
android:label="@string/title_activity_import_entries" />
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:label="${title}">
android:label="${title}"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -54,111 +40,28 @@
<data android:scheme="otpauth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/title_activity_scan_qr"
android:label="Scan a QR code"
android:screenOrientation="portrait" />
<activity
android:name=".ui.EditEntryActivity"
android:label="@string/title_activity_edit_entry" />
android:label="Edit profile" />
<activity
android:name=".ui.IntroActivity"
android:screenOrientation="portrait" />
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
<activity
android:name=".ui.AuthActivity" />
<activity
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
android:name=".ui.SlotManagerActivity"
android:label="Manage key slots" />
<activity
android:name=".ui.GroupManagerActivity"
android:label="@string/title_activity_manage_groups" />
<activity android:name=".ui.AssignIconsActivity"
android:label="@string/title_activity_assign_icons"/>
<activity android:name=".ui.LicensesActivity"
android:label="@string/title_activity_licenses"/>
<activity
android:name=".ui.PanicResponderActivity"
android:exported="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<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" />
android:label="Manage groups" />
<!-- NOTE: Disabled for now. See issue: #1047
<service android:name=".services.NotificationService" />
-->
<service
android:name=".services.LaunchAppTileService"
android:label="@string/tile_open_vault"
android:icon="@drawable/ic_aegis_quicksettings"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>
<service
android:name=".services.LaunchScannerTileService"
android:label="@string/tile_open_scanner"
android:icon="@drawable/ic_aegis_quicksettings"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>
<receiver android:name=".receivers.VaultLockReceiver" android:exported="false">
<intent-filter>
<action android:name="${applicationId}.LOCK_VAULT" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
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>
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
</application>
<queries>
<package android:name="me.jmh.authenticatorpro" />
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />
<package android:name="com.google.android.apps.authenticator2" />
<package android:name="com.azure.authenticator" />
<package android:name="com.valvesoftware.android.steam.community" />
<package android:name="com.authenticator.authservice2" />
<package android:name="com.duosecurity.duomobile" />
<package android:name="com.blizzard.messenger" />
</queries>
</manifest>
</manifest>
-1
View File
@@ -1 +0,0 @@
../../../../LICENSE
+69 -448
View File
@@ -1,449 +1,70 @@
<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 3.1.1</h3>
<h4>Fixes</h4>
<p>
A recent Android Pixel update introduced a bug causing Aegis to sometimes show a black screen after unlocking the vault.
We have reported this issue to the Google Issue Tracker (<a href="https://issuetracker.google.com/issues/352963108">link</a>) and
are awaiting a response from Google. In the meantime, we have implemented a workaround that eliminates this bug.
</p>
<ul>
<li>Group filter now gets applied properly upon unlocking the vault</li>
<li>Advanced entry settings now gets shown correctly</li>
<li>Keyboard when searching for entries now gets hidden when the user starts scrolling through the list</li>
</ul>
<h3>Version 3.1</h3>
<h4>New</h4>
<ul>
<li>A new audit log has been added to check all important events that occurred in your vault</li>
<li>Added the ability to rename groups</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Group selection will now be remembered again upon launch</li>
<li>Various UI improvements</li>
<li>Stability fixes</li>
</ul>
<h3>Version 3.0.1</h3>
<h4>New</h4>
<ul>
<li>Support for importing from the new Battle.net app</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Visual glitches when AMOLED theme was used on old Android versions</li>
<li>Minor UI improvements</li>
</ul>
<h3>Version 3.0</h3>
<h4>New</h4>
<ul>
<li>Material 3 (and Material You)</li>
<li>Automatic assignment of icons to entries</li>
<li>Ability to select all entries in one go</li>
<li>Support for importing 2FAS schema v4 backups</li>
<li>Sort entries based on the last time they were used</li>
<li>Some clarifications related to importing and backup permission errors</li>
<li>Preparations for the ability to assign a single entry to multiple groups</li>
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
<li>A new look for the third-party licenses list</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Directly importing from Authy using root would fail</li>
<li>Minor glitches related to animation duration scale settings</li>
<li>Various stability improvements</li>
</ul>
<h3>Version 2.2.2</h3>
<h4>New</h4>
<ul>
<li>An optional name field for icon packs to bypass filename character restrictions</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The Authenticator Pro importer only supported the legacy backup format</li>
<li>A crash could occur in the tile service</li>
</ul>
<h3>Version 2.2.1</h3>
<h4>New</h4>
<ul>
<li>Ability to automatically skip potential duplicates when importing entries</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Biometrics button on the unlock screen was unresponsive</li>
</ul>
<h3>Version 2.2</h3>
<h4>New</h4>
<ul>
<li>Authenticator Pro encrypted import support</li>
<li>Ability to change account name position</li>
<li>A new dialog explaining how our password reminder works</li>
<li>Ability to change copy behavior</li>
<li>Ability to only show account names when necessary</li>
<li>New view mode: Tiles/Grid</li>
<li>Added translation: Dutch (Frysian)</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Deleting an entry while a search filter is active now shows the correct state</li>
<li>Aegis now fully respects system animation settings</li>
</ul>
<h3>Version 2.1.3</h3>
<h4>New</h4>
<ul>
<li>Option to disable the backup reminder</li>
<li>Improved group selection dropdown during vault export</li>
<li>New translation: Hebrew</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>A crash could occur because a Toast was incorrectly created</li>
</ul>
<h3>Version 2.1.2</h3>
<h4>Fixes</h4>
<ul>
<li>A crash could occur when changing an entry in such a way that it is filtered out from the entry list</li>
</ul>
<h3>Version 2.1.1</h3>
<h4>New</h4>
<ul>
<li>An option to export the vault as an HTML file</li>
<li>Support for importing from Battle.net Authenticator (root required)</li>
<li>An option to hide entry icons</li>
<li>An option to only include certain groups in an export</li>
<li>Copying a token now takes a second tap if tap to reveal is enabled</li>
<li>The ability to copy the URI when transferring entries through QR codes</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The lock notification would remain after locking the vault in certain cases. For now, we've disabled the notification entirely.</li>
<li>Making changes to an entry while having one or more favorited entries in the vault could result in buggy ordering</li>
<li>Tapping to the reveal a token could increase the height of the entry in certain view modes on recent Android versions</li>
<li>The backup reminder was unclear about when the last successful backup took place</li>
<li>Users could accidentally select MD5 as the hash algorithm for non-mOTP entry types, causing crashes at seemingly random intervals. Any users who have gotten themselves into this situation will see these bad entries get reset to SHA1.</li>
<li>Importing from certain apps would cause a crash if an empty password was entered</li>
<li>The andOTP importer could hang indefinitely if the user accidentally selected a non-andOTP file.</li>
<li>Various other stability improvements</li>
</ul>
<h3>Version 2.1</h3>
<h4>New</h4>
<ul>
<li>Support for mOTP</li>
<li>Support for Yandex OTP (Experimental)</li>
<li>An Adaptive Icon for Material You</li>
<li>Ability to favorite certain entries and pin them to the top of the entry list</li>
<li>Ability to filter by entries that are not in a group</li>
<li>Ability to set a separate password that is used for encrypting backups and exports</li>
<li>Support for predictive back gesture</li>
<li>Improved overview of backup status in preferences</li>
<li>Additional options for code digit grouping</li>
<li>Support for importing from Duo</li>
<li>Support for importing from Bitwarden</li>
<li>Support for importing multiple QR code images in one go</li>
<li>Support for scanning Google Authenticator export QR codes from image files</li>
<li>Display some extra information in the dialog displayed when deleting an entry</li>
<li>An option to export through Google Authenticator export QR code images</li>
<li>An option to import an existing vault file from the first page in the intro</li>
<li>An option to minimize the app after copying a token</li>
<li>A count of the total number of entries is displayed at the bottom of the entry list</li>
<li>A backup reminder is shown if changes were made to the vault, but no backup or export has been created yet since then</li>
<li>A warning is shown after a plaintext export has been made</li>
<li>An option to focus search immediately after the app starts</li>
<li>Allow customization of the frequency of the password reminder</li>
<li>Allow sharing text to Aegis in the format of a Google Authenticator URI to add as a new entry</li>
<li>Always allow D2D (device-to-device) Android backups regardless of backup settings</li>
<li>Mark clipboard data as sensitive when copying tokens so that Android will mask them in the UI</li>
<li>Updated translations for almost all languages</li>
<li>New languages: Asturian, Catalan, Galician</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Various reliability improvements for the QR code scanner</li>
<li>The floating action button was glitchy when making small entry list scroll movements</li>
<li>The vault unlocked notification was never shown and was still using the old app icon</li>
<li>The automatically generated entry icon was broken if the entry name/issuer is a multi-codepoint character (certain emoji's, for example)</li>
<li>The PIN keyboard was not disabled after enabling encryption</li>
<li>The password prompt message was unclear when importing from a file</li>
<li>The entry list was not sorted correctly if a change to an entry caused its location to change</li>
<li>Quickly double-tapping on the copy button would cause a crash</li>
<li>Importing an entry with an empty secret would cause a crash loop</li>
<li>On certain devices, it was not possible to import icon packs because the .ZIP files would be grayed out</li>
<li>An unclear error message was shown when trying to import from Steam and Google Authenticator</li>
<li>Various other minor UI and stability improvements</li>
</ul>
<h3>Version 2.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for importing 2FAS Authenticator's new backup format</li>
</ul>
<h3>Version 2.0.2</h3>
<h4>New</h4>
<ul>
<li>Add a note field to entries</li>
<li>An option to pause code updating of highlighted entries</li>
<li>New translation: Lithuanian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Minor UI and stability improvements</li>
<li>The Microsoft Authenticator importer did not accept spaces and dashes in secrets</li>
</ul>
<h3>Version 2.0.1</h3>
<h4>New</h4>
<ul>
<li>Support for sorting on most used tokens</li>
<li>Some minor UX and stability improvements</li>
<li>New translation: Vietnamese</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>QR code information was decoded incorrectly in some cases if the app was set to a certain language (Turkish, for example)</li>
</ul>
<h3>Version 2.0</h3>
<h4>New</h4>
<ul>
<li>Support for icon packs</li>
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
<li>UI refresh (switched to the Material Components theme)</li>
<li>Bottom sheet with chips to filter on groups</li>
<li>Support for importing from 2FAS Authenticator</li>
<li>Search in account names by default (and remove the setting)</li>
<li>Replaced the FAB with a bottom sheet dialog</li>
<li>Reorganization of settings into separate categories</li>
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
<li>Option to save the current group filter</li>
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
<li>The app would vanish from the recent apps list after locking</li>
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
<li>The biometrics prompt would not appear on some devices</li>
<li>The app would lock when selecting a file/icon on certain devices and configurations</li>
<li>There were multiple layout issues on small screen devices</li>
<li>Various other usability, performance and stability improvements</li>
</ul>
<h3>Version 1.4.2</h3>
<h4>Fixes</h4>
<ul>
<li>The app would crash if DocumentsUI is not present on the device</li>
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
<li>Importing from Authy was flaky for entries that have an icon</li>
<li>The dark theme was not properly applied to the QR code scanner view</li>
<li>The app would crash on plain text export on some devices</li>
<li>Importing from Authenticator Plus stopped working</li>
</ul>
<h3>Version 1.4.1</h3>
<h4>Fixes</h4>
<ul>
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
</ul>
<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>
<li>The password reminder popup would be occluded by the autofill popup</li>
<li>Importing from other apps on Android 11 was broken due to some permission issues</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>
<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.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>
-14
View File
@@ -1,14 +0,0 @@
<html>
<head>
<style type="text/css">
body {
background-color: %2$s;
color: %3$s;
font-size: 0.8em;
}
</style>
</head>
<body>
<pre>%1$s</pre>
</body>
</html>
@@ -1,17 +0,0 @@
package com.beemdevelopment.aegis;
public enum AccountNamePosition {
HIDDEN,
END,
BELOW;
private static AccountNamePosition[] _values;
static {
_values = values();
}
public static AccountNamePosition fromInteger(int x) {
return _values[x];
}
}
@@ -1,8 +1,132 @@
package com.beemdevelopment.aegis;
import dagger.hilt.android.HiltAndroidApp;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
@HiltAndroidApp
public class AegisApplication extends AegisApplicationBase {
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
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 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";
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
// 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);
registerReceiver(receiver, intentFilter);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}
}
public DatabaseManager getDatabaseManager() {
return _manager;
}
public Preferences getPreferences() {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
}
public void registerLockListener(LockListener listener) {
_lockListeners.add(listener);
}
public void unregisterLockListener(LockListener listener) {
_lockListeners.remove(listener);
}
public void lock() {
_manager.lock();
for (LockListener listener : _lockListeners) {
listener.onLocked();
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
public class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
}
}
}
public interface LockListener {
void onLocked();
}
}
@@ -1,130 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogEntry;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.topjohnwu.superuser.Shell;
import org.checkerframework.checker.units.qual.A;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
public abstract class AegisApplicationBase extends Application {
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private VaultManager _vaultManager;
static {
// Enable verbose libsu logging in debug builds
Shell.enableVerboseLogging = BuildConfig.DEBUG;
}
@Override
public void onCreate() {
super.onCreate();
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
VaultLockReceiver lockReceiver = new VaultLockReceiver();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// lock the app if the user moves the application to the background
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
// clear the cache directory on startup, to make sure no temporary vault export files remain
IOUtils.clearDirectory(getCacheDir(), false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}*/
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_entry))
.setLongLabel(getString(R.string.add_new_entry))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
private class AppLifecycleObserver implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP
&& _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
&& !_vaultManager.isAutoLockBlocked()) {
_vaultManager.lock(false);
}
}
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
interface EntryPoint {
VaultManager getVaultManager();
}
}
@@ -1,147 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.FullBackupDataOutput;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = AegisBackupAgent.class.getSimpleName();
private Preferences _prefs;
private AuditLogRepository _auditLogRepository;
@Override
public void onCreate() {
super.onCreate();
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this);
AppDatabase appDatabase = AegisModule.provideAppDatabase(this);
_auditLogRepository = AegisModule.provideAuditLogRepository(appDatabase);
}
@Override
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
if (isD2D) {
Log.i(TAG, "onFullBackup(): allowing D2D transfer");
} else if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// We perform a catch of any Exception here to make sure we also
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
fullBackup(data);
_auditLogRepository.addAndroidBackupCreatedEvent();
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
throw e;
}
Log.i(TAG, "onFullBackup() finished");
}
private void fullBackup(FullBackupDataOutput data) throws IOException {
// First copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
VaultFile vaultFile = VaultRepository.readVaultFile(this);
byte[] bytes = vaultFile.exportable().toBytes();
outputStream.write(bytes);
} catch (VaultRepositoryException | IOException e) {
deleteBackupDir();
throw new IOException(e);
}
// Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} finally {
deleteBackupDir();
}
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
super.onRestoreFile(data, size, destination, type, mode, mtime);
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultRepository.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
} finally {
deleteBackupDir();
}
}
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
}
@Override
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
super.onQuotaExceeded(backupDataBytes, quotaBytes);
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
}
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (dir == null || (!dir.exists() && !dir.mkdir())) {
throw new IOException(String.format("Unable to create backup directory: %s", dir));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
if (dir != null) {
IOUtils.clearDirectory(dir, true);
}
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultRepository.FILENAME);
}
}
@@ -1,55 +0,0 @@
package com.beemdevelopment.aegis;
import android.content.Context;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogDao;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;
@Module
@InstallIn(SingletonComponent.class)
public class AegisModule {
@Provides
@Singleton
public static IconPackManager provideIconPackManager(@ApplicationContext Context context) {
return new IconPackManager(context);
}
@Provides
@Singleton
public static AuditLogRepository provideAuditLogRepository(AppDatabase appDatabase) {
AuditLogDao auditLogDao = appDatabase.auditLogDao();
return new AuditLogRepository(auditLogDao);
}
@Provides
@Singleton
public static VaultManager provideVaultManager(@ApplicationContext Context context, AuditLogRepository auditLogRepository) {
return new VaultManager(context, auditLogRepository);
}
@Provides
public static Preferences providePreferences(@ApplicationContext Context context) {
return new Preferences(context);
}
@Provides
@Singleton
public static AppDatabase provideAppDatabase(@ApplicationContext Context context) {
return Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "aegis-db")
.build();
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -1,17 +0,0 @@
package com.beemdevelopment.aegis;
public enum CopyBehavior {
NEVER,
SINGLETAP,
DOUBLETAP;
private static CopyBehavior[] _values;
static {
_values = values();
}
public static CopyBehavior fromInteger(int x) {
return _values[x];
}
}
@@ -1,42 +0,0 @@
package com.beemdevelopment.aegis;
public enum EventType {
VAULT_UNLOCKED,
VAULT_BACKUP_CREATED,
VAULT_ANDROID_BACKUP_CREATED,
VAULT_EXPORTED,
ENTRY_SHARED,
VAULT_UNLOCK_FAILED_PASSWORD,
VAULT_UNLOCK_FAILED_BIOMETRICS;
private static EventType[] _values;
static {
_values = values();
}
public static EventType fromInteger(int x) {
return _values[x];
}
public static int getEventTitleRes(EventType eventType) {
switch (eventType) {
case VAULT_UNLOCKED:
return R.string.event_title_vault_unlocked;
case VAULT_BACKUP_CREATED:
return R.string.event_title_backup_created;
case VAULT_ANDROID_BACKUP_CREATED:
return R.string.event_title_android_backup_created;
case VAULT_EXPORTED:
return R.string.event_title_vault_exported;
case ENTRY_SHARED:
return R.string.event_title_entry_shared;
case VAULT_UNLOCK_FAILED_PASSWORD:
return R.string.event_title_vault_unlock_failed_password;
case VAULT_UNLOCK_FAILED_BIOMETRICS:
return R.string.event_title_vault_unlock_failed_biometrics;
default:
return R.string.event_unknown;
}
}
}
@@ -1,56 +0,0 @@
package com.beemdevelopment.aegis;
import androidx.annotation.StringRes;
import java.util.concurrent.TimeUnit;
public enum PassReminderFreq {
NEVER,
WEEKLY,
BIWEEKLY,
MONTHLY,
QUARTERLY;
public long getDurationMillis() {
long weeks;
switch (this) {
case WEEKLY:
weeks = 1;
break;
case BIWEEKLY:
weeks = 2;
break;
case MONTHLY:
weeks = 4;
break;
case QUARTERLY:
weeks = 13;
break;
default:
weeks = 0;
break;
}
return TimeUnit.MILLISECONDS.convert(weeks * 7L, TimeUnit.DAYS);
}
@StringRes
public int getStringRes() {
switch (this) {
case WEEKLY:
return R.string.password_reminder_freq_weekly;
case BIWEEKLY:
return R.string.password_reminder_freq_biweekly;
case MONTHLY:
return R.string.password_reminder_freq_monthly;
case QUARTERLY:
return R.string.password_reminder_freq_quarterly;
default:
return R.string.password_reminder_freq_never;
}
}
public static PassReminderFreq fromInteger(int i) {
return PassReminderFreq.values()[i];
}
}
@@ -3,175 +3,37 @@ 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 androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
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();
}
migratePreferences();
}
public void migratePreferences() {
// Change copy on tap to copy behavior to new preference and delete the old key
String prefCopyOnTapKey = "pref_copy_on_tap";
if (_prefs.contains(prefCopyOnTapKey)) {
boolean isCopyOnTapEnabled = _prefs.getBoolean(prefCopyOnTapKey, false);
if (isCopyOnTapEnabled) {
setCopyBehavior(CopyBehavior.SINGLETAP);
}
_prefs.edit().remove(prefCopyOnTapKey).apply();
}
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false;
return _prefs.getBoolean("pref_pause_entry", false);
}
public boolean isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public void setIsPanicTriggerEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_panic_trigger", enabled).apply();
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public PassReminderFreq getPasswordReminderFrequency() {
final String key = "pref_password_reminder_freq";
if (_prefs.contains(key) || _prefs.getBoolean("pref_password_reminder", true)) {
int i = _prefs.getInt(key, PassReminderFreq.BIWEEKLY.ordinal());
return PassReminderFreq.fromInteger(i);
}
return PassReminderFreq.NEVER;
}
public void setPasswordReminderFrequency(PassReminderFreq freq) {
_prefs.edit().putInt("pref_password_reminder_freq", freq.ordinal()).apply();
}
public boolean isPasswordReminderNeeded() {
return isPasswordReminderNeeded(new Date().getTime());
}
boolean isPasswordReminderNeeded(long currTime) {
PassReminderFreq freq = getPasswordReminderFrequency();
if (freq == PassReminderFreq.NEVER) {
return false;
}
long duration = currTime - getPasswordReminderTimestamp().getTime();
return duration >= freq.getDurationMillis();
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
void setPasswordReminderTimestamp(long timestamp) {
_prefs.edit().putLong("pref_password_reminder_counter", timestamp).apply();
}
public void resetPasswordReminderTimestamp() {
setPasswordReminderTimestamp(new Date().getTime());
}
public boolean onlyShowNecessaryAccountNames() { return _prefs.getBoolean("pref_shared_issuer_account_name", false); }
public boolean isIconVisible() {
return _prefs.getBoolean("pref_show_icons", true);
}
public CodeGrouping getCodeGroupSize() {
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
return CodeGrouping.valueOf(value);
}
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public boolean isAutoLockEnabled() {
return getAutoLockMask() != AUTO_LOCK_OFF;
}
public boolean isAutoLockTypeEnabled(int autoLockType) {
return (getAutoLockMask() & autoLockType) == autoLockType;
}
public void setAutoLockMask(int autoLock) {
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
return _prefs.getBoolean("pref_auto_lock", true);
}
public void setIntroDone(boolean done) {
@@ -195,17 +57,13 @@ public class Preferences {
}
public Theme getCurrentTheme() {
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
}
public void setCurrentTheme(Theme theme) {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public boolean isDynamicColorsEnabled() {
return _prefs.getBoolean("pref_dynamic_colors", false);
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
@@ -214,117 +72,12 @@ public class Preferences {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public AccountNamePosition getAccountNamePosition() {
return AccountNamePosition.fromInteger(_prefs.getInt("pref_account_name_position", AccountNamePosition.END.ordinal()));
}
public void setAccountNamePosition(AccountNamePosition accountNamePosition) {
_prefs.edit().putInt("pref_account_name_position", accountNamePosition.ordinal()).apply();
}
public Integer getUsageCount(UUID uuid) {
Integer usageCount = getUsageCounts().get(uuid);
return usageCount != null ? usageCount : 0;
}
public void resetUsageCount(UUID uuid) {
Map<UUID, Integer> usageCounts = getUsageCounts();
usageCounts.put(uuid, 0);
setUsageCount(usageCounts);
}
public long getLastUsedTimestamp(UUID uuid) {
Map<UUID, Long> timestamps = getLastUsedTimestamps();
if (timestamps != null && timestamps.size() > 0){
Long timestamp = timestamps.get(uuid);
return timestamp != null ? timestamp : 0;
}
return 0;
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Long> getLastUsedTimestamps() {
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
try {
JSONArray arr = new JSONArray(lastUsedTimestamp);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
}
} catch (JSONException ignored) {
}
return lastUsedTimestamps;
}
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
JSONArray lastUsedTimestampJson = new JSONArray();
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("timestamp", entry.getValue());
lastUsedTimestampJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
try {
JSONArray arr = new JSONArray(usageCount);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
}
} catch (JSONException ignored) {
}
return usageCounts;
}
public void setUsageCount(Map<UUID, Integer> usageCounts) {
JSONArray usageCountJson = new JSONArray();
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("count", entry.getValue());
usageCountJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
public String getLanguage() {
return _prefs.getString("pref_lang", "system");
}
public void setLanguage(String lang) {
_prefs.edit().putString("pref_lang", lang).apply();
}
public Locale getLocale() {
String lang = getLanguage();
String lang = _prefs.getString("pref_lang", "system");
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -334,315 +87,6 @@ public class Preferences {
}
}
String[] parts = lang.split("_");
if (parts.length == 1) {
return new Locale(parts[0]);
}
return new Locale(parts[0], parts[1]);
}
public boolean isAndroidBackupsEnabled() {
return _prefs.getBoolean("pref_android_backups", false);
}
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
setAndroidBackupResult(null);
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
setBuiltInBackupResult(null);
}
public boolean isBackupReminderEnabled() {
return _prefs.getBoolean("pref_backup_reminder", true);
}
public void setIsBackupReminderEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backup_reminder", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
return Uri.parse(str);
}
return null;
}
public boolean getFocusSearchEnabled() {
return _prefs.getBoolean("pref_focus_search", false);
}
public void setFocusSearch(boolean enabled) {
_prefs.edit().putBoolean("pref_focus_search", enabled).apply();
}
public void setLatestExportTimeNow() {
_prefs.edit().putLong("pref_export_latest", new Date().getTime()).apply();
setIsBackupReminderNeeded(false);
}
public Date getLatestBackupOrExportTime() {
List<Date> dates = new ArrayList<>();
long l = _prefs.getLong("pref_export_latest", 0);
if (l > 0) {
dates.add(new Date(l));
}
BackupResult builtinRes = getBuiltInBackupResult();
if (builtinRes != null) {
dates.add(builtinRes.getTime());
}
BackupResult androidRes = getAndroidBackupResult();
if (androidRes != null) {
dates.add(androidRes.getTime());
}
if (dates.size() == 0) {
return null;
}
return Collections.max(dates, Date::compareTo);
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
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 setAndroidBackupResult(@Nullable BackupResult res) {
setBackupResult(false, res);
}
public void setBuiltInBackupResult(@Nullable BackupResult res) {
setBackupResult(true, res);
}
@Nullable
public BackupResult getAndroidBackupResult() {
return getBackupResult(false);
}
@Nullable
public BackupResult getBuiltInBackupResult() {
return getBackupResult(true);
}
@Nullable
public Preferences.BackupResult getErroredBackupResult() {
Preferences.BackupResult res = getBuiltInBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
res = getAndroidBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
return null;
}
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
String json = null;
if (res != null) {
res.setIsBuiltIn(isBuiltInBackup);
json = res.toJson();
}
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
}
@Nullable
private BackupResult getBackupResult(boolean isBuiltInBackup) {
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
if (json == null) {
return null;
}
try {
BackupResult res = BackupResult.fromJson(json);
res.setIsBuiltIn(isBuiltInBackup);
return res;
} catch (JSONException e) {
return null;
}
}
private static String getBackupResultKey(boolean isBuiltInBackup) {
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
}
public void setIsBackupReminderNeeded(boolean needed) {
if (isBackupsReminderNeeded() != needed) {
_prefs.edit().putBoolean("pref_backups_reminder_needed", needed).apply();
}
}
public boolean isBackupsReminderNeeded() {
return _prefs.getBoolean("pref_backups_reminder_needed", false);
}
public void setIsPlaintextBackupWarningNeeded(boolean needed) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_needed", needed).apply();
}
public boolean isPlaintextBackupWarningNeeded() {
return !isPlaintextBackupWarningDisabled()
&& _prefs.getBoolean("pref_plaintext_backup_warning_needed", false);
}
public void setIsPlaintextBackupWarningDisabled(boolean disabled) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_disabled", disabled).apply();
}
public boolean isPlaintextBackupWarningDisabled() {
return _prefs.getBoolean("pref_plaintext_backup_warning_disabled", false);
}
public boolean isPinKeyboardEnabled() {
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 CopyBehavior getCopyBehavior() {
return CopyBehavior.fromInteger(_prefs.getInt("pref_current_copy_behavior", 0));
}
public void setCopyBehavior(CopyBehavior copyBehavior) {
_prefs.edit().putInt("pref_current_copy_behavior", copyBehavior.ordinal()).apply();
}
public boolean isMinimizeOnCopyEnabled() {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptySet();
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
private final boolean _isPermissionError;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
}
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
_time = time;
_error = error;
_isPermissionError = isPermissionError;
}
@Nullable
public String getError() {
return _error;
}
public boolean isSuccessful() {
return _error == null;
}
public Date getTime() {
return _time;
}
public String getElapsedSince(Context context) {
return TimeUtils.getElapsedSince(context, _time);
}
public boolean isBuiltIn() {
return _isBuiltIn;
}
private void setIsBuiltIn(boolean isBuiltIn) {
_isBuiltIn = isBuiltIn;
}
public boolean isPermissionError() {
return _isPermissionError;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
obj.put("isPermissionError", _isPermissionError);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj.toString();
}
public static BackupResult fromJson(String json) throws JSONException {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
boolean isPermissionError = obj.optBoolean("isPermissionError");
return new BackupResult(new Date(time), error, isPermissionError);
}
}
public enum CodeGrouping {
HALVES(-1),
NO_GROUPING(-2),
GROUPING_TWOS(2),
GROUPING_THREES(3),
GROUPING_FOURS(4);
private final int _value;
CodeGrouping(int value) {
_value = value;
}
public int getValue() {
return _value;
}
return new Locale(lang);
}
}
@@ -1,8 +1,6 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
@@ -14,9 +12,7 @@ public enum SortCategory {
ACCOUNT,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED,
USAGE_COUNT,
LAST_USED;
ISSUER_REVERSED;
private static SortCategory[] _values;
@@ -28,27 +24,22 @@ public enum SortCategory {
return _values[x];
}
public Comparator<VaultEntry> getComparator() {
Comparator<VaultEntry> comparator = null;
public Comparator<DatabaseEntry> getComparator() {
Comparator<DatabaseEntry> comparator = null;
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
comparator = new AccountNameComparator();
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
comparator = Collections.reverseOrder(new AccountNameComparator());
break;
case ISSUER:
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
comparator = new IssuerNameComparator();
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
comparator = Collections.reverseOrder(new IssuerNameComparator());
break;
case USAGE_COUNT:
comparator = Collections.reverseOrder(new UsageCountComparator());
break;
case LAST_USED:
comparator = Collections.reverseOrder(new LastUsedComparator());
}
return comparator;
@@ -66,10 +57,6 @@ public enum SortCategory {
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
case USAGE_COUNT:
return R.id.menu_sort_usage_count;
case LAST_USED:
return R.id.menu_sort_last_used;
default:
return R.id.menu_sort_custom;
}
@@ -3,9 +3,7 @@ package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED,
SYSTEM,
SYSTEM_AMOLED;
AMOLED;
private static Theme[] _values;
@@ -1,23 +0,0 @@
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.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
);
}
@@ -5,8 +5,7 @@ import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL,
TILES;
SMALL;
private static ViewMode[] _values;
@@ -27,39 +26,19 @@ public enum ViewMode {
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the offset (in dp) that should exist between entries in this view mode.
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
*/
public float getItemOffset() {
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 1;
} else if (this == ViewMode.TILES) {
return 4;
return 0;
}
return 8;
}
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
return 20;
}
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
return obj;
}
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
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 com.beemdevelopment.aegis.crypto.bc.SCrypt;
import org.spongycastle.crypto.generators.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -2,10 +2,9 @@ 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;
@@ -13,7 +12,6 @@ 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;
@@ -23,6 +21,8 @@ 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,14 +61,6 @@ 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);
}
@@ -96,14 +88,18 @@ public class KeyStoreHandle {
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
} 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;
}
throw new RuntimeException(e);
}
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
public MasterKey(SecretKey key) {
if (key == null) {
throw new IllegalArgumentException("Key cannot be null");
throw new NullPointerException();
}
_key = key;
}
@@ -1,255 +0,0 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Integers;
import org.bouncycastle.util.Pack;
/**
* Implementation of the scrypt a password-based key derivation function.
* <p>
* Scrypt was created by Colin Percival and is specified in <a
* href="https://tools.ietf.org/html/rfc7914">RFC 7914 - The scrypt Password-Based Key Derivation Function</a>
*/
public class SCrypt
{
private SCrypt()
{
// not used.
}
/**
* Generate a key using the scrypt key derivation function.
*
* @param P the bytes of the pass phrase.
* @param S the salt to use for this invocation.
* @param N CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than
* <code>2^(128 * r / 8)</code>.
* @param r the block size, must be &gt;= 1.
* @param p Parallelization parameter. Must be a positive integer less than or equal to
* <code>Integer.MAX_VALUE / (128 * r * 8)</code>.
* @param dkLen the length of the key to generate.
* @return the generated key.
*/
public static byte[] generate(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
if (P == null)
{
throw new IllegalArgumentException("Passphrase P must be provided.");
}
if (S == null)
{
throw new IllegalArgumentException("Salt S must be provided.");
}
if (N <= 1 || !isPowerOf2(N))
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and a power of 2");
}
// Only value of r that cost (as an int) could be exceeded for is 1
if (r == 1 && N >= 65536)
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and < 65536.");
}
if (r < 1)
{
throw new IllegalArgumentException("Block size r must be >= 1.");
}
int maxParallel = Integer.MAX_VALUE / (128 * r * 8);
if (p < 1 || p > maxParallel)
{
throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
+ " (based on block size r of " + r + ")");
}
if (dkLen < 1)
{
throw new IllegalArgumentException("Generated key length dkLen must be >= 1.");
}
return MFcrypt(P, S, N, r, p, dkLen);
}
private static byte[] MFcrypt(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
int MFLenBytes = r * 128;
byte[] bytes = SingleIterationPBKDF2(P, S, p * MFLenBytes);
int[] B = null;
try
{
int BLen = bytes.length >>> 2;
B = new int[BLen];
Pack.littleEndianToInt(bytes, 0, B);
/*
* Chunk memory allocations; We choose 'd' so that there will be 2**d chunks, each not
* larger than 32KiB, except that the minimum chunk size is 2 * r * 32.
*/
int d = 0, total = N * r;
while ((N - d) > 2 && total > (1 << 10))
{
++d;
total >>>= 1;
}
int MFLenWords = MFLenBytes >>> 2;
for (int BOff = 0; BOff < BLen; BOff += MFLenWords)
{
// TODO These can be done in parallel threads
SMix(B, BOff, N, d, r);
}
Pack.intToLittleEndian(B, bytes, 0);
return SingleIterationPBKDF2(P, bytes, dkLen);
}
finally
{
Clear(bytes);
Clear(B);
}
}
private static byte[] SingleIterationPBKDF2(byte[] P, byte[] S, int dkLen)
{
PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(new SHA256Digest());
pGen.init(P, S, 1);
KeyParameter key = (KeyParameter)pGen.generateDerivedMacParameters(dkLen * 8);
return key.getKey();
}
private static void SMix(int[] B, int BOff, int N, int d, int r)
{
int powN = Integers.numberOfTrailingZeros(N);
int blocksPerChunk = N >>> d;
int chunkCount = 1 << d, chunkMask = blocksPerChunk - 1, chunkPow = powN - d;
int BCount = r * 32;
int[] blockX1 = new int[16];
int[] blockX2 = new int[16];
int[] blockY = new int[BCount];
int[] X = new int[BCount];
int[][] VV = new int[chunkCount][];
try
{
System.arraycopy(B, BOff, X, 0, BCount);
for (int c = 0; c < chunkCount; ++c)
{
int[] V = new int[blocksPerChunk * BCount];
VV[c] = V;
int off = 0;
for (int i = 0; i < blocksPerChunk; i += 2)
{
System.arraycopy(X, 0, V, off, BCount);
off += BCount;
BlockMix(X, blockX1, blockX2, blockY, r);
System.arraycopy(blockY, 0, V, off, BCount);
off += BCount;
BlockMix(blockY, blockX1, blockX2, X, r);
}
}
int mask = N - 1;
for (int i = 0; i < N; ++i)
{
int j = X[BCount - 16] & mask;
int[] V = VV[j >>> chunkPow];
int VOff = (j & chunkMask) * BCount;
System.arraycopy(V, VOff, blockY, 0, BCount);
Xor(blockY, X, 0, blockY);
BlockMix(blockY, blockX1, blockX2, X, r);
}
System.arraycopy(X, 0, B, BOff, BCount);
}
finally
{
ClearAll(VV);
ClearAll(new int[][]{X, blockX1, blockX2, blockY});
}
}
private static void BlockMix(int[] B, int[] X1, int[] X2, int[] Y, int r)
{
System.arraycopy(B, B.length - 16, X1, 0, 16);
int BOff = 0, YOff = 0, halfLen = B.length >>> 1;
for (int i = 2 * r; i > 0; --i)
{
Xor(X1, B, BOff, X2);
Salsa20Engine.salsaCore(8, X2, X1);
System.arraycopy(X1, 0, Y, YOff, 16);
YOff = halfLen + BOff - YOff;
BOff += 16;
}
}
private static void Xor(int[] a, int[] b, int bOff, int[] output)
{
for (int i = output.length - 1; i >= 0; --i)
{
output[i] = a[i] ^ b[bOff + i];
}
}
private static void Clear(byte[] array)
{
if (array != null)
{
Arrays.fill(array, (byte)0);
}
}
private static void Clear(int[] array)
{
if (array != null)
{
Arrays.fill(array, 0);
}
}
private static void ClearAll(int[][] arrays)
{
for (int i = 0; i < arrays.length; ++i)
{
Clear(arrays[i]);
}
}
// note: we know X is non-zero
private static boolean isPowerOf2(int x)
{
return ((x & (x - 1)) == 0);
}
}
@@ -1,118 +0,0 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
/**
* Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
*/
public class Salsa20Engine {
private Salsa20Engine()
{
}
public static void salsaCore(int rounds, int[] input, int[] x)
{
if (input.length != 16)
{
throw new IllegalArgumentException();
}
if (x.length != 16)
{
throw new IllegalArgumentException();
}
if (rounds % 2 != 0)
{
throw new IllegalArgumentException("Number of rounds must be even");
}
int x00 = input[ 0];
int x01 = input[ 1];
int x02 = input[ 2];
int x03 = input[ 3];
int x04 = input[ 4];
int x05 = input[ 5];
int x06 = input[ 6];
int x07 = input[ 7];
int x08 = input[ 8];
int x09 = input[ 9];
int x10 = input[10];
int x11 = input[11];
int x12 = input[12];
int x13 = input[13];
int x14 = input[14];
int x15 = input[15];
for (int i = rounds; i > 0; i -= 2)
{
x04 ^= Integer.rotateLeft(x00 + x12, 7);
x08 ^= Integer.rotateLeft(x04 + x00, 9);
x12 ^= Integer.rotateLeft(x08 + x04, 13);
x00 ^= Integer.rotateLeft(x12 + x08, 18);
x09 ^= Integer.rotateLeft(x05 + x01, 7);
x13 ^= Integer.rotateLeft(x09 + x05, 9);
x01 ^= Integer.rotateLeft(x13 + x09, 13);
x05 ^= Integer.rotateLeft(x01 + x13, 18);
x14 ^= Integer.rotateLeft(x10 + x06, 7);
x02 ^= Integer.rotateLeft(x14 + x10, 9);
x06 ^= Integer.rotateLeft(x02 + x14, 13);
x10 ^= Integer.rotateLeft(x06 + x02, 18);
x03 ^= Integer.rotateLeft(x15 + x11, 7);
x07 ^= Integer.rotateLeft(x03 + x15, 9);
x11 ^= Integer.rotateLeft(x07 + x03, 13);
x15 ^= Integer.rotateLeft(x11 + x07, 18);
x01 ^= Integer.rotateLeft(x00 + x03, 7);
x02 ^= Integer.rotateLeft(x01 + x00, 9);
x03 ^= Integer.rotateLeft(x02 + x01, 13);
x00 ^= Integer.rotateLeft(x03 + x02, 18);
x06 ^= Integer.rotateLeft(x05 + x04, 7);
x07 ^= Integer.rotateLeft(x06 + x05, 9);
x04 ^= Integer.rotateLeft(x07 + x06, 13);
x05 ^= Integer.rotateLeft(x04 + x07, 18);
x11 ^= Integer.rotateLeft(x10 + x09, 7);
x08 ^= Integer.rotateLeft(x11 + x10, 9);
x09 ^= Integer.rotateLeft(x08 + x11, 13);
x10 ^= Integer.rotateLeft(x09 + x08, 18);
x12 ^= Integer.rotateLeft(x15 + x14, 7);
x13 ^= Integer.rotateLeft(x12 + x15, 9);
x14 ^= Integer.rotateLeft(x13 + x12, 13);
x15 ^= Integer.rotateLeft(x14 + x13, 18);
}
x[ 0] = x00 + input[ 0];
x[ 1] = x01 + input[ 1];
x[ 2] = x02 + input[ 2];
x[ 3] = x03 + input[ 3];
x[ 4] = x04 + input[ 4];
x[ 5] = x05 + input[ 5];
x[ 6] = x06 + input[ 6];
x[ 7] = x07 + input[ 7];
x[ 8] = x08 + input[ 8];
x[ 9] = x09 + input[ 9];
x[10] = x10 + input[10];
x[11] = x11 + input[11];
x[12] = x12 + input[12];
x[13] = x13 + input[13];
x[14] = x14 + input[14];
x[15] = x15 + input[15];
}
}
@@ -14,21 +14,6 @@ public class HOTP {
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] hash = getHash(secret, algo, counter);
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
return new OTP(otp, digits);
}
public static byte[] getHash(byte[] secret, String algo, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
// encode counter in big endian
@@ -40,6 +25,16 @@ public class HOTP {
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
return mac.doFinal(counterBytes);
byte[] hash = mac.doFinal(counterBytes);
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
return new OTP(otp, digits);
}
}
@@ -1,54 +0,0 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.beemdevelopment.aegis.encoding.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MOTP {
private final String _code;
private final int _digits;
private MOTP(String code, int digits) {
_code = code;
_digits = digits;
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
throws NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
throws NoSuchAlgorithmException {
long timeBasedCounter = time / period;
String secretAsString = Hex.encode(secret);
String toDigest = timeBasedCounter + secretAsString + pin;
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
return new MOTP(code, digits);
}
@VisibleForTesting
@NonNull
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algo);
byte[] digest = md.digest(toDigest);
return Hex.encode(digest);
}
@NonNull
@Override
public String toString() {
return _code.substring(0, _digits);
}
}
@@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private final int _code;
private final int _digits;
private int _code;
private int _digits;
public OTP(int code, int digits) {
_code = code;
@@ -1,71 +0,0 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class YAOTP {
private static final int EN_ALPHABET_LENGTH = 26;
private final long _code;
private final int _digits;
private YAOTP(long code, int digits) {
_code = code;
_digits = digits;
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
long seconds = System.currentTimeMillis() / 1000;
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
byte[] pinWithHash;
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
stream.write(pinBytes);
stream.write(secret);
pinWithHash = stream.toByteArray();
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(pinWithHash);
if (keyHash[0] == 0) {
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
}
long counter = (long) Math.floor((double) seconds / period);
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
int offset = periodHash[periodHash.length - 1] & 0xf;
periodHash[offset] &= 0x7f;
long otp = ByteBuffer.wrap(periodHash)
.order(ByteOrder.BIG_ENDIAN)
.getLong(offset);
return new YAOTP(otp, digits);
}
@NonNull
@Override
public String toString() {
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
char[] chars = new char[_digits];
for (int i = _digits - 1; i >= 0; i--) {
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
code /= EN_ALPHABET_LENGTH;
}
return new String(chars);
}
}
@@ -1,14 +0,0 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class 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},};
}
}
@@ -1,15 +0,0 @@
package com.beemdevelopment.aegis.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {AuditLogEntry.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AuditLogDao auditLogDao();
}
@@ -1,17 +0,0 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface AuditLogDao {
@Insert
void insert(AuditLogEntry log);
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
LiveData<List<AuditLogEntry>> getAll();
}
@@ -1,61 +0,0 @@
package com.beemdevelopment.aegis.database;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.beemdevelopment.aegis.EventType;
@Entity(tableName = "audit_logs")
public class AuditLogEntry {
@PrimaryKey(autoGenerate = true)
protected long id;
@NonNull
@ColumnInfo(name = "event_type")
private final EventType _eventType;
@ColumnInfo(name = "reference")
private final String _reference;
@ColumnInfo(name = "timestamp")
private final long _timestamp;
@Ignore
public AuditLogEntry(@NonNull EventType eventType) {
this(eventType, null);
}
@Ignore
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
_eventType = eventType;
_reference = reference;
_timestamp = System.currentTimeMillis();
}
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
this.id = id;
_eventType = eventType;
_reference = reference;
_timestamp = timestamp;
}
public long getId() {
return id;
}
public EventType getEventType() {
return _eventType;
}
public String getReference() {
return _reference;
}
public long getTimestamp() {
return _timestamp;
}
}
@@ -1,66 +0,0 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import com.beemdevelopment.aegis.EventType;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AuditLogRepository {
private final AuditLogDao _auditLogDao;
private final Executor _executor;
public AuditLogRepository(AuditLogDao auditLogDao) {
_auditLogDao = auditLogDao;
_executor = Executors.newSingleThreadExecutor();
}
public LiveData<List<AuditLogEntry>> getAllAuditLogEntries() {
return _auditLogDao.getAll();
}
public void addVaultUnlockedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
insert(auditLogEntry);
}
public void addBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addAndroidBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addVaultExportedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
insert(auditLogEntry);
}
public void addEntrySharedEvent(String reference) {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
insert(auditLogEntry);
}
public void addVaultUnlockFailedPasswordEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
insert(auditLogEntry);
}
public void addVaultUnlockFailedBiometricsEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
insert(auditLogEntry);
}
public void insert(AuditLogEntry auditLogEntry) {
_executor.execute(() -> {
_auditLogDao.insert(auditLogEntry);
});
}
}
@@ -0,0 +1,56 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Database {
private static final int VERSION = 1;
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
public JSONObject toJson() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
array.put(e.toJson());
}
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
UUIDMap<DatabaseEntry> entries = db.getEntries();
try {
int ver = obj.getInt("version");
if (ver != VERSION) {
throw new DatabaseException("Unsupported version");
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
entries.add(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
}
return db;
}
public UUIDMap<DatabaseEntry> getEntries() {
return _entries;
}
}
@@ -0,0 +1,167 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
public class DatabaseEntry extends UUIDMap.Value {
private String _name = "";
private String _issuer = "";
private String _group;
private OtpInfo _info;
private byte[] _icon;
private DatabaseEntry(UUID uuid, OtpInfo info) {
super(uuid);
_info = info;
}
public DatabaseEntry(OtpInfo info) {
super();
_info = info;
}
public DatabaseEntry(OtpInfo info, String name, String issuer) {
this(info);
setName(name);
setIssuer(issuer);
}
public DatabaseEntry(GoogleAuthInfo info) {
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("type", _info.getType());
obj.put("uuid", getUUID().toString());
obj.put("name", _name);
obj.put("issuer", _issuer);
obj.put("group", _group);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
obj.put("info", _info.toJson());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
// if there is no uuid, generate a new one
UUID uuid;
if (!obj.has("uuid")) {
uuid = UUID.randomUUID();
} else {
uuid = UUID.fromString(obj.getString("uuid"));
}
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
DatabaseEntry entry = new DatabaseEntry(uuid, info);
entry.setName(obj.getString("name"));
entry.setIssuer(obj.getString("issuer"));
entry.setGroup(obj.optString("group", null));
Object icon = obj.get("icon");
if (icon != JSONObject.NULL) {
entry.setIcon(Base64.decode((String) icon));
}
return entry;
}
public String getName() {
return _name;
}
public String getIssuer() {
return _issuer;
}
public String getGroup() {
return _group;
}
public byte[] getIcon() {
return _icon;
}
public OtpInfo getInfo() {
return _info;
}
public void setName(String name) {
_name = name;
}
public void setIssuer(String issuer) {
_issuer = issuer;
}
public void setGroup(String group) {
_group = group;
}
public void setInfo(OtpInfo info) {
_info = info;
}
public void setIcon(byte[] icon) {
_icon = icon;
}
public boolean hasIcon() {
return _icon != null;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DatabaseEntry)) {
return false;
}
DatabaseEntry entry = (DatabaseEntry) o;
return super.equals(entry) && equivalates(entry);
}
/**
* Reports whether this entry is equivalent to the given entry. The UUIDs of these
* entries are ignored during the comparison, so they are not necessarily the same
* instance.
*/
public boolean equivalates(DatabaseEntry entry) {
return getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon());
}
/**
* Reports whether this entry has its values set to the defaults.
*/
public boolean isDefault() {
return equivalates(getDefault());
}
public static DatabaseEntry getDefault() {
try {
return new DatabaseEntry(new TotpInfo(null));
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db;
public class DatabaseException extends Exception {
public DatabaseException(Throwable cause) {
super(cause);
}
public DatabaseException(String message) {
super(message);
}
}
@@ -1,29 +1,30 @@
package com.beemdevelopment.aegis.vault;
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotListException;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vault.slots.SlotListException;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public class VaultFile {
public class DatabaseFile {
public static final byte VERSION = 1;
private Object _content;
private Header _header;
public VaultFile() {
public DatabaseFile() {
}
private VaultFile(Object content, Header header) {
private DatabaseFile(Object content, Header header) {
_content = content;
_header = header;
}
@@ -59,29 +60,29 @@ public class VaultFile {
}
}
public static VaultFile fromJson(JSONObject obj) throws VaultFileException {
public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException {
try {
if (obj.getInt("version") > VERSION) {
throw new VaultFileException("unsupported version");
throw new DatabaseFileException("unsupported version");
}
Header header = Header.fromJson(obj.getJSONObject("header"));
if (!header.isEmpty()) {
return new VaultFile(obj.getString("db"), header);
return new DatabaseFile(obj.getString("db"), header);
}
return new VaultFile(obj.getJSONObject("db"), header);
return new DatabaseFile(obj.getJSONObject("db"), header);
} catch (JSONException e) {
throw new VaultFileException(e);
throw new DatabaseFileException(e);
}
}
public static VaultFile fromBytes(byte[] data) throws VaultFileException {
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
try {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return VaultFile.fromJson(obj);
return DatabaseFile.fromJson(obj);
} catch (JSONException e) {
throw new VaultFileException(e);
throw new DatabaseFileException(e);
}
}
@@ -89,13 +90,13 @@ public class VaultFile {
return (JSONObject) _content;
}
public JSONObject getContent(VaultFileCredentials creds) throws VaultFileException {
public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException {
try {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
} catch (MasterKeyException | JSONException | EncodingException e) {
throw new VaultFileException(e);
} catch (MasterKeyException | JSONException | Base64Exception e) {
throw new DatabaseFileException(e);
}
}
@@ -104,34 +105,19 @@ public class VaultFile {
_header = new Header(null, null);
}
public void setContent(JSONObject obj, VaultFileCredentials creds) throws VaultFileException {
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
try {
String string = obj.toString(4);
byte[] vaultBytes = string.getBytes(StandardCharsets.UTF_8);
byte[] dbBytes = string.getBytes(StandardCharsets.UTF_8);
CryptResult result = creds.encrypt(vaultBytes);
CryptResult result = creds.encrypt(dbBytes);
_content = Base64.encode(result.getData());
_header = new Header(creds.getSlots(), result.getParams());
} catch (MasterKeyException | JSONException e) {
throw new VaultFileException(e);
throw new DatabaseFileException(e);
}
}
/**
* Returns a copy of this VaultFile that's suitable for exporting.
* In case there's a backup password slot, any regular password slots are stripped.
*/
public VaultFile exportable() {
if (!isEncrypted()) {
return this;
}
return new VaultFile(_content, new VaultFile.Header(
getHeader().getSlots().exportable(),
getHeader().getParams()
));
}
public static class Header {
private SlotList _slots;
private CryptParameters _params;
@@ -141,7 +127,7 @@ public class VaultFile {
_params = params;
}
public static Header fromJson(JSONObject obj) throws VaultFileException {
public static Header fromJson(JSONObject obj) throws DatabaseFileException {
if (obj.isNull("slots") && obj.isNull("params")) {
return new Header(null, null);
}
@@ -150,8 +136,8 @@ public class VaultFile {
SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
return new Header(slots, params);
} catch (SlotListException | JSONException | EncodingException e) {
throw new VaultFileException(e);
} catch (SlotListException | JSONException | HexException e) {
throw new DatabaseFileException(e);
}
}
@@ -1,26 +1,23 @@
package com.beemdevelopment.aegis.vault;
import androidx.annotation.NonNull;
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotList;
import java.io.Serializable;
public class VaultFileCredentials implements Serializable {
public class DatabaseFileCredentials implements Serializable {
private MasterKey _key;
private SlotList _slots;
public VaultFileCredentials() {
public DatabaseFileCredentials() {
_key = MasterKey.generate();
_slots = new SlotList();
}
public VaultFileCredentials(MasterKey key, SlotList slots) {
public DatabaseFileCredentials(MasterKey key, SlotList slots) {
_key = key;
_slots = slots;
}
@@ -40,18 +37,4 @@ public class VaultFileCredentials implements Serializable {
public SlotList getSlots() {
return _slots;
}
/**
* Returns a copy of these VaultFileCredentials that is suitable for exporting.
* In case there's a backup password slot, any regular password slots are stripped.
*/
public VaultFileCredentials exportable() {
return new VaultFileCredentials(_key, _slots.exportable());
}
@NonNull
@Override
public VaultFileCredentials clone() {
return Cloner.clone(this);
}
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db;
public class DatabaseFileException extends Exception {
public DatabaseFileException(Throwable cause) {
super(cause);
}
public DatabaseFileException(String message) {
super(message);
}
}
@@ -0,0 +1,239 @@
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");
}
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.db;
public class DatabaseManagerException extends Exception {
public DatabaseManagerException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,20 @@
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,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
@@ -16,19 +16,16 @@ import javax.crypto.SecretKey;
public class PasswordSlot extends RawSlot {
private boolean _repaired;
private boolean _isBackup;
private SCryptParameters _params;
public PasswordSlot() {
super();
}
protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams, boolean repaired, boolean isBackup) {
protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams, boolean repaired) {
super(uuid, key, keyParams);
_params = scryptParams;
_repaired = repaired;
_isBackup = isBackup;
}
@Override
@@ -40,7 +37,6 @@ public class PasswordSlot extends RawSlot {
obj.put("p", _params.getP());
obj.put("salt", Hex.encode(_params.getSalt()));
obj.put("repaired", _repaired);
obj.put("is_backup", _isBackup);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
@@ -74,19 +70,8 @@ public class PasswordSlot extends RawSlot {
return _repaired;
}
/**
* Reports whether this slot is a backup password slot.
*/
public boolean isBackup() {
return _isBackup;
}
public void setIsBackup(boolean isBackup) {
_isBackup = isBackup;
}
@Override
public byte getType() {
return TYPE_PASSWORD;
return TYPE_DERIVED;
}
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
@@ -1,12 +1,12 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
@@ -27,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec;
public abstract class Slot extends UUIDMap.Value {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_PASSWORD = 0x01;
public final static byte TYPE_BIOMETRIC = 0x02;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
private byte[] _encryptedMasterKey;
private CryptParameters _encryptedMasterKeyParams;
@@ -51,7 +51,7 @@ public abstract class Slot extends UUIDMap.Value {
public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
try {
CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
SecretKey key = new SecretKeySpec(res.getData(), "AES");
SecretKey key = new SecretKeySpec(res.getData(), CryptoUtils.CRYPTO_AEAD);
return new MasterKey(key);
} catch (BadPaddingException e) {
throw new SlotIntegrityException(e);
@@ -97,10 +97,6 @@ public abstract class Slot extends UUIDMap.Value {
}
}
protected byte[] getEncryptedMasterKey() {
return _encryptedMasterKey;
}
public JSONObject toJson() {
try {
JSONObject obj = new JSONObject();
@@ -132,24 +128,23 @@ public abstract class Slot extends UUIDMap.Value {
case Slot.TYPE_RAW:
slot = new RawSlot(uuid, key, keyParams);
break;
case Slot.TYPE_PASSWORD:
case Slot.TYPE_DERIVED:
SCryptParameters scryptParams = new SCryptParameters(
obj.getInt("n"),
obj.getInt("r"),
obj.getInt("p"),
Hex.decode(obj.getString("salt"))
obj.getInt("n"),
obj.getInt("r"),
obj.getInt("p"),
Hex.decode(obj.getString("salt"))
);
boolean repaired = obj.optBoolean("repaired", false);
boolean isBackup = obj.optBoolean("is_backup", false);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired, isBackup);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired);
break;
case Slot.TYPE_BIOMETRIC:
slot = new BiometricSlot(uuid, key, keyParams);
case Slot.TYPE_FINGERPRINT:
slot = new FingerprintSlot(uuid, key, keyParams);
break;
default:
throw new SlotException("unrecognized slot type");
}
} catch (JSONException | EncodingException e) {
} catch (JSONException | HexException e) {
throw new SlotException(e);
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
public class SlotException extends Exception {
public SlotException(Throwable cause) {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
public class SlotIntegrityException extends Exception {
public SlotIntegrityException() {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.util.UUIDMap;
@@ -8,7 +8,6 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SlotList extends UUIDMap<Slot> {
public JSONArray toJson() {
@@ -55,48 +54,7 @@ public class SlotList extends UUIDMap<Slot> {
return list;
}
public List<PasswordSlot> findBackupPasswordSlots() {
return findAll(PasswordSlot.class)
.stream()
.filter(PasswordSlot::isBackup)
.collect(Collectors.toList());
}
public List<PasswordSlot> findRegularPasswordSlots() {
return findAll(PasswordSlot.class)
.stream()
.filter(s -> !s.isBackup())
.collect(Collectors.toList());
}
public <T extends Slot> boolean has(Class<T> type) {
return find(type) != null;
}
/**
* Returns a copy of this SlotList that is suitable for exporting.
* In case there's a backup password slot, any regular password slots are stripped.
*/
public SlotList exportable() {
boolean hasBackupSlots = false;
for (Slot slot : this) {
if (slot instanceof PasswordSlot && ((PasswordSlot) slot).isBackup()) {
hasBackupSlots = true;
break;
}
}
if (!hasBackupSlots) {
return this;
}
SlotList slots = new SlotList();
for (Slot slot : this) {
if (!(slot instanceof PasswordSlot) || ((PasswordSlot) slot).isBackup()) {
slots.add(slot);
}
}
return slots;
}
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.vault.slots;
package com.beemdevelopment.aegis.db.slots;
public class SlotListException extends Exception {
public SlotListException(Throwable cause) {
@@ -1,29 +1,155 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
// modified for use in Aegis
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/* (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 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'
};
}
/**
* 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];
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
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);
}
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
/**
* 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);
}
}
return bytes;
}
public static String encode(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return encode(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;
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}
@@ -1,27 +1,24 @@
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 EncodingException {
public static byte[] decode(String s) throws Base64Exception {
try {
return BaseEncoding.base64().decode(s);
return android.util.Base64.decode(s, _flags);
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
throw new Base64Exception(e);
}
}
public static byte[] decode(byte[] s) throws EncodingException {
return decode(new String(s, StandardCharsets.UTF_8));
}
public static String encode(byte[] data) {
return BaseEncoding.base64().encode(data);
byte[] encoded = android.util.Base64.encode(data, _flags);
return new String(encoded, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class Base64Exception extends Exception {
public Base64Exception(Throwable cause) {
super(cause);
}
}
@@ -1,13 +0,0 @@
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
public EncodingException(String message) {
super(message);
}
}
@@ -1,23 +1,46 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
// 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.
public class Hex {
private Hex() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
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);
}
return out;
}
public static String encode(byte[] data) {
return BaseEncoding.base16().lowerCase().encode(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();
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}
@@ -1,54 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.provider.Settings;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
public class AnimationsHelper {
private AnimationsHelper() {
}
public static Animation loadScaledAnimation(Context context, int animationResId) {
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
}
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
animation.setDuration(newDuration);
return animation;
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
Animation animation = controller.getAnimation();
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
return controller;
}
public enum Scale {
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
private final String _setting;
Scale(String setting) {
_setting = setting;
}
public float getValue(Context context) {
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
}
public boolean isZero(Context context) {
return getValue(context) == 0;
}
}
}
@@ -1,136 +0,0 @@
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);
}
}
@@ -1,30 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricPrompt.ERROR_CANCELED
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
@@ -1,31 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
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();
}
}
@@ -1,27 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.ArrayRes;
import com.beemdevelopment.aegis.R;
import java.util.List;
public class DropdownHelper {
private DropdownHelper() {
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
dropdown.setAdapter(adapter);
}
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
dropdown.setAdapter(adapter);
}
}
@@ -9,10 +9,22 @@ public class EditTextHelper {
private EditTextHelper() {
}
public static void clearEditText(EditText text) {
text.getText().clear();
}
public static char[] getEditTextChars(EditText text) {
Editable editable = text.getText();
char[] chars = new char[editable.length()];
editable.getChars(0, editable.length(), chars, 0);
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);
return chars;
}
@@ -6,6 +6,7 @@ import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
@@ -16,27 +17,7 @@ public class FabScrollHelper {
}
public void onScroll(int dx, int dy) {
if (dy > 2 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
setVisible(false);
} else if (dy < -2 && _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 {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
@@ -51,7 +32,18 @@ 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();
}
}
}
@@ -0,0 +1,34 @@
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;
}
}
@@ -0,0 +1,156 @@
// 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();
}
}
@@ -1,14 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.util.DisplayMetrics;
public class MetricsHelper {
private MetricsHelper() {
}
public static int convertDpToPixels(Context context, float dp) {
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
}
@@ -1,73 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
public class PasswordStrengthHelper {
// Limit the password length to prevent zxcvbn4j from exploding
private static final int MAX_PASSWORD_LENGTH = 64;
// Material design color palette
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
private final Zxcvbn _zxcvbn = new Zxcvbn();
private final EditText _textPassword;
private final ProgressBar _barPasswordStrength;
private final TextView _textPasswordStrength;
private final TextInputLayout _textPasswordWrapper;
public PasswordStrengthHelper(
EditText textPassword,
ProgressBar barPasswordStrength,
TextView textPasswordStrength,
TextInputLayout textPasswordWrapper
) {
_textPassword = textPassword;
_barPasswordStrength = barPasswordStrength;
_textPasswordStrength = textPasswordStrength;
_textPasswordWrapper = textPasswordWrapper;
}
public void measure(Context context) {
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
_barPasswordStrength.setProgress(0);
_textPasswordStrength.setText(R.string.password_strength_unknown);
} else {
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
String warning = strength.getFeedback().getWarning();
_textPasswordWrapper.setError(warning);
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
strength.wipe();
}
}
private 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];
}
private static String getColor(int score) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
return COLORS[score];
}
}

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