Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3506fdaad9 | ||
|
|
8824b779f5 | ||
|
|
4cab92d615 | ||
|
|
35a14d3198 | ||
|
|
e46041ece0 | ||
|
|
28ebaf48c8 | ||
|
|
fc96e7919a | ||
|
|
08b80d2e72 | ||
|
|
051cf9370b | ||
|
|
3d13296890 | ||
|
|
93f32c2903 | ||
|
|
f168768033 | ||
|
|
5ab4307963 | ||
|
|
d0baeef064 | ||
|
|
44b83c888c | ||
|
|
da45fa1969 | ||
|
|
5ad3a10251 | ||
|
|
9ec94a472f | ||
|
|
4e4c39eea4 | ||
|
|
3be9aecb88 | ||
|
|
a6a5af781e | ||
|
|
a93ced6e34 | ||
|
|
82135c8b9d | ||
|
|
9f248e0802 | ||
|
|
65aa95504b | ||
|
|
def7c67614 | ||
|
|
be3f30e666 | ||
|
|
b920fe4b8c | ||
|
|
4b6b10526d | ||
|
|
74b2f8f398 | ||
|
|
34ef4b3fb4 | ||
|
|
63b0f6bb99 | ||
|
|
36973669ea | ||
|
|
2e301c5389 | ||
|
|
6a4b0f7bc5 | ||
|
|
8b063815ab | ||
|
|
75b79d3d4a | ||
|
|
b61b73685f | ||
|
|
864e3bd6f4 | ||
|
|
013e4ae28b | ||
|
|
51a0a16afb | ||
|
|
a5165e2345 | ||
|
|
c41a337791 | ||
|
|
e58dec35c0 | ||
|
|
5e7697039a | ||
|
|
45de9cea3a | ||
|
|
9f075e7bc6 | ||
|
|
a88d1cf11e | ||
|
|
a6eec88dcb | ||
|
|
3c4afe36e8 | ||
|
|
5b71df37ec | ||
|
|
d7d3d52a30 | ||
|
|
ac222c4946 | ||
|
|
0200287203 | ||
|
|
5677bd4112 | ||
|
|
cbe3697d68 | ||
|
|
445410fcd7 | ||
|
|
1d513441c6 | ||
|
|
0a31103f91 | ||
|
|
85bf4adc72 | ||
|
|
cc55a6dacb | ||
|
|
e099e2cacd | ||
|
|
658946ff70 | ||
|
|
b6a31ed789 | ||
|
|
9c272a0d4d | ||
|
|
3e626a37db | ||
|
|
e2150e3823 | ||
|
|
6480fef315 | ||
|
|
95df2ceb42 | ||
|
|
189698dddb | ||
|
|
438d5581aa | ||
|
|
7af78b11f5 | ||
|
|
5d68211ce9 | ||
|
|
a7e1a88dc8 | ||
|
|
3054032a37 | ||
|
|
79279630ba | ||
|
|
e019fb6db2 | ||
|
|
85bdecc573 | ||
|
|
21f3d93295 | ||
|
|
888e9d0e4a | ||
|
|
e6d84e6859 | ||
|
|
65371f18d4 | ||
|
|
1b5ab33c25 | ||
|
|
396dd0b169 | ||
|
|
eb40369634 | ||
|
|
0c87f06972 | ||
|
|
c774796422 | ||
|
|
c12c6ab107 | ||
|
|
e3b9b67fee | ||
|
|
2407b4b780 | ||
|
|
e962d91c77 | ||
|
|
d0afa2f61f | ||
|
|
58d13ba9e3 | ||
|
|
631ca76136 | ||
|
|
9cb9d47857 | ||
|
|
2323d89938 | ||
|
|
6769fefd00 | ||
|
|
5dbf1c7ce2 | ||
|
|
8d3c113475 | ||
|
|
087bf08d44 | ||
|
|
32b2ce280c | ||
|
|
37c9b033c5 | ||
|
|
f8d806999d | ||
|
|
86c4011797 | ||
|
|
bbc4c1d61c | ||
|
|
951e908d4b | ||
|
|
536929ff0e | ||
|
|
1f5a46b883 | ||
|
|
4cab47235b | ||
|
|
5524a77736 | ||
|
|
a3cdfe693d | ||
|
|
9e4ad1495d | ||
|
|
914da94c01 | ||
|
|
950dcce803 | ||
|
|
f82e480c32 | ||
|
|
9c5726cbca | ||
|
|
550e53f225 | ||
|
|
6e04ca054d | ||
|
|
b014d95005 | ||
|
|
0123b98e47 | ||
|
|
d7093487dd | ||
|
|
02c9a0cb1c | ||
|
|
eb29be587f | ||
|
|
f64a23d98f | ||
|
|
8bbadb9e57 | ||
|
|
4066cd83cc | ||
|
|
cca35bd5e5 | ||
|
|
187adfa738 | ||
|
|
246c03dea1 | ||
|
|
e68571a7a6 | ||
|
|
128c85fc0c | ||
|
|
c75b25c4b6 | ||
|
|
6abcfcea75 | ||
|
|
b300b72626 |
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "Bug report"
|
||||
about: "Create a report to help us fix a bug"
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!-- Please read the [bug reports section of the contribution
|
||||
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
|
||||
|
||||
###### Info
|
||||
|
||||
* __Version__:
|
||||
* __Source__: (Google Play/GitHub/F-Droid/?)
|
||||
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
|
||||
* __Device:__
|
||||
* __Android version and ROM:__
|
||||
|
||||
###### Steps to reproduce
|
||||
|
||||
A detailed list of reproduction steps.
|
||||
|
||||
###### What do you expect to happen?
|
||||
|
||||
...
|
||||
|
||||
###### What happens instead?
|
||||
|
||||
...
|
||||
|
||||
###### Log
|
||||
|
||||
```
|
||||
If applicable, paste the debug log here.
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Suggest a new feature for this project"
|
||||
labels: proposal
|
||||
---
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
language: android
|
||||
env:
|
||||
global:
|
||||
- ANDROID_HOME=/usr/local/android-sdk
|
||||
android:
|
||||
components:
|
||||
- android-29
|
||||
- build-tools-29.0.2
|
||||
before_install:
|
||||
- mkdir -p $ANDROID_HOME/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
script:
|
||||
- ./gradlew build test
|
||||
@@ -0,0 +1,106 @@
|
||||
# Contributing
|
||||
|
||||
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 on GitHub. Discussing your idea with
|
||||
us first ensures that everyone is on the same page before you start working on
|
||||
your change. We don't like rejecting pull requests.
|
||||
|
||||
## Bug reports
|
||||
|
||||
We use GitHub's issue tracker to track bugs. Bug reports must follow [the
|
||||
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 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
|
||||
|
||||
In some cases, we ask our users to obtain a debug log from their device. This is
|
||||
typically only necessary if Aegis:
|
||||
- Is unable to recover from an error and crashes.
|
||||
- Only shows a generic error to the user, but writes a more detailed one to the
|
||||
log.
|
||||
|
||||
Capturing a log with the Android Debug Bridge (ADB) allows us to see the stack
|
||||
trace and the exception that occurred.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before you can capture a log, you first need to go through a one-time setup
|
||||
process on your Android device and computer.
|
||||
|
||||
##### Prerequisites
|
||||
|
||||
- Your Android device.
|
||||
- A computer with Windows, Mac or Linux.
|
||||
- A USB cable to connect your Android device to your computer.
|
||||
|
||||
##### Setup
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
1. Navigate to ``Settings -> About``, scroll down and start tapping on the build
|
||||
number until developer options are enabled.
|
||||
2. Navigate to ``Settings -> System -> Developer options`` and enable ``USB
|
||||
debugging``.
|
||||
|
||||
These navigation steps may differ slightly across Android versions and ROMs.
|
||||
|
||||
__On your computer__:
|
||||
|
||||
3. Download and extract the SDK platform tools for Android:
|
||||
https://developer.android.com/studio/releases/platform-tools.
|
||||
4. Start your terminal emulator (If you're on Windows, start PowerShell) and
|
||||
navigate to the folder where platform-tools was extracted.
|
||||
5. Execute ``adb devices``.
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
6. A prompt will appear. Select "Always allow from this computer" and accept the
|
||||
connection.
|
||||
|
||||
#### Capturing a log
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
1. Start Aegis.
|
||||
|
||||
__On your PC__:
|
||||
|
||||
2. Start your terminal emulator (If you're on Windows, start PowerShell) and
|
||||
navigate to the folder where platform-tools was extracted.
|
||||
3. Start a log capture by executing the following commands.
|
||||
|
||||
```
|
||||
adb logcat -c
|
||||
adb logcat -f debug.log
|
||||
```
|
||||
|
||||
The logcat command captures the full system log by default, which may expose
|
||||
some sensitive information. While this information can sometimes help with
|
||||
finding the root cause of the issue, it is not always necessary. To only
|
||||
capture the log output of Aegis, replace the last logcat command with the
|
||||
one below:
|
||||
|
||||
```sh
|
||||
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``._
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
4. Reproduce the issue.
|
||||
|
||||
__On your PC__:
|
||||
|
||||
5. Stop the log capture with Ctrl+C.
|
||||
6. Attach the ``debug.log`` file to your issue on GitHub.
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Aegis is a free, secure and open source 2FA app for Android.
|
||||
|
||||
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
|
||||
Aegis' security design and vault format is described in detail in [this
|
||||
document](docs/vault.md).
|
||||
|
||||
# Features
|
||||
|
||||
@@ -10,7 +11,7 @@ Aegis' security design and vault format is described in detail in [this document
|
||||
- Secure
|
||||
- Encryption (AES-256)
|
||||
- Password (scrypt)
|
||||
- Fingerprint (Android Keystore)
|
||||
- Biometrics (Android Keystore)
|
||||
- Screen capture prevention
|
||||
- Tap to reveal ability
|
||||
- Multiple ways to add new entries
|
||||
@@ -42,17 +43,22 @@ Aegis' security design and vault format is described in detail in [this document
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img width=200 alt="Main Activity"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
|
||||
[<img width=200 alt="Settings Activity"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
|
||||
[<img width=200 alt="Edit Activity"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 1"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 2"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 3"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
|
||||
|
||||
[<img width=200 alt="Main Activity"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
|
||||
[<img width=200 alt="Main Activity"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 4"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 5"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 6"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
|
||||
|
||||
[<img width=200 alt="Screenshot 7"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
|
||||
|
||||
## Downloads
|
||||
|
||||
@@ -96,6 +102,16 @@ Certificate fingerprints:
|
||||
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
|
||||
```
|
||||
|
||||
### Community
|
||||
|
||||
- [aegis-icons](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
While we're still working on better icon support in Aegis, krisu5 has started
|
||||
a third-party repository for icons that you may want to check out.
|
||||
|
||||
[<img width=500 alt="Aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
## Libraries
|
||||
|
||||
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
|
||||
@@ -104,7 +120,6 @@ Certificate fingerprints:
|
||||
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
|
||||
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
|
||||
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
|
||||
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
|
||||
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
|
||||
Dodenhof
|
||||
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
|
||||
@@ -114,7 +129,3 @@ Certificate fingerprints:
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0. See the
|
||||
[LICENSE](LICENSE) file for details.
|
||||
|
||||
## Attribution
|
||||
|
||||
The icon is largely based on a design by Freepik from Flaticon.
|
||||
|
||||
@@ -1,27 +1,61 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine cmd
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 12
|
||||
versionName "0.4.2"
|
||||
targetSdkVersion 29
|
||||
versionCode 24
|
||||
versionName "1.1"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
disable "MissingTranslation"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
ignoreFailures false
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title:"AegisDev", iconName:"ic_launcher_debug"]
|
||||
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
manifestPlaceholders = [title:"Aegis", iconName:"ic_launcher"]
|
||||
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
@@ -29,24 +63,33 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def libsuVersion = '2.3.2'
|
||||
def libsuVersion = '2.5.1'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.2'
|
||||
implementation 'com.github.apl-devs:appintro:5.1.0'
|
||||
implementation 'com.github.avito-tech:krop:0.44'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'com.mattprecious.swirl:swirl:1.0.0'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9'
|
||||
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation 'com.github.bumptech.glide:annotations:4.10.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
||||
transitive = false
|
||||
}
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,25 @@
|
||||
package="com.beemdevelopment.aegis">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".AegisApplication"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:theme">
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about"></activity>
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="Select entries" />
|
||||
@@ -26,8 +32,17 @@
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
@@ -48,6 +63,8 @@
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="Manage groups" />
|
||||
|
||||
<service android:name=".services.NotificationService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,89 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
* {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
background-color: %1$s;
|
||||
color: %2$s;
|
||||
}
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding: 0;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
list-style-position: outside;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 1.1</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>
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,6 +1,8 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -10,37 +12,53 @@ import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class AegisApplication extends Application {
|
||||
private DatabaseManager _manager;
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_manager = new DatabaseManager(this);
|
||||
_manager = new VaultManager(this);
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
// listen for SCREEN_OFF events
|
||||
ScreenOffReceiver receiver = new ScreenOffReceiver();
|
||||
registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
intentFilter.addAction(CODE_LOCK_VAULT_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() {
|
||||
public VaultManager getVaultManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
@@ -65,6 +83,8 @@ public class AegisApplication extends Application {
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked();
|
||||
}
|
||||
|
||||
stopService(new Intent(AegisApplication.this, NotificationService.class));
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
|
||||
@@ -81,14 +101,28 @@ public class AegisApplication extends Application {
|
||||
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.qr_scanner))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
|
||||
}
|
||||
|
||||
private class ScreenOffReceiver extends BroadcastReceiver {
|
||||
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()) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum CancelAction {
|
||||
KILL,
|
||||
CLOSE
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Preferences {
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
@@ -15,12 +19,21 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isSearchAccountNameEnabled() {
|
||||
return _prefs.getBoolean("pref_search_names", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isSecureScreenEnabled() {
|
||||
return _prefs.getBoolean("pref_secure_screen", true);
|
||||
// screen security should be enabled by default, but not for debug builds
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
}
|
||||
|
||||
public boolean isAccountNameVisible() {
|
||||
return _prefs.getBoolean("pref_account_name", false);
|
||||
return _prefs.getBoolean("pref_account_name", true);
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
@@ -70,4 +83,18 @@ public class Preferences {
|
||||
public int getTimeout() {
|
||||
return _prefs.getInt("pref_timeout", -1);
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
String lang = _prefs.getString("pref_lang", "system");
|
||||
|
||||
if (lang.equals("system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return Resources.getSystem().getConfiguration().getLocales().get(0);
|
||||
} else {
|
||||
return Resources.getSystem().getConfiguration().locale;
|
||||
}
|
||||
}
|
||||
|
||||
return new Locale(lang);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
|
||||
|
||||
@@ -24,8 +24,8 @@ public enum SortCategory {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public Comparator<DatabaseEntry> getComparator() {
|
||||
Comparator<DatabaseEntry> comparator = null;
|
||||
public Comparator<VaultEntry> getComparator() {
|
||||
Comparator<VaultEntry> comparator = null;
|
||||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
|
||||
@@ -30,4 +30,15 @@ public enum ViewMode {
|
||||
return R.layout.card_entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
|
||||
*/
|
||||
public float getDividerHeight() {
|
||||
if (this == ViewMode.COMPACT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Database {
|
||||
private static final int VERSION = 1;
|
||||
private DatabaseEntryList _entries = new DatabaseEntryList();
|
||||
|
||||
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();
|
||||
|
||||
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));
|
||||
db.addEntry(entry);
|
||||
}
|
||||
} catch (Base64Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseException(e);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
_entries.replace(newEntry);
|
||||
}
|
||||
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_entries.swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
return _entries.getList();
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
return _entries.getByUUID(uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<DatabaseEntry> iterator() {
|
||||
return _entries.iterator();
|
||||
}
|
||||
|
||||
public void add(DatabaseEntry entry) {
|
||||
if (getByUUID(entry.getUUID()) != null) {
|
||||
throw new AssertionError("entry found with the same uuid");
|
||||
}
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void remove(DatabaseEntry entry) {
|
||||
entry = mustGetByUUID(entry.getUUID());
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replace(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
}
|
||||
|
||||
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getList() {
|
||||
return Collections.unmodifiableList(_entries);
|
||||
}
|
||||
|
||||
public DatabaseEntry getByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseEntry mustGetByUUID(UUID uuid) {
|
||||
DatabaseEntry entry = getByUUID(uuid);
|
||||
if (entry == null) {
|
||||
throw new AssertionError("no entry found with the same uuid");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseException extends Exception {
|
||||
public DatabaseException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseFileException extends Exception {
|
||||
public DatabaseFileException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseManagerException extends Exception {
|
||||
public DatabaseManagerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class FingerprintSlot extends RawSlot {
|
||||
public FingerprintSlot() {
|
||||
super();
|
||||
}
|
||||
|
||||
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
|
||||
super(uuid, key, keyParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getType() {
|
||||
return TYPE_FINGERPRINT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* A class that can prepare initialization of a BiometricSlot by generating a new
|
||||
* key in the Android KeyStore and authenticating a cipher for it through a
|
||||
* BiometricPrompt.
|
||||
*/
|
||||
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
|
||||
private BiometricSlot _slot;
|
||||
private Listener _listener;
|
||||
private BiometricPrompt _prompt;
|
||||
|
||||
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new key in the Android KeyStore for the new BiometricSlot,
|
||||
* initializes a cipher with it and shows a BiometricPrompt to the user for
|
||||
* authentication. If authentication is successful, the new slot will be
|
||||
* initialized and delivered back through the listener.
|
||||
*/
|
||||
public void authenticate(BiometricPrompt.PromptInfo info) {
|
||||
if (_slot != null) {
|
||||
throw new IllegalStateException("Biometric authentication already in progress");
|
||||
}
|
||||
|
||||
KeyStoreHandle keyStore;
|
||||
try {
|
||||
keyStore = new KeyStoreHandle();
|
||||
} catch (KeyStoreHandleException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// generate a new Android KeyStore key
|
||||
// and assign it the UUID of the new slot as an alias
|
||||
Cipher cipher;
|
||||
BiometricSlot slot = new BiometricSlot();
|
||||
try {
|
||||
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_slot = slot;
|
||||
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the BiometricPrompt and resets the state of the initializer. It will
|
||||
* also attempt to delete the previously generated Android KeyStore key.
|
||||
*/
|
||||
public void cancelAuthentication() {
|
||||
if (_slot == null) {
|
||||
throw new IllegalStateException("Biometric authentication not in progress");
|
||||
}
|
||||
|
||||
reset();
|
||||
_prompt.cancelAuthentication();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
if (_slot != null) {
|
||||
try {
|
||||
// clean up the unused KeyStore key
|
||||
// this is non-critical, so just fail silently if an error occurs
|
||||
String uuid = _slot.getUUID().toString();
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
if (keyStore.containsKey(uuid)) {
|
||||
keyStore.deleteKey(uuid);
|
||||
}
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
_slot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(int errorCode, CharSequence errString) {
|
||||
reset();
|
||||
_listener.onSlotInitializationFailed(errorCode, errString);
|
||||
}
|
||||
|
||||
private void fail(Exception e) {
|
||||
e.printStackTrace();
|
||||
fail(0, e.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
fail(errorCode, errString.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
|
||||
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.biometric.BiometricConstants;
|
||||
import androidx.biometric.BiometricManager;
|
||||
|
||||
public class BiometricsHelper {
|
||||
private BiometricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static BiometricManager getManager(Context context) {
|
||||
BiometricManager manager = BiometricManager.from(context);
|
||||
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isCanceled(int errorCode) {
|
||||
return errorCode == BiometricConstants.ERROR_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != null;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
|
||||
public class FingerprintHelper {
|
||||
private FingerprintHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static FingerprintManager getManager(Context context) {
|
||||
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
|
||||
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
||||
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
|
||||
// It has been modified to suit Aegis' needs
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* Small helper class to manage text/icon around fingerprint authentication UI.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
|
||||
|
||||
private static final long ERROR_TIMEOUT_MILLIS = 1600;
|
||||
private static final long SUCCESS_DELAY_MILLIS = 100;
|
||||
|
||||
private final FingerprintManager mFingerprintManager;
|
||||
private final SwirlView mIcon;
|
||||
private final TextView mErrorTextView;
|
||||
private final Callback mCallback;
|
||||
private CancellationSignal mCancellationSignal;
|
||||
|
||||
private boolean mSelfCancelled;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FingerprintUiHelper}.
|
||||
*/
|
||||
public FingerprintUiHelper(FingerprintManager fingerprintManager,
|
||||
SwirlView icon, TextView errorTextView, Callback callback) {
|
||||
mFingerprintManager = fingerprintManager;
|
||||
mIcon = icon;
|
||||
mErrorTextView = errorTextView;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public boolean isFingerprintAuthAvailable() {
|
||||
// The line below prevents the false positive inspection from Android Studio
|
||||
// noinspection ResourceType
|
||||
return mFingerprintManager.isHardwareDetected()
|
||||
&& mFingerprintManager.hasEnrolledFingerprints();
|
||||
}
|
||||
|
||||
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
|
||||
if (!isFingerprintAuthAvailable()) {
|
||||
return;
|
||||
}
|
||||
mCancellationSignal = new CancellationSignal();
|
||||
mSelfCancelled = false;
|
||||
// The line below prevents the false positive inspection from Android Studio
|
||||
// noinspection ResourceType
|
||||
mFingerprintManager
|
||||
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
|
||||
public void stopListening() {
|
||||
if (mCancellationSignal != null) {
|
||||
mSelfCancelled = true;
|
||||
mCancellationSignal.cancel();
|
||||
mCancellationSignal = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
if (!mSelfCancelled) {
|
||||
showError(errString);
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallback.onError();
|
||||
}
|
||||
}, ERROR_TIMEOUT_MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
|
||||
showError(helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
showError(mIcon.getResources().getString(
|
||||
R.string.fingerprint_not_recognized));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
||||
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
|
||||
mIcon.setState(SwirlView.State.OFF);
|
||||
mErrorTextView.setText(
|
||||
mErrorTextView.getResources().getString(R.string.fingerprint_success));
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallback.onAuthenticated();
|
||||
}
|
||||
}, SUCCESS_DELAY_MILLIS);
|
||||
|
||||
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private void showError(CharSequence error) {
|
||||
mIcon.setState(SwirlView.State.ERROR);
|
||||
mErrorTextView.setText(error);
|
||||
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
|
||||
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
private Runnable mResetErrorTextRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mErrorTextView.setText(
|
||||
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
};
|
||||
|
||||
public interface Callback {
|
||||
|
||||
void onAuthenticated();
|
||||
|
||||
void onError();
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package com.beemdevelopment.aegis.helpers;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private VaultEntry _selectedEntry;
|
||||
|
||||
private final ItemTouchHelperAdapter _adapter;
|
||||
private boolean _positionChanged = false;
|
||||
private boolean _isLongPressDragEnabled = true;
|
||||
@@ -22,6 +27,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
_isLongPressDragEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
_selectedEntry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
@@ -30,7 +39,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
int swipeFlags = 0;
|
||||
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
|
||||
if (adapter.getEntryAt(position) != _selectedEntry)
|
||||
{
|
||||
dragFlags = 0;
|
||||
}
|
||||
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
}
|
||||
|
||||
@@ -56,4 +73,6 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
_positionChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class ThemeHelper {
|
||||
private ThemeHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
currentTheme.resolveAttribute(attributeId, typedValue, true);
|
||||
@ColorInt int color = typedValue.data;
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ public class UiRefresher {
|
||||
_handler = new Handler();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
stop();
|
||||
_listener = null;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (_running) {
|
||||
return;
|
||||
@@ -22,15 +27,14 @@ public class UiRefresher {
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (_running) {
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
}, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
_handler.removeCallbacksAndMessages(null);
|
||||
_running = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class UiThreadExecutor implements Executor {
|
||||
private final Handler _handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull Runnable command) {
|
||||
_handler.post(command);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class AccountNameComparator implements Comparator<DatabaseEntry> {
|
||||
public class AccountNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(DatabaseEntry a, DatabaseEntry b) {
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getName().compareToIgnoreCase(b.getName());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class IssuerNameComparator implements Comparator<DatabaseEntry> {
|
||||
public class IssuerNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(DatabaseEntry a, DatabaseEntry b) {
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFile;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
||||
@@ -32,23 +32,24 @@ public class AegisImporter extends DatabaseImporter {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
DatabaseFile file = DatabaseFile.fromBytes(bytes);
|
||||
VaultFile file = VaultFile.fromBytes(bytes);
|
||||
if (file.isEncrypted()) {
|
||||
return new EncryptedState(file);
|
||||
}
|
||||
return new DecryptedState(file.getContent());
|
||||
} catch (DatabaseFileException | IOException e) {
|
||||
} catch (VaultFileException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends State {
|
||||
private DatabaseFile _file;
|
||||
private VaultFile _file;
|
||||
|
||||
private EncryptedState(DatabaseFile file) {
|
||||
private EncryptedState(VaultFile file) {
|
||||
super(true);
|
||||
_file = file;
|
||||
}
|
||||
@@ -57,7 +58,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||
return _file.getHeader().getSlots();
|
||||
}
|
||||
|
||||
public State decrypt(DatabaseFileCredentials creds) throws DatabaseFileException {
|
||||
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
return new DecryptedState(obj);
|
||||
}
|
||||
@@ -85,7 +86,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject entryObj = array.getJSONObject(i);
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(entryObj);
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -98,9 +99,9 @@ public class AegisImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return DatabaseEntry.fromJson(obj);
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (JSONException | OtpInfoException | Base64Exception e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
@@ -20,11 +23,14 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
@@ -32,9 +38,18 @@ import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AndOtpImporter extends DatabaseImporter {
|
||||
private static final int INT_SIZE = 4;
|
||||
private static final int NONCE_SIZE = 12;
|
||||
private static final int TAG_SIZE = 16;
|
||||
private static final int SALT_SIZE = 12;
|
||||
private static final int KEY_SIZE = 256; // bits
|
||||
|
||||
private static final int MAX_ITERATIONS = 10000;
|
||||
|
||||
public AndOtpImporter(Context context) {
|
||||
super(context);
|
||||
@@ -50,6 +65,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
byte[] bytes;
|
||||
try {
|
||||
@@ -80,45 +96,76 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
|
||||
try {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support encrypted andOTP backups
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
SecretKey key;
|
||||
int offset = 0;
|
||||
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
key = new SecretKeySpec(keyBytes, "AES");
|
||||
} else {
|
||||
offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1 || iterations > MAX_ITERATIONS) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
}
|
||||
|
||||
// extract nonce and tag
|
||||
byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length);
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
|
||||
int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE;
|
||||
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
|
||||
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
|
||||
return read(result.getData());
|
||||
} catch (IOException | BadPaddingException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| InvalidKeySpecException
|
||||
| NoSuchPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
String[] choices = new String[]{
|
||||
context.getResources().getString(R.string.andotp_new_format),
|
||||
context.getResources().getString(R.string.andotp_old_format)
|
||||
};
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.choose_andotp_importer)
|
||||
.setSingleChoiceItems(choices, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password, i != 0);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.create());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +184,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
try {
|
||||
JSONObject obj = _obj.getJSONObject(i);
|
||||
DatabaseEntry entry = convertEntry(obj);
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
@@ -149,7 +196,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algorithm");
|
||||
@@ -182,7 +229,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, issuer);
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
|
||||
private static final String _pkgName = "com.authy.authy";
|
||||
|
||||
public AuthyImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.nextTag();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
|
||||
array = new JSONArray(entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return new State(array);
|
||||
} catch (XmlPullParserException | JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private JSONArray _obj;
|
||||
|
||||
private State(JSONArray obj) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
JSONObject entryObj = _obj.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
|
||||
authyEntryInfo.OriginalName = entry.getString("originalName");
|
||||
authyEntryInfo.AccountType = entry.getString("accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
sanitizeEntryInfo(authyEntryInfo);
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | Base32Exception e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
|
||||
String seperator = "";
|
||||
|
||||
if (info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
seperator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
seperator = " - ";
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + seperator, "");
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthyEntryInfo {
|
||||
String OriginalName;
|
||||
String AccountType;
|
||||
String Issuer;
|
||||
String Name;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,12 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -31,13 +27,18 @@ public abstract class DatabaseImporter {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new LinkedHashMap<>();
|
||||
_importers.put("Aegis", AegisImporter.class);
|
||||
_importers.put("Authy", AuthyImporter.class);
|
||||
_importers.put("andOTP", AndOtpImporter.class);
|
||||
_importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_importers.put("Steam", SteamImporter.class);
|
||||
_importers.put("WinAuth", WinAuthImporter.class);
|
||||
|
||||
_appImporters = new LinkedHashMap<>();
|
||||
_appImporters.put("Authy", AuthyImporter.class);
|
||||
_appImporters.put("FreeOTP", FreeOtpImporter.class);
|
||||
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_appImporters.put("Steam", SteamImporter.class);
|
||||
}
|
||||
@@ -111,10 +112,10 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
@@ -122,7 +123,7 @@ public abstract class DatabaseImporter {
|
||||
_errors.add(error);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
public UUIDMap<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
@@ -131,43 +132,35 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileReader implements Closeable {
|
||||
public static class FileReader {
|
||||
private InputStream _stream;
|
||||
private boolean _internal;
|
||||
|
||||
private FileReader(InputStream stream) {
|
||||
public FileReader(InputStream stream) {
|
||||
this(stream, false);
|
||||
}
|
||||
|
||||
public FileReader(InputStream stream, boolean internal) {
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
public static FileReader open(String filename)
|
||||
throws FileNotFoundException {
|
||||
FileInputStream stream = new FileInputStream(filename);
|
||||
return new FileReader(stream);
|
||||
}
|
||||
|
||||
public static FileReader open(SuFile file)
|
||||
throws FileNotFoundException {
|
||||
SuFileInputStream stream = new SuFileInputStream(file);
|
||||
return new FileReader(stream);
|
||||
}
|
||||
|
||||
public static FileReader open(Context context, Uri uri)
|
||||
throws FileNotFoundException {
|
||||
InputStream stream = context.getContentResolver().openInputStream(uri);
|
||||
return new FileReader(stream);
|
||||
_internal = internal;
|
||||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
ByteInputStream stream = ByteInputStream.create(_stream);
|
||||
return stream.getBytes();
|
||||
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
|
||||
return stream.getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return _stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
_stream.close();
|
||||
/**
|
||||
* Reports whether this reader reads the internal state of an app.
|
||||
* @return true if reading from internal file, false if reading from external file
|
||||
*/
|
||||
public boolean isInternal() {
|
||||
return _internal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -37,6 +39,7 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
@@ -44,17 +47,22 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.nextTag();
|
||||
|
||||
List<XmlEntry> entries = parse(parser);
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (!entry.Name.equals("tokenOrder")) {
|
||||
entries.add(new JSONObject(entry.Value));
|
||||
}
|
||||
}
|
||||
return new State(entries);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
} catch (XmlPullParserException | IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<XmlEntry> _entries;
|
||||
private List<JSONObject> _entries;
|
||||
|
||||
private State(List<XmlEntry> entries) {
|
||||
public State(List<JSONObject> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
@@ -63,34 +71,37 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (XmlEntry xmlEntry : _entries) {
|
||||
// TODO: order
|
||||
if (!xmlEntry.Name.equals("tokenOrder")) {
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(xmlEntry);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
for (JSONObject obj : _entries) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(entry.Value);
|
||||
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
String name = obj.optString("label");
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
case "totp":
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
int period = obj.getInt("period");
|
||||
if (issuer.equals("Steam")) {
|
||||
info = new SteamInfo(secret, algo, digits, period);
|
||||
} else {
|
||||
info = new TotpInfo(secret, algo, digits, period);
|
||||
}
|
||||
break;
|
||||
case "hotp":
|
||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||
@@ -99,18 +110,16 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
}
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
String name = obj.optString("label");
|
||||
return new DatabaseEntry(info, name, issuer);
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.Value);
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<XmlEntry> parse(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException {
|
||||
List<XmlEntry> entries = new ArrayList<>();
|
||||
private static List<JSONObject> parseXml(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException, JSONException {
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
@@ -123,7 +132,10 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(parseEntry(parser));
|
||||
JSONObject entry = parseXmlEntry(parser);
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
@@ -137,19 +149,21 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
private static JSONObject parseXmlEntry(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException, JSONException {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
String name = parser.getAttributeValue(null, "name");
|
||||
String value = parseText(parser);
|
||||
String value = parseXmlText(parser);
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
|
||||
XmlEntry entry = new XmlEntry();
|
||||
entry.Name = name;
|
||||
entry.Value = value;
|
||||
return entry;
|
||||
if (name.equals("tokenOrder")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new JSONObject(value);
|
||||
}
|
||||
|
||||
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String text = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
text = parser.getText();
|
||||
@@ -176,9 +190,4 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class XmlEntry {
|
||||
String Name;
|
||||
String Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FreeOtpPlusImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/tokens.xml";
|
||||
private static final String _pkgName = "org.liberty.android.freeotpplus";
|
||||
|
||||
public FreeOtpPlusImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
State state;
|
||||
|
||||
if (reader.isInternal()) {
|
||||
state = new FreeOtpImporter(getContext()).read(reader);
|
||||
} else {
|
||||
try {
|
||||
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
JSONArray array = obj.getJSONArray("tokens");
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
entries.add(array.getJSONObject(i));
|
||||
}
|
||||
|
||||
state = new FreeOtpImporter.State(entries);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
@@ -43,6 +43,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
File file;
|
||||
|
||||
@@ -91,7 +92,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
for (Entry sqlEntry : _entries) {
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(sqlEntry);
|
||||
VaultEntry entry = convertEntry(sqlEntry);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -101,7 +102,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
|
||||
|
||||
@@ -123,7 +124,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
name = parts[1];
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, entry.getIssuer());
|
||||
return new VaultEntry(info, name, entry.getIssuer());
|
||||
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
@@ -43,6 +43,7 @@ public class SteamImporter extends DatabaseImporter {
|
||||
return new SuFile(_subDir, files[0].getName()).getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
|
||||
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
|
||||
@@ -65,7 +66,7 @@ public class SteamImporter extends DatabaseImporter {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(_obj);
|
||||
VaultEntry entry = convertEntry(_obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -74,13 +75,13 @@ public class SteamImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base64.decode(obj.getString("shared_secret"));
|
||||
SteamInfo info = new SteamInfo(secret);
|
||||
|
||||
String account = obj.getString("account_name");
|
||||
return new DatabaseEntry(info, account, "Steam");
|
||||
return new VaultEntry(info, account, "Steam");
|
||||
} catch (JSONException | Base64Exception | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class WinAuthImporter extends DatabaseImporter {
|
||||
public WinAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try {
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
lines.add(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new State(lines);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private ArrayList<String> _lines;
|
||||
|
||||
private State(ArrayList<String> lines) {
|
||||
super(false);
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (String line : _lines) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(line);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
|
||||
VaultEntry vaultEntry = new VaultEntry(info);
|
||||
vaultEntry.setIssuer(vaultEntry.getName());
|
||||
vaultEntry.setName("WinAuth");
|
||||
|
||||
return vaultEntry;
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,12 @@ public class TotpInfo extends OtpInfo {
|
||||
}
|
||||
|
||||
public static boolean isPeriodValid(int period) {
|
||||
return period > 0;
|
||||
if (period <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for the possibility of an overflow when converting to milliseconds
|
||||
return period <= Integer.MAX_VALUE / 1000;
|
||||
}
|
||||
|
||||
public void setPeriod(int period) throws OtpInfoException {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.beemdevelopment.aegis.services;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class NotificationService extends Service {
|
||||
public static final int VAULT_UNLOCKED_ID = 1;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent,int flags, int startId){
|
||||
super.onStartCommand(intent, flags, startId);
|
||||
serviceMethod();
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
|
||||
public void serviceMethod() {
|
||||
Intent intentAction = new Intent(CODE_LOCK_VAULT_ACTION);
|
||||
PendingIntent lockDatabaseIntent = PendingIntent.getBroadcast(this, 1, intentAction, 0);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CODE_LOCK_STATUS_ID)
|
||||
.setSmallIcon(R.drawable.ic_fingerprint_black_24dp)
|
||||
.setContentTitle(getString(R.string.app_name_full))
|
||||
.setContentText(getString(R.string.vault_unlocked_state))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(lockDatabaseIntent);
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.notify(VAULT_UNLOCKED_ID, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.cancel(VAULT_UNLOCKED_ID);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideLicense;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
import de.psdev.licensesdialog.LicensesDialog;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
|
||||
private static String GITHUB = "https://github.com/beemdevelopment/Aegis";
|
||||
private static String WEBSITE_ALEXANDER = "https://alexbakker.me";
|
||||
private static String GITHUB_MICHAEL = "https://github.com/michaelschattgen";
|
||||
|
||||
private static String MAIL_BEEMDEVELOPMENT = "beemdevelopment@gmail.com";
|
||||
private static String WEBSITE_BEEMDEVELOPMENT = "https://beem.dev/";
|
||||
private static String PLAYSTORE_BEEMDEVELOPMENT = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentAppVersion());
|
||||
|
||||
View btnAppVersion = findViewById(R.id.btn_app_version);
|
||||
btnAppVersion.setOnClickListener(v -> {
|
||||
copyToClipboard(getCurrentAppVersion(), R.string.version_copied);
|
||||
});
|
||||
|
||||
View btnGithub = findViewById(R.id.btn_github);
|
||||
btnGithub.setOnClickListener(v -> openUrl(GITHUB));
|
||||
|
||||
View btnAlexander = findViewById(R.id.btn_alexander);
|
||||
btnAlexander.setOnClickListener(v -> openUrl(WEBSITE_ALEXANDER));
|
||||
|
||||
View btnMichael = findViewById(R.id.btn_michael);
|
||||
btnMichael.setOnClickListener(v -> openUrl(GITHUB_MICHAEL));
|
||||
|
||||
View btnMail = findViewById(R.id.btn_email);
|
||||
btnMail.setOnClickListener(v -> openMail(MAIL_BEEMDEVELOPMENT));
|
||||
|
||||
View btnWebsite = findViewById(R.id.btn_website);
|
||||
btnWebsite.setOnClickListener(v -> openUrl(WEBSITE_BEEMDEVELOPMENT));
|
||||
|
||||
View btnRate = findViewById(R.id.btn_rate);
|
||||
btnRate.setOnClickListener(v -> openUrl(PLAYSTORE_BEEMDEVELOPMENT ));
|
||||
|
||||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
});
|
||||
}
|
||||
|
||||
private static String getCurrentAppVersion() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
|
||||
}
|
||||
|
||||
return BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
private void openUrl(String url) {
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW);
|
||||
browserIntent.setData(Uri.parse(url));
|
||||
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private void copyToClipboard(String text, @StringRes int messageId) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData data = ClipData.newPlainText("text/plain", text);
|
||||
clipboard.setPrimaryClip(data);
|
||||
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void openMail(String mailaddress) {
|
||||
Intent mailIntent = new Intent(Intent.ACTION_SENDTO);
|
||||
mailIntent.setData(Uri.parse("mailto:" + mailaddress));
|
||||
mailIntent.putExtra(Intent.EXTRA_EMAIL, mailaddress);
|
||||
mailIntent.putExtra(Intent.EXTRA_SUBJECT, R.string.app_name_full);
|
||||
|
||||
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
|
||||
}
|
||||
|
||||
private void showLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
|
||||
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
|
||||
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
|
||||
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.licenses)
|
||||
.setNoticesCssStyle(stylesheet)
|
||||
.setIncludeOwnLicense(true)
|
||||
.build()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.WindowManager;
|
||||
@@ -16,16 +16,21 @@ import com.beemdevelopment.aegis.Theme;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
|
||||
private boolean _resumed;
|
||||
private AegisApplication _app;
|
||||
private Theme _currentTheme;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
_app = (AegisApplication) getApplication();
|
||||
|
||||
// set the theme and create the activity
|
||||
setPreferredTheme(getPreferences().getCurrentTheme());
|
||||
// set the theme and locale before creating the activity
|
||||
Preferences prefs = getPreferences();
|
||||
setPreferredTheme(prefs.getCurrentTheme());
|
||||
setLocale(prefs.getLocale());
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// if the app was killed, relaunch MainActivity and close everything else
|
||||
@@ -84,6 +89,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
_currentTheme = theme;
|
||||
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme);
|
||||
@@ -97,6 +104,15 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
}
|
||||
|
||||
protected void setLocale(Locale locale) {
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Configuration config = new Configuration();
|
||||
config.locale = locale;
|
||||
|
||||
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity has been resumed. (i.e. onResume was called)
|
||||
*/
|
||||
@@ -109,7 +125,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
*/
|
||||
private boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && _app.getDatabaseManager().isLocked();
|
||||
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
|
||||
}
|
||||
|
||||
private void setGlobalAnimationDurationScale() {
|
||||
@@ -122,4 +138,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Theme getCurrentTheme() {
|
||||
return _currentTheme;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,56 +2,55 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
|
||||
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
private FingerprintUiHelper _fingerHelper;
|
||||
private FingerprintManager.CryptoObject _fingerCryptoObj;
|
||||
private BiometricPrompt.CryptoObject _bioCryptoObj;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_auth);
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
|
||||
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
|
||||
TextView textFingerprint = findViewById(R.id.text_fingerprint);
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
Button decryptButton = findViewById(R.id.button_decrypt);
|
||||
Button biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
@@ -60,23 +59,17 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
return false;
|
||||
});
|
||||
|
||||
SwirlView imgFingerprint = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert);
|
||||
imgFingerprint = new SwirlView(this);
|
||||
insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
|
||||
|
||||
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
|
||||
FingerprintManager manager = FingerprintHelper.getManager(this);
|
||||
if (manager != null && _slots.has(FingerprintSlot.class)) {
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
boolean invalidated = false;
|
||||
|
||||
try {
|
||||
// find a fingerprint slot with an id that matches an alias in the keystore
|
||||
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
||||
// find a biometric slot with an id that matches an alias in the keystore
|
||||
for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) {
|
||||
String id = slot.getUUID().toString();
|
||||
KeyStoreHandle handle = new KeyStoreHandle();
|
||||
if (handle.containsKey(id)) {
|
||||
@@ -86,10 +79,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
invalidated = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
|
||||
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
|
||||
boxFingerprint.setVisibility(View.VISIBLE);
|
||||
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
biometricsButton.setVisibility(View.VISIBLE);
|
||||
invalidated = false;
|
||||
break;
|
||||
}
|
||||
@@ -100,7 +94,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
|
||||
// display a help message if a matching invalidated keystore entry was found
|
||||
if (invalidated) {
|
||||
boxFingerprintInfo.setVisibility(View.VISIBLE);
|
||||
boxBiometricInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +109,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
}
|
||||
});
|
||||
|
||||
if (_fingerHelper == null) {
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
showBiometricPrompt();
|
||||
});
|
||||
|
||||
if (_bioCryptoObj == null) {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
@@ -138,43 +136,49 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// ignore back button presses
|
||||
switch (_cancelAction) {
|
||||
case KILL:
|
||||
finishAffinity();
|
||||
|
||||
case CLOSE:
|
||||
Intent intent = new Intent();
|
||||
setResult(RESULT_CANCELED, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_fingerHelper != null) {
|
||||
_fingerHelper.startListening(_fingerCryptoObj);
|
||||
if (_bioPrompt != null) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.authentication))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.build();
|
||||
_bioPrompt.authenticate(info, _bioCryptoObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (_fingerHelper != null) {
|
||||
_fingerHelper.stopListening();
|
||||
if (_bioPrompt != null) {
|
||||
_bioPrompt.cancelAuthentication();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
if (result != null) {
|
||||
@@ -185,7 +189,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new DatabaseFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
@@ -193,4 +197,25 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.InflateException;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class ChangelogDialog extends DialogFragment {
|
||||
private Theme _themeStyle;
|
||||
|
||||
public static ChangelogDialog create() {
|
||||
return new ChangelogDialog();
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View customView;
|
||||
try {
|
||||
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
|
||||
} catch (InflateException e) {
|
||||
e.printStackTrace();
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setTitle(android.R.string.dialog_alert_title)
|
||||
.setMessage(getString(R.string.webview_error))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
AlertDialog dialog = new AlertDialog.Builder(getActivity())
|
||||
.setTitle("Changelog")
|
||||
.setView(customView)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
|
||||
final WebView webView = customView.findViewById(R.id.web_view);
|
||||
StringBuilder buf = new StringBuilder();
|
||||
|
||||
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
|
||||
String str;
|
||||
while ((str = in.readLine()) != null)
|
||||
buf.append(str);
|
||||
|
||||
in.close();
|
||||
String changelog = buf.toString();
|
||||
changelog = replaceStylesheet(changelog);
|
||||
webView.loadData(changelog, "text/html", "UTF-8");
|
||||
} catch (IOException e) {
|
||||
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private String replaceStylesheet(String changelog) {
|
||||
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
|
||||
String textColor = colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
|
||||
|
||||
return String.format(changelog, backgroundColor, textColor);
|
||||
}
|
||||
|
||||
private static String colorToCSS(int color) {
|
||||
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
public ChangelogDialog setTheme(Theme theme) {
|
||||
_themeStyle = theme;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,33 @@ import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class Dialogs {
|
||||
private Dialogs() {
|
||||
@@ -76,6 +70,18 @@ public class Dialogs {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
|
||||
EditText textPassword = view.findViewById(R.id.text_password);
|
||||
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
|
||||
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
textPassword.setTransformationMethod(null);
|
||||
textPassword.clearFocus();
|
||||
textPasswordConfirm.setEnabled(false);
|
||||
} else {
|
||||
textPassword.setTransformationMethod(new PasswordTransformationMethod());
|
||||
textPasswordConfirm.setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.set_password)
|
||||
@@ -92,7 +98,7 @@ public class Dialogs {
|
||||
|
||||
// replace the default listener
|
||||
button.setOnClickListener(v -> {
|
||||
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
|
||||
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ public class Dialogs {
|
||||
TextWatcher watcher = new TextWatcher() {
|
||||
public void onTextChanged(CharSequence c, int start, int before, int count) {
|
||||
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
|
||||
buttonOK.get().setEnabled(equal);
|
||||
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
|
||||
@@ -178,51 +184,6 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
|
||||
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
|
||||
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
|
||||
|
||||
FingerprintManager.CryptoObject obj;
|
||||
FingerprintSlot slot;
|
||||
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
|
||||
FingerprintManager manager = FingerprintHelper.getManager(activity);
|
||||
|
||||
try {
|
||||
slot = new FingerprintSlot();
|
||||
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
|
||||
Cipher cipher = Slot.createEncryptCipher(key);
|
||||
obj = new FingerprintManager.CryptoObject(cipher);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.register_fingerprint)
|
||||
.setView(view)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setOnDismissListener(d -> {
|
||||
helper.get().stopListening();
|
||||
})
|
||||
.create();
|
||||
|
||||
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
listener.onSlotResult(slot, obj.getCipher());
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
helper.get().startListening(obj);
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
void onNumberInputResult(int number);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -28,7 +25,7 @@ import android.widget.TableRow;
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
@@ -39,14 +36,13 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -54,6 +50,8 @@ import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
@@ -62,7 +60,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private static final int PICK_IMAGE_REQUEST = 0;
|
||||
|
||||
private boolean _isNew = false;
|
||||
private DatabaseEntry _origEntry;
|
||||
private VaultEntry _origEntry;
|
||||
private TreeSet<String> _groups;
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
@@ -101,7 +99,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
// retrieve info from the calling activity
|
||||
Intent intent = getIntent();
|
||||
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
|
||||
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
|
||||
_isNew = intent.getBooleanExtra("isNew", false);
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
@@ -109,6 +107,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
setTitle(R.string.add_new_profile);
|
||||
}
|
||||
|
||||
String selectedGroup = intent.getStringExtra("selectedGroup");
|
||||
|
||||
// set up fields
|
||||
_iconView = findViewById(R.id.profile_drawable);
|
||||
_kropView = findViewById(R.id.krop_view);
|
||||
@@ -133,51 +133,49 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
|
||||
// fill the fields with values if possible
|
||||
if (_origEntry != null) {
|
||||
if (_origEntry.hasIcon()) {
|
||||
byte[] imageBytes = _origEntry.getIcon();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
_iconView.setImageBitmap(bitmap);
|
||||
_hasCustomIcon = true;
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
_textName.setText(_origEntry.getName());
|
||||
_textIssuer.setText(_origEntry.getIssuer());
|
||||
|
||||
OtpInfo info = _origEntry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
} else if (info instanceof HotpInfo) {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
_textDigits.setText(Integer.toString(info.getDigits()));
|
||||
|
||||
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = _origEntry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
|
||||
|
||||
String algo = _origEntry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String group = _origEntry.getGroup();
|
||||
if (group != null) {
|
||||
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
|
||||
_spinnerGroup.setSelection(pos + 1, false);
|
||||
}
|
||||
if (_origEntry.hasIcon()) {
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(_origEntry)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
_hasCustomIcon = true;
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
_textName.setText(_origEntry.getName());
|
||||
_textIssuer.setText(_origEntry.getIssuer());
|
||||
|
||||
OtpInfo info = _origEntry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
} else if (info instanceof HotpInfo) {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
_textDigits.setText(Integer.toString(info.getDigits()));
|
||||
|
||||
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = _origEntry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
|
||||
|
||||
String algo = _origEntry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String group = _origEntry.getGroup();
|
||||
setGroup(group);
|
||||
|
||||
// update the icon if the text changed
|
||||
_textIssuer.addTextChangedListener(_iconChangeListener);
|
||||
_textName.addTextChangedListener(_iconChangeListener);
|
||||
@@ -252,9 +250,19 @@ public class EditEntryActivity extends AegisActivity {
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if (_isNew) {
|
||||
openAdvancedSettings();
|
||||
|
||||
setGroup(selectedGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private void setGroup(String groupName) {
|
||||
if (groupName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
|
||||
_spinnerGroup.setSelection(pos + 1, false);
|
||||
}
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
@@ -312,7 +320,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
AtomicReference<String> msg = new AtomicReference<>();
|
||||
AtomicReference<DatabaseEntry> entry = new AtomicReference<>();
|
||||
AtomicReference<VaultEntry> entry = new AtomicReference<>();
|
||||
|
||||
try {
|
||||
entry.set(parseEntry());
|
||||
@@ -321,7 +329,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
// close the activity if the entry has not been changed
|
||||
if (_origEntry != null && !_hasChangedIcon && _origEntry.equals(entry.get())) {
|
||||
if (!_hasChangedIcon && _origEntry.equals(entry.get())) {
|
||||
super.onBackPressed();
|
||||
return;
|
||||
}
|
||||
@@ -380,7 +388,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void finish(DatabaseEntry entry, boolean delete) {
|
||||
private void finish(VaultEntry entry, boolean delete) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
intent.putExtra("delete", delete);
|
||||
@@ -391,24 +399,30 @@ public class EditEntryActivity extends AegisActivity {
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
Uri inputFile = (data.getData());
|
||||
InputStream inputStream;
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
inputStream = getContentResolver().openInputStream(inputFile);
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
_kropView.setBitmap(bitmap);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data.getData())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
@@ -427,7 +441,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseEntry parseEntry() throws ParseException {
|
||||
private VaultEntry parseEntry() throws ParseException {
|
||||
if (_textSecret.length() == 0) {
|
||||
throw new ParseException("Secret is a required field.");
|
||||
}
|
||||
@@ -449,7 +463,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
throw new ParseException("Secret is not valid base32.");
|
||||
}
|
||||
|
||||
// set otp info
|
||||
OtpInfo info;
|
||||
try {
|
||||
switch (type.toLowerCase()) {
|
||||
@@ -478,14 +491,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
throw new ParseException("The entered info is incorrect: " + e.getMessage());
|
||||
}
|
||||
|
||||
// set database entry info
|
||||
DatabaseEntry entry;
|
||||
if (_origEntry == null) {
|
||||
entry = new DatabaseEntry(info);
|
||||
} else {
|
||||
entry = cloneEntry(_origEntry);
|
||||
entry.setInfo(info);
|
||||
}
|
||||
VaultEntry entry = Cloner.clone(_origEntry);
|
||||
entry.setInfo(info);
|
||||
entry.setIssuer(_textIssuer.getText().toString());
|
||||
entry.setName(_textName.getText().toString());
|
||||
|
||||
@@ -499,8 +506,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
if (_hasChangedIcon) {
|
||||
if (_hasCustomIcon) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream);
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data);
|
||||
} else {
|
||||
@@ -520,7 +528,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private boolean onSave() {
|
||||
DatabaseEntry entry;
|
||||
VaultEntry entry;
|
||||
try {
|
||||
entry = parseEntry();
|
||||
} catch (ParseException e) {
|
||||
@@ -560,41 +568,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Bitmap drawableToBitmap(Drawable drawable) {
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
|
||||
final int width = !drawable.getBounds().isEmpty() ? drawable
|
||||
.getBounds().width() : drawable.getIntrinsicWidth();
|
||||
|
||||
final int height = !drawable.getBounds().isEmpty() ? drawable
|
||||
.getBounds().height() : drawable.getIntrinsicHeight();
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width,
|
||||
height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888);
|
||||
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private static DatabaseEntry cloneEntry(DatabaseEntry entry) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
oos.writeObject(entry);
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
ObjectInputStream ois = new ObjectInputStream(bais);
|
||||
return (DatabaseEntry) ois.readObject();
|
||||
} catch (ClassNotFoundException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParseException extends Exception {
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
|
||||
@@ -4,19 +4,20 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.Database;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFile;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManagerException;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
@@ -29,8 +30,6 @@ import org.json.JSONObject;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
|
||||
public static final int RESULT_OK = 0;
|
||||
public static final int RESULT_EXCEPTION = 1;
|
||||
@@ -39,8 +38,8 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
private CustomAuthenticationSlide _authenticationSlide;
|
||||
private Fragment _endSlide;
|
||||
|
||||
private Database _database;
|
||||
private DatabaseFile _databaseFile;
|
||||
private Vault _vault;
|
||||
private VaultFile _vaultFile;
|
||||
private PasswordSlot _passwordSlot;
|
||||
private Cipher _passwordCipher;
|
||||
|
||||
@@ -65,7 +64,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
|
||||
SliderPage homeSliderPage = new SliderPage();
|
||||
homeSliderPage.setTitle(getString(R.string.welcome));
|
||||
homeSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
|
||||
homeSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setDescription(getString(R.string.app_description));
|
||||
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
|
||||
@@ -83,14 +82,13 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
SliderPage endSliderPage = new SliderPage();
|
||||
endSliderPage.setTitle(getString(R.string.setup_completed));
|
||||
endSliderPage.setDescription(getString(R.string.setup_completed_description));
|
||||
endSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
|
||||
endSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
_endSlide = AppIntroFragment.newInstance(endSliderPage);
|
||||
addSlide(_endSlide);
|
||||
|
||||
// create the database and database file
|
||||
_database = new Database();
|
||||
_databaseFile = new DatabaseFile();
|
||||
_vault = new Vault();
|
||||
_vaultFile = new VaultFile();
|
||||
}
|
||||
|
||||
private void setException(Exception e) {
|
||||
@@ -130,12 +128,11 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
}
|
||||
|
||||
// generate the master key
|
||||
DatabaseFileCredentials creds = null;
|
||||
VaultFileCredentials creds = null;
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
creds = new DatabaseFileCredentials();
|
||||
creds = new VaultFileCredentials();
|
||||
}
|
||||
|
||||
SlotList slots = null;
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// encrypt the master key with a key derived from the user's password
|
||||
// and add it to the list of slots
|
||||
@@ -150,30 +147,26 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
}
|
||||
}
|
||||
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
|
||||
try {
|
||||
// encrypt the master key with the fingerprint key
|
||||
// and add it to the list of slots
|
||||
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
|
||||
Cipher cipher = _authenticatedSlide.getFingerCipher();
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
creds.getSlots().add(slot);
|
||||
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
return;
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
}
|
||||
|
||||
// finally, save the database
|
||||
// finally, save the vault
|
||||
try {
|
||||
JSONObject obj = _database.toJson();
|
||||
JSONObject obj = _vault.toJson();
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_databaseFile.setContent(obj);
|
||||
_vaultFile.setContent(obj);
|
||||
} else {
|
||||
_databaseFile.setContent(obj, creds);
|
||||
_vaultFile.setContent(obj, creds);
|
||||
}
|
||||
DatabaseManager.save(getApplicationContext(), _databaseFile);
|
||||
} catch (DatabaseManagerException | DatabaseFileException e) {
|
||||
VaultManager.save(getApplicationContext(), _vaultFile);
|
||||
} catch (VaultManagerException | VaultFileException e) {
|
||||
setException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,27 +11,30 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManagerException;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
@@ -62,23 +65,32 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
// permission request codes
|
||||
private static final int CODE_PERM_CAMERA = 0;
|
||||
private static final int CODE_PERM_READ_STORAGE = 1;
|
||||
|
||||
private AegisApplication _app;
|
||||
private DatabaseManager _db;
|
||||
private VaultManager _vault;
|
||||
private boolean _loaded;
|
||||
private String _checkedGroup;
|
||||
private String _selectedGroup;
|
||||
private boolean _searchSubmitted;
|
||||
|
||||
private VaultEntry _selectedEntry;
|
||||
private ActionMode _actionMode;
|
||||
|
||||
private Menu _menu;
|
||||
private SearchView _searchView;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
private EntryListView _entryListView;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
_db = _app.getDatabaseManager();
|
||||
_vault = _app.getVaultManager();
|
||||
_loaded = false;
|
||||
|
||||
// set up the main view
|
||||
@@ -88,6 +100,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
|
||||
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
|
||||
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
|
||||
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
|
||||
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
|
||||
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
|
||||
@@ -101,12 +115,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
});
|
||||
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
|
||||
startScanImageActivity();
|
||||
});
|
||||
findViewById(R.id.fab_scan).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
@@ -116,6 +125,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
_entryListView.setListener(null);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
// collapse the fab menu on touch
|
||||
@@ -148,7 +163,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && _db.isLocked()) {
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,6 +192,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
case CODE_SCAN_IMAGE:
|
||||
onScanImageResult(resultCode, data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,6 +207,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
case CODE_PERM_CAMERA:
|
||||
startScanActivity();
|
||||
break;
|
||||
case CODE_PERM_READ_STORAGE:
|
||||
startScanImageActivity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +219,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
@@ -210,51 +234,50 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
|
||||
private void startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
if (entry != null) {
|
||||
intent.putExtra("entry", entry);
|
||||
}
|
||||
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
|
||||
intent.putExtra("isNew", isNew);
|
||||
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
|
||||
intent.putExtra("selectedGroup", _selectedGroup);
|
||||
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
deleteEntry(entry);
|
||||
} else {
|
||||
// this profile has been serialized/deserialized and is no longer the same instance it once was
|
||||
// to deal with this, the replaceEntry functions are used
|
||||
_db.replaceEntry(entry);
|
||||
_entryListView.replaceEntry(entry);
|
||||
saveDatabase();
|
||||
VaultEntry oldEntry = _vault.replaceEntry(entry);
|
||||
_entryListView.replaceEntry(oldEntry, entry);
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnterEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +303,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
DatabaseEntry entry = new DatabaseEntry(info);
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
@@ -301,19 +324,23 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
// if the group no longer exists, switch back to 'All'
|
||||
TreeSet<String> groups = _db.getGroups();
|
||||
if (_checkedGroup != null && !groups.contains(_checkedGroup)) {
|
||||
TreeSet<String> groups = _vault.getGroups();
|
||||
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
|
||||
menu.findItem(R.id.menu_filter_all).setChecked(true);
|
||||
setGroupFilter(null);
|
||||
}
|
||||
|
||||
for (String group : groups) {
|
||||
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
|
||||
if (group.equals(_checkedGroup)) {
|
||||
if (group.equals(_selectedGroup)) {
|
||||
item.setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (groups.size() > 0) {
|
||||
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
|
||||
}
|
||||
|
||||
menu.setGroupCheckable(R.id.action_filter_group, true, true);
|
||||
}
|
||||
|
||||
@@ -324,12 +351,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void setGroupFilter(String group) {
|
||||
getSupportActionBar().setSubtitle(group);
|
||||
_checkedGroup = group;
|
||||
_selectedGroup = group;
|
||||
_entryListView.setGroupFilter(group, true);
|
||||
}
|
||||
|
||||
private void addEntry(DatabaseEntry entry) {
|
||||
_db.addEntry(entry);
|
||||
private void addEntry(VaultEntry entry) {
|
||||
_vault.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
@@ -340,18 +367,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockDatabase(creds);
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockVault(creds);
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
|
||||
boolean unlocked = unlockDatabase(creds);
|
||||
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
|
||||
boolean unlocked = unlockVault(creds);
|
||||
|
||||
// save the database in case a slot was repaired
|
||||
// save the vault in case a slot was repaired
|
||||
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
|
||||
_db.setCredentials(creds);
|
||||
saveDatabase();
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
}
|
||||
|
||||
doShortcutActions();
|
||||
@@ -366,10 +393,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startActivityForResult(scannerActivity, CODE_SCAN);
|
||||
}
|
||||
|
||||
private void startScanImageActivity() {
|
||||
if (!PermissionHelper.request(this, CODE_PERM_READ_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
|
||||
}
|
||||
|
||||
private void doShortcutActions() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getStringExtra("action");
|
||||
if (action == null || _db.isLocked()) {
|
||||
if (action == null || _vault.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,14 +421,41 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
intent.removeExtra("action");
|
||||
}
|
||||
|
||||
private void handleDeeplink() {
|
||||
if (_vault.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
Uri uri = intent.getData();
|
||||
getIntent().setData(null);
|
||||
getIntent().setAction(null);
|
||||
|
||||
GoogleAuthInfo info = null;
|
||||
try {
|
||||
info = GoogleAuthInfo.parseUri(uri);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_db.isLocked()) {
|
||||
// start the intro if the database file doesn't exist
|
||||
if (!_db.isLoaded() && !_db.fileExists()) {
|
||||
// the db doesn't exist, start the intro
|
||||
if (_vault.isLocked()) {
|
||||
// start the intro if the vault file doesn't exist
|
||||
if (!_vault.isLoaded() && !_vault.fileExists()) {
|
||||
// the vault doesn't exist, start the intro
|
||||
if (getPreferences().isIntroDone()) {
|
||||
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -397,7 +463,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
return;
|
||||
} else {
|
||||
unlockDatabase(null);
|
||||
unlockVault(null);
|
||||
}
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
@@ -411,61 +477,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
handleDeeplink();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!_searchView.isIconified() || _searchSubmitted) {
|
||||
_searchSubmitted = false;
|
||||
_entryListView.setSearchFilter(null);
|
||||
|
||||
collapseSearchView();
|
||||
setTitle("Aegis");
|
||||
setGroupFilter(_selectedGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_app.isAutoLockEnabled()) {
|
||||
_app.lock();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
|
||||
dialog.setCancelable(true);
|
||||
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
dialog.show();
|
||||
private void deleteEntry(VaultEntry entry) {
|
||||
VaultEntry oldEntry = _vault.removeEntry(entry);
|
||||
saveVault();
|
||||
|
||||
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
|
||||
deleteEntry(entry);
|
||||
// update the filter list if the group no longer exists
|
||||
if (entry.getGroup() != null) {
|
||||
if (!_db.getGroups().contains(entry.getGroup())) {
|
||||
updateGroupFilterMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void deleteEntry(DatabaseEntry entry) {
|
||||
_db.removeEntry(entry);
|
||||
saveDatabase();
|
||||
|
||||
_entryListView.removeEntry(entry);
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -477,16 +518,53 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
updateGroupFilterMenu();
|
||||
updateSortCategoryMenu();
|
||||
}
|
||||
|
||||
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
|
||||
|
||||
_searchView = (SearchView) searchViewMenuItem.getActionView();
|
||||
_searchView.setFocusable(false);
|
||||
_searchView.setQueryHint(getString(R.string.search));
|
||||
_searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String s) {
|
||||
setTitle(getString(R.string.search));
|
||||
getSupportActionBar().setSubtitle(s);
|
||||
_searchSubmitted = true;
|
||||
collapseSearchView();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String s) {
|
||||
if (!_searchSubmitted) {
|
||||
_entryListView.setSearchFilter(s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
_searchView.setOnSearchClickListener(v -> {
|
||||
if (_searchSubmitted) {
|
||||
_searchSubmitted = false;
|
||||
_entryListView.setSearchFilter(null);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
Intent intent = new Intent(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_lock:
|
||||
_app.lock();
|
||||
return true;
|
||||
@@ -531,20 +609,25 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private boolean unlockDatabase(DatabaseFileCredentials creds) {
|
||||
private void collapseSearchView() {
|
||||
_searchView.setQuery(null, false);
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private boolean unlockVault(VaultFileCredentials creds) {
|
||||
try {
|
||||
if (!_db.isLoaded()) {
|
||||
_db.load();
|
||||
if (!_vault.isLoaded()) {
|
||||
_vault.load();
|
||||
}
|
||||
if (_db.isLocked()) {
|
||||
if (_vault.isLocked()) {
|
||||
if (creds == null) {
|
||||
startAuthActivity();
|
||||
return false;
|
||||
} else {
|
||||
_db.unlock(creds);
|
||||
_vault.unlock(creds);
|
||||
}
|
||||
}
|
||||
} catch (DatabaseManagerException e) {
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
|
||||
startAuthActivity();
|
||||
return false;
|
||||
@@ -556,7 +639,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void loadEntries() {
|
||||
// load all entries
|
||||
List<DatabaseEntry> entries = new ArrayList<DatabaseEntry>(_db.getEntries());
|
||||
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.getEntries());
|
||||
_entryListView.addEntries(entries);
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
@@ -564,45 +647,65 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void startAuthActivity() {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("slots", _db.getFileHeader().getSlots());
|
||||
intent.putExtra("slots", _vault.getFileHeader().getSlots());
|
||||
intent.putExtra("requiresUnlock", false);
|
||||
intent.putExtra("cancelAction", CancelAction.KILL);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
}
|
||||
|
||||
private void saveDatabase() {
|
||||
private void saveVault() {
|
||||
try {
|
||||
_db.save();
|
||||
} catch (DatabaseManagerException e) {
|
||||
_vault.save();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockIcon() {
|
||||
// hide the lock icon if the database is not unlocked
|
||||
if (_menu != null && !_db.isLocked()) {
|
||||
// hide the lock icon if the vault is not unlocked
|
||||
if (_menu != null && !_vault.isLocked()) {
|
||||
MenuItem item = _menu.findItem(R.id.action_lock);
|
||||
item.setVisible(_db.isEncryptionEnabled());
|
||||
item.setVisible(_vault.isEncryptionEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
createBottomSheet(entry).show();
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
if (_selectedEntry != null) {
|
||||
if (_selectedEntry == entry) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
copyEntryCode(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_db.swapEntries(entry1, entry2);
|
||||
public void onLongEntryClick(VaultEntry entry) {
|
||||
if (_selectedEntry != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedEntry = entry;
|
||||
_entryListView.setActionModeState(true, entry);
|
||||
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryDrop(DatabaseEntry entry) {
|
||||
saveDatabase();
|
||||
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
|
||||
_vault.swapEntries(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(DatabaseEntry entry) {
|
||||
saveDatabase();
|
||||
public void onEntryDrop(VaultEntry entry) {
|
||||
saveVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(VaultEntry entry) {
|
||||
saveVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -612,6 +715,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
public void onLocked() {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
|
||||
@@ -621,4 +728,62 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
super.onLocked();
|
||||
}
|
||||
|
||||
private void copyEntryCode(VaultEntry entry) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private class ActionModeCallbacks implements ActionMode.Callback {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = mode.getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_action_mode, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_copy:
|
||||
copyEntryCode(_selectedEntry);
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_edit:
|
||||
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntry(_selectedEntry);
|
||||
|
||||
if (_selectedEntry.getGroup() != null) {
|
||||
if (!_vault.getGroups().contains(_selectedEntry.getGroup())) {
|
||||
updateGroupFilterMenu();
|
||||
}
|
||||
}
|
||||
mode.finish();
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
_entryListView.setActionModeState(false, null);
|
||||
_selectedEntry = null;
|
||||
_actionMode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,36 +12,48 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManagerException;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.importers.AegisImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -51,9 +63,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
// activity request codes
|
||||
private static final int CODE_IMPORT = 0;
|
||||
@@ -67,24 +76,26 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private static final int CODE_PERM_EXPORT = 1;
|
||||
|
||||
private Intent _result;
|
||||
private DatabaseManager _db;
|
||||
private VaultManager _vault;
|
||||
|
||||
// keep a reference to the type of database converter the user selected
|
||||
private Class<? extends DatabaseImporter> _importerType;
|
||||
private AegisImporter.State _importerState;
|
||||
private UUIDMap<VaultEntry> _importerEntries;
|
||||
|
||||
private SwitchPreference _encryptionPreference;
|
||||
private SwitchPreference _fingerprintPreference;
|
||||
private SwitchPreference _biometricsPreference;
|
||||
private Preference _autoLockPreference;
|
||||
private Preference _setPasswordPreference;
|
||||
private Preference _slotsPreference;
|
||||
private Preference _groupsPreference;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
AegisApplication app = (AegisApplication) getActivity().getApplication();
|
||||
_db = app.getDatabaseManager();
|
||||
_vault = app.getVaultManager();
|
||||
|
||||
// set the result intent in advance
|
||||
setResult(new Intent());
|
||||
@@ -108,13 +119,20 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Preference langPreference = findPreference("pref_lang");
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
|
||||
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
|
||||
Preference viewModePreference = findPreference("pref_view_mode");
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
|
||||
@@ -132,7 +150,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
|
||||
return true;
|
||||
@@ -186,6 +204,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
Preference searchAccountNamePreference = findPreference("pref_search_names");
|
||||
searchAccountNamePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
|
||||
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
|
||||
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
@@ -228,7 +258,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (!_db.isEncryptionEnabled()) {
|
||||
if (!_vault.isEncryptionEnabled()) {
|
||||
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
@@ -237,8 +267,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
_db.disableEncryption();
|
||||
} catch (DatabaseManagerException e) {
|
||||
_vault.disableEncryption();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -251,6 +281,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
})
|
||||
@@ -261,20 +292,27 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
|
||||
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
_biometricsPreference = (SwitchPreference) findPreference("pref_biometrics");
|
||||
_biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
DatabaseFileCredentials creds = _db.getCredentials();
|
||||
VaultFileCredentials creds = _vault.getCredentials();
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
if (!slots.has(FingerprintSlot.class)) {
|
||||
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
|
||||
if (!slots.has(BiometricSlot.class)) {
|
||||
if (BiometricsHelper.isAvailable(getContext())) {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.set_up_biometric))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.build();
|
||||
initializer.authenticate(info);
|
||||
}
|
||||
} else {
|
||||
// remove the fingerprint slot
|
||||
FingerprintSlot slot = slots.find(FingerprintSlot.class);
|
||||
// remove the biometric slot
|
||||
BiometricSlot slot = slots.find(BiometricSlot.class);
|
||||
slots.remove(slot);
|
||||
_db.setCredentials(creds);
|
||||
_vault.setCredentials(creds);
|
||||
|
||||
// remove the KeyStore key
|
||||
try {
|
||||
@@ -284,7 +322,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
saveDatabase();
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@@ -303,7 +341,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
|
||||
intent.putExtra("creds", _db.getCredentials());
|
||||
intent.putExtra("creds", _vault.getCredentials());
|
||||
startActivityForResult(intent, CODE_SLOTS);
|
||||
return true;
|
||||
}
|
||||
@@ -314,15 +352,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
|
||||
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
|
||||
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
|
||||
startActivityForResult(intent, CODE_GROUPS);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated (Bundle savedInstanceState) {
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
@@ -428,7 +468,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
|
||||
SuFile file = importer.getAppPath();
|
||||
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(file)) {
|
||||
try (SuFileInputStream stream = new SuFileInputStream(file)) {
|
||||
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true);
|
||||
importDatabase(importer, reader);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
@@ -444,12 +485,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
try {
|
||||
DatabaseImporter.State state = importer.read(reader);
|
||||
if (state.isEncrypted()) {
|
||||
// temporary special case for encrypted Aegis databases
|
||||
// temporary special case for encrypted Aegis vaults
|
||||
if (state instanceof AegisImporter.EncryptedState) {
|
||||
_importerState = state;
|
||||
|
||||
Intent intent = new Intent(getActivity(), AuthActivity.class);
|
||||
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
|
||||
intent.putExtra("cancelAction", CancelAction.CLOSE);
|
||||
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
|
||||
} else {
|
||||
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
|
||||
@@ -480,11 +522,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
DatabaseImporter.State state;
|
||||
try {
|
||||
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
|
||||
} catch (DatabaseFileException e) {
|
||||
} catch (VaultFileException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
@@ -500,8 +542,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(getContext(), uri)) {
|
||||
try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) {
|
||||
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
|
||||
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream);
|
||||
importDatabase(importer, reader);
|
||||
} catch (FileNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
|
||||
@@ -522,12 +565,15 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
List<DatabaseEntry> entries = result.getEntries();
|
||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||
_importerEntries = result.getEntries();
|
||||
List<ImportEntry> entries = new ArrayList<>();
|
||||
for (VaultEntry entry : _importerEntries) {
|
||||
entries.add(new ImportEntry(entry));
|
||||
}
|
||||
|
||||
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
|
||||
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
|
||||
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) errors);
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) result.getErrors());
|
||||
startActivityForResult(intent, CODE_SELECT_ENTRIES);
|
||||
}
|
||||
|
||||
@@ -539,24 +585,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
// TODO: create a custom layout to show a message AND a checkbox
|
||||
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle("Export the database")
|
||||
.setTitle(R.string.pref_export_summary)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
String filename;
|
||||
try {
|
||||
filename = _db.export(checked.get());
|
||||
} catch (DatabaseManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.exporting_database_error, Toast.LENGTH_SHORT).show();
|
||||
filename = _vault.export(checked.get());
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the new file is visible
|
||||
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
|
||||
|
||||
Toast.makeText(getActivity(), getString(R.string.export_database_location) + filename, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getActivity(), getString(R.string.export_vault_location) + filename, Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
if (_db.isEncryptionEnabled()) {
|
||||
final String[] items = {"Keep the database encrypted"};
|
||||
if (_vault.isEncryptionEnabled()) {
|
||||
final String[] items = {"Keep the vault encrypted"};
|
||||
final boolean[] checkedItems = {true};
|
||||
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
|
||||
@Override
|
||||
@@ -575,9 +621,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
|
||||
_db.setCredentials(creds);
|
||||
saveDatabase();
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@@ -588,7 +634,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
|
||||
|
||||
for (DatabaseEntry entry : _db.getEntries()) {
|
||||
for (VaultEntry entry : _vault.getEntries()) {
|
||||
if (!groups.contains(entry.getGroup())) {
|
||||
entry.setGroup(null);
|
||||
}
|
||||
@@ -600,30 +646,33 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) data.getSerializableExtra("entries");
|
||||
for (DatabaseEntry entry : entries) {
|
||||
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
|
||||
for (ImportEntry selectedEntry : selectedEntries) {
|
||||
VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
|
||||
|
||||
// temporary: randomize the UUID of duplicate entries and add them anyway
|
||||
if (_db.getEntryByUUID(entry.getUUID()) != null) {
|
||||
entry.resetUUID();
|
||||
if (_vault.isEntryDuplicate(savedEntry)) {
|
||||
savedEntry.resetUUID();
|
||||
}
|
||||
|
||||
_db.addEntry(entry);
|
||||
_vault.addEntry(savedEntry);
|
||||
}
|
||||
|
||||
if (!saveDatabase()) {
|
||||
_importerEntries = null;
|
||||
if (!saveVault()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String toastMessage = getResources().getString(R.string.imported_entries_count, entries.size());
|
||||
String toastMessage = getResources().getString(R.string.imported_entries_count, selectedEntries.size());
|
||||
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
_result.putExtra("needsRecreate", true);
|
||||
}
|
||||
|
||||
private boolean saveDatabase() {
|
||||
private boolean saveVault() {
|
||||
try {
|
||||
_db.save();
|
||||
} catch (DatabaseManagerException e) {
|
||||
_vault.save();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
@@ -632,25 +681,27 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
|
||||
private void updateEncryptionPreferences() {
|
||||
boolean encrypted = _db.isEncryptionEnabled();
|
||||
boolean encrypted = _vault.isEncryptionEnabled();
|
||||
_encryptionPreference.setChecked(encrypted, true);
|
||||
_setPasswordPreference.setVisible(encrypted);
|
||||
_fingerprintPreference.setVisible(encrypted);
|
||||
_biometricsPreference.setVisible(encrypted);
|
||||
_slotsPreference.setEnabled(encrypted);
|
||||
_autoLockPreference.setVisible(encrypted);
|
||||
|
||||
if (encrypted) {
|
||||
SlotList slots = _db.getCredentials().getSlots();
|
||||
SlotList slots = _vault.getCredentials().getSlots();
|
||||
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
|
||||
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
|
||||
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
|
||||
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
|
||||
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
|
||||
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
|
||||
_setPasswordPreference.setEnabled(!multiPassword);
|
||||
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
|
||||
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
|
||||
_biometricsPreference.setEnabled(canUseBio && !multiBio);
|
||||
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
|
||||
_slotsPreference.setVisible(showSlots);
|
||||
} else {
|
||||
_setPasswordPreference.setEnabled(false);
|
||||
_fingerprintPreference.setEnabled(false);
|
||||
_fingerprintPreference.setChecked(false, true);
|
||||
_biometricsPreference.setEnabled(false);
|
||||
_biometricsPreference.setChecked(false, true);
|
||||
_slotsPreference.setVisible(false);
|
||||
}
|
||||
}
|
||||
@@ -658,7 +709,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private class SetPasswordListener implements Dialogs.SlotListener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
DatabaseFileCredentials creds = _db.getCredentials();
|
||||
VaultFileCredentials creds = _vault.getCredentials();
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
try {
|
||||
@@ -676,8 +727,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
_db.setCredentials(creds);
|
||||
saveDatabase();
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -687,46 +738,46 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private class RegisterFingerprintListener implements Dialogs.SlotListener {
|
||||
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
DatabaseFileCredentials creds = _db.getCredentials();
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = _vault.getCredentials();
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
_vault.setCredentials(creds);
|
||||
|
||||
slots.add(slot);
|
||||
_db.setCredentials(creds);
|
||||
|
||||
saveDatabase();
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EnableEncryptionListener implements Dialogs.SlotListener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
DatabaseFileCredentials creds = new DatabaseFileCredentials();
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
creds.getSlots().add(slot);
|
||||
_db.enableEncryption(creds);
|
||||
} catch (DatabaseManagerException | SlotException e) {
|
||||
_vault.enableEncryption(creds);
|
||||
} catch (VaultManagerException | SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().startService(new Intent(getActivity(), NotificationService.class));
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SquareFinderView;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
@@ -109,7 +109,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
try {
|
||||
// parse google auth uri
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
|
||||
DatabaseEntry entry = new DatabaseEntry(info);
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
|
||||
@@ -17,7 +17,6 @@ import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
@@ -56,12 +55,11 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
entriesView.setNestedScrollingEnabled(false);
|
||||
|
||||
Intent intent = getIntent();
|
||||
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) intent.getSerializableExtra("entries");
|
||||
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
|
||||
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
|
||||
|
||||
for (DatabaseEntry entry : entries) {
|
||||
ImportEntry importEntry = new ImportEntry(entry);
|
||||
_adapter.addEntry(importEntry);
|
||||
for (ImportEntry entry : entries) {
|
||||
_adapter.addEntry(entry);
|
||||
}
|
||||
|
||||
if (errors.size() > 0) {
|
||||
@@ -103,9 +101,9 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void returnSelectedEntries() {
|
||||
List<DatabaseEntry> entries = _adapter.getSelectedEntries();
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -7,27 +7,30 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
|
||||
private DatabaseFileCredentials _creds;
|
||||
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener {
|
||||
private VaultFileCredentials _creds;
|
||||
private SlotAdapter _adapter;
|
||||
|
||||
private boolean _edited;
|
||||
@@ -42,12 +45,19 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
|
||||
Dialogs.showFingerprintDialog(this ,this);
|
||||
findViewById(R.id.button_add_biometric).setOnClickListener(view -> {
|
||||
if (BiometricsHelper.isAvailable(this)) {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SlotManagerActivity.this, new RegisterBiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.add_biometric_slot))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.build();
|
||||
initializer.authenticate(info);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.button_add_password).setOnClickListener(view -> {
|
||||
Dialogs.showSetPasswordDialog(this, this);
|
||||
Dialogs.showSetPasswordDialog(this, new PasswordListener());
|
||||
});
|
||||
|
||||
// set up the recycler view
|
||||
@@ -59,22 +69,22 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
slotsView.setNestedScrollingEnabled(false);
|
||||
|
||||
// load the slots and masterKey
|
||||
_creds = (DatabaseFileCredentials) getIntent().getSerializableExtra("creds");
|
||||
_creds = (VaultFileCredentials) getIntent().getSerializableExtra("creds");
|
||||
for (Slot slot : _creds.getSlots()) {
|
||||
_adapter.addSlot(slot);
|
||||
}
|
||||
|
||||
updateFingerprintButton();
|
||||
updateBiometricsButton();
|
||||
}
|
||||
|
||||
private void updateFingerprintButton() {
|
||||
// only show the fingerprint option if we can get an instance of the fingerprint manager
|
||||
private void updateBiometricsButton() {
|
||||
// only show the biometrics option if we can get an instance of the biometrics manager
|
||||
// and if none of the slots in the collection has a matching alias in the keystore
|
||||
int visibility = View.VISIBLE;
|
||||
if (FingerprintHelper.getManager(this) != null) {
|
||||
if (BiometricsHelper.isAvailable(this)) {
|
||||
try {
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
|
||||
for (BiometricSlot slot : _creds.getSlots().findAll(BiometricSlot.class)) {
|
||||
if (keyStore.containsKey(slot.getUUID().toString())) {
|
||||
visibility = View.GONE;
|
||||
break;
|
||||
@@ -86,7 +96,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
} else {
|
||||
visibility = View.GONE;
|
||||
}
|
||||
findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
|
||||
findViewById(R.id.button_add_biometric).setVisibility(visibility);
|
||||
}
|
||||
|
||||
private void onSave() {
|
||||
@@ -162,29 +172,60 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
slots.remove(slot);
|
||||
_adapter.removeSlot(slot);
|
||||
_edited = true;
|
||||
updateFingerprintButton();
|
||||
updateBiometricsButton();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
try {
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
private void addSlot(Slot slot) {
|
||||
_creds.getSlots().add(slot);
|
||||
_adapter.addSlot(slot);
|
||||
_edited = true;
|
||||
updateFingerprintButton();
|
||||
updateBiometricsButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
private void showSlotError(String error) {
|
||||
Toast.makeText(SlotManagerActivity.this, getString(R.string.adding_new_slot_error) + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
|
||||
@Override
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
try {
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
addSlot(slot);
|
||||
} catch (SlotException e) {
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
showSlotError(errString.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordListener implements Dialogs.SlotListener {
|
||||
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
try {
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
addSlot(slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
showSlotError(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@GlideModule
|
||||
public class AegisGlideModule extends AppGlideModule {
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class GlideLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Glide License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://github.com/bumptech/glide/blob/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
@Override
|
||||
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull VaultEntry model) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Fetcher implements DataFetcher<ByteBuffer> {
|
||||
private VaultEntry _model;
|
||||
|
||||
private Fetcher(VaultEntry model) {
|
||||
_model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
|
||||
byte[] bytes = _model.getIcon();
|
||||
ByteBuffer buf = ByteBuffer.wrap(bytes);
|
||||
callback.onDataReady(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<ByteBuffer> getDataClass() {
|
||||
return ByteBuffer.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.MEMORY_CACHE;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> {
|
||||
@NonNull
|
||||
@Override
|
||||
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
|
||||
return new IconLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UUIDKey implements Key {
|
||||
private UUID _uuid;
|
||||
|
||||
public UUIDKey(UUID uuid) {
|
||||
_uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(_uuid.toString().getBytes(CHARSET));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return _uuid.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return _uuid.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,40 @@
|
||||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
public class ImportEntry {
|
||||
private DatabaseEntry _entry;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImportEntry implements Serializable {
|
||||
private UUID _uuid;
|
||||
private String _name;
|
||||
private String _issuer;
|
||||
|
||||
private transient Listener _listener;
|
||||
private boolean _isChecked = true;
|
||||
private Listener _listener;
|
||||
|
||||
public ImportEntry(DatabaseEntry entry) {
|
||||
_entry = entry;
|
||||
public ImportEntry(VaultEntry entry) {
|
||||
_uuid = entry.getUUID();
|
||||
_name = entry.getName();
|
||||
_issuer = entry.getIssuer();
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public void setOnCheckedChangedListener(Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public DatabaseEntry getDatabaseEntry() {
|
||||
return _entry;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return _isChecked;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,58 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy;
|
||||
import com.github.paolorotolo.appintro.ISlideSelectionListener;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
|
||||
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
|
||||
private int _cryptType;
|
||||
private EditText _textPassword;
|
||||
private EditText _textPasswordConfirm;
|
||||
private CheckBox _checkPasswordVisibility;
|
||||
private int _bgColor;
|
||||
|
||||
private LinearLayout _boxFingerprint;
|
||||
private SwirlView _imgFingerprint;
|
||||
private TextView _textFingerprint;
|
||||
private FingerprintUiHelper _fingerHelper;
|
||||
private KeyStoreHandle _storeHandle;
|
||||
private FingerprintSlot _fingerSlot;
|
||||
private FingerprintManager.CryptoObject _fingerCryptoObj;
|
||||
private boolean _fingerAuthenticated;
|
||||
private BiometricSlot _bioSlot;
|
||||
private Cipher _bioCipher;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
|
||||
_textPassword = view.findViewById(R.id.text_password);
|
||||
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
_boxFingerprint = view.findViewById(R.id.box_fingerprint);
|
||||
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert);
|
||||
_imgFingerprint = new SwirlView(getContext());
|
||||
insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
_textPassword.setTransformationMethod(null);
|
||||
_textPassword.clearFocus();
|
||||
_textPasswordConfirm.setEnabled(false);
|
||||
} else {
|
||||
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
|
||||
_textPasswordConfirm.setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
_textFingerprint = view.findViewById(R.id.text_fingerprint);
|
||||
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
|
||||
return view;
|
||||
}
|
||||
@@ -66,114 +61,96 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
||||
return _cryptType;
|
||||
}
|
||||
|
||||
public BiometricSlot getBiometricSlot() {
|
||||
return _bioSlot;
|
||||
}
|
||||
|
||||
public Cipher getBiometriCipher() {
|
||||
return _bioCipher;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return EditTextHelper.getEditTextChars(_textPassword);
|
||||
}
|
||||
|
||||
public Cipher getFingerCipher() {
|
||||
return _fingerCryptoObj.getCipher();
|
||||
}
|
||||
|
||||
public FingerprintSlot getFingerSlot() {
|
||||
return _fingerSlot;
|
||||
}
|
||||
|
||||
public void setBgColor(int color) {
|
||||
_bgColor = color;
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.set_up_biometric))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.build();
|
||||
initializer.authenticate(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideSelected() {
|
||||
Intent intent = getActivity().getIntent();
|
||||
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
switch(_cryptType) {
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
|
||||
break;
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
|
||||
_boxFingerprint.setVisibility(View.VISIBLE);
|
||||
|
||||
SecretKey key;
|
||||
try {
|
||||
if (_storeHandle == null) {
|
||||
_storeHandle = new KeyStoreHandle();
|
||||
_fingerSlot = new FingerprintSlot();
|
||||
}
|
||||
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
|
||||
} catch (KeyStoreHandleException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (_fingerHelper == null) {
|
||||
FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
|
||||
_fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this);
|
||||
}
|
||||
|
||||
try {
|
||||
Cipher cipher = Slot.createEncryptCipher(key);
|
||||
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
_fingerHelper.startListening(_fingerCryptoObj);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideDeselected() {
|
||||
if (_fingerHelper != null) {
|
||||
_fingerAuthenticated = false;
|
||||
_boxFingerprint.setVisibility(View.INVISIBLE);
|
||||
_fingerHelper.stopListening();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPolicyRespected() {
|
||||
switch(_cryptType) {
|
||||
switch (_cryptType) {
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
|
||||
return true;
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
|
||||
if (!_fingerAuthenticated) {
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
|
||||
if (_bioSlot == null) {
|
||||
return false;
|
||||
}
|
||||
// intentional fallthrough
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
|
||||
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
|
||||
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
|
||||
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
|
||||
}
|
||||
|
||||
return false;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserIllegallyRequestedNextPage() {
|
||||
String message;
|
||||
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
|
||||
message = getString(R.string.password_equality_error);
|
||||
} else if (!_fingerAuthenticated) {
|
||||
message = getString(R.string.register_fingerprint);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
} else if (_bioSlot == null) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
_fingerAuthenticated = true;
|
||||
}
|
||||
private class BiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
@Override
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
_bioSlot = slot;
|
||||
_bioCipher = cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -10,18 +9,18 @@ import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
|
||||
public static final int CRYPT_TYPE_INVALID = 0;
|
||||
public static final int CRYPT_TYPE_NONE = 1;
|
||||
public static final int CRYPT_TYPE_PASS = 2;
|
||||
public static final int CRYPT_TYPE_FINGER = 3;
|
||||
public static final int CRYPT_TYPE_BIOMETRIC = 3;
|
||||
|
||||
private RadioGroup _buttonGroup;
|
||||
private int _bgColor;
|
||||
@@ -34,10 +33,9 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
|
||||
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
|
||||
|
||||
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
|
||||
FingerprintManager manager = FingerprintHelper.getManager(getContext());
|
||||
if (manager != null) {
|
||||
RadioButton button = view.findViewById(R.id.rb_fingerprint);
|
||||
TextView text = view.findViewById(R.id.text_rb_fingerprint);
|
||||
if (BiometricsHelper.isAvailable(getContext())) {
|
||||
RadioButton button = view.findViewById(R.id.rb_biometrics);
|
||||
TextView text = view.findViewById(R.id.text_rb_biometrics);
|
||||
button.setEnabled(true);
|
||||
text.setEnabled(true);
|
||||
}
|
||||
@@ -75,8 +73,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
|
||||
case R.id.rb_password:
|
||||
id = CRYPT_TYPE_PASS;
|
||||
break;
|
||||
case R.id.rb_fingerprint:
|
||||
id = CRYPT_TYPE_FINGER;
|
||||
case R.id.rb_biometrics:
|
||||
id = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
|
||||
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
|
||||
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
|
||||
private ProgressDialog _dialog;
|
||||
|
||||
public ProgressDialogTask(Context context, String message) {
|
||||
@@ -33,6 +33,13 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(String... values) {
|
||||
if (values.length == 1) {
|
||||
_dialog.setMessage(values[0]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setPriority() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -38,8 +37,8 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[]) params.getObj();
|
||||
return decryptPasswordSlot((PasswordSlot) slot, password);
|
||||
} else if (slot instanceof FingerprintSlot) {
|
||||
return decryptFingerprintSlot((FingerprintSlot) slot, (Cipher) params.getObj());
|
||||
} else if (slot instanceof BiometricSlot) {
|
||||
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
|
||||
}
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -51,7 +50,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
return null;
|
||||
}
|
||||
|
||||
private Result decryptFingerprintSlot(FingerprintSlot slot, Cipher cipher)
|
||||
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
MasterKey key = slot.getKey(cipher);
|
||||
return new Result(key, slot);
|
||||
@@ -72,8 +71,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
throw e;
|
||||
}
|
||||
|
||||
ProgressDialog dialog = getDialog();
|
||||
dialog.setMessage(dialog.getContext().getString(R.string.unlocking_vault_repair));
|
||||
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
|
||||
|
||||
// try to decrypt the password slot with the old key
|
||||
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
@@ -17,30 +21,41 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
|
||||
private List<DatabaseEntry> _entries;
|
||||
private List<DatabaseEntry> _shownEntries;
|
||||
private static Listener _listener;
|
||||
private EntryListView _view;
|
||||
private List<VaultEntry> _entries;
|
||||
private List<VaultEntry> _shownEntries;
|
||||
private VaultEntry _selectedEntry;
|
||||
private VaultEntry _focusedEntry;
|
||||
private boolean _showAccountName;
|
||||
private boolean _searchAccountName;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private String _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
private String _searchFilter;
|
||||
private boolean _isPeriodUniform = true;
|
||||
private Handler _dimHandler;
|
||||
|
||||
// keeps track of the viewholders that are currently bound
|
||||
private List<EntryHolder> _holders;
|
||||
|
||||
public EntryAdapter(Listener listener) {
|
||||
public EntryAdapter(EntryListView view) {
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_holders = new ArrayList<>();
|
||||
_listener = listener;
|
||||
_dimHandler = new Handler();
|
||||
_view = view;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.destroy();
|
||||
}
|
||||
_view = null;
|
||||
}
|
||||
|
||||
public void setShowAccountName(boolean showAccountName) {
|
||||
@@ -55,14 +70,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_tapToRevealTime = number;
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
public void setSearchAccountName(boolean searchAccountName) {
|
||||
_searchAccountName = searchAccountName;
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_highlightEntry = highlightEntry;
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAt(int position) {
|
||||
return _shownEntries.get(position);
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
if (isEntryFiltered(entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean added = false;
|
||||
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
// insert the entry in the correct order
|
||||
// note: this assumes that _shownEntries has already been sorted
|
||||
@@ -90,14 +117,13 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void addEntries(List<DatabaseEntry> entries) {
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity(true);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
entry = getEntryByUUID(entry.getUUID());
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_entries.remove(entry);
|
||||
|
||||
if (_shownEntries.contains(entry)) {
|
||||
@@ -113,10 +139,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_entries.clear();
|
||||
_shownEntries.clear();
|
||||
notifyDataSetChanged();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
|
||||
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
|
||||
if (_shownEntries.contains(oldEntry)) {
|
||||
@@ -139,21 +165,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
private boolean isEntryFiltered(DatabaseEntry entry) {
|
||||
private boolean isEntryFiltered(VaultEntry entry) {
|
||||
String group = entry.getGroup();
|
||||
if (_groupFilter == null) {
|
||||
return false;
|
||||
}
|
||||
return group == null || !group.equals(_groupFilter);
|
||||
}
|
||||
String issuer = entry.getIssuer().toLowerCase();
|
||||
String name = entry.getName().toLowerCase();
|
||||
|
||||
private DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
if (_groupFilter != null) {
|
||||
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group == null || !group.equals(_groupFilter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no entry found with the same id");
|
||||
|
||||
if (_searchFilter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
@@ -181,25 +212,31 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
if (_sortCategory == category) {
|
||||
return;
|
||||
}
|
||||
|
||||
_sortCategory = category;
|
||||
if (apply) {
|
||||
updateShownEntries();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_searchFilter = search != null ? search.toLowerCase() : null;
|
||||
updateShownEntries();
|
||||
}
|
||||
|
||||
private void updateShownEntries() {
|
||||
// clear the list of shown entries first
|
||||
_shownEntries.clear();
|
||||
|
||||
// add entries back that are not filtered out
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
for (VaultEntry entry : _entries) {
|
||||
if (!isEntryFiltered(entry)) {
|
||||
_shownEntries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// sort the remaining list of entries
|
||||
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
Collections.sort(_shownEntries, comparator);
|
||||
}
|
||||
@@ -223,7 +260,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return;
|
||||
}
|
||||
|
||||
_listener.onEntryDrop(_shownEntries.get(position));
|
||||
_view.onEntryDrop(_shownEntries.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -233,8 +270,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the database first
|
||||
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
|
||||
// notify the vault first
|
||||
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
|
||||
|
||||
// update our side of things
|
||||
Collections.swap(_entries, firstPosition, secondPosition);
|
||||
@@ -251,7 +288,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
|
||||
return new EntryHolder(view);
|
||||
EntryHolder holder = new EntryHolder(view);
|
||||
_view.setPreloadView(holder.getIconView());
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -262,19 +301,33 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final EntryHolder holder, int position) {
|
||||
DatabaseEntry entry = _shownEntries.get(position);
|
||||
VaultEntry entry = _shownEntries.get(position);
|
||||
holder.setFocused(entry == _selectedEntry);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
|
||||
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
|
||||
holder.setTapToRevealTime(_tapToRevealTime);
|
||||
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
|
||||
holder.loadIcon(_view);
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
if (_tapToReveal && holder.isCodeHidden()) {
|
||||
holder.revealCode();
|
||||
} else {
|
||||
_listener.onEntryClick(_shownEntries.get(position));
|
||||
boolean handled = false;
|
||||
|
||||
if (_selectedEntry == null) {
|
||||
if (_highlightEntry || _tapToReveal) {
|
||||
if (_focusedEntry == entry) {
|
||||
resetFocus();
|
||||
handled = true;
|
||||
} else {
|
||||
focusEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
_view.onEntryClick(entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -282,7 +335,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
return _listener.onLongEntryClick(_shownEntries.get(position));
|
||||
if (_selectedEntry == null) {
|
||||
setSelectedEntry(_shownEntries.get(position));
|
||||
holder.setFocused(true);
|
||||
}
|
||||
|
||||
return _view.onLongEntryClick(_shownEntries.get(position));
|
||||
}
|
||||
});
|
||||
holder.setOnRefreshClickListener(new View.OnClickListener() {
|
||||
@@ -296,8 +354,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
// notify the listener that the counter has been incremented
|
||||
// this gives it a chance to save the database
|
||||
_listener.onEntryChange(entry);
|
||||
// this gives it a chance to save the vault
|
||||
_view.onEntryChange(entry);
|
||||
|
||||
// finally, refresh the code in the UI
|
||||
holder.refreshCode();
|
||||
@@ -322,12 +380,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
holder.setShowProgress(!_isPeriodUniform);
|
||||
}
|
||||
|
||||
_listener.onPeriodUniformityChanged(_isPeriodUniform);
|
||||
_view.onPeriodUniformityChanged(_isPeriodUniform);
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (DatabaseEntry entry : _shownEntries) {
|
||||
for (VaultEntry entry : _shownEntries) {
|
||||
OtpInfo info = entry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
infos.add((TotpInfo) info);
|
||||
@@ -348,6 +406,58 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return period;
|
||||
}
|
||||
|
||||
private void focusEntry(VaultEntry entry) {
|
||||
_focusedEntry = entry;
|
||||
_dimHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() != _focusedEntry) {
|
||||
if (_highlightEntry) {
|
||||
holder.dim();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
holder.hideCode();
|
||||
}
|
||||
} else {
|
||||
if (_highlightEntry) {
|
||||
holder.highlight();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
holder.revealCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
|
||||
}
|
||||
|
||||
private void resetFocus() {
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (_highlightEntry) {
|
||||
holder.highlight();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
holder.hideCode();
|
||||
}
|
||||
}
|
||||
|
||||
_focusedEntry = null;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
if (entry == null) {
|
||||
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
|
||||
} else if (_highlightEntry) {
|
||||
resetFocus();
|
||||
}
|
||||
|
||||
_selectedEntry = entry;
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
}
|
||||
@@ -358,11 +468,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(DatabaseEntry entry);
|
||||
boolean onLongEntryClick(DatabaseEntry entry);
|
||||
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
|
||||
void onEntryDrop(DatabaseEntry entry);
|
||||
void onEntryChange(DatabaseEntry entry);
|
||||
void onEntryClick(VaultEntry entry);
|
||||
boolean onLongEntryClick(VaultEntry entry);
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onPeriodUniformityChanged(boolean uniform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
private static final float DIMMED_ALPHA = 0.2f;
|
||||
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _profileIssuer;
|
||||
private ImageView _profileDrawable;
|
||||
private DatabaseEntry _entry;
|
||||
private VaultEntry _entry;
|
||||
private ImageView _buttonRefresh;
|
||||
private View _entryDivider;
|
||||
|
||||
private boolean _hidden;
|
||||
private int _tapToRevealTime;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
private View _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
private Handler _hiddenHandler;
|
||||
|
||||
public EntryHolder(final View view) {
|
||||
super(view);
|
||||
|
||||
_view = view.findViewById(R.id.rlCardEntry);
|
||||
|
||||
_profileName = view.findViewById(R.id.profile_account_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
_profileIssuer = view.findViewById(R.id.profile_issuer);
|
||||
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
|
||||
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
|
||||
|
||||
_entryDivider = view.findViewById(R.id.entryDivider);
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
|
||||
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
|
||||
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
|
||||
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (!isCodeHidden()) {
|
||||
if (_hidden) {
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
@@ -67,11 +70,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
});
|
||||
|
||||
_hiddenHandler = new Handler();
|
||||
}
|
||||
|
||||
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress, boolean hidden) {
|
||||
public void setData(VaultEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
|
||||
_entry = entry;
|
||||
_hidden = hidden;
|
||||
|
||||
@@ -87,27 +88,35 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_profileName.setText(" - " + entry.getName());
|
||||
}
|
||||
|
||||
if (_entry.hasIcon()) {
|
||||
byte[] imageBytes = entry.getIcon();
|
||||
Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
_profileDrawable.setImageBitmap(image);
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable);
|
||||
_profileDrawable.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
// cancel any scheduled hideCode calls
|
||||
_hiddenHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
if (_hidden) {
|
||||
hideCode();
|
||||
} else {
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
public void setTapToRevealTime(int number) {
|
||||
_tapToRevealTime = number;
|
||||
public VaultEntry getEntry() {
|
||||
return _entry;
|
||||
}
|
||||
|
||||
public void loadIcon(Fragment fragment) {
|
||||
if (_entry.hasIcon()) {
|
||||
Glide.with(fragment)
|
||||
.asDrawable()
|
||||
.load(_entry)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_profileDrawable);
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
|
||||
_profileDrawable.setImageDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
public ImageView getIconView() {
|
||||
return _profileDrawable;
|
||||
}
|
||||
|
||||
public void setOnRefreshClickListener(View.OnClickListener listener) {
|
||||
@@ -122,17 +131,25 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
|
||||
if (showProgress) {
|
||||
_progressBar.setPeriod(((TotpInfo) _entry.getInfo()).getPeriod());
|
||||
|
||||
if (_entryDivider != null) {
|
||||
_entryDivider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
startRefreshLoop();
|
||||
} else {
|
||||
stopRefreshLoop();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFocused(boolean focused) {
|
||||
if (focused) {
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
|
||||
} else {
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
|
||||
}
|
||||
_view.setSelected(focused);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
_refresher.destroy();
|
||||
}
|
||||
|
||||
public void startRefreshLoop() {
|
||||
_refresher.start();
|
||||
}
|
||||
@@ -142,7 +159,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
public void refreshCode() {
|
||||
if (!isCodeHidden()) {
|
||||
if (!_hidden) {
|
||||
updateCode();
|
||||
}
|
||||
}
|
||||
@@ -150,31 +167,40 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private void updateCode() {
|
||||
OtpInfo info = _entry.getInfo();
|
||||
|
||||
String text;
|
||||
if (info instanceof SteamInfo) {
|
||||
text = info.getOtp();
|
||||
} else {
|
||||
String otp = info.getOtp();
|
||||
text = otp.substring(0, (otp.length() / 2)
|
||||
+ (otp.length() % 2)) + " "
|
||||
+ otp.substring(otp.length() / 2);
|
||||
String otp = info.getOtp();
|
||||
if (!(info instanceof SteamInfo)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < otp.length(); i++) {
|
||||
if (i != 0 && i % 3 == 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(otp.charAt(i));
|
||||
}
|
||||
otp = sb.toString();
|
||||
}
|
||||
|
||||
_profileCode.setText(text);
|
||||
_profileCode.setText(otp);
|
||||
}
|
||||
|
||||
public void revealCode() {
|
||||
updateCode();
|
||||
_hiddenHandler.postDelayed(this::hideCode, _tapToRevealTime * 1000);
|
||||
_hidden = false;
|
||||
}
|
||||
|
||||
private void hideCode() {
|
||||
public void hideCode() {
|
||||
_profileCode.setText(R.string.tap_to_reveal);
|
||||
_hidden = true;
|
||||
}
|
||||
|
||||
public boolean isCodeHidden() {
|
||||
return _hidden;
|
||||
public void dim() {
|
||||
animateAlphaTo(DIMMED_ALPHA);
|
||||
}
|
||||
|
||||
public void highlight() {
|
||||
animateAlphaTo(DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
private void animateAlphaTo(float alpha) {
|
||||
itemView.animate().alpha(alpha).setDuration(200).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,53 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
private SimpleItemTouchHelperCallback _touchCallback;
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _dividerDecoration;
|
||||
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
||||
private PeriodProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private LinearLayout _emptyStateView;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@@ -41,6 +58,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_showProgress = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
_adapter.destroy();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
|
||||
@@ -56,8 +79,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
|
||||
_recyclerView.setLayoutManager(mLayoutManager);
|
||||
// set up icon preloading
|
||||
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
|
||||
IconPreloadProvider modelProvider = new IconPreloadProvider();
|
||||
RecyclerViewPreloader<VaultEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
|
||||
_recyclerView.addOnScrollListener(preloader);
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
|
||||
_recyclerView.setLayoutManager(layoutManager);
|
||||
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
|
||||
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
|
||||
touchHelper.attachToRecyclerView(_recyclerView);
|
||||
@@ -79,9 +108,21 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
});
|
||||
|
||||
_emptyStateView = view.findViewById(R.id.vEmptyList);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setPreloadView(View view) {
|
||||
_preloadSizeProvider.setView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
_refresher.destroy();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setGroupFilter(String group, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(group == null);
|
||||
_adapter.setGroupFilter(group, apply);
|
||||
@@ -91,6 +132,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setActionModeState(boolean enabled, VaultEntry entry) {
|
||||
_touchCallback.setSelectedEntry(entry);
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
|
||||
_adapter.setSelectedEntry(entry);
|
||||
}
|
||||
|
||||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
|
||||
_adapter.setSortCategory(sortCategory, apply);
|
||||
@@ -100,8 +147,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_touchCallback.setIsLongPressDragEnabled(search == null);
|
||||
_adapter.setSearchFilter(search);
|
||||
}
|
||||
|
||||
public void setViewMode(ViewMode mode) {
|
||||
_adapter.setViewMode(mode);
|
||||
_viewMode = mode;
|
||||
updateDividerDecoration();
|
||||
_adapter.setViewMode(_viewMode);
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
@@ -116,33 +170,33 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
_listener.onEntryClick(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongEntryClick(DatabaseEntry entry) {
|
||||
return false;
|
||||
public boolean onLongEntryClick(VaultEntry entry) {
|
||||
_listener.onLongEntryClick(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
|
||||
_listener.onEntryMove(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryDrop(DatabaseEntry entry) {
|
||||
public void onEntryDrop(VaultEntry entry) {
|
||||
_listener.onEntryDrop(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(DatabaseEntry entry) {
|
||||
public void onEntryChange(VaultEntry entry) {
|
||||
_listener.onEntryChange(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeriodUniformityChanged(boolean isUniform) {
|
||||
_showProgress = isUniform;
|
||||
setShowProgress(isUniform);
|
||||
if (_showProgress) {
|
||||
_progressBar.setVisibility(View.VISIBLE);
|
||||
_progressBar.setPeriod(_adapter.getUniformPeriod());
|
||||
@@ -157,6 +211,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setShowAccountName(showAccountName);
|
||||
}
|
||||
|
||||
public void setSearchAccountName(boolean searchAccountName) {
|
||||
_adapter.setSearchAccountName(searchAccountName);
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_adapter.setHighlightEntry(highlightEntry);
|
||||
}
|
||||
|
||||
public void setTapToReveal(boolean tapToReveal) {
|
||||
_adapter.setTapToReveal(tapToReveal);
|
||||
}
|
||||
@@ -165,24 +227,27 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setTapToRevealTime(number);
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void addEntries(List<DatabaseEntry> entries) {
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_adapter.removeEntry(entry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
_adapter.replaceEntry(entry);
|
||||
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
|
||||
_adapter.replaceEntry(oldEntry, newEntry);
|
||||
}
|
||||
|
||||
public void runEntriesAnimation() {
|
||||
@@ -194,11 +259,84 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_recyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
private void setShowProgress(boolean showProgress) {
|
||||
_showProgress = showProgress;
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
private void updateDividerDecoration() {
|
||||
if (_dividerDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_dividerDecoration);
|
||||
}
|
||||
|
||||
float height = _viewMode.getDividerHeight();
|
||||
if (_showProgress && height == 0) {
|
||||
DividerItemDecoration divider = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);
|
||||
divider.setDrawable(ContextCompat.getDrawable(getContext(), R.drawable.entry_divider));
|
||||
_dividerDecoration = divider;
|
||||
} else {
|
||||
_dividerDecoration = new VerticalSpaceItemDecoration(height);
|
||||
}
|
||||
|
||||
_recyclerView.addItemDecoration(_dividerDecoration);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_recyclerView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_recyclerView.setVisibility(View.GONE);
|
||||
_emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(DatabaseEntry entry);
|
||||
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
|
||||
void onEntryDrop(DatabaseEntry entry);
|
||||
void onEntryChange(DatabaseEntry entry);
|
||||
void onEntryClick(VaultEntry entry);
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onLongEntryClick(VaultEntry entry);
|
||||
void onScroll(int dx, int dy);
|
||||
}
|
||||
|
||||
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private int _height;
|
||||
|
||||
private VerticalSpaceItemDecoration(float dp) {
|
||||
// convert dp to pixels
|
||||
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
// the first item should also have a top margin
|
||||
outRect.top = _height;
|
||||
}
|
||||
outRect.bottom = _height;
|
||||
}
|
||||
}
|
||||
|
||||
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<VaultEntry> getPreloadItems(int position) {
|
||||
VaultEntry entry = _adapter.getEntryAt(position);
|
||||
if (!entry.hasIcon()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.singletonList(entry);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
return Glide.with(EntryListView.this)
|
||||
.asDrawable()
|
||||
.load(entry)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -56,17 +55,7 @@ public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getSelectedEntries() {
|
||||
List<DatabaseEntry> entries = new ArrayList<>();
|
||||
|
||||
for (ImportEntry entry : getCheckedEntries()) {
|
||||
entries.add(entry.getDatabaseEntry());
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private List<ImportEntry> getCheckedEntries() {
|
||||
public List<ImportEntry> getCheckedEntries() {
|
||||
List<ImportEntry> entries = new ArrayList<>();
|
||||
|
||||
for (ImportEntry entry : _entries) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -31,9 +30,8 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
|
||||
_entry = entry;
|
||||
|
||||
Context context = itemView.getContext();
|
||||
DatabaseEntry dbEntry = entry.getDatabaseEntry();
|
||||
_issuer.setText(!dbEntry.getIssuer().isEmpty() ? dbEntry.getIssuer() : context.getString(R.string.unknown_issuer));
|
||||
_accountName.setText(!dbEntry.getName().isEmpty() ? dbEntry.getName() : context.getString(R.string.unknown_account_name));
|
||||
_issuer.setText(!entry.getIssuer().isEmpty() ? entry.getIssuer() : context.getString(R.string.unknown_issuer));
|
||||
_accountName.setText(!entry.getName().isEmpty() ? entry.getName() : context.getString(R.string.unknown_account_name));
|
||||
_checkbox.setChecked(entry.isChecked());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.RawSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.Slot;
|
||||
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.RawSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
|
||||
public class SlotHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _slotUsed;
|
||||
@@ -36,10 +36,10 @@ public class SlotHolder extends RecyclerView.ViewHolder {
|
||||
if (slot instanceof PasswordSlot) {
|
||||
_slotName.setText(R.string.password);
|
||||
_slotImg.setImageResource(R.drawable.ic_create_black_24dp);
|
||||
} else if (slot instanceof FingerprintSlot) {
|
||||
_slotName.setText(R.string.authentication_method_fingerprint);
|
||||
} else if (slot instanceof BiometricSlot) {
|
||||
_slotName.setText(R.string.authentication_method_biometrics);
|
||||
_slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
|
||||
if (FingerprintHelper.isSupported()) {
|
||||
if (BiometricsHelper.isAvailable(itemView.getContext())) {
|
||||
try {
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
if (keyStore.containsKey(slot.getUUID().toString())) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Cloner {
|
||||
private Cloner() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an exact clone of the given Serializable object.
|
||||
*/
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public static <T extends Serializable> T clone(T obj) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
oos.writeObject(obj);
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
ObjectInputStream ois = new ObjectInputStream(bais);
|
||||
return (T) ois.readObject();
|
||||
} catch (ClassNotFoundException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreferenceParser {
|
||||
private PreferenceParser() {
|
||||
|
||||
}
|
||||
|
||||
public static List<XmlEntry> parse(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
List<XmlEntry> entries = new ArrayList<>();
|
||||
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parser.getName().equals("string")) {
|
||||
skip(parser);
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(parseEntry(parser));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
String name = parser.getAttributeValue(null, "name");
|
||||
String value = parseText(parser);
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
|
||||
XmlEntry entry = new XmlEntry();
|
||||
entry.Name = name;
|
||||
entry.Value = value;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String text = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
text = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
// source: https://developer.android.com/training/basics/network-ops/xml.html
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
int depth = 1;
|
||||
while (depth != 0) {
|
||||
switch (parser.next()) {
|
||||
case XmlPullParser.END_TAG:
|
||||
depth--;
|
||||
break;
|
||||
case XmlPullParser.START_TAG:
|
||||
depth++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class XmlEntry {
|
||||
public String Name;
|
||||
public String Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A map data structure abstraction for storing values with a UUID as the key. Keys
|
||||
* must be specified by the value itself, instead of separately. It uses a
|
||||
* LinkedHashMap internally (a hash map with a separate linked list that maintains
|
||||
* the order).
|
||||
* @param <T> The type of values in this map
|
||||
*/
|
||||
public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializable {
|
||||
private LinkedHashMap<UUID, T> _map = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Adds a value to the internal map.
|
||||
* @throws AssertionError if a map value with the UUID of the given value already exists.
|
||||
*/
|
||||
public void add(T value) {
|
||||
UUID uuid = value.getUUID();
|
||||
if (_map.containsKey(uuid)) {
|
||||
throw new AssertionError(String.format("Existing value found with UUID: %s", uuid));
|
||||
}
|
||||
_map.put(uuid, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a value from the internal map.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given value.
|
||||
* @return The old value that is now no longer present in the internal map.
|
||||
*/
|
||||
public T remove(T value) {
|
||||
T oldValue = getByUUID(value.getUUID());
|
||||
_map.remove(oldValue.getUUID());
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an old value (with the same UUID as the new given value) in the
|
||||
* internal map with the new given value.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given value.
|
||||
* @return The old value that is now no longer present in the internal map.
|
||||
*/
|
||||
public T replace(T newValue) {
|
||||
T oldValue = getByUUID(newValue.getUUID());
|
||||
_map.put(oldValue.getUUID(), newValue);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the position of value1 and value2 in the internal map. This operation is
|
||||
* quite expensive because it has to reallocate the entire underlying LinkedHashMap.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given entries.
|
||||
*/
|
||||
public void swap(T value1, T value2) {
|
||||
boolean found1 = false;
|
||||
boolean found2 = false;
|
||||
List<T> values = new ArrayList<>();
|
||||
|
||||
for (T value : _map.values()) {
|
||||
if (value.getUUID().equals(value1.getUUID())) {
|
||||
values.add(value2);
|
||||
found1 = true;
|
||||
} else if (value.getUUID().equals(value2.getUUID())) {
|
||||
values.add(value1);
|
||||
found2 = true;
|
||||
} else {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found1) {
|
||||
throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID()));
|
||||
}
|
||||
if (!found2) {
|
||||
throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID()));
|
||||
}
|
||||
|
||||
_map.clear();
|
||||
for (T value : values) {
|
||||
_map.put(value.getUUID(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the internal map contains a value with the UUID of the given value.
|
||||
*/
|
||||
public boolean has(T value) {
|
||||
return _map.containsKey(value.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a read-only view of the values in the internal map.
|
||||
*/
|
||||
public Collection<T> getValues() {
|
||||
return Collections.unmodifiableCollection(_map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an entry from the internal map that has the given UUID.
|
||||
* @throws AssertionError if no map value exists with the given UUID.
|
||||
*/
|
||||
public T getByUUID(UUID uuid) {
|
||||
T value = _map.get(uuid);
|
||||
if (value == null) {
|
||||
throw new AssertionError(String.format("No value found with UUID: %s", uuid));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return _map.values().iterator();
|
||||
}
|
||||
|
||||
public static abstract class Value implements Serializable {
|
||||
private UUID _uuid;
|
||||
|
||||
protected Value(UUID uuid) {
|
||||
_uuid = uuid;
|
||||
}
|
||||
|
||||
protected Value() {
|
||||
this(UUID.randomUUID());
|
||||
}
|
||||
|
||||
public final UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the UUID of this value by generating a new random one.
|
||||
* The caller must ensure that this Value is not in a UUIDMap yet. Otherwise, bad things will happen.
|
||||
*/
|
||||
public final void resetUUID() {
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof Value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getUUID().equals(((Value) o).getUUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
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 Vault {
|
||||
private static final int VERSION = 1;
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
|
||||
public JSONObject toJson() {
|
||||
try {
|
||||
JSONArray array = new JSONArray();
|
||||
for (VaultEntry 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 Vault fromJson(JSONObject obj) throws VaultException {
|
||||
Vault vault = new Vault();
|
||||
UUIDMap<VaultEntry> entries = vault.getEntries();
|
||||
|
||||
try {
|
||||
int ver = obj.getInt("version");
|
||||
if (ver != VERSION) {
|
||||
throw new VaultException("Unsupported version");
|
||||
}
|
||||
|
||||
JSONArray array = obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (Base64Exception | OtpInfoException | JSONException e) {
|
||||
throw new VaultException(e);
|
||||
}
|
||||
|
||||
return vault;
|
||||
}
|
||||
|
||||
public UUIDMap<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,44 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DatabaseEntry implements Serializable {
|
||||
private UUID _uuid;
|
||||
public class VaultEntry extends UUIDMap.Value {
|
||||
private String _name = "";
|
||||
private String _issuer = "";
|
||||
private String _group;
|
||||
private OtpInfo _info;
|
||||
private byte[] _icon;
|
||||
|
||||
private DatabaseEntry(UUID uuid, OtpInfo info) {
|
||||
_uuid = uuid;
|
||||
private VaultEntry(UUID uuid, OtpInfo info) {
|
||||
super(uuid);
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public DatabaseEntry(OtpInfo info) {
|
||||
this(UUID.randomUUID(), info);
|
||||
public VaultEntry(OtpInfo info) {
|
||||
super();
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public DatabaseEntry(OtpInfo info, String name, String issuer) {
|
||||
public VaultEntry(OtpInfo info, String name, String issuer) {
|
||||
this(info);
|
||||
setName(name);
|
||||
setIssuer(issuer);
|
||||
}
|
||||
|
||||
public DatabaseEntry(GoogleAuthInfo info) {
|
||||
public VaultEntry(GoogleAuthInfo info) {
|
||||
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ public class DatabaseEntry implements Serializable {
|
||||
|
||||
try {
|
||||
obj.put("type", _info.getType());
|
||||
obj.put("uuid", _uuid.toString());
|
||||
obj.put("uuid", getUUID().toString());
|
||||
obj.put("name", _name);
|
||||
obj.put("issuer", _issuer);
|
||||
obj.put("group", _group);
|
||||
@@ -59,7 +60,7 @@ public class DatabaseEntry implements Serializable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
|
||||
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
|
||||
// if there is no uuid, generate a new one
|
||||
UUID uuid;
|
||||
if (!obj.has("uuid")) {
|
||||
@@ -69,7 +70,7 @@ public class DatabaseEntry implements Serializable {
|
||||
}
|
||||
|
||||
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
|
||||
DatabaseEntry entry = new DatabaseEntry(uuid, info);
|
||||
VaultEntry entry = new VaultEntry(uuid, info);
|
||||
entry.setName(obj.getString("name"));
|
||||
entry.setIssuer(obj.getString("issuer"));
|
||||
entry.setGroup(obj.optString("group", null));
|
||||
@@ -82,14 +83,6 @@ public class DatabaseEntry implements Serializable {
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void resetUUID() {
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
@@ -136,19 +129,39 @@ public class DatabaseEntry implements Serializable {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof DatabaseEntry)) {
|
||||
if (!(o instanceof VaultEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseEntry entry = (DatabaseEntry) o;
|
||||
return getUUID().equals(entry.getUUID())
|
||||
&& getName().equals(entry.getName())
|
||||
VaultEntry entry = (VaultEntry) o;
|
||||
return super.equals(entry) && equivalates(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this entry is equivalent to the given entry. The UUIDs of these
|
||||
* entries are ignored during the comparison, so they are not necessarily the same
|
||||
* instance.
|
||||
*/
|
||||
public boolean equivalates(VaultEntry entry) {
|
||||
return getName().equals(entry.getName())
|
||||
&& getIssuer().equals(entry.getIssuer())
|
||||
&& Objects.equals(getGroup(), entry.getGroup())
|
||||
&& getInfo().equals(entry.getInfo())
|
||||
&& Arrays.equals(getIcon(), entry.getIcon());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this entry has its values set to the defaults.
|
||||
*/
|
||||
public boolean isDefault() {
|
||||
return equivalates(getDefault());
|
||||
}
|
||||
|
||||
public static VaultEntry getDefault() {
|
||||
try {
|
||||
return new VaultEntry(new TotpInfo(null));
|
||||
} catch (OtpInfoException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultException extends Exception {
|
||||
public VaultException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKeyException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotListException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotListException;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
@@ -14,17 +14,17 @@ import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DatabaseFile {
|
||||
public class VaultFile {
|
||||
public static final byte VERSION = 1;
|
||||
|
||||
private Object _content;
|
||||
private Header _header;
|
||||
|
||||
public DatabaseFile() {
|
||||
public VaultFile() {
|
||||
|
||||
}
|
||||
|
||||
private DatabaseFile(Object content, Header header) {
|
||||
private VaultFile(Object content, Header header) {
|
||||
_content = content;
|
||||
_header = header;
|
||||
}
|
||||
@@ -60,29 +60,29 @@ public class DatabaseFile {
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException {
|
||||
public static VaultFile fromJson(JSONObject obj) throws VaultFileException {
|
||||
try {
|
||||
if (obj.getInt("version") > VERSION) {
|
||||
throw new DatabaseFileException("unsupported version");
|
||||
throw new VaultFileException("unsupported version");
|
||||
}
|
||||
|
||||
Header header = Header.fromJson(obj.getJSONObject("header"));
|
||||
if (!header.isEmpty()) {
|
||||
return new DatabaseFile(obj.getString("db"), header);
|
||||
return new VaultFile(obj.getString("db"), header);
|
||||
}
|
||||
|
||||
return new DatabaseFile(obj.getJSONObject("db"), header);
|
||||
return new VaultFile(obj.getJSONObject("db"), header);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
|
||||
public static VaultFile fromBytes(byte[] data) throws VaultFileException {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
|
||||
return DatabaseFile.fromJson(obj);
|
||||
return VaultFile.fromJson(obj);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,13 +90,13 @@ public class DatabaseFile {
|
||||
return (JSONObject) _content;
|
||||
}
|
||||
|
||||
public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException {
|
||||
public JSONObject getContent(VaultFileCredentials creds) throws VaultFileException {
|
||||
try {
|
||||
byte[] bytes = Base64.decode((String) _content);
|
||||
CryptResult result = creds.decrypt(bytes, _header.getParams());
|
||||
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
|
||||
} catch (MasterKeyException | JSONException | Base64Exception e) {
|
||||
throw new DatabaseFileException(e);
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +105,16 @@ public class DatabaseFile {
|
||||
_header = new Header(null, null);
|
||||
}
|
||||
|
||||
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
|
||||
public void setContent(JSONObject obj, VaultFileCredentials creds) throws VaultFileException {
|
||||
try {
|
||||
String string = obj.toString(4);
|
||||
byte[] dbBytes = string.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] vaultBytes = string.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
CryptResult result = creds.encrypt(dbBytes);
|
||||
CryptResult result = creds.encrypt(vaultBytes);
|
||||
_content = Base64.encode(result.getData());
|
||||
_header = new Header(creds.getSlots(), result.getParams());
|
||||
} catch (MasterKeyException | JSONException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DatabaseFile {
|
||||
_params = params;
|
||||
}
|
||||
|
||||
public static Header fromJson(JSONObject obj) throws DatabaseFileException {
|
||||
public static Header fromJson(JSONObject obj) throws VaultFileException {
|
||||
if (obj.isNull("slots") && obj.isNull("params")) {
|
||||
return new Header(null, null);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class DatabaseFile {
|
||||
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
|
||||
return new Header(slots, params);
|
||||
} catch (SlotListException | JSONException | HexException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKeyException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class DatabaseFileCredentials implements Serializable {
|
||||
public class VaultFileCredentials implements Serializable {
|
||||
private MasterKey _key;
|
||||
private SlotList _slots;
|
||||
|
||||
public DatabaseFileCredentials() {
|
||||
public VaultFileCredentials() {
|
||||
_key = MasterKey.generate();
|
||||
_slots = new SlotList();
|
||||
}
|
||||
|
||||
public DatabaseFileCredentials(MasterKey key, SlotList slots) {
|
||||
public VaultFileCredentials(MasterKey key, SlotList slots) {
|
||||
_key = key;
|
||||
_slots = slots;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultFileException extends Exception {
|
||||
public VaultFileException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -14,23 +16,22 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.Collator;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DatabaseManager {
|
||||
public class VaultManager {
|
||||
private static final String FILENAME = "aegis.json";
|
||||
private static final String FILENAME_EXPORT = "aegis_export.json";
|
||||
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
|
||||
|
||||
private Database _db;
|
||||
private DatabaseFile _file;
|
||||
private DatabaseFileCredentials _creds;
|
||||
private Vault _vault;
|
||||
private VaultFile _file;
|
||||
private VaultFileCredentials _creds;
|
||||
private boolean _encrypt;
|
||||
|
||||
private Context _context;
|
||||
|
||||
public DatabaseManager(Context context) {
|
||||
public VaultManager(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ public class DatabaseManager {
|
||||
return file.exists() && file.isFile();
|
||||
}
|
||||
|
||||
public void load() throws DatabaseManagerException {
|
||||
public void load() throws VaultManagerException {
|
||||
assertState(true, false);
|
||||
|
||||
try (FileInputStream file = _context.openFileInput(FILENAME)) {
|
||||
@@ -48,69 +49,70 @@ public class DatabaseManager {
|
||||
stream.readFully(fileBytes);
|
||||
stream.close();
|
||||
|
||||
_file = DatabaseFile.fromBytes(fileBytes);
|
||||
_file = VaultFile.fromBytes(fileBytes);
|
||||
_encrypt = _file.isEncrypted();
|
||||
if (!isEncryptionEnabled()) {
|
||||
JSONObject obj = _file.getContent();
|
||||
_db = Database.fromJson(obj);
|
||||
_vault = Vault.fromJson(obj);
|
||||
}
|
||||
} catch (IOException | DatabaseFileException | DatabaseException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
} catch (IOException | VaultFileException | VaultException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_db = null;
|
||||
_vault = null;
|
||||
}
|
||||
|
||||
public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException {
|
||||
public void unlock(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(true, true);
|
||||
|
||||
try {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
_db = Database.fromJson(obj);
|
||||
_vault = Vault.fromJson(obj);
|
||||
_creds = creds;
|
||||
} catch (DatabaseFileException | DatabaseException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
_context.startService(new Intent(_context, NotificationService.class));
|
||||
} catch (VaultFileException | VaultException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(Context context, DatabaseFile file) throws DatabaseManagerException {
|
||||
public static void save(Context context, VaultFile file) throws VaultManagerException {
|
||||
byte[] bytes = file.toBytes();
|
||||
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
|
||||
stream.write(bytes);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws DatabaseManagerException {
|
||||
public void save() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
JSONObject obj = _db.toJson();
|
||||
JSONObject obj = _vault.toJson();
|
||||
if (isEncryptionEnabled()) {
|
||||
_file.setContent(obj, _creds);
|
||||
} else {
|
||||
_file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
} catch (DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
} catch (VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String export(boolean encrypt) throws DatabaseManagerException {
|
||||
public String export(boolean encrypt) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
DatabaseFile dbFile = new DatabaseFile();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
if (encrypt && isEncryptionEnabled()) {
|
||||
dbFile.setContent(_db.toJson(), _creds);
|
||||
vaultFile.setContent(_vault.toJson(), _creds);
|
||||
} else {
|
||||
dbFile.setContent(_db.toJson());
|
||||
vaultFile.setContent(_vault.toJson());
|
||||
}
|
||||
|
||||
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
|
||||
@@ -119,53 +121,53 @@ public class DatabaseManager {
|
||||
throw new IOException("error creating external storage directory");
|
||||
}
|
||||
|
||||
byte[] bytes = dbFile.toBytes();
|
||||
byte[] bytes = vaultFile.toBytes();
|
||||
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
|
||||
try (FileOutputStream stream = new FileOutputStream(file)) {
|
||||
stream.write(bytes);
|
||||
}
|
||||
|
||||
return file.getAbsolutePath();
|
||||
} catch (IOException | DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
} catch (IOException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
public void addEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.addEntry(entry);
|
||||
_vault.getEntries().add(entry);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
public VaultEntry removeEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.removeEntry(entry);
|
||||
return _vault.getEntries().remove(entry);
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.replaceEntry(entry);
|
||||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
|
||||
assertState(false, true);
|
||||
_db.swapEntries(entry1, entry2);
|
||||
_vault.getEntries().swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
public boolean isEntryDuplicate(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _db.getEntries();
|
||||
return _vault.getEntries().has(entry);
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
public Collection<VaultEntry> getEntries() {
|
||||
assertState(false, true);
|
||||
return _db.getEntryByUUID(uuid);
|
||||
return _vault.getEntries().getValues();
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroups() {
|
||||
assertState(false, true);
|
||||
|
||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
||||
for (DatabaseEntry entry : getEntries()) {
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
String group = entry.getGroup();
|
||||
if (group != null) {
|
||||
groups.add(group);
|
||||
@@ -174,17 +176,17 @@ public class DatabaseManager {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public DatabaseFileCredentials getCredentials() {
|
||||
public VaultFileCredentials getCredentials() {
|
||||
assertState(false, true);
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setCredentials(DatabaseFileCredentials creds) {
|
||||
public void setCredentials(VaultFileCredentials creds) {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public DatabaseFile.Header getFileHeader() {
|
||||
public VaultFile.Header getFileHeader() {
|
||||
assertLoaded(true);
|
||||
return _file.getHeader();
|
||||
}
|
||||
@@ -194,14 +196,14 @@ public class DatabaseManager {
|
||||
return _encrypt;
|
||||
}
|
||||
|
||||
public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException {
|
||||
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
_encrypt = true;
|
||||
save();
|
||||
}
|
||||
|
||||
public void disableEncryption() throws DatabaseManagerException {
|
||||
public void disableEncryption() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_encrypt = false;
|
||||
@@ -213,24 +215,24 @@ public class DatabaseManager {
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return _db == null;
|
||||
return _vault == null;
|
||||
}
|
||||
|
||||
private void assertState(boolean locked, boolean loaded) {
|
||||
assertLoaded(loaded);
|
||||
|
||||
if (isLocked() && !locked) {
|
||||
throw new AssertionError("database file has not been unlocked yet");
|
||||
throw new AssertionError("vault file has not been unlocked yet");
|
||||
} else if (!isLocked() && locked) {
|
||||
throw new AssertionError("database file has already been unlocked");
|
||||
throw new AssertionError("vault file has already been unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLoaded(boolean loaded) {
|
||||
if (isLoaded() && !loaded) {
|
||||
throw new AssertionError("database file has already been loaded");
|
||||
throw new AssertionError("vault file has already been loaded");
|
||||
} else if (!isLoaded() && loaded) {
|
||||
throw new AssertionError("database file has not been loaded yet");
|
||||
throw new AssertionError("vault file has not been loaded yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultManagerException extends Exception {
|
||||
public VaultManagerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class BiometricSlot extends RawSlot {
|
||||
public BiometricSlot() {
|
||||
super();
|
||||
}
|
||||
|
||||
BiometricSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
|
||||
super(uuid, key, keyParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getType() {
|
||||
return TYPE_BIOMETRIC;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
@@ -7,12 +7,12 @@ import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -25,26 +25,29 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public abstract class Slot implements Serializable {
|
||||
public abstract class Slot extends UUIDMap.Value {
|
||||
public final static byte TYPE_RAW = 0x00;
|
||||
public final static byte TYPE_DERIVED = 0x01;
|
||||
public final static byte TYPE_FINGERPRINT = 0x02;
|
||||
public final static byte TYPE_BIOMETRIC = 0x02;
|
||||
|
||||
private UUID _uuid;
|
||||
private byte[] _encryptedMasterKey;
|
||||
private CryptParameters _encryptedMasterKeyParams;
|
||||
|
||||
protected Slot() {
|
||||
_uuid = UUID.randomUUID();
|
||||
super();
|
||||
}
|
||||
|
||||
protected Slot(UUID uuid, byte[] key, CryptParameters keyParams) {
|
||||
_uuid = uuid;
|
||||
super(uuid);
|
||||
_encryptedMasterKey = key;
|
||||
_encryptedMasterKeyParams = keyParams;
|
||||
}
|
||||
|
||||
// getKey decrypts the encrypted master key in this slot using the given cipher and returns it.
|
||||
/**
|
||||
* Decrypts the encrypted master key in this slot using the given cipher and returns it.
|
||||
* @throws SlotException if a generic crypto operation error occurred.
|
||||
* @throws SlotIntegrityException if an error occurred while verifying the integrity of the slot.
|
||||
*/
|
||||
public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
|
||||
try {
|
||||
CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
|
||||
@@ -57,7 +60,10 @@ public abstract class Slot implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
// setKey encrypts the given master key using the given cipher and stores the result in this slot.
|
||||
/**
|
||||
* Encrypts the given master key using the given cipher and stores the result in this slot.
|
||||
* @throws SlotException if a generic crypto operation error occurred.
|
||||
*/
|
||||
public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException {
|
||||
try {
|
||||
byte[] masterKeyBytes = masterKey.getBytes();
|
||||
@@ -95,7 +101,7 @@ public abstract class Slot implements Serializable {
|
||||
try {
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("type", getType());
|
||||
obj.put("uuid", _uuid.toString());
|
||||
obj.put("uuid", getUUID().toString());
|
||||
obj.put("key", Hex.encode(_encryptedMasterKey));
|
||||
obj.put("key_params", _encryptedMasterKeyParams.toJson());
|
||||
return obj;
|
||||
@@ -132,8 +138,8 @@ public abstract class Slot implements Serializable {
|
||||
boolean repaired = obj.optBoolean("repaired", false);
|
||||
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired);
|
||||
break;
|
||||
case Slot.TYPE_FINGERPRINT:
|
||||
slot = new FingerprintSlot(uuid, key, keyParams);
|
||||
case Slot.TYPE_BIOMETRIC:
|
||||
slot = new BiometricSlot(uuid, key, keyParams);
|
||||
break;
|
||||
default:
|
||||
throw new SlotException("unrecognized slot type");
|
||||
@@ -146,8 +152,4 @@ public abstract class Slot implements Serializable {
|
||||
}
|
||||
|
||||
public abstract byte getType();
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
public class SlotException extends Exception {
|
||||
public SlotException(Throwable cause) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
public class SlotIntegrityException extends Exception {
|
||||
public SlotIntegrityException() {
|
||||
@@ -1,18 +1,15 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SlotList implements Iterable<Slot>, Serializable {
|
||||
private List<Slot> _slots = new ArrayList<>();
|
||||
|
||||
public class SlotList extends UUIDMap<Slot> {
|
||||
public JSONArray toJson() {
|
||||
JSONArray array = new JSONArray();
|
||||
for (Slot slot : this) {
|
||||
@@ -38,23 +35,6 @@ public class SlotList implements Iterable<Slot>, Serializable {
|
||||
return slots;
|
||||
}
|
||||
|
||||
public void add(Slot slot) {
|
||||
for (Slot s : this) {
|
||||
if (s.getUUID().equals(slot.getUUID())) {
|
||||
throw new AssertionError("slot found with the same uuid");
|
||||
}
|
||||
}
|
||||
_slots.add(slot);
|
||||
}
|
||||
|
||||
public void remove(Slot slot) {
|
||||
_slots.remove(slot);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return _slots.size();
|
||||
}
|
||||
|
||||
public <T extends Slot> T find(Class<T> type) {
|
||||
for (Slot slot : this) {
|
||||
if (slot.getClass() == type) {
|
||||
@@ -77,31 +57,4 @@ public class SlotList implements Iterable<Slot>, Serializable {
|
||||
public <T extends Slot> boolean has(Class<T> type) {
|
||||
return find(type) != null;
|
||||
}
|
||||
|
||||
public void replace(Slot newSlot) {
|
||||
Slot oldSlot = mustGetByUUID(newSlot.getUUID());
|
||||
_slots.set(_slots.indexOf(oldSlot), newSlot);
|
||||
}
|
||||
|
||||
public Slot getByUUID(UUID uuid) {
|
||||
for (Slot slot : _slots) {
|
||||
if (slot.getUUID().equals(uuid)) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Slot mustGetByUUID(UUID uuid) {
|
||||
Slot slot = getByUUID(uuid);
|
||||
if (slot == null) {
|
||||
throw new AssertionError(String.format("no slot found with UUID: %s", uuid.toString()));
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Slot> iterator() {
|
||||
return _slots.iterator();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
package com.beemdevelopment.aegis.vault.slots;
|
||||
|
||||
public class SlotListException extends Exception {
|
||||
public SlotListException(Throwable cause) {
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="137.93103"
|
||||
android:viewportHeight="137.93103">
|
||||
<group android:translateX="-11.034483"
|
||||
android:translateY="-11.034483">
|
||||
<path
|
||||
android:pathData="M55.78,0h48.44C120.28,0 128.31,0 137,2.73A34,34 0,0 1,157.27 23C160,31.69 160,39.72 160,55.78v48.44c0,16.06 0,24.09 -2.73,32.74A34,34 0,0 1,137 157.27C128.31,160 120.28,160 104.22,160H55.78c-16.06,0 -24.09,0 -32.74,-2.73A34,34 0,0 1,2.73 137C0,128.31 0,120.28 0,104.22V55.78C0,39.72 0,31.69 2.73,23A34,34 0,0 1,23 2.73C31.69,0 39.72,0 55.78,0h0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="28.48"
|
||||
android:startX="51.92"
|
||||
android:endY="189.51"
|
||||
android:endX="139.67"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="0.37" android:color="#FFF5FAFC"/>
|
||||
<item android:offset="1" android:color="#FFDBEBF5"/>
|
||||
<item android:offset="1" android:color="#FFDBEBF5"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |