Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb80dfe43 | ||
|
|
0f439506bd | ||
|
|
6e54497492 | ||
|
|
9fde453ead | ||
|
|
7cd48f1c09 | ||
|
|
4c3d277fce | ||
|
|
0111626d80 | ||
|
|
0b83546db7 | ||
|
|
08ab8237e7 | ||
|
|
ae5502b650 | ||
|
|
821d0b5147 | ||
|
|
b4ed1f35ce | ||
|
|
a468342187 | ||
|
|
4f6035dc49 | ||
|
|
506adcf81f | ||
|
|
49c8c2d57d | ||
|
|
2ab698c2d3 | ||
|
|
770f5f9724 | ||
|
|
992dcd651b | ||
|
|
98a38b03e4 | ||
|
|
626995ec91 | ||
|
|
00b0c8deaf | ||
|
|
8e5b51c42f | ||
|
|
b81ec0f073 | ||
|
|
d49d42254b | ||
|
|
95d5b4afa1 | ||
|
|
cca8c9bdf7 | ||
|
|
4e00191039 | ||
|
|
dd09595866 | ||
|
|
b27bfc3fa9 | ||
|
|
c6a9716d16 | ||
|
|
acd7a54ee9 | ||
|
|
e7d240549e | ||
|
|
70fedc53b9 | ||
|
|
bed8c9172d | ||
|
|
ef1ac3e5e9 | ||
|
|
cc163f952f | ||
|
|
95180ba963 | ||
|
|
56d7f0ef7b | ||
|
|
fbd6a5931e | ||
|
|
66dd3561cf | ||
|
|
58db3fdaa2 | ||
|
|
92b6738ff4 | ||
|
|
63f57667ba | ||
|
|
34718a7b89 | ||
|
|
6ac91e6cf3 | ||
|
|
a39d6ab346 | ||
|
|
91d89670d2 | ||
|
|
6a2992530e | ||
|
|
1f839a886d | ||
|
|
91f9a6756a | ||
|
|
89137c0873 | ||
|
|
ffa31c7aa3 | ||
|
|
e12bfdbe58 | ||
|
|
3c887d8392 | ||
|
|
b4d2b6b113 | ||
|
|
fe40c4b952 | ||
|
|
9319c74932 | ||
|
|
4c60b04057 | ||
|
|
1a24f62074 | ||
|
|
1824d38f75 | ||
|
|
e41f200bcd | ||
|
|
ccc990f854 | ||
|
|
56bde0e19b | ||
|
|
6b650e777f | ||
|
|
49a7fda932 | ||
|
|
8ff817856e | ||
|
|
3a85a05078 | ||
|
|
cda78c56c5 | ||
|
|
499f930086 | ||
|
|
393b815409 | ||
|
|
984010dc7a | ||
|
|
0bbc413423 | ||
|
|
98a90e3e1e | ||
|
|
3ccbece822 | ||
|
|
0f81233d4f | ||
|
|
f6b1f02d11 | ||
|
|
301476ff5c | ||
|
|
b84f44bf1e | ||
|
|
842afe6745 | ||
|
|
bc5fa68de8 | ||
|
|
fec2f3ff94 | ||
|
|
77718809ef | ||
|
|
9b328893f7 | ||
|
|
45378b8128 | ||
|
|
b830c6db1b | ||
|
|
7773a51f2a | ||
|
|
f68badb659 | ||
|
|
4ea8501fa9 | ||
|
|
1f86697d2d | ||
|
|
faf24e15bd | ||
|
|
592d482a9b | ||
|
|
ad05e375f1 | ||
|
|
8b30d451a4 | ||
|
|
abe78370ea | ||
|
|
00e2e90aa7 | ||
|
|
f37d70b58e | ||
|
|
09065c705b | ||
|
|
ad58960831 | ||
|
|
7ce74d378f | ||
|
|
8c0ba2dd1b | ||
|
|
016c64610e | ||
|
|
c578543e18 | ||
|
|
d247750e62 | ||
|
|
b8896ec1df | ||
|
|
4a69e9efb4 | ||
|
|
b43bac37db | ||
|
|
0c7b41747e | ||
|
|
8fda6937a5 | ||
|
|
0a4a933a2a | ||
|
|
866466d158 | ||
|
|
d89d5eac9c | ||
|
|
6885463b11 | ||
|
|
a273fdaa1d | ||
|
|
ec60e98b2e | ||
|
|
dc0f7c4668 | ||
|
|
ba7fe15a57 | ||
|
|
3d51b84188 | ||
|
|
f66cebb727 | ||
|
|
9e355c8e52 | ||
|
|
9ce0982191 | ||
|
|
f543802f9e | ||
|
|
32aecadf75 | ||
|
|
d39e3d7b90 | ||
|
|
bd66659b92 | ||
|
|
1a7a794dc9 | ||
|
|
2519633265 | ||
|
|
e38121efee | ||
|
|
261b036f75 | ||
|
|
7e857b53a7 | ||
|
|
73e1d72403 | ||
|
|
5731cccddc | ||
|
|
48d22fc06d | ||
|
|
964ed294a5 | ||
|
|
2a4bb902df | ||
|
|
d3463b82bc | ||
|
|
ef536a0c07 | ||
|
|
7e7c5317b1 | ||
|
|
2bf3546971 | ||
|
|
cd42b0dc65 | ||
|
|
4ba3caeaf4 | ||
|
|
10ac1af6b0 | ||
|
|
833e75ade1 | ||
|
|
7bbd80ed21 | ||
|
|
dbedc9d1d3 | ||
|
|
3bdcc25ebe | ||
|
|
d0108e9859 | ||
|
|
025c89d78c | ||
|
|
c52e60b410 | ||
|
|
3cc644193f | ||
|
|
8020792024 | ||
|
|
837a27f9d7 | ||
|
|
5c71f6254b | ||
|
|
bbea6c2ae4 | ||
|
|
fa799e9542 | ||
|
|
7f1ce1e645 | ||
|
|
dc6bf3795a | ||
|
|
a3538f95e2 | ||
|
|
85c358043d | ||
|
|
ab8f01284a | ||
|
|
97d824d779 | ||
|
|
eedf9d2935 | ||
|
|
1ede56e137 | ||
|
|
f8c106bcb9 | ||
|
|
10c206270a | ||
|
|
f7a0ee7174 | ||
|
|
f5e8ba96e6 |
@@ -0,0 +1 @@
|
||||
custom: ['https://www.buymeacoffee.com/beemdevelopment']
|
||||
@@ -39,3 +39,4 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
+64
-25
@@ -1,4 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
@@ -17,10 +18,11 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 24
|
||||
versionName "1.1"
|
||||
versionCode 36
|
||||
versionName "1.2.1"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
}
|
||||
@@ -43,16 +45,27 @@ android {
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode false
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode true
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,34 +75,60 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def glideVersion = '4.11.0'
|
||||
def guavaVersion = '29.0'
|
||||
def junitVersion = '5.6.2'
|
||||
def libsuVersion = '2.5.1'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.apl-devs:appintro:5.1.0'
|
||||
implementation 'com.github.apl-devs:appintro:6.0.0'
|
||||
implementation 'com.github.avito-tech:krop:0.44'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation 'com.github.bumptech.glide:annotations:4.10.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.12.1'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.0'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
</lint>
|
||||
Vendored
+5
@@ -15,3 +15,8 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".AegisApplication"
|
||||
@@ -20,9 +22,11 @@
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about"></activity>
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="Select entries" />
|
||||
@@ -51,12 +55,15 @@
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="Edit profile" />
|
||||
android:label="Edit entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro" />
|
||||
<activity android:name=".ui.AuthActivity" />
|
||||
<activity android:name=".ui.PreferencesActivity" />
|
||||
<activity android:name=".ui.AuthActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="Manage key slots" />
|
||||
|
||||
@@ -31,6 +31,76 @@
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 1.2.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix a rare issue where the intro could end up in a bad state</li>
|
||||
</ul>
|
||||
<h3>Version 1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Add navigation bar color to themes</li>
|
||||
<li>Add support for importing from TOTP Authenticator</li>
|
||||
<li>Add support for importing from Microsoft Authenticator</li>
|
||||
<li>Add support for importing from Authenticator Plus</li>
|
||||
<li>Add support for importing a plain text Google Authenticator URI file</li>
|
||||
<li>Add support for importing from the new Google Authenticator export QR codes</li>
|
||||
<li>Add support for otpauth://steam URI's</li>
|
||||
<li>Add an option to copy tokens on tap (and disable it by default)</li>
|
||||
<li>Improve method to notify users on copy</li>
|
||||
<li>Add support for backups</li>
|
||||
<li>Improve multiselect flow</li>
|
||||
<li>Automatically adapt to system theme</li>
|
||||
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
|
||||
<li>Use most frequent period to show progress</li>
|
||||
<li>Append a timestamp to the filename of exported vaults</li>
|
||||
<li>Add Hungarian translation</li>
|
||||
<li>Add Turkish translation</li>
|
||||
<li>Display a warning if automatic time sync is not enabled</li>
|
||||
<li>Minor card entry layout overhaul</li>
|
||||
<li>Ability to transfer tokens with qr codes</li>
|
||||
<li>Lockscreen overhaul</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Improve overall exception handling and error feedback to the user</li>
|
||||
<li>Improve icon editing flow</li>
|
||||
<li>Protect writes of the vault file against corruption with AtomicFile</li>
|
||||
<li>Make the parsing logic of the QR code URI more robust</li>
|
||||
<li>Importing from Authy now asks for password if needed</li>
|
||||
<li>Update Russian localization</li>
|
||||
<li>Increase password reminder period to 30 days</li>
|
||||
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
|
||||
<li>Respect the global animator duration scale setting</li>
|
||||
</ul>
|
||||
<p>Various other minor improvements</p>
|
||||
<h3>Version 1.1.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The export filename was missing the ".json" extension in some cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Password reminder for users who use biometric unlock</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Tokens would not refresh in some rare cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to select multiple entries</li>
|
||||
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
|
||||
<li>Explanation and warning for the security options</li>
|
||||
<li>Removed external storage permissions</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Exporting the vault did not work on Android 10</li>
|
||||
</ul>
|
||||
<h3>Version 1.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
|
||||
@@ -16,7 +16,11 @@ import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
@@ -25,6 +29,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class AegisApplication extends Application {
|
||||
private VaultFile _vaultFile;
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
@@ -35,7 +40,6 @@ public class AegisApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_manager = new VaultManager(this);
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
|
||||
@@ -58,6 +62,47 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isVaultLocked() {
|
||||
return _manager == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the vault file from disk at the default location, stores an internal
|
||||
* reference to it for future use and returns it. This must only be called before
|
||||
* initVaultManager() or after lock().
|
||||
*/
|
||||
public VaultFile loadVaultFile() throws VaultManagerException {
|
||||
if (!isVaultLocked()) {
|
||||
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
|
||||
}
|
||||
|
||||
if (_vaultFile == null) {
|
||||
_vaultFile = VaultManager.readFile(this);
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager by decrypting the given vaultFile with the given
|
||||
* creds. This removes the internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
|
||||
_vaultFile = null;
|
||||
_manager = VaultManager.init(this, vaultFile, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager with the given vault and creds. This removes the
|
||||
* internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
|
||||
_vaultFile = null;
|
||||
_manager = new VaultManager(this, vault, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public VaultManager getVaultManager() {
|
||||
return _manager;
|
||||
}
|
||||
@@ -67,7 +112,7 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
|
||||
return _prefs.isAutoLockEnabled() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
@@ -79,7 +124,7 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
_manager.lock();
|
||||
_manager = null;
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked();
|
||||
}
|
||||
@@ -99,8 +144,8 @@ public class AegisApplication extends Application {
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
|
||||
.setShortLabel(getString(R.string.new_profile))
|
||||
.setLongLabel(getString(R.string.add_new_profile))
|
||||
.setShortLabel(getString(R.string.new_entry))
|
||||
.setLongLabel(getString(R.string.add_new_entry))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
@@ -3,16 +3,23 @@ package com.beemdevelopment.aegis;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Preferences {
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
if (getPasswordReminderTimestamp().getTime() == 0) {
|
||||
resetPasswordReminderTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTapToRevealEnabled() {
|
||||
@@ -32,10 +39,36 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderEnabled() {
|
||||
return _prefs.getBoolean("pref_password_reminder", true);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderNeeded() {
|
||||
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
|
||||
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
|
||||
return isPasswordReminderEnabled() && days >= 30;
|
||||
}
|
||||
|
||||
public Date getPasswordReminderTimestamp() {
|
||||
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
|
||||
}
|
||||
|
||||
public void resetPasswordReminderTimestamp() {
|
||||
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
|
||||
}
|
||||
|
||||
public boolean isAccountNameVisible() {
|
||||
return _prefs.getBoolean("pref_account_name", true);
|
||||
}
|
||||
|
||||
public int getCodeGroupSize() {
|
||||
if (_prefs.getBoolean("pref_code_group_size", false)) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
@@ -65,7 +98,7 @@ public class Preferences {
|
||||
}
|
||||
|
||||
public Theme getCurrentTheme() {
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
|
||||
}
|
||||
|
||||
public void setCurrentTheme(Theme theme) {
|
||||
@@ -97,4 +130,53 @@ public class Preferences {
|
||||
|
||||
return new Locale(lang);
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
public void setIsBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public Uri getBackupsLocation() {
|
||||
String str = _prefs.getString("pref_backups_location", null);
|
||||
if (str != null) {
|
||||
return Uri.parse(str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBackupsLocation(Uri location) {
|
||||
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
|
||||
}
|
||||
|
||||
public int getBackupsVersionCount() {
|
||||
return _prefs.getInt("pref_backups_versions", 5);
|
||||
}
|
||||
|
||||
public void setBackupsVersionCount(int versions) {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
|
||||
public void setBackupsError(Exception e) {
|
||||
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
|
||||
}
|
||||
|
||||
public String getBackupsError() {
|
||||
return _prefs.getString("pref_backups_error", null);
|
||||
}
|
||||
|
||||
public boolean isTimeSyncWarningEnabled() {
|
||||
return _prefs.getBoolean("pref_warn_time_sync", true);
|
||||
}
|
||||
|
||||
public void setIsTimeSyncWarningEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isCopyOnTapEnabled() {
|
||||
return _prefs.getBoolean("pref_copy_on_tap", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.beemdevelopment.aegis;
|
||||
public enum Theme {
|
||||
LIGHT,
|
||||
DARK,
|
||||
AMOLED;
|
||||
AMOLED,
|
||||
SYSTEM,
|
||||
SYSTEM_AMOLED;
|
||||
|
||||
private static Theme[] _values;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
|
||||
byte[] nonce = Hex.decode(obj.getString("nonce"));
|
||||
byte[] tag = Hex.decode(obj.getString("tag"));
|
||||
return new CryptParameters(nonce, tag);
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import org.spongycastle.crypto.generators.SCrypt;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -12,6 +13,7 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
@@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class KeyStoreHandle {
|
||||
private final KeyStore _keyStore;
|
||||
private static final String STORE_NAME = "AndroidKeyStore";
|
||||
@@ -61,6 +61,14 @@ public class KeyStoreHandle {
|
||||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof KeyStoreException) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
@@ -88,18 +96,14 @@ public class KeyStoreHandle {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher
|
||||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
|
||||
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
|
||||
// so instead, check for it here
|
||||
if (e instanceof KeyPermanentlyInvalidatedException) {
|
||||
return true;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
|
||||
|
||||
public MasterKey(SecretKey key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException();
|
||||
throw new IllegalArgumentException("Key cannot be null");
|
||||
}
|
||||
_key = key;
|
||||
}
|
||||
|
||||
@@ -1,155 +1,21 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// modified for use in Aegis
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
/* (PD) 2001 The Bitzi Corporation
|
||||
* Please see http://bitzi.com/publicdomain for more info.
|
||||
*
|
||||
* As modified by Patrick Woodworth:
|
||||
*
|
||||
* Copyright 2011 Patrick Woodworth
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Base32 - encodes and decodes RFC3548 Base32
|
||||
* (see http://www.faqs.org/rfcs/rfc3548.html )
|
||||
*
|
||||
* @author Robert Kaye
|
||||
* @author Gordon Mohr
|
||||
*/
|
||||
public class Base32 {
|
||||
private static final String base32Chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
private static final int[] base32Lookup =
|
||||
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
|
||||
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
|
||||
};
|
||||
private Base32() {
|
||||
|
||||
/**
|
||||
* Encodes byte array to Base32 String.
|
||||
*
|
||||
* @param bytes Bytes to encode.
|
||||
* @return Encoded byte array <code>bytes</code> as a String.
|
||||
*
|
||||
*/
|
||||
public static char[] encode(final byte[] bytes) {
|
||||
int i = 0, index = 0, digit = 0, j = 0;
|
||||
int currByte, nextByte;
|
||||
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
|
||||
|
||||
while (i < bytes.length) {
|
||||
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
|
||||
|
||||
/* Is the current digit going to span a byte boundary? */
|
||||
if (index > 3) {
|
||||
if ((i + 1) < bytes.length) {
|
||||
nextByte =
|
||||
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
|
||||
} else {
|
||||
nextByte = 0;
|
||||
}
|
||||
|
||||
digit = currByte & (0xFF >> index);
|
||||
index = (index + 5) % 8;
|
||||
digit <<= index;
|
||||
digit |= nextByte >> (8 - index);
|
||||
i++;
|
||||
} else {
|
||||
digit = (currByte >> (8 - (index + 5))) & 0x1F;
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0)
|
||||
i++;
|
||||
}
|
||||
base32[j++] = base32Chars.charAt(digit);
|
||||
}
|
||||
|
||||
return Arrays.copyOf(base32, j);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given Base32 String to a raw byte array.
|
||||
*
|
||||
* @param base32
|
||||
* @return Decoded <code>base32</code> String as a raw byte array.
|
||||
*/
|
||||
public static byte[] decode(final char[] base32) throws Base32Exception {
|
||||
int i, index, lookup, offset, digit;
|
||||
byte[] bytes = new byte[base32.length * 5 / 8];
|
||||
|
||||
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
|
||||
// stop decoding when a padding char is encountered
|
||||
if (base32[i] == '=') {
|
||||
// make sure the rest is also padding, but don't bother verifying the length
|
||||
for (int j = i + 1; j < base32.length; j++) {
|
||||
if (base32[j] != '=') {
|
||||
throw new Base32Exception("bad padding");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
lookup = base32[i] - '0';
|
||||
digit = decodeDigit(lookup);
|
||||
|
||||
if (index <= 3) {
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0) {
|
||||
bytes[offset] |= digit;
|
||||
offset++;
|
||||
if (offset >= bytes.length)
|
||||
break;
|
||||
} else {
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
} else {
|
||||
index = (index + 5) % 8;
|
||||
bytes[offset] |= (digit >>> index);
|
||||
offset++;
|
||||
|
||||
if (offset >= bytes.length) {
|
||||
break;
|
||||
}
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base32().decode(s.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int decodeDigit(int c) throws Base32Exception {
|
||||
/* Skip chars outside the lookup table */
|
||||
if (c < 0 || c >= base32Lookup.length) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
int digit = base32Lookup[c];
|
||||
|
||||
/* If this digit is not in the table, ignore it */
|
||||
if (digit == 0xFF) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
return digit;
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base32().omitPadding().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base32Exception extends Exception {
|
||||
public Base32Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class Base64 {
|
||||
private static final int _flags = android.util.Base64.NO_WRAP;
|
||||
|
||||
private Base64() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws Base64Exception {
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return android.util.Base64.decode(s, _flags);
|
||||
return BaseEncoding.base64().decode(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new Base64Exception(e);
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] s) throws EncodingException {
|
||||
return decode(new String(s, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
byte[] encoded = android.util.Base64.encode(data, _flags);
|
||||
return new String(encoded, StandardCharsets.UTF_8);
|
||||
return BaseEncoding.base64().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base64Exception extends Exception {
|
||||
public Base64Exception(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EncodingException extends IOException {
|
||||
public EncodingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,21 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
|
||||
// It is licensed under GPLv2 with a classpath exception.
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
public class Hex {
|
||||
private Hex() {
|
||||
|
||||
}
|
||||
|
||||
private static int hexToBin(char ch) {
|
||||
if ('0' <= ch && ch <= '9') return ch - '0';
|
||||
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
|
||||
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static final char[] hexCode = "0123456789abcdef".toCharArray();
|
||||
|
||||
public static byte[] decode(String s) throws HexException {
|
||||
final int len = s.length();
|
||||
|
||||
if (len % 2 != 0)
|
||||
throw new HexException("hexBinary needs to be even-length: " + s);
|
||||
|
||||
byte[] out = new byte[len / 2];
|
||||
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
int h = hexToBin(s.charAt(i));
|
||||
int l = hexToBin(s.charAt(i + 1));
|
||||
if (h == -1 || l == -1)
|
||||
throw new HexException("contains illegal character for hexBinary: " + s);
|
||||
|
||||
out[i / 2] = (byte) (h * 16 + l);
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base16().decode(s.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
StringBuilder r = new StringBuilder(data.length * 2);
|
||||
for (byte b : data) {
|
||||
r.append(hexCode[(b >> 4) & 0xF]);
|
||||
r.append(hexCode[(b & 0xF)]);
|
||||
}
|
||||
return r.toString();
|
||||
return BaseEncoding.base16().lowerCase().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class HexException extends Exception {
|
||||
public HexException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -102,7 +102,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (JSONException | OtpInfoException | Base64Exception e) {
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,16 @@ import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -49,8 +50,6 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
private static final int SALT_SIZE = 12;
|
||||
private static final int KEY_SIZE = 256; // bits
|
||||
|
||||
private static final int MAX_ITERATIONS = 10000;
|
||||
|
||||
public AndOtpImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -96,38 +95,12 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
|
||||
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
try {
|
||||
SecretKey key;
|
||||
int offset = 0;
|
||||
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
key = new SecretKeySpec(keyBytes, "AES");
|
||||
} else {
|
||||
offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1 || iterations > MAX_ITERATIONS) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
}
|
||||
|
||||
// extract nonce and tag
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
|
||||
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
|
||||
@@ -137,13 +110,50 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| InvalidKeySpecException
|
||||
| NoSuchPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
DecryptedState state = decryptData(key, 0);
|
||||
listener.onStateDecrypted(state);
|
||||
} else {
|
||||
int offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
|
||||
try {
|
||||
DecryptedState state = decryptData(key1, offset);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
task.execute(params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
String[] choices = new String[]{
|
||||
@@ -158,8 +168,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password, i != 0);
|
||||
listener.onStateDecrypted(state);
|
||||
decrypt(context, password, i != 0, listener);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
@@ -201,7 +210,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algorithm");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
@@ -221,18 +230,83 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
String name;
|
||||
String issuer = "";
|
||||
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
if (obj.has("issuer")) {
|
||||
name = obj.getString("label");
|
||||
issuer = obj.getString("issuer");
|
||||
} else {
|
||||
name = parts[0];
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
|
||||
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
||||
public AndOtpKeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
AndOtpKeyDerivationTask.Params params = args[0];
|
||||
SecretKey key;
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private char[] _password;
|
||||
private byte[] _salt;
|
||||
private int _iterations;
|
||||
|
||||
public Params(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.LocalFileHeader;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
private static final String FILENAME = "Accounts.txt";
|
||||
|
||||
public AuthenticatorPlusImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try {
|
||||
return new EncryptedState(reader.readAll());
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
private EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
|
||||
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
|
||||
LocalFileHeader header;
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
FileReader reader = new FileReader(zipStream);
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
|
||||
GoogleAuthUriImporter.State state = importer.read(reader);
|
||||
listener.onStateDecrypted(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(FILENAME);
|
||||
} catch (IOException | DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,16 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -18,11 +21,33 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
|
||||
private static final String _pkgName = "com.authy.authy";
|
||||
|
||||
private static final int ITERATIONS = 1000;
|
||||
private static final int KEY_SIZE = 256;
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
public AuthyImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -52,18 +77,76 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
return new State(array);
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
if (!array.getJSONObject(i).has("decryptedSecret")) {
|
||||
return new EncryptedState(array);
|
||||
}
|
||||
}
|
||||
|
||||
return new DecryptedState(array);
|
||||
} catch (XmlPullParserException | JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private JSONArray _obj;
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private State(JSONArray obj) {
|
||||
private EncryptedState(JSONArray array) {
|
||||
super(true);
|
||||
_array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = obj.optString("encryptedSecret", null);
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] encryptedSecret = Base64.decode(secretString);
|
||||
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
|
||||
byte[] secret = cipher.doFinal(encryptedSecret);
|
||||
obj.remove("encryptedSecret");
|
||||
obj.remove("salt");
|
||||
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
DecryptedState state = new DecryptedState(_array);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (JSONException
|
||||
| EncodingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private DecryptedState(JSONArray array) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
_array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,8 +154,8 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
JSONObject entryObj = _obj.getJSONObject(i);
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject entryObj = _array.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
@@ -90,42 +173,46 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
|
||||
authyEntryInfo.OriginalName = entry.getString("originalName");
|
||||
authyEntryInfo.OriginalName = entry.optString("originalName", null);
|
||||
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
|
||||
authyEntryInfo.AccountType = entry.getString("accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
sanitizeEntryInfo(authyEntryInfo);
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
|
||||
byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | Base32Exception e) {
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
|
||||
String seperator = "";
|
||||
String separator = "";
|
||||
|
||||
if (info.OriginalName.contains(":")) {
|
||||
if (info.OriginalIssuer != null) {
|
||||
info.Issuer = info.OriginalIssuer;
|
||||
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
seperator = ":";
|
||||
separator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
seperator = " - ";
|
||||
separator = " - ";
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + seperator, "");
|
||||
info.Name = info.Name.replace(info.Issuer + separator, "");
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthyEntryInfo {
|
||||
String OriginalName;
|
||||
String OriginalIssuer;
|
||||
String AccountType;
|
||||
String Issuer;
|
||||
String Name;
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
@@ -27,12 +27,16 @@ public abstract class DatabaseImporter {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new LinkedHashMap<>();
|
||||
_importers.put("Aegis", AegisImporter.class);
|
||||
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.class);
|
||||
_importers.put("Authy", AuthyImporter.class);
|
||||
_importers.put("andOTP", AndOtpImporter.class);
|
||||
_importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
|
||||
_importers.put("Plain text", GoogleAuthUriImporter.class);
|
||||
_importers.put("Steam", SteamImporter.class);
|
||||
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||
_importers.put("WinAuth", WinAuthImporter.class);
|
||||
|
||||
_appImporters = new LinkedHashMap<>();
|
||||
@@ -40,7 +44,9 @@ public abstract class DatabaseImporter {
|
||||
_appImporters.put("FreeOTP", FreeOtpImporter.class);
|
||||
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
|
||||
_appImporters.put("Steam", SteamImporter.class);
|
||||
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||
}
|
||||
|
||||
public DatabaseImporter(Context context) {
|
||||
@@ -146,9 +152,7 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
|
||||
return stream.getBytes();
|
||||
}
|
||||
return IOUtils.readAll(_stream);
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
|
||||
+5
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
||||
public class DatabaseImporterEntryException extends Exception {
|
||||
private String _text;
|
||||
|
||||
public DatabaseImporterEntryException(String message, String text) {
|
||||
super(message);
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public DatabaseImporterEntryException(Throwable cause, String text) {
|
||||
super(cause);
|
||||
_text = text;
|
||||
|
||||
@@ -2,26 +2,17 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_HOTP = 1;
|
||||
@@ -45,37 +36,9 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
File file;
|
||||
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
ShellUtils.pump(reader.getStream(), out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Entry entry = new Entry(cursor);
|
||||
entries.add(entry);
|
||||
} while(cursor.moveToNext());
|
||||
}
|
||||
|
||||
return new State(entries);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
@@ -104,7 +67,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
|
||||
byte[] secret = Base32.decode(entry.getSecret());
|
||||
|
||||
OtpInfo info;
|
||||
switch (entry.getType()) {
|
||||
@@ -125,33 +88,13 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, entry.getIssuer());
|
||||
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
|
||||
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName) {
|
||||
return getString(cursor, columnName, null);
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _email;
|
||||
@@ -159,11 +102,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private long _counter;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
_type = getInt(cursor, "type");
|
||||
_secret = getString(cursor, "secret");
|
||||
_email = getString(cursor, "email", "");
|
||||
_issuer = getString(cursor, "issuer", "");
|
||||
_counter = getLong(cursor, "counter");
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "secret");
|
||||
_email = SqlImporterHelper.getString(cursor, "email", "");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
|
||||
_counter = SqlImporterHelper.getLong(cursor, "counter");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GoogleAuthUriImporter extends DatabaseImporter {
|
||||
public GoogleAuthUriImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoogleAuthUriImporter.State read(DatabaseImporter.FileReader reader) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try (InputStreamReader streamReader = new InputStreamReader(reader.getStream());
|
||||
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new GoogleAuthUriImporter.State(lines);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private ArrayList<String> _lines;
|
||||
|
||||
private State(ArrayList<String> lines) {
|
||||
super(false);
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.Result convert() {
|
||||
DatabaseImporter.Result result = new DatabaseImporter.Result();
|
||||
|
||||
for (String line : _lines) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(line);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
|
||||
return new VaultEntry(info);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "databases/PhoneFactor";
|
||||
private static final String _pkgName = "com.azure.authenticator";
|
||||
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_MICROSOFT = 1;
|
||||
|
||||
public MicrosoftAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<Entry> _entries;
|
||||
|
||||
private State(List<Entry> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (Entry sqlEntry : _entries) {
|
||||
try {
|
||||
int type = sqlEntry.getType();
|
||||
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
|
||||
VaultEntry entry = convertEntry(sqlEntry);
|
||||
result.addEntry(entry);
|
||||
}
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret;
|
||||
int digits = 6;
|
||||
|
||||
switch (entry.getType()) {
|
||||
case TYPE_TOTP:
|
||||
secret = Base32.decode(entry.getSecret());
|
||||
break;
|
||||
case TYPE_MICROSOFT:
|
||||
digits = 8;
|
||||
secret = Base64.decode(entry.getSecret());
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
|
||||
}
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _issuer;
|
||||
private String _userName;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "account_type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "name");
|
||||
_userName = SqlImporterHelper.getString(cursor, "username");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return _secret;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return _userName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class SqlImporterHelper {
|
||||
private Context _context;
|
||||
|
||||
public SqlImporterHelper(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
|
||||
File file;
|
||||
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("db-import-", "", _context.getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
ShellUtils.pump(inStream, out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
|
||||
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
|
||||
List<T> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
|
||||
entries.add(entry);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
|
||||
return entries;
|
||||
} catch (InstantiationException | IllegalAccessException
|
||||
| NoSuchMethodException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getString(Cursor cursor, String columnName) {
|
||||
return cursor.getString(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static abstract class Entry {
|
||||
public Entry(Cursor cursor) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -45,8 +44,9 @@ public class SteamImporter extends DatabaseImporter {
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
|
||||
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
@@ -82,7 +82,7 @@ public class SteamImporter extends DatabaseImporter {
|
||||
|
||||
String account = obj.getString("account_name");
|
||||
return new VaultEntry(info, account, "Steam");
|
||||
} catch (JSONException | Base64Exception | OtpInfoException e) {
|
||||
} catch (JSONException | EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
|
||||
private static final String _pkgName = "com.authenticator.authservice2";
|
||||
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
|
||||
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
|
||||
|
||||
public TotpAuthenticatorImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try {
|
||||
if (reader.isInternal()) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.nextTag();
|
||||
|
||||
String data = null;
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(PREF_KEY)) {
|
||||
data = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
|
||||
}
|
||||
|
||||
List<JSONObject> entries = parse(data);
|
||||
return new DecryptedState(entries);
|
||||
} else {
|
||||
byte[] base64 = reader.readAll();
|
||||
byte[] cipherText = Base64.decode(base64);
|
||||
return new EncryptedState(cipherText);
|
||||
}
|
||||
} catch (IOException | XmlPullParserException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<JSONObject> parse(String data) throws JSONException {
|
||||
JSONArray array = new JSONArray(data);
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
String s = array.getString(i);
|
||||
entries.add(new JSONObject(s));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
public EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
IvParameterSpec spec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
|
||||
byte[] bytes = cipher.doFinal(_data);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
JSONArray keys = obj.names();
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
if (keys != null && keys.length() > 0) {
|
||||
entries = parse((String) keys.get(0));
|
||||
}
|
||||
|
||||
return new DecryptedState(entries);
|
||||
} catch (NoSuchAlgorithmException
|
||||
| NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException
|
||||
| JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.choose_totpauth_importer)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
decrypt(password, listener);
|
||||
});
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
decrypt(PASSWORD, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void decrypt(char[] password, DecryptListener listener) {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private List<JSONObject> _objs;
|
||||
|
||||
private DecryptedState(List<JSONObject> objs) {
|
||||
super(false);
|
||||
_objs = objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (JSONObject obj : _objs) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
int base = obj.getInt("base");
|
||||
String secretString = obj.getString("key");
|
||||
|
||||
byte[] secret;
|
||||
switch (base) {
|
||||
case 16:
|
||||
secret = Hex.decode(secretString);
|
||||
break;
|
||||
case 32:
|
||||
secret = Base32.decode(secretString);
|
||||
break;
|
||||
case 64:
|
||||
secret = Base64.decode(secretString);
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
|
||||
}
|
||||
|
||||
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
|
||||
String name = obj.optString("name");
|
||||
String issuer = obj.optString("issuer");
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
public class WinAuthImporter extends DatabaseImporter {
|
||||
public WinAuthImporter(Context context) {
|
||||
@@ -27,57 +21,29 @@ public class WinAuthImporter extends DatabaseImporter {
|
||||
|
||||
@Override
|
||||
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try {
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
lines.add(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new State(lines);
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
|
||||
GoogleAuthUriImporter.State state = importer.read(reader);
|
||||
return new State(state);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private ArrayList<String> _lines;
|
||||
private GoogleAuthUriImporter.State _state;
|
||||
|
||||
private State(ArrayList<String> lines) {
|
||||
private State(GoogleAuthUriImporter.State state) {
|
||||
super(false);
|
||||
_lines = lines;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
Result result = _state.convert();
|
||||
|
||||
for (String line : _lines) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(line);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
for (VaultEntry entry : result.getEntries()) {
|
||||
entry.setIssuer(entry.getName());
|
||||
entry.setName("WinAuth");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
|
||||
VaultEntry vaultEntry = new VaultEntry(info);
|
||||
vaultEntry.setIssuer(vaultEntry.getName());
|
||||
vaultEntry.setName("WinAuth");
|
||||
|
||||
return vaultEntry;
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,20 @@ package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.GoogleAuthProtos;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthInfo implements Serializable {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
public class GoogleAuthInfo {
|
||||
private OtpInfo _info;
|
||||
private String _accountName;
|
||||
private String _issuer;
|
||||
@@ -22,16 +32,20 @@ public class GoogleAuthInfo {
|
||||
|
||||
public Uri getUri() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("otpauth");
|
||||
builder.scheme(SCHEME);
|
||||
|
||||
if (_info instanceof TotpInfo) {
|
||||
builder.authority("totp");
|
||||
if (_info instanceof SteamInfo) {
|
||||
builder.authority("steam");
|
||||
} else {
|
||||
builder.authority("totp");
|
||||
}
|
||||
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
|
||||
} else if (_info instanceof HotpInfo) {
|
||||
builder.authority("hotp");
|
||||
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
||||
}
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||
@@ -51,35 +65,39 @@ public class GoogleAuthInfo {
|
||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("bad uri format");
|
||||
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals("otpauth")) {
|
||||
throw new GoogleAuthInfoException("unsupported protocol");
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("'secret' is not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
// decode secret
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(encodedSecret.toCharArray());
|
||||
} catch (Base32Exception e) {
|
||||
throw new GoogleAuthInfoException("bad secret", e);
|
||||
secret = Base32.decode(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException("Bad secret", e);
|
||||
}
|
||||
|
||||
// check the otp type
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "totp":
|
||||
TotpInfo totpInfo = new TotpInfo(secret);
|
||||
@@ -89,19 +107,27 @@ public class GoogleAuthInfo {
|
||||
}
|
||||
info = totpInfo;
|
||||
break;
|
||||
case "steam":
|
||||
SteamInfo steamInfo = new SteamInfo(secret);
|
||||
period = uri.getQueryParameter("period");
|
||||
if (period != null) {
|
||||
steamInfo.setPeriod(Integer.parseInt(period));
|
||||
}
|
||||
info = steamInfo;
|
||||
break;
|
||||
case "hotp":
|
||||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("'counter' was not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
@@ -141,13 +167,79 @@ public class GoogleAuthInfo {
|
||||
if (digits != null) {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException("Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
try {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_HOTP:
|
||||
otp = new HotpInfo(secret, params.getCounter());
|
||||
break;
|
||||
case OTP_TOTP:
|
||||
otp = new TotpInfo(secret);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
String issuer = params.getIssuer();
|
||||
int colonI = name.indexOf(':');
|
||||
if (issuer.isEmpty() && colonI != -1) {
|
||||
issuer = name.substring(0, colonI);
|
||||
name = name.substring(colonI + 1);
|
||||
}
|
||||
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
|
||||
infos.add(info);
|
||||
}
|
||||
|
||||
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
@@ -155,4 +247,34 @@ public class GoogleAuthInfo {
|
||||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public static class Export {
|
||||
private int _batchId;
|
||||
private int _batchIndex;
|
||||
private int _batchSize;
|
||||
private List<GoogleAuthInfo> _entries;
|
||||
|
||||
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
|
||||
_batchId = batchId;
|
||||
_batchIndex = batchIndex;
|
||||
_batchSize = batchSize;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public List<GoogleAuthInfo> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return _batchSize;
|
||||
}
|
||||
|
||||
public int getBatchIndex() {
|
||||
return _batchIndex;
|
||||
}
|
||||
|
||||
public int getBatchId() {
|
||||
return _batchId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class HotpInfo extends OtpInfo {
|
||||
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -93,7 +93,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
OtpInfo info;
|
||||
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
|
||||
@@ -110,7 +110,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
default:
|
||||
throw new OtpInfoException("unsupported otp type: " + type);
|
||||
}
|
||||
} catch (Base32Exception | JSONException e) {
|
||||
} catch (EncodingException | JSONException e) {
|
||||
throw new OtpInfoException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
@@ -40,6 +44,11 @@ public class AboutActivity extends AegisActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
|
||||
@@ -110,11 +119,12 @@ public class AboutActivity extends AegisActivity {
|
||||
private void showLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
|
||||
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
|
||||
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
String textColor = getThemeColorAsHex(R.attr.primaryText);
|
||||
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
|
||||
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
|
||||
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
new LicensesDialog.Builder(this)
|
||||
@@ -125,4 +135,21 @@ public class AboutActivity extends AegisActivity {
|
||||
.build()
|
||||
.show();
|
||||
}
|
||||
|
||||
private String getThemeColorAsHex(@AttrRes int attributeId) {
|
||||
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -46,9 +45,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
// apply a dirty hack to make progress bars work even if animations are disabled
|
||||
setGlobalAnimationDurationScale();
|
||||
|
||||
// register a callback to listen for lock events
|
||||
_app.registerLockListener(this);
|
||||
}
|
||||
@@ -89,6 +85,19 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
// set the theme based on the system theme
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (currentNightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
theme = Theme.LIGHT;
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_currentTheme = theme;
|
||||
|
||||
switch (theme) {
|
||||
@@ -110,7 +119,17 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
Configuration config = new Configuration();
|
||||
config.locale = locale;
|
||||
|
||||
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
|
||||
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
protected boolean saveVault(boolean backup) {
|
||||
try {
|
||||
getApp().getVaultManager().save(backup);
|
||||
return true;
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,18 +144,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
*/
|
||||
private boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
|
||||
}
|
||||
|
||||
private void setGlobalAnimationDurationScale() {
|
||||
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
|
||||
if (durationScale != 1) {
|
||||
try {
|
||||
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
|
||||
} catch (Throwable t) {
|
||||
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
return !(this instanceof MainActivity) && _app.isVaultLocked();
|
||||
}
|
||||
|
||||
protected Theme getCurrentTheme() {
|
||||
|
||||
@@ -2,55 +2,75 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
|
||||
public class AuthActivity extends AegisActivity {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
private BiometricPrompt.CryptoObject _bioCryptoObj;
|
||||
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
|
||||
private Preferences _prefs;
|
||||
private boolean _stateless;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
_prefs = new Preferences(this);
|
||||
setContentView(R.layout.activity_auth);
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
Button decryptButton = findViewById(R.id.button_decrypt);
|
||||
Button biometricsButton = findViewById(R.id.button_biometrics);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
@@ -60,8 +80,21 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
});
|
||||
|
||||
Intent intent = getIntent();
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_stateless = _slots != null;
|
||||
if (!_stateless) {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
vaultFile = getApp().loadVaultFile();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
|
||||
return;
|
||||
}
|
||||
|
||||
_slots = vaultFile.getHeader().getSlots();
|
||||
}
|
||||
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
@@ -80,57 +113,66 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
continue;
|
||||
}
|
||||
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
_bioSlot = slot;
|
||||
_bioKey = key;
|
||||
biometricsButton.setVisibility(View.VISIBLE);
|
||||
invalidated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
}
|
||||
|
||||
// display a help message if a matching invalidated keystore entry was found
|
||||
if (invalidated) {
|
||||
boxBiometricInfo.setVisibility(View.VISIBLE);
|
||||
biometricsButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
decryptButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
decryptButton.setOnClickListener(v -> {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
trySlots(PasswordSlot.class, password);
|
||||
}
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
|
||||
});
|
||||
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
showBiometricPrompt();
|
||||
});
|
||||
}
|
||||
|
||||
if (_bioCryptoObj == null) {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
@Override
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
// set the theme based on the system theme
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (currentNightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
theme = Theme.LIGHT;
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showError() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
}
|
||||
|
||||
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
|
||||
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
|
||||
new SlotListTask<>(type, this, this).execute(params);
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme_Light_NoActionBar);
|
||||
break;
|
||||
case DARK:
|
||||
setTheme(R.style.AppTheme_Dark_NoActionBar);
|
||||
break;
|
||||
case AMOLED:
|
||||
setTheme(R.style.AppTheme_TrueBlack_NoActionBar);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
@@ -145,10 +187,7 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
switch (_cancelAction) {
|
||||
case KILL:
|
||||
finishAffinity();
|
||||
|
||||
case CLOSE:
|
||||
Intent intent = new Intent();
|
||||
setResult(RESULT_CANCELED, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -157,17 +196,60 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_bioPrompt != null) {
|
||||
showBiometricPrompt();
|
||||
if (_bioKey != null) {
|
||||
if (_prefs.isPasswordReminderNeeded()) {
|
||||
focusPasswordField();
|
||||
} else {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
} else {
|
||||
focusPasswordField();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
|
||||
showPasswordReminder();
|
||||
}
|
||||
}
|
||||
|
||||
private void focusPasswordField() {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
|
||||
private void showPasswordReminder() {
|
||||
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
|
||||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
|
||||
_textPassword.postDelayed(popup::dismiss, 5000);
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = _bioSlot.createDecryptCipher(_bioKey);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.authentication))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
_bioPrompt.authenticate(info, _bioCryptoObj);
|
||||
_bioPrompt.authenticate(info, cryptoObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,22 +261,55 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
if (_stateless) {
|
||||
// send the master key back to the calling activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
saveVault(true);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} else {
|
||||
showError();
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
}
|
||||
|
||||
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
|
||||
_prefs.resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +325,19 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
|
||||
|
||||
MasterKey key;
|
||||
BiometricSlot slot = _slots.find(BiometricSlot.class);
|
||||
|
||||
try {
|
||||
key = slot.getKey(result.getCryptoObject().getCipher());
|
||||
} catch (SlotException | SlotIntegrityException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
finish(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,8 +2,12 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
@@ -16,17 +20,18 @@ import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -57,6 +62,24 @@ public class Dialogs {
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showDeleteEntriesDialog(Activity activity, DialogInterface.OnClickListener onDelete, int totalEntries) {
|
||||
String title, message;
|
||||
if (totalEntries > 1) {
|
||||
title = activity.getString(R.string.delete_entries);
|
||||
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
|
||||
} else {
|
||||
title = activity.getString(R.string.delete_entry);
|
||||
message = activity.getString(R.string.delete_entry_description);
|
||||
}
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(activity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.yes, onDelete)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
|
||||
showSecureDialog(new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.discard_changes))
|
||||
@@ -75,11 +98,12 @@ public class Dialogs {
|
||||
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
textPassword.setTransformationMethod(null);
|
||||
textPasswordConfirm.setTransformationMethod(null);
|
||||
textPassword.clearFocus();
|
||||
textPasswordConfirm.setEnabled(false);
|
||||
textPasswordConfirm.clearFocus();
|
||||
} else {
|
||||
textPassword.setTransformationMethod(new PasswordTransformationMethod());
|
||||
textPasswordConfirm.setEnabled(true);
|
||||
textPasswordConfirm.setTransformationMethod(new PasswordTransformationMethod());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,13 +122,13 @@ public class Dialogs {
|
||||
|
||||
// replace the default listener
|
||||
button.setOnClickListener(v -> {
|
||||
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
|
||||
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
|
||||
return;
|
||||
}
|
||||
|
||||
char[] password = EditTextHelper.getEditTextChars(textPassword);
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
DerivationTask task = new DerivationTask(activity, key -> {
|
||||
KeyDerivationTask task = new KeyDerivationTask(activity, (passSlot, key) -> {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
@@ -116,14 +140,14 @@ public class Dialogs {
|
||||
listener.onSlotResult(slot, cipher);
|
||||
dialog.dismiss();
|
||||
});
|
||||
task.execute(new DerivationTask.Params(slot, password));
|
||||
task.execute(new KeyDerivationTask.Params(slot, password));
|
||||
});
|
||||
});
|
||||
|
||||
TextWatcher watcher = new TextWatcher() {
|
||||
public void onTextChanged(CharSequence c, int start, int before, int count) {
|
||||
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
|
||||
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
|
||||
buttonOK.get().setEnabled(equal);
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
|
||||
@@ -138,7 +162,7 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
|
||||
EditText input = view.findViewById(R.id.text_input);
|
||||
if (isSecret) {
|
||||
@@ -146,18 +170,25 @@ public class Dialogs {
|
||||
}
|
||||
input.setHint(hintId);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
char[] text = EditTextHelper.getEditTextChars(input);
|
||||
listener.onTextInputResult(text);
|
||||
})
|
||||
.create();
|
||||
char[] text = EditTextHelper.getEditTextChars(input);
|
||||
listener.onTextInputResult(text);
|
||||
});
|
||||
if (messageId != 0) {
|
||||
builder.setMessage(messageId);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
showTextInputDialog(context, titleId, hintId, listener, false);
|
||||
}
|
||||
@@ -166,6 +197,10 @@ public class Dialogs {
|
||||
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
|
||||
}
|
||||
|
||||
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
@@ -184,6 +219,107 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showBackupVersionsPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
final int max = 30;
|
||||
String[] numbers = new String[max / 5];
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
numbers[i] = Integer.toString(i * 5 + 5);
|
||||
}
|
||||
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setDisplayedValues(numbers);
|
||||
numberPicker.setMaxValue(numbers.length - 1);
|
||||
numberPicker.setMinValue(0);
|
||||
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getBackupsVersionCount() / 5 - 1);
|
||||
numberPicker.setWrapSelectorWheel(false);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
listener.onNumberInputResult(numberPicker.getValue()))
|
||||
.create();
|
||||
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e) {
|
||||
showErrorDialog(context, message, e, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error) {
|
||||
showErrorDialog(context, message, error, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e, DialogInterface.OnClickListener listener) {
|
||||
showErrorDialog(context, message, e.toString(), listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error, DialogInterface.OnClickListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null);
|
||||
TextView textDetails = view.findViewById(R.id.error_details);
|
||||
textDetails.setText(error);
|
||||
TextView textMessage = view.findViewById(R.id.error_message);
|
||||
textMessage.setText(message);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(R.string.details, (dialog1, which) -> {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
})
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(v -> {
|
||||
if (textDetails.getVisibility() == View.GONE) {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
button.setText(R.string.copy);
|
||||
} else {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard != null) {
|
||||
ClipData clip = ClipData.newPlainText("text/plain", error);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showTimeSyncWarningDialog(Context context, Dialog.OnClickListener listener) {
|
||||
Preferences prefs = new Preferences(context);
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.time_sync_warning_title)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog, which);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
}
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
void onNumberInputResult(int number);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -22,12 +23,17 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
@@ -36,24 +42,21 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class EditEntryActivity extends AegisActivity {
|
||||
@@ -65,6 +68,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private boolean _isEditingIcon;
|
||||
private CircleImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
@@ -88,23 +92,29 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private RelativeLayout _advancedSettingsHeader;
|
||||
private RelativeLayout _advancedSettings;
|
||||
|
||||
private VaultManager _vault;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
|
||||
_vault = getApp().getVaultManager();
|
||||
_groups = _vault.getGroups();
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// retrieve info from the calling activity
|
||||
Intent intent = getIntent();
|
||||
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
|
||||
_isNew = intent.getBooleanExtra("isNew", false);
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
if (_isNew) {
|
||||
setTitle(R.string.add_new_profile);
|
||||
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
|
||||
if (entryUUID != null) {
|
||||
_origEntry = _vault.getEntryByUUID(entryUUID);
|
||||
} else {
|
||||
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
|
||||
_isNew = true;
|
||||
setTitle(R.string.add_new_entry);
|
||||
}
|
||||
|
||||
String selectedGroup = intent.getStringExtra("selectedGroup");
|
||||
@@ -157,14 +167,14 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
|
||||
}
|
||||
_textDigits.setText(Integer.toString(info.getDigits()));
|
||||
|
||||
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
String secretString = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretString);
|
||||
}
|
||||
|
||||
String type = _origEntry.getInfo().getType();
|
||||
@@ -184,9 +194,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String type = _spinnerType.getSelectedItem().toString();
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
|
||||
switch (type) {
|
||||
case TotpInfo.ID:
|
||||
case SteamInfo.ID:
|
||||
_rowCounter.setVisibility(View.GONE);
|
||||
@@ -197,7 +206,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,11 +247,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
});
|
||||
|
||||
_iconView.setOnClickListener(v -> {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
|
||||
startIconSelectionActivity();
|
||||
});
|
||||
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
@@ -250,7 +255,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if (_isNew) {
|
||||
openAdvancedSettings();
|
||||
|
||||
setGroup(selectedGroup);
|
||||
}
|
||||
}
|
||||
@@ -263,6 +267,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
|
||||
_spinnerGroup.setSelection(pos + 1, false);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
@@ -319,6 +324,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(false);
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicReference<String> msg = new AtomicReference<>();
|
||||
AtomicReference<VaultEntry> entry = new AtomicReference<>();
|
||||
|
||||
@@ -343,7 +353,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
finish(entry.get(), false);
|
||||
addAndFinish(entry.get());
|
||||
},
|
||||
(dialog, which) -> super.onBackPressed()
|
||||
);
|
||||
@@ -360,9 +370,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
break;
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
|
||||
finish(_origEntry, true);
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelectionActivity();
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
@@ -375,6 +388,52 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startIconSelectionActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_icon)), PICK_IMAGE_REQUEST);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(v -> {
|
||||
stopEditingIcon(true);
|
||||
});
|
||||
|
||||
_isEditingIcon = true;
|
||||
}
|
||||
|
||||
private void stopEditingIcon(boolean save) {
|
||||
if (save) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
}
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = _hasCustomIcon || save;
|
||||
_hasChangedIcon = save;
|
||||
_isEditingIcon = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_edit, menu);
|
||||
@@ -388,46 +447,36 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void finish(VaultEntry entry, boolean delete) {
|
||||
private void addAndFinish(VaultEntry entry) {
|
||||
if (_isNew) {
|
||||
_vault.addEntry(entry);
|
||||
} else {
|
||||
_vault.replaceEntry(entry);
|
||||
}
|
||||
|
||||
saveAndFinish(entry, false);
|
||||
}
|
||||
|
||||
private void deleteAndFinish(VaultEntry entry) {
|
||||
_vault.removeEntry(entry);
|
||||
saveAndFinish(entry, true);
|
||||
}
|
||||
|
||||
private void saveAndFinish(VaultEntry entry, boolean delete) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
intent.putExtra("delete", delete);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
||||
if (saveVault(true)) {
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data.getData())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
}
|
||||
});
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -458,8 +507,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
|
||||
} catch (Base32Exception e) {
|
||||
secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
|
||||
if (secret.length == 0) {
|
||||
throw new ParseException("Secret cannot be empty");
|
||||
}
|
||||
} catch (EncodingException e) {
|
||||
throw new ParseException("Secret is not valid base32.");
|
||||
}
|
||||
|
||||
@@ -482,7 +534,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
info = new HotpInfo(secret, algo, digits, counter);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
|
||||
info.setDigits(digits);
|
||||
@@ -528,6 +580,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private boolean onSave() {
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(true);
|
||||
}
|
||||
|
||||
VaultEntry entry;
|
||||
try {
|
||||
entry = parseEntry();
|
||||
@@ -536,7 +592,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
finish(entry, false);
|
||||
addAndFinish(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -29,9 +28,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
// set up the recycler view
|
||||
_adapter = new GroupAdapter(this);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
@@ -16,39 +18,30 @@ import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
import com.github.paolorotolo.appintro.AppIntro2;
|
||||
import com.github.paolorotolo.appintro.AppIntroFragment;
|
||||
import com.github.paolorotolo.appintro.model.SliderPage;
|
||||
import com.github.appintro.AppIntro2;
|
||||
import com.github.appintro.AppIntroFragment;
|
||||
import com.github.appintro.model.SliderPage;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
|
||||
|
||||
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
|
||||
public static final int RESULT_OK = 0;
|
||||
public static final int RESULT_EXCEPTION = 1;
|
||||
|
||||
private CustomAuthenticatedSlide _authenticatedSlide;
|
||||
private CustomAuthenticationSlide _authenticationSlide;
|
||||
public class IntroActivity extends AppIntro2 {
|
||||
private SecuritySetupSlide securitySetupSlide;
|
||||
private SecurityPickerSlide _securityPickerSlide;
|
||||
private Fragment _endSlide;
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFile _vaultFile;
|
||||
private PasswordSlot _passwordSlot;
|
||||
private Cipher _passwordCipher;
|
||||
|
||||
private AegisApplication _app;
|
||||
private Preferences _prefs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
// set FLAG_SECURE on the window of every IntroActivity
|
||||
_prefs = new Preferences(this);
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
@@ -56,10 +49,9 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
}
|
||||
|
||||
setWizardMode(true);
|
||||
showSkipButton(false);
|
||||
pager.setPagingEnabled(false);
|
||||
//showPagerIndicator(false);
|
||||
setGoBackLock(true);
|
||||
setSkipButtonEnabled(false);
|
||||
showStatusBar(true);
|
||||
setSystemBackButtonLocked(true);
|
||||
setBarColor(getResources().getColor(R.color.colorPrimary));
|
||||
|
||||
SliderPage homeSliderPage = new SliderPage();
|
||||
@@ -67,130 +59,93 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
||||
homeSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setDescription(getString(R.string.app_description));
|
||||
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(AppIntroFragment.newInstance(homeSliderPage));
|
||||
|
||||
_authenticationSlide = new CustomAuthenticationSlide();
|
||||
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
//_authenticationSlide.setDescColor(getResources().getColor(R.color.primary_text_dark));
|
||||
addSlide(_authenticationSlide);
|
||||
_authenticatedSlide = new CustomAuthenticatedSlide();
|
||||
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(_authenticatedSlide);
|
||||
_securityPickerSlide = new SecurityPickerSlide();
|
||||
_securityPickerSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(_securityPickerSlide);
|
||||
securitySetupSlide = new SecuritySetupSlide();
|
||||
securitySetupSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(securitySetupSlide);
|
||||
|
||||
SliderPage endSliderPage = new SliderPage();
|
||||
endSliderPage.setTitle(getString(R.string.setup_completed));
|
||||
endSliderPage.setDescription(getString(R.string.setup_completed_description));
|
||||
endSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
|
||||
_endSlide = AppIntroFragment.newInstance(endSliderPage);
|
||||
addSlide(_endSlide);
|
||||
|
||||
_vault = new Vault();
|
||||
_vaultFile = new VaultFile();
|
||||
}
|
||||
|
||||
private void setException(Exception e) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("exception", e);
|
||||
setResult(RESULT_EXCEPTION, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
|
||||
Intent intent = getIntent();
|
||||
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_passwordSlot = new PasswordSlot();
|
||||
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
|
||||
new DerivationTask(this, this).execute(params);
|
||||
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
|
||||
if (oldFragment == _securityPickerSlide && newFragment != _endSlide) {
|
||||
// skip to the last slide if no encryption will be used
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
int cryptType = getIntent().getIntExtra("cryptType", CRYPT_TYPE_INVALID);
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
// TODO: no magic indices
|
||||
getPager().setCurrentItem(4);
|
||||
goToNextSlide(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (newFragment == _endSlide) {
|
||||
setWizardMode(false);
|
||||
}
|
||||
|
||||
setSwipeLock(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDonePressed(Fragment currentFragment) {
|
||||
super.onDonePressed(currentFragment);
|
||||
|
||||
int cryptType = _authenticatedSlide.getCryptType();
|
||||
// wait for the key derivation background task
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
|
||||
(_passwordSlot == null || _passwordCipher == null)) {
|
||||
int cryptType = securitySetupSlide.getCryptType();
|
||||
VaultFileCredentials creds = securitySetupSlide.getCredentials();
|
||||
if (cryptType == CRYPT_TYPE_INVALID
|
||||
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|
||||
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|
||||
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
|
||||
Dialogs.showErrorDialog(this, R.string.intro_crypto_error, String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds), (dialog, which) -> finishIntro(CRYPT_TYPE_NONE, null));
|
||||
return;
|
||||
}
|
||||
|
||||
// generate the master key
|
||||
VaultFileCredentials creds = null;
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
creds = new VaultFileCredentials();
|
||||
}
|
||||
finishIntro(cryptType, creds);
|
||||
}
|
||||
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// encrypt the master key with a key derived from the user's password
|
||||
// and add it to the list of slots
|
||||
if (_passwordSlot == null || _passwordCipher == null) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
try {
|
||||
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
|
||||
creds.getSlots().add(_passwordSlot);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
|
||||
try {
|
||||
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
}
|
||||
|
||||
// finally, save the vault
|
||||
private void finishIntro(int cryptType, VaultFileCredentials creds) {
|
||||
Vault vault = new Vault();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
try {
|
||||
JSONObject obj = _vault.toJson();
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_vaultFile.setContent(obj);
|
||||
JSONObject obj = vault.toJson();
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
vaultFile.setContent(obj);
|
||||
} else {
|
||||
_vaultFile.setContent(obj, creds);
|
||||
vaultFile.setContent(obj, creds);
|
||||
}
|
||||
VaultManager.save(getApplicationContext(), _vaultFile);
|
||||
|
||||
VaultManager.save(getApplicationContext(), vaultFile);
|
||||
} catch (VaultManagerException | VaultFileException e) {
|
||||
setException(e);
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent result = new Intent();
|
||||
result.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, result);
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
_app.initVaultManager(vault, null);
|
||||
} else {
|
||||
_app.initVaultManager(vault, creds);
|
||||
}
|
||||
|
||||
// skip the intro from now on
|
||||
_prefs.setIntroDone(true);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SecretKey key) {
|
||||
if (key != null) {
|
||||
try {
|
||||
_passwordCipher = Slot.createEncryptCipher(key);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
} else {
|
||||
setException(new NullPointerException());
|
||||
}
|
||||
public void goToNextSlide() {
|
||||
super.goToNextSlide(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,14 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
@@ -31,7 +34,7 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
@@ -51,17 +54,17 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
private static final int CODE_SCAN = 0;
|
||||
private static final int CODE_ADD_ENTRY = 1;
|
||||
private static final int CODE_EDIT_ENTRY = 2;
|
||||
private static final int CODE_ENTER_ENTRY = 3;
|
||||
private static final int CODE_DO_INTRO = 4;
|
||||
private static final int CODE_DECRYPT = 5;
|
||||
private static final int CODE_PREFERENCES = 6;
|
||||
private static final int CODE_SCAN_IMAGE = 7;
|
||||
private static final int CODE_DO_INTRO = 3;
|
||||
private static final int CODE_DECRYPT = 4;
|
||||
private static final int CODE_PREFERENCES = 5;
|
||||
private static final int CODE_SCAN_IMAGE = 6;
|
||||
|
||||
// permission request codes
|
||||
private static final int CODE_PERM_CAMERA = 0;
|
||||
@@ -73,13 +76,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private String _selectedGroup;
|
||||
private boolean _searchSubmitted;
|
||||
|
||||
private VaultEntry _selectedEntry;
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private ActionMode _actionMode;
|
||||
|
||||
private Menu _menu;
|
||||
private SearchView _searchView;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
private EntryListView _entryListView;
|
||||
private LinearLayout _btnBackupError;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
@@ -99,6 +103,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
|
||||
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
|
||||
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
|
||||
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
|
||||
@@ -106,12 +111,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
|
||||
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
|
||||
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
|
||||
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
|
||||
});
|
||||
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
@@ -122,7 +128,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startScanActivity();
|
||||
});
|
||||
|
||||
_btnBackupError = findViewById(R.id.btn_backup_error);
|
||||
_btnBackupError.setOnClickListener(view -> {
|
||||
startPreferencesActivity("pref_backups");
|
||||
});
|
||||
|
||||
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,39 +170,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data == null) {
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN:
|
||||
onScanResult(resultCode, data);
|
||||
onScanResult(data);
|
||||
break;
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(resultCode, data);
|
||||
onAddEntryResult(data);
|
||||
break;
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ENTER_ENTRY:
|
||||
onEnterEntryResult(resultCode, data);
|
||||
onEditEntryResult(data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
onDoIntroResult();
|
||||
break;
|
||||
case CODE_DECRYPT:
|
||||
onDecryptResult(resultCode, data);
|
||||
onDecryptResult();
|
||||
break;
|
||||
case CODE_PREFERENCES:
|
||||
onPreferencesResult(resultCode, data);
|
||||
onPreferencesResult(data);
|
||||
break;
|
||||
case CODE_SCAN_IMAGE:
|
||||
onScanImageResult(resultCode, data);
|
||||
onScanImageResult(data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -213,103 +222,108 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult(int resultCode, Intent data) {
|
||||
private void onPreferencesResult(Intent data) {
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.refresh(true);
|
||||
if (_loaded) {
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
int codeGroupSize = getPreferences().getCodeGroupSize();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
|
||||
_entryListView.refresh(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
|
||||
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
|
||||
intent.putExtra("isNew", isNew);
|
||||
if (isNew) {
|
||||
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
|
||||
} else {
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
}
|
||||
intent.putExtra("selectedGroup", _selectedGroup);
|
||||
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
deleteEntry(entry);
|
||||
} else {
|
||||
// this profile has been serialized/deserialized and is no longer the same instance it once was
|
||||
// to deal with this, the replaceEntry functions are used
|
||||
VaultEntry oldEntry = _vault.replaceEntry(entry);
|
||||
_entryListView.replaceEntry(oldEntry, entry);
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnterEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(int resultCode, Intent intent) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
private void onScanResult(Intent data) {
|
||||
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vault.addEntry(entry);
|
||||
if (_loaded) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new MultiFormatReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
saveVault(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(Intent data) {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(Intent intent) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new MultiFormatReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,33 +369,26 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setGroupFilter(group, true);
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
_vault.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
private void onDoIntroResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void onDoIntroResult(int resultCode, Intent data) {
|
||||
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
|
||||
// TODO: user feedback
|
||||
Exception e = (Exception) data.getSerializableExtra("exception");
|
||||
throw new RuntimeException(e);
|
||||
private void checkTimeSyncSetting() {
|
||||
boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1;
|
||||
if (!autoTime && getPreferences().isTimeSyncWarningEnabled()) {
|
||||
Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> {
|
||||
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockVault(creds);
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
|
||||
boolean unlocked = unlockVault(creds);
|
||||
|
||||
// save the vault in case a slot was repaired
|
||||
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
}
|
||||
|
||||
doShortcutActions();
|
||||
private void onDecryptResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
@@ -394,10 +401,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void startScanImageActivity() {
|
||||
if (!PermissionHelper.request(this, CODE_PERM_READ_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
@@ -405,10 +408,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity(String preference) {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
intent.putExtra("pref", preference);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
}
|
||||
|
||||
private void doShortcutActions() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getStringExtra("action");
|
||||
if (action == null || _vault.isLocked()) {
|
||||
if (action == null || _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,7 +431,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void handleDeeplink() {
|
||||
if (_vault.isLocked()) {
|
||||
if (_app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,35 +445,48 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
try {
|
||||
info = GoogleAuthInfo.parseUri(uri);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_vault.isLocked()) {
|
||||
if (_vault == null) {
|
||||
// start the intro if the vault file doesn't exist
|
||||
if (!_vault.isLoaded() && !_vault.fileExists()) {
|
||||
// the vault doesn't exist, start the intro
|
||||
if (!VaultManager.fileExists(this)) {
|
||||
if (getPreferences().isIntroDone()) {
|
||||
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
return;
|
||||
} else {
|
||||
unlockVault(null);
|
||||
}
|
||||
|
||||
// read the vault from disk
|
||||
// if this fails, show the error to the user and close the app
|
||||
try {
|
||||
VaultFile vaultFile = _app.loadVaultFile();
|
||||
if (!vaultFile.isEncrypted()) {
|
||||
_vault = _app.initVaultManager(vaultFile, null);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog1, which) -> finish());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_app.isVaultLocked()) {
|
||||
startAuthActivity();
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
if (_menu != null) {
|
||||
@@ -475,11 +497,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.refresh(false);
|
||||
} else {
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
handleDeeplink();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
updateBackupErrorBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -502,11 +526,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void deleteEntry(VaultEntry entry) {
|
||||
VaultEntry oldEntry = _vault.removeEntry(entry);
|
||||
saveVault();
|
||||
private void deleteEntries(List<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
VaultEntry oldEntry = _vault.removeEntry(entry);
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -556,8 +582,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
startPreferencesActivity(null);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
@@ -614,81 +639,73 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private boolean unlockVault(VaultFileCredentials creds) {
|
||||
try {
|
||||
if (!_vault.isLoaded()) {
|
||||
_vault.load();
|
||||
}
|
||||
if (_vault.isLocked()) {
|
||||
if (creds == null) {
|
||||
startAuthActivity();
|
||||
return false;
|
||||
} else {
|
||||
_vault.unlock(creds);
|
||||
}
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
|
||||
startAuthActivity();
|
||||
return false;
|
||||
}
|
||||
|
||||
loadEntries();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
// load all entries
|
||||
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.getEntries());
|
||||
_entryListView.addEntries(entries);
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
if (!_loaded) {
|
||||
_entryListView.addEntries(_vault.getEntries());
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void startAuthActivity() {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("slots", _vault.getFileHeader().getSlots());
|
||||
intent.putExtra("requiresUnlock", false);
|
||||
intent.putExtra("cancelAction", CancelAction.KILL);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
}
|
||||
|
||||
private void saveVault() {
|
||||
try {
|
||||
_vault.save();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockIcon() {
|
||||
// hide the lock icon if the vault is not unlocked
|
||||
if (_menu != null && !_vault.isLocked()) {
|
||||
if (_menu != null && !_app.isVaultLocked()) {
|
||||
MenuItem item = _menu.findItem(R.id.action_lock);
|
||||
item.setVisible(_vault.isEncryptionEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupErrorBar() {
|
||||
String error = null;
|
||||
if (_app.getPreferences().isBackupsEnabled()) {
|
||||
error = _app.getPreferences().getBackupsError();
|
||||
}
|
||||
|
||||
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
if (_selectedEntry != null) {
|
||||
if (_selectedEntry == entry) {
|
||||
if (_actionMode != null) {
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
_actionMode.finish();
|
||||
} else {
|
||||
setIsMultipleSelected(_selectedEntries.size() > 1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
copyEntryCode(entry);
|
||||
@Override
|
||||
public void onSelect(VaultEntry entry) {
|
||||
_selectedEntries.add(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeselect(VaultEntry entry) {
|
||||
_selectedEntries.remove(entry);
|
||||
}
|
||||
|
||||
private void setIsMultipleSelected(boolean multipleSelected) {
|
||||
_entryListView.setIsLongPressDragEnabled(!multipleSelected);
|
||||
_actionMode.getMenu().findItem(R.id.action_edit).setVisible(!multipleSelected);
|
||||
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongEntryClick(VaultEntry entry) {
|
||||
if (_selectedEntry != null) {
|
||||
if (!_selectedEntries.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedEntry = entry;
|
||||
_selectedEntries.add(entry);
|
||||
_entryListView.setActionModeState(true, entry);
|
||||
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
|
||||
}
|
||||
@@ -700,12 +717,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
public void onEntryDrop(VaultEntry entry) {
|
||||
saveVault();
|
||||
saveVault(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(VaultEntry entry) {
|
||||
saveVault();
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
public void onEntryCopy(VaultEntry entry) {
|
||||
copyEntryCode(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -733,7 +754,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private class ActionModeCallbacks implements ActionMode.Callback {
|
||||
@@ -753,26 +773,43 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_copy:
|
||||
copyEntryCode(_selectedEntry);
|
||||
copyEntryCode(_selectedEntries.get(0));
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_edit:
|
||||
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
|
||||
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_share_qr:
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
authInfos.add(authInfo);
|
||||
}
|
||||
|
||||
intent.putExtra("authInfos", authInfos);
|
||||
startActivity(intent);
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntry(_selectedEntry);
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
|
||||
if (_selectedEntry.getGroup() != null) {
|
||||
if (!_vault.getGroups().contains(_selectedEntry.getGroup())) {
|
||||
updateGroupFilterMenu();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
if (entry.getGroup() != null) {
|
||||
if (!_vault.getGroups().contains(entry.getGroup())) {
|
||||
updateGroupFilterMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode.finish();
|
||||
});
|
||||
}, _selectedEntries.size());
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -782,7 +819,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
_entryListView.setActionModeState(false, null);
|
||||
_selectedEntry = null;
|
||||
_selectedEntries.clear();
|
||||
_actionMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity {
|
||||
private PreferencesFragment _fragment;
|
||||
@@ -12,9 +11,30 @@ public class PreferencesActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
|
||||
} else {
|
||||
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
Intent intent = getIntent();
|
||||
String preference = intent.getStringExtra("pref");
|
||||
if (preference != null) {
|
||||
_fragment.scrollToPreference(preference);
|
||||
intent.removeExtra("pref");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,19 +61,15 @@ public class PreferencesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme);
|
||||
break;
|
||||
|
||||
case DARK:
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
break;
|
||||
|
||||
case AMOLED:
|
||||
setTheme(R.style.AppTheme_TrueBlack_Preferences);
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -17,15 +16,28 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.importers.AegisImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
@@ -36,17 +48,6 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.importers.AegisImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
@@ -54,6 +55,7 @@ import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -70,12 +72,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private static final int CODE_SLOTS = 2;
|
||||
private static final int CODE_GROUPS = 3;
|
||||
private static final int CODE_SELECT_ENTRIES = 4;
|
||||
|
||||
// permission request codes
|
||||
private static final int CODE_PERM_IMPORT = 0;
|
||||
private static final int CODE_PERM_EXPORT = 1;
|
||||
private static final int CODE_EXPORT = 5;
|
||||
private static final int CODE_EXPORT_ENCRYPT = 6;
|
||||
private static final int CODE_BACKUPS = 7;
|
||||
|
||||
private Intent _result;
|
||||
private Preferences _prefs;
|
||||
private VaultManager _vault;
|
||||
|
||||
// keep a reference to the type of database converter the user selected
|
||||
@@ -89,30 +91,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private Preference _setPasswordPreference;
|
||||
private Preference _slotsPreference;
|
||||
private Preference _groupsPreference;
|
||||
private Preference _passwordReminderPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
private Preference _backupsVersionsPreference;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
AegisApplication app = (AegisApplication) getActivity().getApplication();
|
||||
_prefs = app.getPreferences();
|
||||
_vault = app.getVaultManager();
|
||||
|
||||
// set the result intent in advance
|
||||
setResult(new Intent());
|
||||
|
||||
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
|
||||
int currentTheme = _prefs.getCurrentTheme().ordinal();
|
||||
Preference darkModePreference = findPreference("pref_dark_mode");
|
||||
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
|
||||
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
|
||||
int currentTheme = _prefs.getCurrentTheme().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_theme)
|
||||
.setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
|
||||
_prefs.setCurrentTheme(Theme.fromInteger(i));
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
@@ -127,25 +135,30 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
|
||||
Preference langPreference = findPreference("pref_lang");
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
// Setting locale doesn't work on Marshmallow or below
|
||||
langPreference.setVisible(false);
|
||||
}
|
||||
|
||||
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
|
||||
int currentViewMode = _prefs.getCurrentViewMode().ordinal();
|
||||
Preference viewModePreference = findPreference("pref_view_mode");
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
|
||||
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
|
||||
int currentViewMode = _prefs.getCurrentViewMode().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_view_mode)
|
||||
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
|
||||
_prefs.setCurrentViewMode(ViewMode.fromInteger(i));
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
|
||||
_result.putExtra("needsRefresh", true);
|
||||
dialog.dismiss();
|
||||
@@ -161,7 +174,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
onImport();
|
||||
startImport();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -175,12 +188,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Preference exportPreference = findPreference("pref_export");
|
||||
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
onExport();
|
||||
startExport();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -195,6 +207,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());*/
|
||||
|
||||
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
|
||||
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference issuerPreference = findPreference("pref_account_name");
|
||||
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
@@ -210,6 +228,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
|
||||
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
|
||||
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
@@ -269,7 +293,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
try {
|
||||
_vault.disableEncryption();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -358,30 +383,61 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
_backupsPreference = findPreference("pref_backups");
|
||||
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if ((boolean) newValue) {
|
||||
selectBackupsLocation();
|
||||
} else {
|
||||
_prefs.setIsBackupsEnabled(false);
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Uri backupLocation = _prefs.getBackupsLocation();
|
||||
_backupsLocationPreference = findPreference("pref_backups_location");
|
||||
if (backupLocation != null) {
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
|
||||
}
|
||||
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
|
||||
selectBackupsLocation();
|
||||
return false;
|
||||
});
|
||||
|
||||
_backupsTriggerPreference = findPreference("pref_backups_trigger");
|
||||
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
|
||||
if (_prefs.isBackupsEnabled()) {
|
||||
try {
|
||||
_vault.backup();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
_backupsVersionsPreference = findPreference("pref_backups_versions");
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
|
||||
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
|
||||
number = number * 5 + 5;
|
||||
_prefs.setBackupsVersionCount(number);
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
_passwordReminderPreference = findPreference("pref_password_reminder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (!PermissionHelper.checkResults(grantResults)) {
|
||||
Toast.makeText(getActivity(), R.string.permission_denied, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_PERM_IMPORT:
|
||||
onImport();
|
||||
break;
|
||||
case CODE_PERM_EXPORT:
|
||||
onExport();
|
||||
break;
|
||||
}
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -406,6 +462,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
case CODE_SELECT_ENTRIES:
|
||||
onSelectEntriesResult(resultCode, data);
|
||||
break;
|
||||
case CODE_EXPORT:
|
||||
// intentional fallthrough
|
||||
case CODE_EXPORT_ENCRYPT:
|
||||
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
|
||||
break;
|
||||
case CODE_BACKUPS:
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,11 +482,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
getActivity().setResult(Activity.RESULT_OK, _result);
|
||||
}
|
||||
|
||||
private void onImport() {
|
||||
if (!PermissionHelper.request(getActivity(), CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private void startImport() {
|
||||
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
|
||||
String[] names = importers.keySet().toArray(new String[0]);
|
||||
|
||||
@@ -502,7 +562,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -511,8 +572,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
|
||||
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +588,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
|
||||
} catch (VaultFileException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,7 +610,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,8 +620,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
result = state.convert();
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,28 +636,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
startActivityForResult(intent, CODE_SELECT_ENTRIES);
|
||||
}
|
||||
|
||||
private void onExport() {
|
||||
if (!PermissionHelper.request(getActivity(), CODE_PERM_EXPORT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
// TODO: create a custom layout to show a message AND a checkbox
|
||||
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pref_export_summary)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
String filename;
|
||||
try {
|
||||
filename = _vault.export(checked.get());
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
String filename = checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN;
|
||||
filename = new VaultBackupManager.FileInfo(filename).toString();
|
||||
|
||||
// make sure the new file is visible
|
||||
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/json")
|
||||
.putExtra(Intent.EXTRA_TITLE, filename);
|
||||
|
||||
Toast.makeText(getActivity(), getString(R.string.export_vault_location) + filename, Toast.LENGTH_SHORT).show();
|
||||
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
if (_vault.isEncryptionEnabled()) {
|
||||
@@ -669,11 +721,45 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
}
|
||||
|
||||
private void onExportResult(int resultCode, Intent data, boolean encrypt) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
|
||||
_vault.export(stream, encrypt);
|
||||
} catch (IOException | VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
|
||||
|
||||
_prefs.setBackupsLocation(uri);
|
||||
_prefs.setIsBackupsEnabled(true);
|
||||
_prefs.setBackupsError(null);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
private boolean saveVault() {
|
||||
try {
|
||||
_vault.save();
|
||||
_vault.save(true);
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -698,12 +784,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_biometricsPreference.setEnabled(canUseBio && !multiBio);
|
||||
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
|
||||
_slotsPreference.setVisible(showSlots);
|
||||
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
|
||||
} else {
|
||||
_setPasswordPreference.setEnabled(false);
|
||||
_biometricsPreference.setEnabled(false);
|
||||
_biometricsPreference.setChecked(false, true);
|
||||
_slotsPreference.setVisible(false);
|
||||
_passwordReminderPreference.setVisible(false);
|
||||
}
|
||||
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
private void updateBackupPreference() {
|
||||
boolean encrypted = _vault.isEncryptionEnabled();
|
||||
boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted;
|
||||
_backupsPreference.setChecked(backupEnabled);
|
||||
_backupsPreference.setEnabled(encrypted);
|
||||
_backupsLocationPreference.setVisible(backupEnabled);
|
||||
_backupsTriggerPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled);
|
||||
}
|
||||
|
||||
private void selectBackupsLocation() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
startActivityForResult(intent, CODE_BACKUPS);
|
||||
}
|
||||
|
||||
private class SetPasswordListener implements Dialogs.SlotListener {
|
||||
@@ -733,8 +843,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
e.printStackTrace();
|
||||
updateEncryptionPreferences();
|
||||
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +856,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
@@ -758,7 +870,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,7 +895,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -10,14 +11,16 @@ import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SquareFinderView;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
@@ -30,10 +33,15 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
private Menu _menu;
|
||||
private int _facing = CAMERA_FACING_BACK;
|
||||
|
||||
private int _batchId = 0;
|
||||
private int _batchIndex = -1;
|
||||
private List<VaultEntry> _entries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_scannerView = new ZXingScannerView(this) {
|
||||
@Override
|
||||
protected IViewFinder createViewFinderView(Context context) {
|
||||
@@ -107,19 +115,61 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
try {
|
||||
// parse google auth uri
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
Uri uri = Uri.parse(rawResult.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
List<VaultEntry> entries = new ArrayList<>();
|
||||
entries.add(new VaultEntry(info));
|
||||
finish(entries);
|
||||
}
|
||||
|
||||
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
|
||||
|
||||
if (_batchId == 0) {
|
||||
_batchId = export.getBatchId();
|
||||
}
|
||||
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
int batchIndex = export.getBatchIndex();
|
||||
if (_batchId != export.getBatchId()) {
|
||||
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
|
||||
for (GoogleAuthInfo info : export.getEntries()) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
_batchIndex = batchIndex;
|
||||
if (_batchIndex + 1 == export.getBatchSize()) {
|
||||
finish(_entries);
|
||||
}
|
||||
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex != batchIndex) {
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void finish(List<VaultEntry> entries) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entries", (ArrayList<VaultEntry>) entries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void updateCameraIcon() {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<GoogleAuthInfo> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
private TextView _entriesCount;
|
||||
private Button _nextButton;
|
||||
private Button _previousButton;
|
||||
|
||||
private VaultManager _vault;
|
||||
private int _currentEntryCount = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
_vault = getApp().getVaultManager();
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_issuer = findViewById(R.id.tvIssuer);
|
||||
_accountName = findViewById(R.id.tvAccountName);
|
||||
_entriesCount = findViewById(R.id.tvEntriesCount);
|
||||
_nextButton = findViewById(R.id.btnNext);
|
||||
_previousButton = findViewById(R.id.btnPrevious);
|
||||
|
||||
if (getSupportActionBar() != null){
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
|
||||
|
||||
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
|
||||
_entriesCount.setVisibility(controlVisibility);
|
||||
_nextButton.setVisibility(controlVisibility);
|
||||
|
||||
_nextButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount < _authInfos.size()) {
|
||||
_previousButton.setVisibility(View.VISIBLE);
|
||||
_currentEntryCount++;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == _authInfos.size()) {
|
||||
_nextButton.setText(R.string.done);
|
||||
}
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
_previousButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount > 1 ) {
|
||||
_nextButton.setText(R.string.next);
|
||||
_currentEntryCount--;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == 1) {
|
||||
_previousButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
generateQR();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void generateQR() {
|
||||
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||
_issuer.setText(selectedEntry.getIssuer());
|
||||
_accountName.setText(selectedEntry.getAccountName());
|
||||
_entriesCount.setText(String.format(getString(R.string.entries_count), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
QRCodeWriter writer = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = null;
|
||||
try {
|
||||
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
|
||||
} catch (WriterException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = bitMatrix.getWidth();
|
||||
int height = bitMatrix.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
_qrImage.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -13,10 +13,10 @@ import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy;
|
||||
import com.github.appintro.SlidePolicy;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
|
||||
public class SecurityPickerSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
|
||||
public static final int CRYPT_TYPE_INVALID = 0;
|
||||
public static final int CRYPT_TYPE_NONE = 1;
|
||||
public static final int CRYPT_TYPE_PASS = 2;
|
||||
@@ -27,7 +27,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_authentication_slide, container, false);
|
||||
final View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
|
||||
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
|
||||
_buttonGroup.setOnCheckedChangeListener(this);
|
||||
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
|
||||
@@ -38,6 +38,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
|
||||
TextView text = view.findViewById(R.id.text_rb_biometrics);
|
||||
button.setEnabled(true);
|
||||
text.setEnabled(true);
|
||||
_buttonGroup.check(R.id.rb_biometrics);
|
||||
}
|
||||
|
||||
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
|
||||
@@ -77,7 +78,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
|
||||
id = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
|
||||
}
|
||||
|
||||
Intent intent = getActivity().getIntent();
|
||||
+69
-39
@@ -8,36 +8,42 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy;
|
||||
import com.github.paolorotolo.appintro.ISlideSelectionListener;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.github.appintro.SlidePolicy;
|
||||
import com.github.appintro.SlideSelectionListener;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
|
||||
private int _cryptType;
|
||||
public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
|
||||
private int _bgColor;
|
||||
private EditText _textPassword;
|
||||
private EditText _textPasswordConfirm;
|
||||
private CheckBox _checkPasswordVisibility;
|
||||
private int _bgColor;
|
||||
|
||||
private BiometricSlot _bioSlot;
|
||||
private Cipher _bioCipher;
|
||||
private int _cryptType;
|
||||
private VaultFileCredentials _creds;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
|
||||
final View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
|
||||
_textPassword = view.findViewById(R.id.text_password);
|
||||
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
@@ -45,11 +51,12 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
_textPassword.setTransformationMethod(null);
|
||||
_textPasswordConfirm.setTransformationMethod(null);
|
||||
_textPassword.clearFocus();
|
||||
_textPasswordConfirm.setEnabled(false);
|
||||
_textPasswordConfirm.clearFocus();
|
||||
} else {
|
||||
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
|
||||
_textPasswordConfirm.setEnabled(true);
|
||||
_textPasswordConfirm.setTransformationMethod(new PasswordTransformationMethod());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,23 +68,15 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
return _cryptType;
|
||||
}
|
||||
|
||||
public BiometricSlot getBiometricSlot() {
|
||||
return _bioSlot;
|
||||
}
|
||||
|
||||
public Cipher getBiometriCipher() {
|
||||
return _bioCipher;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return EditTextHelper.getEditTextChars(_textPassword);
|
||||
public VaultFileCredentials getCredentials() {
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setBgColor(int color) {
|
||||
_bgColor = color;
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
private void showBiometricPrompt() {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.set_up_biometric))
|
||||
@@ -86,13 +85,18 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
initializer.authenticate(info);
|
||||
}
|
||||
|
||||
private void deriveKey() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, EditTextHelper.getEditTextChars(_textPassword));
|
||||
new KeyDerivationTask(getContext(), new PasswordDerivationListener()).execute(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideSelected() {
|
||||
Intent intent = getActivity().getIntent();
|
||||
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
showBiometricPrompt();
|
||||
_cryptType = intent.getIntExtra("cryptType", SecurityPickerSlide.CRYPT_TYPE_INVALID);
|
||||
if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_NONE) {
|
||||
_creds = new VaultFileCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,16 +108,16 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
@Override
|
||||
public boolean isPolicyRespected() {
|
||||
switch (_cryptType) {
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
|
||||
case SecurityPickerSlide.CRYPT_TYPE_NONE:
|
||||
return true;
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
|
||||
if (_bioSlot == null) {
|
||||
case SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC:
|
||||
if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
return false;
|
||||
}
|
||||
// intentional fallthrough
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
|
||||
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
|
||||
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
|
||||
case SecurityPickerSlide.CRYPT_TYPE_PASS:
|
||||
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
return _creds.getSlots().has(PasswordSlot.class);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -125,7 +129,7 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
@Override
|
||||
public void onUserIllegallyRequestedNextPage() {
|
||||
String message;
|
||||
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
|
||||
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
message = getString(R.string.password_equality_error);
|
||||
|
||||
View view = getView();
|
||||
@@ -133,23 +137,49 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy,
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
} else if (_bioSlot == null) {
|
||||
} else if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
deriveKey();
|
||||
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
|
||||
try {
|
||||
Cipher cipher = Slot.createEncryptCipher(key);
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
_creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.enable_encryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
((IntroActivity) getActivity()).goToNextSlide();
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
@Override
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
_bioSlot = slot;
|
||||
_bioCipher = cipher;
|
||||
try {
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
_creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
deriveKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-8
@@ -9,16 +9,16 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
|
||||
public class KeyDerivationTask extends ProgressDialogTask<KeyDerivationTask.Params, KeyDerivationTask.Result> {
|
||||
private Callback _cb;
|
||||
|
||||
public DerivationTask(Context context, Callback cb) {
|
||||
public KeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.encrypting_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(DerivationTask.Params... args) {
|
||||
protected Result doInBackground(KeyDerivationTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
@@ -29,13 +29,16 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
||||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
return params.getSlot().deriveKey(params.getPassword(), scryptParams);
|
||||
|
||||
PasswordSlot slot = params.getSlot();
|
||||
SecretKey key = slot.deriveKey(params.getPassword(), scryptParams);
|
||||
return new Result(slot, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
protected void onPostExecute(Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result.getSlot(), result.getKey());
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
@@ -56,7 +59,25 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private PasswordSlot _slot;
|
||||
private SecretKey _key;
|
||||
|
||||
public Result(PasswordSlot slot, SecretKey key) {
|
||||
_slot = slot;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public PasswordSlot getSlot() {
|
||||
return _slot;
|
||||
}
|
||||
|
||||
public SecretKey getKey() {
|
||||
return _key;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
void onTaskFinished(PasswordSlot slot, SecretKey key);
|
||||
}
|
||||
}
|
||||
+17
-32
@@ -5,41 +5,32 @@ import android.content.Context;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, SlotListTask.Result> {
|
||||
public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecryptTask.Params, PasswordSlotDecryptTask.Result> {
|
||||
private Callback _cb;
|
||||
private Class<T> _type;
|
||||
|
||||
public SlotListTask(Class<T> type, Context context, Callback cb) {
|
||||
public PasswordSlotDecryptTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(SlotListTask.Params... args) {
|
||||
protected Result doInBackground(PasswordSlotDecryptTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
SlotList slots = params.getSlots();
|
||||
|
||||
for (Slot slot : slots.findAll(_type)) {
|
||||
for (PasswordSlot slot : params.getSlots()) {
|
||||
try {
|
||||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[]) params.getObj();
|
||||
return decryptPasswordSlot((PasswordSlot) slot, password);
|
||||
} else if (slot instanceof BiometricSlot) {
|
||||
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
|
||||
}
|
||||
return decryptPasswordSlot(slot, params.getPassword());
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SlotIntegrityException ignored) {
|
||||
@@ -50,12 +41,6 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
return null;
|
||||
}
|
||||
|
||||
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
MasterKey key = slot.getKey(cipher);
|
||||
return new Result(key, slot);
|
||||
}
|
||||
|
||||
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
|
||||
throws SlotIntegrityException, SlotException {
|
||||
MasterKey masterKey;
|
||||
@@ -103,35 +88,35 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private SlotList _slots;
|
||||
private Object _obj;
|
||||
private List<PasswordSlot> _slots;
|
||||
private char[] _password;
|
||||
|
||||
public Params(SlotList slots, Object obj) {
|
||||
public Params(List<PasswordSlot> slots, char[] password) {
|
||||
_slots = slots;
|
||||
_obj = obj;
|
||||
_password = password;
|
||||
}
|
||||
|
||||
public SlotList getSlots() {
|
||||
public List<PasswordSlot> getSlots() {
|
||||
return _slots;
|
||||
}
|
||||
|
||||
public Object getObj() {
|
||||
return _obj;
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private MasterKey _key;
|
||||
private Slot _slot;
|
||||
private PasswordSlot _slot;
|
||||
private boolean _repaired;
|
||||
|
||||
public Result(MasterKey key, Slot slot, boolean repaired) {
|
||||
public Result(MasterKey key, PasswordSlot slot, boolean repaired) {
|
||||
_key = key;
|
||||
_slot = slot;
|
||||
_repaired = repaired;
|
||||
}
|
||||
|
||||
public Result(MasterKey key, Slot slot) {
|
||||
public Result(MasterKey key, PasswordSlot slot) {
|
||||
this(key, slot, false);
|
||||
}
|
||||
|
||||
@@ -10,34 +10,41 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
|
||||
private EntryListView _view;
|
||||
private List<VaultEntry> _entries;
|
||||
private List<VaultEntry> _shownEntries;
|
||||
private VaultEntry _selectedEntry;
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private VaultEntry _focusedEntry;
|
||||
private int _codeGroupSize;
|
||||
private boolean _showAccountName;
|
||||
private boolean _searchAccountName;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private boolean _copyOnTap;
|
||||
private String _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
private String _searchFilter;
|
||||
private boolean _isPeriodUniform = true;
|
||||
private int _uniformPeriod = -1;
|
||||
private Handler _dimHandler;
|
||||
|
||||
// keeps track of the viewholders that are currently bound
|
||||
@@ -46,6 +53,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
public EntryAdapter(EntryListView view) {
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_selectedEntries = new ArrayList<>();
|
||||
_holders = new ArrayList<>();
|
||||
_dimHandler = new Handler();
|
||||
_view = view;
|
||||
@@ -58,6 +66,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_view = null;
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(int codeGroupeSize) {
|
||||
_codeGroupSize = codeGroupeSize;
|
||||
}
|
||||
|
||||
public void setShowAccountName(boolean showAccountName) {
|
||||
_showAccountName = showAccountName;
|
||||
}
|
||||
@@ -78,6 +90,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_highlightEntry = highlightEntry;
|
||||
}
|
||||
|
||||
public void setIsCopyOnTapEnabled(boolean enabled) {
|
||||
_copyOnTap = enabled;
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAt(int position) {
|
||||
return _shownEntries.get(position);
|
||||
}
|
||||
@@ -117,7 +133,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity(true);
|
||||
@@ -135,6 +151,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
VaultEntry entry = getEntryByUUID(uuid);
|
||||
removeEntry(entry);
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_entries.clear();
|
||||
_shownEntries.clear();
|
||||
@@ -142,7 +163,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
VaultEntry oldEntry = getEntryByUUID(uuid);
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
|
||||
if (_shownEntries.contains(oldEntry)) {
|
||||
@@ -165,6 +187,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
private VaultEntry getEntryByUUID(UUID uuid) {
|
||||
for (VaultEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isEntryFiltered(VaultEntry entry) {
|
||||
String group = entry.getGroup();
|
||||
String issuer = entry.getIssuer().toLowerCase();
|
||||
@@ -192,7 +224,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.refreshCode();
|
||||
holder.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,12 +334,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
@Override
|
||||
public void onBindViewHolder(final EntryHolder holder, int position) {
|
||||
VaultEntry entry = _shownEntries.get(position);
|
||||
holder.setFocused(entry == _selectedEntry);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
|
||||
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
|
||||
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
|
||||
holder.setData(entry, _codeGroupSize, _showAccountName, showProgress, hidden, dimmed);
|
||||
holder.setFocused(_selectedEntries.contains(entry));
|
||||
holder.loadIcon(_view);
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@@ -315,7 +347,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
public void onClick(View v) {
|
||||
boolean handled = false;
|
||||
|
||||
if (_selectedEntry == null) {
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
if (_copyOnTap) {
|
||||
_view.onEntryCopy(entry);
|
||||
holder.animateCopyText();
|
||||
}
|
||||
|
||||
if (_highlightEntry || _tapToReveal) {
|
||||
if (_focusedEntry == entry) {
|
||||
resetFocus();
|
||||
@@ -324,6 +361,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
focusEntry(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_selectedEntries.contains(entry)) {
|
||||
_view.onDeselect(entry);
|
||||
removeSelectedEntry(entry);
|
||||
holder.setFocusedAndAnimate(false);
|
||||
} else {
|
||||
holder.setFocusedAndAnimate(true);
|
||||
addSelectedEntry(entry);
|
||||
_view.onSelect(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
@@ -335,9 +382,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
if (_selectedEntry == null) {
|
||||
setSelectedEntry(_shownEntries.get(position));
|
||||
holder.setFocused(true);
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
holder.setFocusedAndAnimate(true);
|
||||
}
|
||||
|
||||
return _view.onLongEntryClick(_shownEntries.get(position));
|
||||
@@ -370,20 +416,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity(boolean force) {
|
||||
int mostFrequentPeriod = getMostFrequentPeriod();
|
||||
boolean uniform = isPeriodUniform();
|
||||
if (!force && uniform == _isPeriodUniform) {
|
||||
|
||||
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isPeriodUniform = uniform;
|
||||
_uniformPeriod = mostFrequentPeriod;
|
||||
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.setShowProgress(!_isPeriodUniform);
|
||||
if ((holder.getEntry().getInfo() instanceof TotpInfo)) {
|
||||
holder.setShowProgress(((TotpInfo) holder.getEntry().getInfo()).getPeriod() != mostFrequentPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
_view.onPeriodUniformityChanged(_isPeriodUniform);
|
||||
_view.onPeriodUniformityChanged(_isPeriodUniform, _uniformPeriod);
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
public int getMostFrequentPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (VaultEntry entry : _shownEntries) {
|
||||
OtpInfo info = entry.getInfo();
|
||||
@@ -396,14 +448,26 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return -1;
|
||||
}
|
||||
|
||||
int period = infos.get(0).getPeriod();
|
||||
Map<Integer, Integer> occurrences = new HashMap<>();
|
||||
for (TotpInfo info : infos) {
|
||||
if (period != info.getPeriod()) {
|
||||
return -1;
|
||||
int period = info.getPeriod();
|
||||
if(occurrences.containsKey(period)) {
|
||||
occurrences.put(period, occurrences.get(period) + 1);
|
||||
} else {
|
||||
occurrences.put(period, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return period;
|
||||
Integer maxValue = 0;
|
||||
Integer maxKey = 0;
|
||||
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()){
|
||||
if(entry.getValue() > maxValue){
|
||||
maxValue = entry.getValue();
|
||||
maxKey = entry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
return maxValue > 1 ? maxKey : -1;
|
||||
}
|
||||
|
||||
private void focusEntry(VaultEntry entry) {
|
||||
@@ -444,14 +508,29 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_focusedEntry = null;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
if (entry == null) {
|
||||
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
|
||||
} else if (_highlightEntry) {
|
||||
public void removeSelectedEntry(VaultEntry entry) {
|
||||
_selectedEntries.remove(entry);
|
||||
}
|
||||
|
||||
public void addSelectedEntry(VaultEntry entry) {
|
||||
if (_highlightEntry) {
|
||||
resetFocus();
|
||||
}
|
||||
|
||||
_selectedEntry = entry;
|
||||
_selectedEntries.add(entry);
|
||||
}
|
||||
|
||||
public void deselectAllEntries() {
|
||||
for (VaultEntry entry: _selectedEntries) {
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
holder.setFocusedAndAnimate(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectedEntries.clear();
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
@@ -459,7 +538,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
return isPeriodUniform(getMostFrequentPeriod());
|
||||
}
|
||||
|
||||
private static boolean isPeriodUniform(int period) {
|
||||
return period != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -473,6 +556,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onPeriodUniformityChanged(boolean uniform);
|
||||
void onEntryCopy(VaultEntry entry);
|
||||
void onPeriodUniformityChanged(boolean uniform, int period);
|
||||
void onSelect(VaultEntry entry);
|
||||
void onDeselect(VaultEntry entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -10,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
@@ -18,6 +21,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
@@ -28,16 +32,27 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _profileIssuer;
|
||||
private TextView _profileCopied;
|
||||
private ImageView _profileDrawable;
|
||||
private VaultEntry _entry;
|
||||
private ImageView _buttonRefresh;
|
||||
private RelativeLayout _description;
|
||||
|
||||
private final ImageView _selected;
|
||||
private final Handler _selectedHandler;
|
||||
|
||||
private int _codeGroupSize = 6;
|
||||
|
||||
private boolean _hidden;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
private TotpProgressBar _progressBar;
|
||||
private View _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
private Handler _animationHandler;
|
||||
|
||||
private Animation _scaleIn;
|
||||
private Animation _scaleOut;
|
||||
|
||||
public EntryHolder(final View view) {
|
||||
super(view);
|
||||
@@ -47,46 +62,64 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_profileName = view.findViewById(R.id.profile_account_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
_profileIssuer = view.findViewById(R.id.profile_issuer);
|
||||
_profileCopied = view.findViewById(R.id.profile_copied);
|
||||
_description = view.findViewById(R.id.description);
|
||||
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
|
||||
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
|
||||
_selected = view.findViewById(R.id.ivSelected);
|
||||
_selectedHandler = new Handler();
|
||||
_animationHandler = new Handler();
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
|
||||
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
|
||||
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
|
||||
|
||||
_scaleIn = AnimationUtils.loadAnimation(view.getContext(), R.anim.item_scale_in);
|
||||
_scaleOut = AnimationUtils.loadAnimation(view.getContext(), R.anim.item_scale_out);
|
||||
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (_hidden) {
|
||||
if (!_hidden) {
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
_progressBar.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
|
||||
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setData(VaultEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
|
||||
public void setData(VaultEntry entry, int codeGroupSize, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
|
||||
_entry = entry;
|
||||
_hidden = hidden;
|
||||
|
||||
if (codeGroupSize <= 0)
|
||||
throw new IllegalArgumentException("Code group size cannot be zero or negative");
|
||||
|
||||
_codeGroupSize = codeGroupSize;
|
||||
|
||||
_selected.clearAnimation();
|
||||
_selected.setVisibility(View.GONE);
|
||||
_selectedHandler.removeCallbacksAndMessages(null);
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
|
||||
setShowProgress(showProgress);
|
||||
|
||||
// only show the button if this entry is of type HotpInfo
|
||||
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
|
||||
|
||||
_profileIssuer.setText(entry.getIssuer());
|
||||
_profileName.setText("");
|
||||
if (showAccountName) {
|
||||
_profileName.setText(" - " + entry.getName());
|
||||
String profileIssuer = entry.getIssuer();
|
||||
String profileName = showAccountName ? entry.getName() : "";
|
||||
if (!profileIssuer.isEmpty() && !profileName.isEmpty()) {
|
||||
profileName = String.format(" (%s)", profileName);
|
||||
}
|
||||
_profileIssuer.setText(profileIssuer);
|
||||
_profileName.setText(profileName);
|
||||
|
||||
if (_hidden) {
|
||||
hideCode();
|
||||
@@ -139,6 +172,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public void setFocused(boolean focused) {
|
||||
if (focused) {
|
||||
_selected.setVisibility(View.VISIBLE);
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
|
||||
} else {
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
|
||||
@@ -146,16 +180,34 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_view.setSelected(focused);
|
||||
}
|
||||
|
||||
public void setFocusedAndAnimate(boolean focused) {
|
||||
setFocused(focused);
|
||||
|
||||
if (focused) {
|
||||
_selected.startAnimation(_scaleIn);
|
||||
} else {
|
||||
_selected.startAnimation(_scaleOut);
|
||||
_selectedHandler.postDelayed(() -> _selected.setVisibility(View.GONE), 150);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
_refresher.destroy();
|
||||
}
|
||||
|
||||
public void startRefreshLoop() {
|
||||
_refresher.start();
|
||||
_progressBar.start();
|
||||
}
|
||||
|
||||
public void stopRefreshLoop() {
|
||||
_refresher.stop();
|
||||
_progressBar.stop();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
_progressBar.restart();
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
public void refreshCode() {
|
||||
@@ -171,7 +223,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
if (!(info instanceof SteamInfo)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < otp.length(); i++) {
|
||||
if (i != 0 && i % 3 == 0) {
|
||||
if (i != 0 && i % _codeGroupSize == 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(otp.charAt(i));
|
||||
@@ -200,6 +252,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
animateAlphaTo(DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
public void animateCopyText() {
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
|
||||
Animation slideDownFadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
|
||||
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
|
||||
Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in);
|
||||
|
||||
_profileCopied.startAnimation(slideDownFadeIn);
|
||||
_description.startAnimation(slideDownFadeOut);
|
||||
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
_description.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
private void animateAlphaTo(float alpha) {
|
||||
itemView.animate().alpha(alpha).setDuration(200).start();
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
@@ -33,8 +33,10 @@ import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
@@ -44,7 +46,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _dividerDecoration;
|
||||
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
||||
private PeriodProgressBar _progressBar;
|
||||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private LinearLayout _emptyStateView;
|
||||
@@ -104,7 +106,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,23 +126,36 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setGroupFilter(String group, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(group == null);
|
||||
_adapter.setGroupFilter(group, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
|
||||
}
|
||||
|
||||
public void setIsCopyOnTapEnabled(boolean enabled) {
|
||||
_adapter.setIsCopyOnTapEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setActionModeState(boolean enabled, VaultEntry entry) {
|
||||
_touchCallback.setSelectedEntry(entry);
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
|
||||
_adapter.setSelectedEntry(entry);
|
||||
|
||||
if (enabled) {
|
||||
_adapter.addSelectedEntry(entry);
|
||||
} else {
|
||||
_adapter.deselectAllEntries();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
|
||||
_adapter.setSortCategory(sortCategory, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
@@ -148,8 +163,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_touchCallback.setIsLongPressDragEnabled(search == null);
|
||||
_adapter.setSearchFilter(search);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
}
|
||||
|
||||
public void setViewMode(ViewMode mode) {
|
||||
@@ -160,8 +175,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
if (_showProgress) {
|
||||
_progressBar.refresh();
|
||||
_progressBar.restart();
|
||||
}
|
||||
|
||||
_adapter.refresh(hard);
|
||||
}
|
||||
|
||||
@@ -195,18 +211,35 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeriodUniformityChanged(boolean isUniform) {
|
||||
public void onEntryCopy(VaultEntry entry) {
|
||||
_listener.onEntryCopy(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelect(VaultEntry entry) { _listener.onSelect(entry); }
|
||||
|
||||
@Override
|
||||
public void onDeselect(VaultEntry entry) { _listener.onDeselect(entry); }
|
||||
|
||||
@Override
|
||||
public void onPeriodUniformityChanged(boolean isUniform, int period) {
|
||||
setShowProgress(isUniform);
|
||||
if (_showProgress) {
|
||||
_progressBar.setVisibility(View.VISIBLE);
|
||||
_progressBar.setPeriod(_adapter.getUniformPeriod());
|
||||
_progressBar.setPeriod(period);
|
||||
_progressBar.start();
|
||||
_refresher.start();
|
||||
} else {
|
||||
_progressBar.setVisibility(View.GONE);
|
||||
_progressBar.stop();
|
||||
_refresher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(int codeGrouping) {
|
||||
_adapter.setCodeGroupSize(codeGrouping);
|
||||
}
|
||||
|
||||
public void setShowAccountName(boolean showAccountName) {
|
||||
_adapter.setShowAccountName(showAccountName);
|
||||
}
|
||||
@@ -232,7 +265,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
}
|
||||
@@ -242,12 +275,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
_adapter.removeEntry(uuid);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
}
|
||||
|
||||
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
|
||||
_adapter.replaceEntry(oldEntry, newEntry);
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
_adapter.replaceEntry(uuid, newEntry);
|
||||
}
|
||||
|
||||
public void runEntriesAnimation() {
|
||||
@@ -296,8 +334,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onEntryCopy(VaultEntry entry);
|
||||
void onLongEntryClick(VaultEntry entry);
|
||||
void onScroll(int dx, int dy);
|
||||
void onSelect(VaultEntry entry);
|
||||
void onDeselect(VaultEntry entry);
|
||||
}
|
||||
|
||||
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class PeriodProgressBar extends ProgressBar {
|
||||
private int _period;
|
||||
|
||||
public PeriodProgressBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
// reset the progress bar
|
||||
int maxProgress = getMax();
|
||||
setProgress(maxProgress);
|
||||
|
||||
// calculate the progress the bar should start at
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
long period = _period * maxProgress;
|
||||
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
|
||||
|
||||
// start progress animation
|
||||
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, 0);
|
||||
animation.setDuration(millisTillRotation);
|
||||
animation.setInterpolator(new LinearInterpolator());
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class SlotHolder extends RecyclerView.ViewHolder {
|
||||
_slotName.setText(R.string.authentication_method_raw);
|
||||
_slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported Slot type: %s", slot.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
public class TotpProgressBar extends ProgressBar {
|
||||
private int _period = 30;
|
||||
private Handler _handler;
|
||||
private float _animDurationScale;
|
||||
|
||||
public TotpProgressBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TotpProgressBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
stop();
|
||||
_handler = new Handler();
|
||||
_animDurationScale = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (_handler != null) {
|
||||
_handler.removeCallbacksAndMessages(null);
|
||||
_handler = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
stop();
|
||||
start();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
// calculate the current progress the bar should start at
|
||||
int maxProgress = getMax();
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
int currentProgress = (int) (maxProgress * ((float) millisTillRotation / (_period * 1000)));
|
||||
|
||||
// start progress animation, compensating for any changes to the animator duration scale settings
|
||||
int animPart = maxProgress / _period;
|
||||
int animEnd = (currentProgress / animPart) * animPart;
|
||||
int animPartDuration = (int) (1000 / _animDurationScale);
|
||||
float animDurationFraction = (float) (currentProgress - animEnd) / animPart;
|
||||
int realAnimDuration = (int) (1000 * animDurationFraction);
|
||||
int animDuration = (int) (animPartDuration * animDurationFraction);
|
||||
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, animEnd);
|
||||
animation.setDuration(animDuration);
|
||||
animation.setInterpolator(new LinearInterpolator());
|
||||
animation.start();
|
||||
|
||||
// the animation only lasts for (less than) one second, so restart it after that
|
||||
_handler.postDelayed(this::refresh, realAnimDuration);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ByteInputStream extends ByteArrayInputStream {
|
||||
private ByteInputStream(byte[] buf) {
|
||||
super(buf);
|
||||
}
|
||||
|
||||
public static ByteInputStream create(InputStream fileStream) throws IOException {
|
||||
int read;
|
||||
byte[] buf = new byte[4096];
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
|
||||
outStream.write(buf, 0, read);
|
||||
}
|
||||
|
||||
return new ByteInputStream(outStream.toByteArray());
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return this.buf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class IOUtils {
|
||||
private IOUtils() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] readFile(FileInputStream inStream) throws IOException {
|
||||
try (DataInputStream outStream = new DataInputStream(inStream)) {
|
||||
byte[] fileBytes = new byte[(int) inStream.getChannel().size()];
|
||||
outStream.readFully(fileBytes);
|
||||
return fileBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readAll(InputStream inStream) throws IOException {
|
||||
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
|
||||
copy(inStream, outStream);
|
||||
return outStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static void copy(InputStream inStream, OutputStream outStream) throws IOException {
|
||||
int read;
|
||||
byte[] buf = new byte[4096];
|
||||
while ((read = inStream.read(buf, 0, buf.length)) != -1) {
|
||||
outStream.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
@@ -43,7 +43,7 @@ public class Vault {
|
||||
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (Base64Exception | OtpInfoException | JSONException e) {
|
||||
} catch (EncodingException | OtpInfoException | JSONException e) {
|
||||
throw new VaultException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.UriPermission;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class VaultBackupManager {
|
||||
private static final String TAG = VaultBackupManager.class.getSimpleName();
|
||||
|
||||
private static final StrictDateFormat _dateFormat =
|
||||
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
|
||||
|
||||
public static final String FILENAME_PREFIX = "aegis-backup";
|
||||
|
||||
private Context _context;
|
||||
|
||||
public VaultBackupManager(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
|
||||
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
|
||||
|
||||
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
|
||||
|
||||
if (!hasPermissionsAt(dirUri)) {
|
||||
Log.e(TAG, "Unable to create file for backup, no persisted URI permissions");
|
||||
throw new VaultManagerException("No persisted URI permissions");
|
||||
}
|
||||
|
||||
// If we create a file with a name that already exists, SAF will append a number
|
||||
// to the filename and write to that instead. We can't overwrite existing files, so
|
||||
// just avoid that altogether by checking beforehand.
|
||||
if (dir.findFile(fileInfo.toString()) != null) {
|
||||
throw new VaultManagerException("Backup file already exists");
|
||||
}
|
||||
|
||||
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
|
||||
if (file == null) {
|
||||
Log.e(TAG, "Unable to create file for backup, createFile returned null");
|
||||
throw new VaultManagerException("createFile returned null");
|
||||
}
|
||||
|
||||
try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME);
|
||||
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to create backup", e);
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
enforceVersioning(dir, versionsToKeep);
|
||||
}
|
||||
|
||||
public boolean hasPermissionsAt(Uri uri) {
|
||||
for (UriPermission perm : _context.getContentResolver().getPersistedUriPermissions()) {
|
||||
if (perm.getUri().equals(uri)) {
|
||||
return perm.isReadPermission() && perm.isWritePermission();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
|
||||
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
|
||||
|
||||
List<File> files = new ArrayList<>();
|
||||
for (DocumentFile docFile : dir.listFiles()) {
|
||||
if (docFile.isFile() && !docFile.isVirtual()) {
|
||||
try {
|
||||
files.add(new File(docFile));
|
||||
} catch (ParseException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(files, new FileComparator());
|
||||
for (File file : files) {
|
||||
Log.i(TAG, file.getFile().getName());
|
||||
}
|
||||
|
||||
if (files.size() > versionsToKeep) {
|
||||
for (File file : files.subList(0, files.size() - versionsToKeep)) {
|
||||
Log.i(TAG, String.format("Deleting %s", file.getFile().getName()));
|
||||
if (!file.getFile().delete()) {
|
||||
Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileInfo {
|
||||
private String _filename;
|
||||
private Date _date;
|
||||
|
||||
public FileInfo(String filename, Date date) {
|
||||
_filename = filename;
|
||||
_date = date;
|
||||
}
|
||||
|
||||
public FileInfo(String filename) {
|
||||
this(filename, Calendar.getInstance().getTime());
|
||||
}
|
||||
|
||||
public static FileInfo parseFilename(String filename) throws ParseException {
|
||||
if (filename == null) {
|
||||
throw new ParseException("The filename must not be null", 0);
|
||||
}
|
||||
|
||||
final String ext = ".json";
|
||||
if (!filename.endsWith(ext)) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
filename = filename.substring(0, filename.length() - ext.length());
|
||||
|
||||
final String delim = "-";
|
||||
String[] parts = filename.split(delim);
|
||||
if (parts.length < 3) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
filename = TextUtils.join(delim, Arrays.copyOf(parts, parts.length - 2));
|
||||
if (!filename.equals(FILENAME_PREFIX)) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
Date date = _dateFormat.parse(parts[parts.length - 2] + delim + parts[parts.length - 1]);
|
||||
if (date == null) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
return new FileInfo(filename, date);
|
||||
}
|
||||
|
||||
private static void throwBadFormat(String filename) throws ParseException {
|
||||
throw new ParseException(String.format("Bad backup filename format: %s", filename), 0);
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return _filename;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return _date;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s-%s.json", _filename, _dateFormat.format(_date));
|
||||
}
|
||||
}
|
||||
|
||||
private static class File {
|
||||
private DocumentFile _file;
|
||||
private FileInfo _info;
|
||||
|
||||
public File(DocumentFile file) throws ParseException {
|
||||
_file = file;
|
||||
_info = FileInfo.parseFilename(file.getName());
|
||||
}
|
||||
|
||||
public DocumentFile getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
public FileInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileComparator implements Comparator<File> {
|
||||
@Override
|
||||
public int compare(File o1, File o2) {
|
||||
return o1.getInfo().getDate().compareTo(o2.getInfo().getDate());
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/19503019
|
||||
private static class StrictDateFormat extends SimpleDateFormat {
|
||||
public StrictDateFormat(String pattern, Locale locale) {
|
||||
super(pattern, locale);
|
||||
setLenient(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date parse(@NonNull String text, ParsePosition pos) {
|
||||
int posIndex = pos.getIndex();
|
||||
Date d = super.parse(text, pos);
|
||||
if (!isLenient() && d != null) {
|
||||
String format = this.format(d);
|
||||
if (posIndex + format.length() != text.length() ||
|
||||
!text.endsWith(format)) {
|
||||
d = null; // Not exact match
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
@@ -38,6 +38,13 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
setIssuer(issuer);
|
||||
}
|
||||
|
||||
public VaultEntry(OtpInfo info, String name, String issuer, String group) {
|
||||
this(info);
|
||||
setName(name);
|
||||
setIssuer(issuer);
|
||||
setGroup(group);
|
||||
}
|
||||
|
||||
public VaultEntry(GoogleAuthInfo info) {
|
||||
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
||||
}
|
||||
@@ -60,7 +67,7 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
|
||||
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, EncodingException {
|
||||
// if there is no uuid, generate a new one
|
||||
UUID uuid;
|
||||
if (!obj.has("uuid")) {
|
||||
|
||||
@@ -3,11 +3,10 @@ package com.beemdevelopment.aegis.vault;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKeyException;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotListException;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -95,7 +94,7 @@ public class VaultFile {
|
||||
byte[] bytes = Base64.decode((String) _content);
|
||||
CryptResult result = creds.decrypt(bytes, _header.getParams());
|
||||
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
|
||||
} catch (MasterKeyException | JSONException | Base64Exception e) {
|
||||
} catch (MasterKeyException | JSONException | EncodingException e) {
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +135,7 @@ public class VaultFile {
|
||||
SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
|
||||
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
|
||||
return new Header(slots, params);
|
||||
} catch (SlotListException | JSONException | HexException e) {
|
||||
} catch (SlotListException | JSONException | EncodingException e) {
|
||||
throw new VaultFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,128 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.text.Collator;
|
||||
import java.util.Collection;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultManager {
|
||||
private static final String FILENAME = "aegis.json";
|
||||
private static final String FILENAME_EXPORT = "aegis_export.json";
|
||||
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
|
||||
public static final String FILENAME = "aegis.json";
|
||||
public static final String FILENAME_EXPORT = "aegis-export.json";
|
||||
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFile _file;
|
||||
private VaultFileCredentials _creds;
|
||||
private boolean _encrypt;
|
||||
|
||||
private Context _context;
|
||||
private Preferences _prefs;
|
||||
private VaultBackupManager _backups;
|
||||
|
||||
public VaultManager(Context context) {
|
||||
public VaultManager(Context context, Vault vault, VaultFileCredentials creds) {
|
||||
_context = context;
|
||||
_prefs = new Preferences(context);
|
||||
_backups = new VaultBackupManager(context);
|
||||
_vault = vault;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public boolean fileExists() {
|
||||
File file = new File(_context.getFilesDir(), FILENAME);
|
||||
public VaultManager(Context context, Vault vault) {
|
||||
this(context, vault, null);
|
||||
}
|
||||
|
||||
public static boolean fileExists(Context context) {
|
||||
File file = new File(context.getFilesDir(), FILENAME);
|
||||
return file.exists() && file.isFile();
|
||||
}
|
||||
|
||||
public void load() throws VaultManagerException {
|
||||
assertState(true, false);
|
||||
|
||||
try (FileInputStream file = _context.openFileInput(FILENAME)) {
|
||||
byte[] fileBytes = new byte[(int) file.getChannel().size()];
|
||||
DataInputStream stream = new DataInputStream(file);
|
||||
stream.readFully(fileBytes);
|
||||
stream.close();
|
||||
|
||||
_file = VaultFile.fromBytes(fileBytes);
|
||||
_encrypt = _file.isEncrypted();
|
||||
if (!isEncryptionEnabled()) {
|
||||
JSONObject obj = _file.getContent();
|
||||
_vault = Vault.fromJson(obj);
|
||||
}
|
||||
} catch (IOException | VaultFileException | VaultException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_vault = null;
|
||||
}
|
||||
|
||||
public void unlock(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(true, true);
|
||||
public static VaultFile readFile(Context context) throws VaultManagerException {
|
||||
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
|
||||
try {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
_vault = Vault.fromJson(obj);
|
||||
_creds = creds;
|
||||
_context.startService(new Intent(_context, NotificationService.class));
|
||||
} catch (VaultFileException | VaultException e) {
|
||||
byte[] fileBytes = file.readFully();
|
||||
return VaultFile.fromBytes(fileBytes);
|
||||
} catch (IOException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(Context context, VaultFile file) throws VaultManagerException {
|
||||
byte[] bytes = file.toBytes();
|
||||
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
|
||||
public static VaultManager init(Context context, VaultFile file, VaultFileCredentials creds) throws VaultManagerException {
|
||||
if (file.isEncrypted() && creds == null) {
|
||||
throw new IllegalArgumentException("The VaultFile is encrypted but the given VaultFileCredentials is null");
|
||||
}
|
||||
|
||||
Vault vault;
|
||||
try {
|
||||
JSONObject obj;
|
||||
if (!file.isEncrypted()) {
|
||||
obj = file.getContent();
|
||||
} else {
|
||||
obj = file.getContent(creds);
|
||||
}
|
||||
|
||||
vault = Vault.fromJson(obj);
|
||||
} catch (VaultException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
return new VaultManager(context, vault, creds);
|
||||
}
|
||||
|
||||
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
|
||||
byte[] bytes = vaultFile.toBytes();
|
||||
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
|
||||
FileOutputStream stream = null;
|
||||
try {
|
||||
stream = file.startWrite();
|
||||
stream.write(bytes);
|
||||
file.finishWrite(stream);
|
||||
} catch (IOException e) {
|
||||
if (stream != null) {
|
||||
file.failWrite(stream);
|
||||
}
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
public void save(boolean backup) throws VaultManagerException {
|
||||
try {
|
||||
JSONObject obj = _vault.toJson();
|
||||
|
||||
VaultFile file = new VaultFile();
|
||||
if (isEncryptionEnabled()) {
|
||||
_file.setContent(obj, _creds);
|
||||
file.setContent(obj, _creds);
|
||||
} else {
|
||||
_file.setContent(obj);
|
||||
file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
|
||||
save(_context, file);
|
||||
} catch (VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
if (backup && _prefs.isBackupsEnabled()) {
|
||||
try {
|
||||
backup();
|
||||
_prefs.setBackupsError(null);
|
||||
} catch (VaultManagerException e) {
|
||||
_prefs.setBackupsError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String export(boolean encrypt) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
|
||||
try {
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
if (encrypt && isEncryptionEnabled()) {
|
||||
@@ -115,57 +131,46 @@ public class VaultManager {
|
||||
vaultFile.setContent(_vault.toJson());
|
||||
}
|
||||
|
||||
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("error creating external storage directory");
|
||||
}
|
||||
|
||||
byte[] bytes = vaultFile.toBytes();
|
||||
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
|
||||
try (FileOutputStream stream = new FileOutputStream(file)) {
|
||||
stream.write(bytes);
|
||||
}
|
||||
|
||||
return file.getAbsolutePath();
|
||||
stream.write(bytes);
|
||||
} catch (IOException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void backup() throws VaultManagerException {
|
||||
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_vault.getEntries().add(entry);
|
||||
}
|
||||
|
||||
public VaultEntry getEntryByUUID(UUID uuid) {
|
||||
return _vault.getEntries().getByUUID(uuid);
|
||||
}
|
||||
|
||||
public VaultEntry removeEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().remove(entry);
|
||||
}
|
||||
|
||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
|
||||
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
|
||||
assertState(false, true);
|
||||
_vault.getEntries().swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public boolean isEntryDuplicate(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().has(entry);
|
||||
}
|
||||
|
||||
public Collection<VaultEntry> getEntries() {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().getValues();
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroups() {
|
||||
assertState(false, true);
|
||||
|
||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
String group = entry.getGroup();
|
||||
@@ -177,62 +182,24 @@ public class VaultManager {
|
||||
}
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
assertState(false, true);
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setCredentials(VaultFileCredentials creds) {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public VaultFile.Header getFileHeader() {
|
||||
assertLoaded(true);
|
||||
return _file.getHeader();
|
||||
}
|
||||
|
||||
public boolean isEncryptionEnabled() {
|
||||
assertLoaded(true);
|
||||
return _encrypt;
|
||||
return _creds != null;
|
||||
}
|
||||
|
||||
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
_encrypt = true;
|
||||
save();
|
||||
save(true);
|
||||
}
|
||||
|
||||
public void disableEncryption() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_encrypt = false;
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return _file != null;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return _vault == null;
|
||||
}
|
||||
|
||||
private void assertState(boolean locked, boolean loaded) {
|
||||
assertLoaded(loaded);
|
||||
|
||||
if (isLocked() && !locked) {
|
||||
throw new AssertionError("vault file has not been unlocked yet");
|
||||
} else if (!isLocked() && locked) {
|
||||
throw new AssertionError("vault file has already been unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLoaded(boolean loaded) {
|
||||
if (isLoaded() && !loaded) {
|
||||
throw new AssertionError("vault file has already been loaded");
|
||||
} else if (!isLoaded() && loaded) {
|
||||
throw new AssertionError("vault file has not been loaded yet");
|
||||
}
|
||||
save(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,8 @@ public class VaultManagerException extends Exception {
|
||||
public VaultManagerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultManagerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -144,7 +144,7 @@ public abstract class Slot extends UUIDMap.Value {
|
||||
default:
|
||||
throw new SlotException("unrecognized slot type");
|
||||
}
|
||||
} catch (JSONException | HexException e) {
|
||||
} catch (JSONException | EncodingException e) {
|
||||
throw new SlotException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.beemdevelopment.aegis";
|
||||
option java_outer_classname = "GoogleAuthProtos";
|
||||
|
||||
message MigrationPayload {
|
||||
enum Algorithm {
|
||||
ALGO_INVALID = 0;
|
||||
ALGO_SHA1 = 1;
|
||||
}
|
||||
|
||||
enum OtpType {
|
||||
OTP_INVALID = 0;
|
||||
OTP_HOTP = 1;
|
||||
OTP_TOTP = 2;
|
||||
}
|
||||
|
||||
message OtpParameters {
|
||||
bytes secret = 1;
|
||||
string name = 2;
|
||||
string issuer = 3;
|
||||
Algorithm algorithm = 4;
|
||||
int32 digits = 5;
|
||||
OtpType type = 6;
|
||||
int64 counter = 7;
|
||||
}
|
||||
|
||||
repeated OtpParameters otp_parameters = 1;
|
||||
int32 version = 2;
|
||||
int32 batch_size = 3;
|
||||
int32 batch_index = 4;
|
||||
int32 batch_id = 5;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="0"
|
||||
android:fromYDelta="-100%"
|
||||
android:toYDelta="0" />
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0"
|
||||
android:duration="300"/>
|
||||
</set>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:duration="300"/>
|
||||
|
||||
</set>
|
||||
@@ -0,0 +1,11 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="150">
|
||||
|
||||
<scale
|
||||
android:fromXScale="0"
|
||||
android:fromYScale="0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="100%"
|
||||
android:toYScale="100%" />
|
||||
</set>
|
||||
@@ -0,0 +1,11 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="150">
|
||||
|
||||
<scale
|
||||
android:fromXScale="100%"
|
||||
android:fromYScale="100%"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="0"
|
||||
android:toYScale="0" />
|
||||
</set>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fillAfter="true"
|
||||
android:fillEnabled="true">
|
||||
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="100%" />
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0"
|
||||
android:duration="500"/>
|
||||
|
||||
</set>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fillAfter="true"
|
||||
android:fillEnabled="true">
|
||||
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="100%" />
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:duration="500"/>
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:duration="500"/>
|
||||
|
||||
</set>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/auth_button"/>
|
||||
<stroke android:color="@color/auth_button" android:width="1dp" />
|
||||
<!--corners allow us to make the rounded corners button-->
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:top="7dp" android:left="7dp" android:right="7dp" android:bottom="7dp">
|
||||
<vector android:height="24dp" android:viewportHeight="3328" android:tint="#FFFFFF"
|
||||
android:viewportWidth="3333" android:width="24.036058dp">
|
||||
<path android:fillColor="#FF000000" android:fillType="evenOdd" android:pathData="M5,0h1211v1206L5,1206L5,0zM3025,3025h309v304h-309v-304zM2432,3025h309v290h-618v-592h299v-300h304v-598h309v294h294v304h-294v304h-603v299zM1515,2417h299v-304h-289v-304h289v-304h-294v304h-309v-304h304L1515,603h309v902h299v304h294v-304h309v304h-294v304h-309v598h-299v603h-309v-897zM3020,1505h309v304h-309v-304zM608,1505h309v304L608,1809v-304zM5,1505h309v304L5,1809v-304zM1515,0h309v304h-309L1515,0zM0,2118h1211v1206L0,3324L0,2118zM294,2411h623v620L294,3031v-620zM2118,0h1211v1206L2118,1206L2118,0zM2412,293h623v620h-623L2412,293zM299,293h623v620L299,913L299,293z"/>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector android:height="60dp" android:tint="#F7F7F7"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5"
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
>
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -7,37 +7,44 @@
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="?attr/background"
|
||||
tools:context="com.beemdevelopment.aegis.ui.AuthActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/linearLayout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="32dp"
|
||||
tools:layout_editor_absoluteY="8dp"
|
||||
tools:layout_editor_absoluteX="8dp">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="48dp"
|
||||
android:orientation="vertical"
|
||||
tools:layout_editor_absoluteX="8dp"
|
||||
tools:layout_editor_absoluteY="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView2"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:srcCompat="@drawable/ic_aegis_iconx" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/authentication"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="24sp"
|
||||
android:id="@+id/textView2" />
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:singleLine="false"
|
||||
android:text="@string/authentication_multiline"
|
||||
android:textColor="?attr/authText"
|
||||
android:textFontWeight="500"
|
||||
android:textSize="46sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/authentication_enter_password"
|
||||
android:textColor="?attr/secondaryText" />
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="25dp"
|
||||
app:hintEnabled="false"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:passwordToggleTint="#949494">
|
||||
@@ -46,48 +53,72 @@
|
||||
android:id="@+id/text_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:textColorHint="?attr/authText"
|
||||
app:backgroundTint="?attr/authText" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="25dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_decrypt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/button_rounded_corners"
|
||||
android:minWidth="125dp"
|
||||
android:text="@string/unlock" />
|
||||
<Button
|
||||
android:id="@+id/button_biometrics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="125dp"
|
||||
android:text="@string/biometrics"
|
||||
android:visibility="gone" />
|
||||
android:text="@string/unlock"
|
||||
android:textColor="@color/primary_text_inverted"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/box_biometric_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
<TextView
|
||||
android:id="@+id/button_biometrics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_info_outline_black_24dp"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
android:layout_gravity="center"
|
||||
android:minWidth="125dp"
|
||||
android:text="@string/auth_use_biometrics"
|
||||
android:textColor="?attr/authText"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/box_biometric_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/invalidated_biometrics"/>
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_info_outline_black_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/invalidated_biometrics" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:textStyle="bold"
|
||||
android:textAllCaps="true"
|
||||
android:text="@string/app_name_full"
|
||||
android:paddingBottom="50dp"
|
||||
android:textColor="@color/divider" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -8,12 +8,43 @@
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="com.beemdevelopment.aegis.ui.MainActivity">
|
||||
|
||||
<fragment
|
||||
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
|
||||
android:id="@+id/key_profiles"
|
||||
android:layout_height="match_parent"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:id="@+id/btn_backup_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:background="@color/colorAccent"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center">
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/icon_primary_inverted"
|
||||
android:src="@drawable/ic_alert_black_24dp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_error_bar_message"
|
||||
android:textColor="@color/primary_text_inverted"
|
||||
android:layout_marginStart="5dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<fragment
|
||||
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
|
||||
android:id="@+id/key_profiles"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- note: the fab should always be the last element to be sure it's displayed on top -->
|
||||
<com.getbase.floatingactionbutton.FloatingActionsMenu
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/cardBackground"
|
||||
tools:context="com.beemdevelopment.aegis.ui.TransferEntriesActivity">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivQrCode"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="250dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.496"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.399" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTransfer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/transfer_entry"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tvDescription"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="@string/transfer_entry_description"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ivQrCode"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIssuer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Issuer"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ivQrCode" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Accountname"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvIssuer" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNext"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:text="@string/next"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/code_primary_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPrevious"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:text="@string/previous"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="invisible"
|
||||
android:textColor="@color/code_primary_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEntriesCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="@string/entries_count"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/btnNext"
|
||||
app:layout_constraintHorizontal_bias="0.506"
|
||||
app:layout_constraintStart_toStartOf="@+id/btnPrevious" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -2,11 +2,12 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
@@ -30,6 +31,17 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
/>
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivSelected"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/item_selected"
|
||||
android:visibility="gone"
|
||||
app:civ_circle_background_color="@color/colorSecondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
@@ -42,41 +54,61 @@
|
||||
android:gravity="bottom"
|
||||
android:id="@+id/relativeLayout"
|
||||
android:layout_alignParentStart="false"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingTop="18dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingStart="16dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_copied"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/profile_issuer"
|
||||
android:text="@string/issuer"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="15sp"
|
||||
android:layout_below="@+id/profile_code"
|
||||
android:maxLines="1"
|
||||
android:text="@string/copied"
|
||||
android:layout_above="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"/>
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:textSize="16sp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/profile_issuer"
|
||||
android:text="@string/issuer"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="16sp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_account_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/profile_code"
|
||||
android:layout_toRightOf="@id/profile_issuer"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/extra_info_text"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/secondary_text"
|
||||
android:textSize="16sp"
|
||||
tools:text=" - AccountName" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -85,12 +117,12 @@
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="34sp"
|
||||
android:textColor="?attr/codePrimaryText"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
@@ -120,12 +152,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
|
||||
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="4dp"
|
||||
android:id="@+id/progressBar"
|
||||
android:max="1000"
|
||||
android:max="5000"
|
||||
android:layout_weight="1"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
@@ -30,6 +31,17 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
/>
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivSelected"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/item_selected"
|
||||
android:visibility="gone"
|
||||
app:civ_circle_background_color="@color/colorSecondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
@@ -42,25 +54,44 @@
|
||||
android:gravity="bottom"
|
||||
android:id="@+id/relativeLayout"
|
||||
android:layout_alignParentStart="false"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_copied"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/copied"
|
||||
android:layout_above="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:textSize="13sp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/profile_issuer"
|
||||
android:text="@string/issuer"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="13sp"
|
||||
android:layout_below="@+id/profile_code"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"/>
|
||||
|
||||
@@ -68,7 +99,6 @@
|
||||
android:id="@+id/profile_account_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/profile_code"
|
||||
android:layout_toRightOf="@id/profile_issuer"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
@@ -76,6 +106,7 @@
|
||||
android:textColor="@color/extra_info_text"
|
||||
android:textSize="13sp"
|
||||
tools:text=" - AccountName" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
@@ -85,12 +116,12 @@
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/codePrimaryText"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
@@ -120,7 +151,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
|
||||
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="3dp"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
@@ -30,6 +31,17 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
/>
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivSelected"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/item_selected"
|
||||
android:visibility="gone"
|
||||
app:civ_circle_background_color="@color/colorSecondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
@@ -42,25 +54,44 @@
|
||||
android:gravity="bottom"
|
||||
android:id="@+id/relativeLayout"
|
||||
android:layout_alignParentStart="false"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_copied"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/copied"
|
||||
android:layout_above="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:textSize="13sp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/profile_issuer"
|
||||
android:text="@string/issuer"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="13sp"
|
||||
android:layout_below="@+id/profile_code"
|
||||
android:layout_alignLeft="@+id/profile_code"
|
||||
android:layout_alignStart="@+id/profile_code"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"/>
|
||||
|
||||
@@ -68,7 +99,6 @@
|
||||
android:id="@+id/profile_account_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/profile_code"
|
||||
android:layout_toRightOf="@id/profile_issuer"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
@@ -76,7 +106,7 @@
|
||||
android:textColor="@color/extra_info_text"
|
||||
android:textSize="13sp"
|
||||
tools:text=" - AccountName" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -85,12 +115,12 @@
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/codePrimaryText"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
@@ -120,7 +150,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
|
||||
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="3dp"
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-info-outline"
|
||||
app:ico_size="16dp" />
|
||||
@@ -71,7 +72,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -97,7 +97,7 @@
|
||||
android:id="@+id/btn_changelog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
@@ -108,6 +108,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-time-restore"
|
||||
app:ico_size="16dp" />
|
||||
@@ -116,7 +117,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -150,6 +150,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-github"
|
||||
app:ico_size="16dp" />
|
||||
@@ -158,7 +159,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -192,6 +192,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-file-text"
|
||||
app:ico_size="16dp" />
|
||||
@@ -200,7 +201,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -267,6 +267,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-account"
|
||||
app:ico_size="16dp" />
|
||||
@@ -275,7 +276,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -312,6 +312,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-account"
|
||||
app:ico_size="16dp" />
|
||||
@@ -320,7 +321,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -357,6 +357,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-email"
|
||||
app:ico_size="16dp" />
|
||||
@@ -365,7 +366,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/email_us"
|
||||
@@ -389,6 +389,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-globe"
|
||||
app:ico_size="16dp" />
|
||||
@@ -396,7 +397,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/visit_website"
|
||||
@@ -438,7 +438,7 @@
|
||||
android:id="@+id/btn_rate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/list_item_height"
|
||||
@@ -450,6 +450,7 @@
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:ico_color="@color/icon_about"
|
||||
app:ico_icon="gmi-star"
|
||||
app:ico_size="16dp" />
|
||||
@@ -458,7 +459,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="25dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingEnd="25dp"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<TextView
|
||||
android:id="@+id/error_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="italic"/>
|
||||
</LinearLayout>
|
||||
@@ -7,6 +7,14 @@
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/authentication_method_password_explanation"
|
||||
android:textColor="#FF0000"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"/>
|
||||
<EditText
|
||||
android:id="@+id/text_password"
|
||||
android:hint="@string/password"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/time_sync_warning_message"/>
|
||||
<CheckBox
|
||||
android:id="@+id/check_warning_disable"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/time_sync_warning_disable"/>
|
||||
</LinearLayout>
|
||||
@@ -6,14 +6,14 @@
|
||||
android:background="?attr/background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
|
||||
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:progressDrawable="@drawable/progress_horizontal"
|
||||
android:id="@+id/progressBar"
|
||||
android:visibility="gone"
|
||||
android:max="1000"/>
|
||||
android:max="5000"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
+9
-3
@@ -20,13 +20,19 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/authentication_method_explanation"/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_authenticationMethod"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_none"
|
||||
+9
-1
@@ -14,11 +14,19 @@
|
||||
android:textColor="@color/primary_text_inverted"
|
||||
android:id="@+id/textView2" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/authentication_method_password_explanation"
|
||||
android:textColor="#FFFF00"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp">
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/cardBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/password_reminder"/>
|
||||
</LinearLayout>
|
||||
@@ -22,4 +22,12 @@
|
||||
android:icon="@drawable/ic_delete_white"
|
||||
android:tint="?attr/iconColorPrimary"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share_qr"
|
||||
android:title="@string/action_transfer"
|
||||
android:orderInCategory="100"
|
||||
android:icon="@drawable/ic_qr_code_full"
|
||||
android:tint="?attr/iconColorPrimary"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -8,12 +8,15 @@
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/save"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/action_delete"
|
||||
android:id="@+id/action_edit_icon"
|
||||
android:title="@string/action_edit_icon"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_default_icon"
|
||||
android:title="@string/action_default_icon"
|
||||
app:showAsAction="never"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/action_delete"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<license>MIT License</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Spongycastle</name>
|
||||
<url>https://github.com/rtyley/spongycastle/</url>
|
||||
<copyright>Copyright (c) 2000-2017 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
<name>Bouncy Castle</name>
|
||||
<url>https://www.bouncycastle.org/</url>
|
||||
<copyright>Copyright (c) 2000-2019 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)</copyright>
|
||||
<license>MIT License</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>CircleImageView</name>
|
||||
@@ -70,4 +70,9 @@
|
||||
<copyright>Copyright (C) 2016 The Android Open Source Project</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>SQLCipher for Android</name>
|
||||
<url>https://github.com/sqlcipher/android-database-sqlcipher</url>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
</notices>
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="settings">Předvolby</string>
|
||||
<string name="pref_appearance_group_title">Vzhled</string>
|
||||
<string name="pref_general_group_title">Obecné</string>
|
||||
<string name="pref_security_group_title">Bezpečnost</string>
|
||||
<string name="pref_security_group_title">Zabezpečení</string>
|
||||
<string name="pref_tools_group_title">Nástroje</string>
|
||||
<string name="pref_select_theme_title">Vzhled</string>
|
||||
<string name="pref_view_mode_title">Režim zobrazení</string>
|
||||
@@ -21,37 +21,47 @@
|
||||
<string name="pref_timeout_title">Časový limit</string>
|
||||
<string name="pref_timeout_summary">Automaticky zamknout trezor po %1$s sekundách nečinnosti</string>
|
||||
<string name="pref_slots_title">Sloty klíčů</string>
|
||||
<string name="pref_slots_summary">Správa seznamu klíčů, které mohou dešifrovat databázi</string>
|
||||
<string name="pref_slots_summary">Správa seznamu klíčů, které mohou dešifrovat trezor</string>
|
||||
<string name="pref_import_file_title">Importovat ze souboru</string>
|
||||
<string name="pref_import_file_summary">Importovat databázi ze souboru</string>
|
||||
<string name="pref_import_file_summary">Importovat tokeny ze souboru</string>
|
||||
<string name="pref_import_app_title">Importovat z aplikace</string>
|
||||
<string name="pref_import_app_summary">Importovat databázi z aplikace (vyžaduje root přístup)</string>
|
||||
<string name="pref_import_app_summary">Importovat tokeny z aplikace (vyžaduje root přístup)</string>
|
||||
<string name="pref_export_title">Exportovat</string>
|
||||
<string name="pref_export_summary">Exportovat databázi</string>
|
||||
<string name="pref_export_summary">Exportovat trezor</string>
|
||||
<string name="pref_password_reminder_title">Připomenutí hesla</string>
|
||||
<string name="pref_password_reminder_summary">Jednou za čas zobrazit výzvu pro zadání hesla, abyste ho nezapomněli.</string>
|
||||
<string name="pref_secure_screen_title">Zabezpečení obrazovky</string>
|
||||
<string name="pref_secure_screen_summary">Blokovat snímky obrazovky a další pokusy o zachycení obrazovky v rámci aplikace</string>
|
||||
<string name="pref_tap_to_reveal_title">Zobrazit po klepnutí</string>
|
||||
<string name="pref_tap_to_reveal_summary">Ve výchozím nastavení budou tokeny skryté. Kód zobrazíte klepnutím na token.</string>
|
||||
<string name="pref_tap_to_reveal_time_title">Čas do skrytí zobrazeného kódu</string>
|
||||
<string name="pref_tap_to_reveal_title">Zobrazit klepnutím</string>
|
||||
<string name="pref_tap_to_reveal_summary">Ve výchozím stavu budou kódy skryté. Kód zobrazíte klepnutím na token.</string>
|
||||
<string name="pref_tap_to_reveal_time_title">Prodleva zobrazení kódu</string>
|
||||
<string name="pref_auto_lock_title">Automatické uzamknutí</string>
|
||||
<string name="pref_auto_lock_summary">Automaticky zamknout trezor po zavření aplikace, nebo uzamknutí zařízení.</string>
|
||||
<string name="pref_auto_lock_summary">Automaticky uzamknout trezor při zavření aplikace, nebo uzamčení zařízení.</string>
|
||||
<string name="pref_encryption_title">Šifrování</string>
|
||||
<string name="pref_encryption_summary">Šifrovat databázi a odemknout ji pomocí hesla nebo otisku prstu</string>
|
||||
<string name="pref_encryption_summary">Šifrovat trezor a odemykat jej pomocí hesla, nebo biometrie</string>
|
||||
<string name="pref_biometrics_title">Biometrické odemknutí</string>
|
||||
<string name="pref_biometrics_summary">Povolit biometrické ověření pro odemčení trezoru</string>
|
||||
<string name="pref_set_password_title">Změnit heslo</string>
|
||||
<string name="pref_set_password_summary">Nastavení nového hesla potřebného pro odemknutí trezoru</string>
|
||||
<string name="pref_set_password_summary">Nastavení nového hesla potřebného pro odemčení trezoru</string>
|
||||
<string name="choose_authentication_method">Zabezpečení</string>
|
||||
<string name="authentication_method_explanation">Aegis je 2FA aplikace zaměřená na bezpečnost. Tokeny jsou uloženy v trezoru, který může být zašifrovaný vámi zvoleným heslem. Získá-li útočník váš zašifrovaný trezor, bez hesla se do něj nedostane.\n\nPředvybrali jsme možnost, která by podle nás měla nejvíce vyhovovat vašemu zařízení.</string>
|
||||
<string name="authentication_method_none">Žádné</string>
|
||||
<string name="authentication_method_none_description">K odemknutí trezoru není potřeba heslo a trezor nebude šifrován. Tato volba není doporučena.</string>
|
||||
<string name="authentication_method_none_description">Trezor nebude šifrován a nebude potřeba heslo pro jeho odemčení. Tuto možnost <b>nedoporučujeme</b>.</string>
|
||||
<string name="authentication_method_password">Heslo</string>
|
||||
<string name="authentication_method_password_description">K odemknutí trezoru potřebujete heslo.</string>
|
||||
<string name="authentication_method_password_description">K odemčení trezoru je potřeba heslo.</string>
|
||||
<string name="authentication_method_password_explanation">Upozornění: Pokud zapomenete své heslo, trvale ztratíte přístup ke svým tokenům. Bez hesla neexistuje způsob, jak je obnovit.</string>
|
||||
<string name="authentication_method_biometrics">Biometrie</string>
|
||||
<string name="authentication_method_biometrics_description">Kromě hesla půjde k odemčení trezoru použít také biometrické prvky registrované v tomto zařízení jako je otisk prstu nebo tvář.</string>
|
||||
<string name="authentication_method_set_password">Heslo</string>
|
||||
<string name="authentication_enter_password">Zadejte své heslo</string>
|
||||
<string name="authentication">Odemknout trezor</string>
|
||||
<string name="set_password">Zadejte heslo</string>
|
||||
<string name="set_group">Zadejte název skupiny</string>
|
||||
<string name="set_number">Zadejte číslo</string>
|
||||
<string name="set_password_confirm">Potvrďte heslo</string>
|
||||
<string name="invalidated_biometrics">Zjištěna změna nastavení zabezpečení zařízení. Přejděte do „Aegis“ → „Nastavení“ → „Biometrie“ a povolte biometrické odemknutí znovu.</string>
|
||||
<string name="password_reminder">Od posledního zadání vašeho hesla uplynulo už mnoho vody. Stále si ho pamatujete?</string>
|
||||
<string name="unlock">Odemknout</string>
|
||||
<string name="biometrics">Biometrie</string>
|
||||
<string name="advanced">Pokročilé</string>
|
||||
<string name="seconds">sekund</string>
|
||||
<string name="counter">Počítadlo</string>
|
||||
@@ -60,70 +70,78 @@
|
||||
<string name="scan">Naskenovat QR kód</string>
|
||||
<string name="scan_image">Skenovat obrázek</string>
|
||||
<string name="enter_manually">Zadat ručně</string>
|
||||
<string name="add_biometric">Přidat biometriku</string>
|
||||
<string name="add_biometric_slot">Přidat biometrický slot</string>
|
||||
<string name="set_up_biometric">Nastavení biometrického odemčení</string>
|
||||
<string name="add_password">Přidat heslo</string>
|
||||
<string name="slots_warning">Trezor je bezpečný jen jako nejslabší místo. Pokud bude do zařízení přidán nový otisk prstu, budete muset v Aegis reaktivovat ověření otiskem prstu.</string>
|
||||
<string name="slots_warning">Trezor je stejně bezpečný jako jeho nejslabší místo. Změníte-li nastavení biometrického ověření ve vašem zařízení, budete muset v Aegis znovu aktivovat biometrické odemknutí.</string>
|
||||
<string name="copy">Kopírovat</string>
|
||||
<string name="edit">Upravit</string>
|
||||
<string name="delete">Odstranit</string>
|
||||
<string name="password">Heslo</string>
|
||||
<string name="confirm_password">Potvrzení hesla</string>
|
||||
<string name="new_profile">Nový profil</string>
|
||||
<string name="add_new_profile">Přidat nový profil</string>
|
||||
<string name="confirm_password">Potvrdit heslo</string>
|
||||
<string name="show_password">Zobrazit heslo</string>
|
||||
<string name="unlock_vault_error">Trezor nelze odemknout</string>
|
||||
<string name="unlock_vault_error_description">Nesprávné heslo. Ujistěte se, že je heslo správně zadané.</string>
|
||||
<string name="unlock_vault_error_description">Nesprávné heslo. Ujistěte se, že je zadané správně.</string>
|
||||
<string name="password_equality_error">Hesla musí být stejná a nesmí být prázdná</string>
|
||||
<string name="snackbar_authentication_method">Zvolte způsob ověření</string>
|
||||
<string name="snackbar_authentication_method">Zvolte prosím způsob ověření</string>
|
||||
<string name="encrypting_vault">Šifrování trezoru</string>
|
||||
<string name="delete_entry">Odstranit položku</string>
|
||||
<string name="delete_entry_description">Opravdu chcete odstranit tento záznam?</string>
|
||||
<string name="delete_entry_description">Opravdu chcete odstranit tuto položku?</string>
|
||||
<string name="delete_entries">Odstranit položky</string>
|
||||
<string name="delete_entries_description">Opravdu chcete odstranit %d položek?</string>
|
||||
<string name="discard_changes">Zahodit změny?</string>
|
||||
<string name="discard_changes_description">Změny nebyly uloženy</string>
|
||||
<string name="folder">Složka</string>
|
||||
<string name="tap_to_select">Klepnutím vyberte</string>
|
||||
<string name="tap_to_select">Vyberte klepnutím</string>
|
||||
<string name="saving_profile_error">Chyba při ukládání profilu</string>
|
||||
<string name="welcome">Vítejte</string>
|
||||
<string name="app_description">Aegis je bezpečná open source 2FA aplikace. Aegis je zdarma.</string>
|
||||
<string name="app_description">Aegis je bezpečná, bezplatná a open source 2FA aplikace</string>
|
||||
<string name="setup_completed">Nastavení dokončeno</string>
|
||||
<string name="setup_completed_description">Nastavení Aegis dokončeno. Můžeme začít.</string>
|
||||
<string name="vault_not_found">Trezor nenalezen, spouštění nastavení…</string>
|
||||
<string name="code_copied">Kód zkopírován do schránky</string>
|
||||
<string name="setup_completed_description">Aegis je nastaven a připraven k používání.</string>
|
||||
<string name="vault_not_found">Trezor nebyl nenalezen, spouštím nastavení…</string>
|
||||
<string name="errors_copied">Chyby zkopírovány do schránky</string>
|
||||
<string name="version_copied">Verze zkopírována do schránky</string>
|
||||
<string name="decryption_error">Při odemykání trezoru došlo k chybě</string>
|
||||
<string name="saving_error">Při pokusu o uložení trezoru došlo k chybě</string>
|
||||
<string name="disable_encryption">Vypnout šifrování</string>
|
||||
<string name="disable_encryption_description">Opravdu chcete vypnout šifrování úložiště? Obsah trezoru bude uložen jako prostý text.</string>
|
||||
<string name="disable_encryption_description">Opravdu chcete vypnout šifrování úložiště? Obsah trezoru pak bude uložen jako prostý text.</string>
|
||||
<string name="enable_encryption_error">Při zapínání šifrování došlo k chybě</string>
|
||||
<string name="disable_encryption_error">Při vypínání šifrování došlo k chybě</string>
|
||||
<string name="permission_denied">Přístup zamítnut</string>
|
||||
<string name="choose_application">Zvolte aplikaci, ze které chcete importovat databázi</string>
|
||||
<string name="andotp_new_format">Nový formát (v0.6.3 nebo novější) </string>
|
||||
<string name="andotp_old_format">Starý formát (v0.6.2 a starší) </string>
|
||||
<string name="choose_andotp_importer">Jaký formát má záložní andOTP soubor?</string>
|
||||
<string name="choose_application">Zvolte aplikaci, ze které chcete importovat údaje</string>
|
||||
<string name="choose_theme">Vyberte si vzhled rozhraní</string>
|
||||
<string name="choose_view_mode">Vyberte si režim zobrazení</string>
|
||||
<string name="parsing_file_error">Při zpracování souboru došlo k chybě</string>
|
||||
<string name="file_not_found">Chyba: Soubor nenalezen</string>
|
||||
<string name="reading_file_error">Při čtení souboru došlo k chybě</string>
|
||||
<string name="reading_file_error">Při pokusu o čtení souboru došlo k chybě</string>
|
||||
<string name="app_lookup_error">Chyba: Aplikace není nainstalována</string>
|
||||
<string name="root_error">Chyba: Nelze získat root přístup</string>
|
||||
<string name="imported_entries_count">Počet importovaných záznamů: %d</string>
|
||||
<string name="read_entries_count">Čtení %d záznamů. %d chyby.</string>
|
||||
<string name="imported_entries_count">Naimportováno %d záznamů</string>
|
||||
<string name="read_entries_count">Čtení %d záznamů. %d chyb.</string>
|
||||
<string name="import_error_title">Při importu došlo k jedné nebo více chybám</string>
|
||||
<string name="export_warning">Databáze bude exportována mimo soukromé úložiště aplikace Aegis.</string>
|
||||
<string name="encryption_set_password_error">Při pokusu o nastavení hesla došlo k chybě: </string>
|
||||
<string name="no_cameras_available">Fotoaparát je nedostupný</string>
|
||||
<string name="exporting_vault_error">Při pokusu o export trezoru došlo k chybě</string>
|
||||
<string name="exported_vault">Trezor byl exportován</string>
|
||||
<string name="export_warning">Trezor bude exportován mimo soukromé úložiště aplikace Aegis.</string>
|
||||
<string name="no_cameras_available">Fotoaparát není dostupný</string>
|
||||
<string name="read_qr_error">Při pokusu o přečtení QR kódu došlo k chybě</string>
|
||||
<string name="authentication_method_raw">Prosté</string>
|
||||
<string name="unlocking_vault">Odemykání trezoru</string>
|
||||
<string name="unlocking_vault_repair">Odemykání trezoru (oprava)</string>
|
||||
<string name="unlocking_vault_repair">Odemykání trezoru (opravuji)</string>
|
||||
<string name="password_slot_error">Je potřeba alespoň jeden slot hesla</string>
|
||||
<string name="remove_slot">Odstranit slot</string>
|
||||
<string name="remove_slot_description">Opravdu chcete tento slot odstranit?</string>
|
||||
<string name="remove_group">Odstranit skupinu</string>
|
||||
<string name="remove_group_description">Opravdu chcete odstranit tuto skupinu? Záznamy v této skupině budou automaticky přeřazeny do „Žádná skupina“.</string>
|
||||
<string name="remove_group_description">Opravdu chcete odstranit tuto skupinu? Záznamy z ní budou automaticky přemístěny do „Žádná skupina“.</string>
|
||||
<string name="adding_new_slot_error">Při pokusu o přidání nového slotu došlo k chybě:</string>
|
||||
<string name="progressbar_error">Měřítko trvání animace nelze resetovat. Ukazatele průběhu budou neviditelné.</string>
|
||||
<string name="progressbar_error">Měřítko trvání animace nelze resetovat. Ukazatele průběhu nebudou viditelné.</string>
|
||||
<string name="details">Podrobnosti</string>
|
||||
<string name="filter">Filtr</string>
|
||||
<string name="lock">Zamknout</string>
|
||||
<string name="filter_ungrouped">Bez skupiny</string>
|
||||
<string name="lock">Uzamknout</string>
|
||||
<string name="all">Vše</string>
|
||||
<string name="name">Název</string>
|
||||
<string name="no_group">Žádná skupina</string>
|
||||
@@ -139,24 +157,25 @@
|
||||
<string name="preference_manage_groups_summary">Správa a odstranění skupin</string>
|
||||
<string name="pref_search_name_title">Hledat v názvech účtů</string>
|
||||
<string name="pref_search_name_summary">Zahrnout shody názvů účtů do výsledků hledání</string>
|
||||
<string name="tap_to_reveal">Skryto</string>
|
||||
<string name="pref_highlight_entry_title">Zvýraznit tokeny po klepnutí</string>
|
||||
<string name="pref_highlight_entry_summary">Usnadnit vzájemné rozlišení tokenů dočasným zvýrazněním po kleputí</string>
|
||||
<string name="tap_to_reveal">– skryto –</string>
|
||||
<string name="selected">Vybráno</string>
|
||||
<string name="dark_theme_title">Tmavý vzhled</string>
|
||||
<string name="light_theme_title">Světlý vzhled</string>
|
||||
<string name="amoled_theme_title">Amoled vzhled</string>
|
||||
<string name="normal_viewmode_title">Normální</string>
|
||||
<string name="compact_mode_title">Kompaktní</string>
|
||||
<string name="small_mode_title">Malý</string>
|
||||
<string name="unknown_issuer">Neznámý poskytovatel</string>
|
||||
<string name="unknown_account_name">Neznámý název účtu</string>
|
||||
<string name="import_error_dialog">Aegis nedokáže importovat některé tokeny (%d). Tyto tokeny budou přeskočeny. Klepněte na „Podrobnosti“ pro více informací o chybách.</string>
|
||||
<string name="import_error_dialog">Nepovedlo se naimportovat %d tokenů a byly přeskočeny. Klepněte na „podrobnosti“ pro více informací o chybách.</string>
|
||||
<string name="unable_to_read_qrcode">QR kód nelze přečíst a zpracovat</string>
|
||||
<string name="select_picture">Vyberte obrázek</string>
|
||||
<string name="toggle_checkboxes">Přepnout zaškrtávací políčka</string>
|
||||
<string name="search">Hledat</string>
|
||||
<string name="channel_name_lock_status">Stav zámku</string>
|
||||
<string name="channel_description_lock_status">Aegis může vytvořit trvalé oznámení, které vás upozorní, když je trezor uzamčen</string>
|
||||
<string name="vault_unlocked_state">Trezor odemknut. Klepnutím ho zamknete.</string>
|
||||
<string name="vault_unlocked_state">Trezor je odemčený. Klepnutím uzamkněte.</string>
|
||||
<string name="title_activity_about">O aplikaci</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="changelog">Seznam změn</string>
|
||||
@@ -169,9 +188,9 @@
|
||||
<string name="visit_website">Navštivte náš web</string>
|
||||
<string name="about_support">Podpora</string>
|
||||
<string name="support_rate">Ohodnoťte aplikaci</string>
|
||||
<string name="support_rate_description">Podpořte nás zanecháním recenze v obchodě Google Play</string>
|
||||
<string name="webview_error">Vaše zařízení nepodporuje WebView, které je nutné pro zobrazení seznamu změn. Tato komponenta v systému zřejmě chybí.</string>
|
||||
<string name="support_rate_description">Podpořte nás napsáním recenze v obchodě Google Play</string>
|
||||
<string name="webview_error">Vaše zařízení nepodporuje WebView, které je nutné pro zobrazení seznamu změn. Tato komponenta zřejmě chybí v systému.</string>
|
||||
<string name="email">E-mail</string>
|
||||
<string name="empty_list">Žádné kódy. Začněte přidáním položek pomocí tlačítka plus v pravém dolním rohu.</string>
|
||||
<string name="empty_list_title">Nenalezeny žádné záznamy</string>
|
||||
<string name="empty_list">Žádné kódy. Začněte přidáním položek pomocí tlačítka + v pravém dolním rohu.</string>
|
||||
<string name="empty_list_title">Žádné záznamy nenalezeny</string>
|
||||
</resources>
|
||||
|
||||
@@ -67,8 +67,6 @@
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="password">Passwort</string>
|
||||
<string name="confirm_password">Passwort bestätigen</string>
|
||||
<string name="new_profile">Neues Profil</string>
|
||||
<string name="add_new_profile">Neues Profil hinzufügen</string>
|
||||
<string name="unlock_vault_error">Der Tresor konnte nicht entsperrt werden.</string>
|
||||
<string name="unlock_vault_error_description">Falsches Passwort. Achte darauf, dass du dein Passwort nicht falsch eingibst.</string>
|
||||
<string name="password_equality_error">Passwörter sollten identisch und nicht leer sein.</string>
|
||||
@@ -86,7 +84,6 @@
|
||||
<string name="setup_completed">Einrichten abgeschlossen</string>
|
||||
<string name="setup_completed_description">Aegis ist eingerichtet und einsatzbereit.</string>
|
||||
<string name="vault_not_found">Tresor nicht gefunden, Starten der Einrichtung…</string>
|
||||
<string name="code_copied">Code in die Zwischenablage kopiert</string>
|
||||
<string name="errors_copied">Fehlermeldungen in die Zwischenablage kopiert</string>
|
||||
<string name="version_copied">Version in die Zwischenablage kopiert</string>
|
||||
<string name="decryption_error">Beim Versuch, den Tresor zu entsperren, ist ein Fehler aufgetreten.</string>
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="action_settings">Ρυθμίσεις</string>
|
||||
<string name="action_about">Σχετικά</string>
|
||||
<string name="action_import">Εισαγωγή</string>
|
||||
<string name="action_delete">Διαγραφή</string>
|
||||
<string name="action_default_icon">Επαναφορά εικονιδίου</string>
|
||||
<string name="discard">Απόρριψη</string>
|
||||
<string name="save">Αποθήκευση</string>
|
||||
<string name="issuer">Εκδότης</string>
|
||||
<string name="settings">Προτιμήσεις</string>
|
||||
<string name="pref_appearance_group_title">Εμφάνιση</string>
|
||||
<string name="pref_general_group_title">Γενικές</string>
|
||||
<string name="pref_security_group_title">Ασφάλεια</string>
|
||||
<string name="pref_tools_group_title">Εργαλεία</string>
|
||||
<string name="pref_select_theme_title">Θέμα</string>
|
||||
<string name="pref_view_mode_title">Λειτουργία προβολής</string>
|
||||
<string name="pref_lang_title">Γλώσσα</string>
|
||||
<string name="pref_account_name_title">Προβολή ονόματος λογαριασμού</string>
|
||||
<string name="pref_account_name_summary">Ενεργοποίηση προβολής ονόματος λογαριασμού δίπλα στον εκδότη</string>
|
||||
<string name="pref_timeout_title">Χρονικό όριο</string>
|
||||
<string name="pref_timeout_summary">Αυτόματο κλείδωμα λίστας μετά από %1$s δευτερόλεπτα αδράνειας</string>
|
||||
<string name="pref_slots_title">Υποδοχή κλειδιών</string>
|
||||
<string name="pref_slots_summary">Διαχείριση λίστας κλειδιών που μπορούν να αποκρυπτογραφήσουν τη λίστα</string>
|
||||
<string name="pref_import_file_title">Εισαγωγή από αρχείο</string>
|
||||
<string name="pref_import_file_summary">Εισαγωγή αναγνωριστικών από αρχείο</string>
|
||||
<string name="pref_import_app_title">Εισαγωγή από εφαρμογή</string>
|
||||
<string name="pref_import_app_summary">Εισαγωγή αναγνωριστικών από εφαρμογή (απαιτείται πρόσβαση root)</string>
|
||||
<string name="pref_export_title">Εξαγωγή</string>
|
||||
<string name="pref_export_summary">Εξαγωγή λίστας</string>
|
||||
<string name="pref_password_reminder_title">Υπενθύμιση κωδικού</string>
|
||||
<string name="pref_password_reminder_summary">Προβολή υπενθύμισης για εισαγωγή κωδικού περιστασιακά, έτσι ώστε να μην τον ξεχάσετε.</string>
|
||||
<string name="pref_secure_screen_title">Ασφάλεια οθόνης</string>
|
||||
<string name="pref_secure_screen_summary">Αποκλεισμός στιγμιότυπων οθόνης και άλλων προσπαθειών για καταγραφή της οθόνης μέσα στην εφαρμογή</string>
|
||||
<string name="pref_tap_to_reveal_title">Πατήστε για προβολή</string>
|
||||
<string name="pref_tap_to_reveal_summary">Απόκρυψη αναγνωριστικών από προεπιλογή. Πατήστε στα αναγνωριστικά για προβολή κωδικού.</string>
|
||||
<string name="pref_tap_to_reveal_time_title">Χρονικό όριο πατήματος προβολής</string>
|
||||
<string name="pref_auto_lock_title">Αυτόματο κλείδωμα</string>
|
||||
<string name="pref_auto_lock_summary">Αυτόματο κλείδωμα στο κλείσιμο της εφαρμογής ή στο κλείδωμα της συσκευή σας.</string>
|
||||
<string name="pref_encryption_title">Κρυπτογράφηση</string>
|
||||
<string name="pref_encryption_summary">Κρυπτογράφηση λίστας και ξεκλείδωμα με κωδικό ή βιομετρική μέθοδο</string>
|
||||
<string name="pref_biometrics_title">Βιομετρικό ξεκλείδωμα</string>
|
||||
<string name="pref_biometrics_summary">Χορήγηση βιομετρικής πιστοποίησης για ξεκλείδωμα λίστας</string>
|
||||
<string name="pref_set_password_title">Αλλαγή κωδικού</string>
|
||||
<string name="pref_set_password_summary">Ορισμός νέου κωδικού που θα χρειαστεί για να ξεκλειδώσετε τη λίστα σας</string>
|
||||
<string name="choose_authentication_method">Ασφάλεια</string>
|
||||
<string name="authentication_method_explanation">Το Aegis είναι μια εφαρμογή 2FA που εστιάζει στην ασφάλεια. Τα αναγνωριστικά αποθηκεύονται σε μία λίστα, η οποία μπορεί προαιρετικά να κρυπτογραφηθεί με έναν κωδικό της επιλογής σας. Αν κάποιος εισβολέας αποκτήσει το κρυπτογραφημένο αρχείο της λίστας, δεν θα έχει πρόσβαση στα περιεχόμενα χωρίς να γνωρίζει τον κωδικό.\n\nΈχουμε προεπιλέξει την επιλογή που νομίζουμε ότι θα ταιριάζει καλύτερα στη συσκευή σας.</string>
|
||||
<string name="authentication_method_none">Χωρίς</string>
|
||||
<string name="authentication_method_none_description">Χωρίς απαίτηση κωδικού για ξεκλείδωμα λίστας η οποία δεν θα κρυπτογραφηθεί. <b>Αυτή η επιλογή δεν συνιστάται</b>.</string>
|
||||
<string name="authentication_method_password">Κωδικός</string>
|
||||
<string name="authentication_method_password_description">Απαίτηση κωδικού για ξεκλείδωμα λίστας.</string>
|
||||
<string name="authentication_method_password_explanation">Προειδοποίηση: Εάν ξεχάσετε τον κωδικό σας, θα χάσετε μόνιμα πρόσβαση στα αναγνωριστικά σας. Δεν υπάρχει τρόπος να τα ανακτήσετε χωρίς τον κωδικό.</string>
|
||||
<string name="authentication_method_biometrics">Βιομετρική σύνδεση</string>
|
||||
<string name="authentication_method_biometrics_description">Εκτός από έναν κωδικό, τα βιομετρικά στοιχεία που έχουν καταχωρηθεί σε αυτήν τη συσκευή, όπως ένα δακτυλικό αποτύπωμα ή το πρόσωπο σας, μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε τη λίστα σας.</string>
|
||||
<string name="authentication_method_set_password">Κωδικός</string>
|
||||
<string name="authentication_enter_password">Εισάγετε τον κωδικό σας</string>
|
||||
<string name="set_password">Παρακαλώ εισάγετε κωδικό</string>
|
||||
<string name="set_group">Παρακαλώ εισάγετε όνομα ομάδας</string>
|
||||
<string name="set_number">Παρακαλώ εισάγετε αριθμό</string>
|
||||
<string name="set_password_confirm">Παρακαλώ επιβεβαιώστε τον κωδικό</string>
|
||||
<string name="invalidated_biometrics">Έχει εντοπιστεί μια αλλαγή στις ρυθμίσεις ασφαλείας της συσκευής σας. Παρακαλώ πηγαίνετε στο \"Aegis -> Ρυθμίσεις -> Βιομετρική σύνδεση και ενεργοποιήστε ξανά το βιομετρικό ξεκλείδωμα.</string>
|
||||
<string name="password_reminder">Έχει περάσει λίγος καιρός από τότε που εισάγατε τον κωδικό σας. Τον θυμάστε ακόμα;</string>
|
||||
<string name="unlock">Ξεκλείδωμα</string>
|
||||
<string name="biometrics">Βιομετρική σύνδεση</string>
|
||||
<string name="advanced">Σύνθετες</string>
|
||||
<string name="seconds">δευτερόλεπτα</string>
|
||||
<string name="counter">Μετρητής</string>
|
||||
<string name="digits">Ψηφία</string>
|
||||
<string name="secret">Μυστικός κωδικός</string>
|
||||
<string name="scan">Σάρωση κώδικα QR</string>
|
||||
<string name="scan_image">Σάρωση εικόνας</string>
|
||||
<string name="enter_manually">Χειροκίνητη εισαγωγή</string>
|
||||
<string name="add_biometric">Προσθήκη βιομετρίας</string>
|
||||
<string name="add_biometric_slot">Προσθήκη βιομετρικής υποδοχής</string>
|
||||
<string name="set_up_biometric">Ρύθμιση βιομετρικού ξεκλειδώματος</string>
|
||||
<string name="add_password">Προσθήκη κωδικού</string>
|
||||
<string name="slots_warning">Η λίστα σας είναι τόσο ασφαλής όσο με το πιο αδύναμο μυστικό σας. Εάν αλλάξετε τις ρυθμίσεις βιομετρικού ελέγχου ταυτότητας της συσκευής σας, θα χρειαστεί να ενεργοποιήσετε ξανά το βιομετρικό ξεκλείδωμα μέσα από το Aegis.</string>
|
||||
<string name="copy">Αντιγραφή</string>
|
||||
<string name="edit">Επεξεργασία</string>
|
||||
<string name="delete">Διαγραφή</string>
|
||||
<string name="password">Κωδικός</string>
|
||||
<string name="confirm_password">Επιβεβαίωση κωδικού</string>
|
||||
<string name="show_password">Προβολή κωδικού</string>
|
||||
<string name="unlock_vault_error">Αδυναμία ξεκλειδώματος λίστας</string>
|
||||
<string name="unlock_vault_error_description">Εσφαλμένος κωδικός. Βεβαιωθείτε ότι δεν τον πληκτρολογήσατε λάθος.</string>
|
||||
<string name="password_equality_error">Οι κωδικοί πρέπει να είναι ίδιοι και να μην είναι κενοί</string>
|
||||
<string name="snackbar_authentication_method">Παρακαλώ επιλέξτε μέθοδο ελέγχου ταυτότητας</string>
|
||||
<string name="encrypting_vault">Κρυπτογράφηση λίστας</string>
|
||||
<string name="delete_entry">Διαγραφή καταχώρησης</string>
|
||||
<string name="delete_entry_description">Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή την καταχώρηση;</string>
|
||||
<string name="delete_entries">Διαγραφή καταχωρήσεων</string>
|
||||
<string name="delete_entries_description">Είστε βέβαιοι ότι θέλετε να διαγράψετε %d καταχωρήσεις;</string>
|
||||
<string name="discard_changes">Απόρριψη αλλαγών;</string>
|
||||
<string name="discard_changes_description">Οι αλλαγές σας δεν έχουν αποθηκευτεί</string>
|
||||
<string name="folder">Φάκελος</string>
|
||||
<string name="tap_to_select">Πατήστε για επιλογή</string>
|
||||
<string name="saving_profile_error">Σφάλμα κατά την αποθήκευση προφίλ</string>
|
||||
<string name="welcome">Καλώς ήρθατε</string>
|
||||
<string name="app_description">Το Aegis είναι μια δωρεάν εφαρμογή 2FA, ασφαλής και ανοικτού κώδικα</string>
|
||||
<string name="setup_completed">Η εγκατάσταση ολοκληρώθηκε</string>
|
||||
<string name="setup_completed_description">Το Aegis έχει ρυθμιστεί και είναι έτοιμο προς χρήση.</string>
|
||||
<string name="vault_not_found">Η λίστα δεν βρέθηκε, έναρξη ρύθμισης…</string>
|
||||
<string name="errors_copied">Σφάλμα αντιγραφής στο πρόχειρο</string>
|
||||
<string name="version_copied">Η έκδοση αντιγράφτηκε στο πρόχειρο</string>
|
||||
<string name="decryption_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ξεκλειδώματος της λίστας</string>
|
||||
<string name="saving_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια αποθήκευσης της λίστας</string>
|
||||
<string name="disable_encryption">Απενεργοποίηση κρυπτογράφησης</string>
|
||||
<string name="disable_encryption_description">Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε την κρυπτογράφηση; Αυτό θα έχει ως αποτέλεσμα την αποθήκευση της λίστας σε απλό κείμενο.</string>
|
||||
<string name="enable_encryption_error">Παρουσιάστηκε σφάλμα κατά την ενεργοποίηση της κρυπτογράφησης</string>
|
||||
<string name="disable_encryption_error">Παρουσιάστηκε σφάλμα κατά την απενεργοποίηση της κρυπτογράφησης</string>
|
||||
<string name="permission_denied">Η άδεια απορρίφθηκε</string>
|
||||
<string name="andotp_new_format">Νέα μορφή (v0.6.3 ή νεότερη) </string>
|
||||
<string name="andotp_old_format">Παλιά μορφή (v0.6.2 ή παλαιότερη) </string>
|
||||
<string name="choose_andotp_importer">Ποια μορφή έχει to αρχείο αντιγράφου ασφαλείας andOTP;</string>
|
||||
<string name="choose_application">Επιλέξτε εφαρμογή εισαγωγής</string>
|
||||
<string name="choose_theme">Επιλέξτε επιθυμητό θέμα</string>
|
||||
<string name="choose_view_mode">Επιλέξτε επιθυμητή λειτουργία προβολής</string>
|
||||
<string name="parsing_file_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάλυσης του αρχείου</string>
|
||||
<string name="file_not_found">Σφάλμα: Το αρχείο δεν βρέθηκε</string>
|
||||
<string name="reading_file_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάγνωσης του αρχείου</string>
|
||||
<string name="app_lookup_error">Σφάλμα: Η εφαρμογή δεν έχει εγκατασταθεί</string>
|
||||
<string name="root_error">Σφάλμα: δεν είναι δυνατή η απόκτηση πρόσβασης root</string>
|
||||
<string name="imported_entries_count">Εισήχθησαν %d καταχωρήσεις</string>
|
||||
<string name="read_entries_count">Αναγνώστηκαν %d καταχωρήσεις. %d σφάλματα.</string>
|
||||
<string name="import_error_title">Παρουσιάστηκαν ένα ή περισσότερα σφάλματα κατά την εισαγωγή</string>
|
||||
<string name="exporting_vault_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια εξαγωγής της λίστας</string>
|
||||
<string name="exported_vault">Η λίστα έχει εξαχθεί</string>
|
||||
<string name="export_warning">Αυτή η ενέργεια θα εξάγει τη λίστα από την ιδιωτική αποθήκη του Aegis.</string>
|
||||
<string name="no_cameras_available">Δεν υπάρχουν διαθέσιμες κάμερες</string>
|
||||
<string name="read_qr_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανάγνωσης του κώδικα QR</string>
|
||||
<string name="authentication_method_raw">Ακατέργαστη</string>
|
||||
<string name="unlocking_vault">Ξεκλείδωμα λίστας</string>
|
||||
<string name="unlocking_vault_repair">Ξεκλείδωμα λίστας (επισκευή)</string>
|
||||
<string name="password_slot_error">Πρέπει να έχετε τουλάχιστον μία υποδοχή κωδικού</string>
|
||||
<string name="remove_slot">Κατάργηση υποδοχής</string>
|
||||
<string name="remove_slot_description">Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτή την υποδοχή;</string>
|
||||
<string name="remove_group">Κατάργηση ομάδας</string>
|
||||
<string name="remove_group_description">Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτήν την ομάδα; Οι καταχωρίσεις σε αυτήν θα μεταβούν αυτόματα στο \"Χωρίς ομάδα\".</string>
|
||||
<string name="adding_new_slot_error">Παρουσιάστηκε σφάλμα κατά την προσπάθεια προσθήκης νέας υποδοχής:</string>
|
||||
<string name="progressbar_error">Δεν είναι δυνατή η επαναφορά της κλίμακας. Οι γραμμές προόδου θα είναι αόρατες.</string>
|
||||
<string name="details">Λεπτομέρειες</string>
|
||||
<string name="filter">Φίλτρο</string>
|
||||
<string name="filter_ungrouped">Χωρίς ομάδα</string>
|
||||
<string name="lock">Κλείδωμα</string>
|
||||
<string name="all">Όλα</string>
|
||||
<string name="name">Όνομα</string>
|
||||
<string name="no_group">Χωρίς ομάδα</string>
|
||||
<string name="sort_alphabetically">Εκδότης (Α προς Ζ)</string>
|
||||
<string name="sort_alphabetically_reverse">Εκδότης (Ζ προς Α)</string>
|
||||
<string name="sort_alphabetically_name">Λογαριασμός (Α προς Ζ)</string>
|
||||
<string name="sort_alphabetically_name_reverse">Λογαριασμός (Z προς A)</string>
|
||||
<string name="sort_custom">Προσαρμογή</string>
|
||||
<string name="new_group">Νέα ομάδα…</string>
|
||||
<string name="enter_group_name">Εισαγωγή ονόματος ομάδας</string>
|
||||
<string name="group_name_hint">Όνομα ομάδας</string>
|
||||
<string name="preference_manage_groups">Επεξεργασία ομάδων</string>
|
||||
<string name="preference_manage_groups_summary">Διαχείριση και διαγραφή ομάδων</string>
|
||||
<string name="pref_search_name_title">Αναζήτηση στα ονόματα λογαριασμών</string>
|
||||
<string name="pref_search_name_summary">Συμπερίληψη αντιστοίχισης ονόματος λογαριασμού στα αποτελέσματα αναζήτησης</string>
|
||||
<string name="pref_highlight_entry_title">Επισήμανση αναγνωριστικών όταν πατηθεί</string>
|
||||
<string name="pref_highlight_entry_summary">Κάνετε τα αναγνωριστικά να ξεχωρίζουν ευκολότερα το ένα από το άλλο, επισημαίνοντας τα προσωρινά όταν τα πατήσετε</string>
|
||||
<string name="tap_to_reveal">Κρυφό</string>
|
||||
<string name="selected">Επιλεγμένο</string>
|
||||
<string name="dark_theme_title">Σκούρο θέμα</string>
|
||||
<string name="light_theme_title">Φωτεινό θέμα</string>
|
||||
<string name="normal_viewmode_title">Κανονική</string>
|
||||
<string name="compact_mode_title">Συμπαγής</string>
|
||||
<string name="small_mode_title">Μικρή</string>
|
||||
<string name="unknown_issuer">Άγνωστος εκδότης</string>
|
||||
<string name="unknown_account_name">Άγνωστο όνομα λογαριασμού</string>
|
||||
<string name="import_error_dialog">Το Aegis δεν μπόρεσε να εισαγάγει %d αναγνωριστικά. Αυτά θα παραληφθούν. Πατήστε \'Λεπτομέρειες\' για να δείτε περισσότερες πληροφορίες σχετικά με τα σφάλματα.</string>
|
||||
<string name="unable_to_read_qrcode">Αδυναμία ανάγνωσης και επεξεργασίας κώδικα QR</string>
|
||||
<string name="select_picture">Επιλέξτε εικόνα</string>
|
||||
<string name="toggle_checkboxes">Εναλλαγή πλαισίων ελέγχου</string>
|
||||
<string name="search">Αναζήτηση</string>
|
||||
<string name="channel_name_lock_status">Κατάσταση κλειδώματος</string>
|
||||
<string name="channel_description_lock_status">To Aegis μπορεί να δημιουργήσει μια επίμονη ειδοποίηση για να σας ειδοποιήσει όταν η λίστα είναι κλειδωμένη</string>
|
||||
<string name="vault_unlocked_state">Λίστα ξεκλείδωτη. Πατήστε για κλείδωμα.</string>
|
||||
<string name="title_activity_about">Σχετικά</string>
|
||||
<string name="version">Έκδοση</string>
|
||||
<string name="changelog">Αρχείο καταγραφής αλλαγών</string>
|
||||
<string name="whats_new">Τι νέο υπάρχει</string>
|
||||
<string name="github_description">Πηγαίος κώδικας, προβλήματα και πληροφορίες</string>
|
||||
<string name="licenses">Άδειες</string>
|
||||
<string name="licenses_description">Άδειες χρήσης βιβλιοθηκών του Aegis</string>
|
||||
<string name="country_netherlands">Ολλανδία</string>
|
||||
<string name="email_us">Γράψτε ένα email</string>
|
||||
<string name="visit_website">Επισκεφτείτε την ιστοσελίδα μας</string>
|
||||
<string name="about_support">Υποστήριξη</string>
|
||||
<string name="support_rate">Αξιολόγηση</string>
|
||||
<string name="support_rate_description">Υποστηρίξτε μας αφήνοντας μια κριτική στο Google Play Store</string>
|
||||
<string name="webview_error">Αυτή η συσκευή δεν υποστηρίζει προβολή ιστού, η οποία είναι απαραίτητη για την προβολή του αρχείου καταγραφής αλλαγών. Λείπει ένα στοιχείο συστήματος.</string>
|
||||
<string name="email">Email</string>
|
||||
<string name="empty_list">Δεν υπάρχουν κωδικοί προς εμφάνιση. Προσθήκη καταχωρήσεων πατώντας στο σύμβολο συν στην κάτω δεξιά γωνία</string>
|
||||
<string name="empty_list_title">Δεν βρέθηκε καμία καταχώρηση</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user