Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/passman-android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbinsky08 <timo@binsky.org>2022-03-27 21:53:51 +0300
committerbinsky08 <timo@binsky.org>2022-03-27 21:53:51 +0300
commitb0a025bb5657998300939a6ede9603fbfe441f37 (patch)
treee8d6fb6fb65efd34ab9dfa2a1e50f98412610b15
parentdcc2bb2ab8ffbb2198e15a7a55754e63f7d851f8 (diff)
parent982fc25fc67c2c6267ad00d68235fc0ccdc08bdf (diff)
Merge branch 'upstream-master' into sso-3-backport
Signed-off-by: binsky08 <timo@binsky.org>
-rw-r--r--README.md22
-rw-r--r--app/build.gradle12
-rw-r--r--app/src/main/AndroidManifest.xml6
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java22
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java77
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java22
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java33
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java146
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java33
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java22
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java22
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java133
-rw-r--r--app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java161
-rw-r--r--app/src/main/java/es/wolfi/app/passman/EditPasswordTextItem.java11
-rw-r--r--app/src/main/java/es/wolfi/app/passman/OfflineStorage.java228
-rw-r--r--app/src/main/java/es/wolfi/app/passman/OfflineStorageValues.java38
-rw-r--r--app/src/main/java/es/wolfi/app/passman/PassmanReceiver.java22
-rw-r--r--app/src/main/java/es/wolfi/app/passman/SettingValues.java6
-rw-r--r--app/src/main/java/es/wolfi/app/passman/SettingsCache.java165
-rw-r--r--app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java16
-rw-r--r--app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java49
-rw-r--r--app/src/main/java/es/wolfi/app/passman/adapters/CredentialViewAdapter.java7
-rw-r--r--app/src/main/java/es/wolfi/app/passman/adapters/VaultViewAdapter.java121
-rw-r--r--app/src/main/java/es/wolfi/app/passman/autofill/AutofillField.java22
-rw-r--r--app/src/main/java/es/wolfi/app/passman/autofill/AutofillFieldCollection.java22
-rw-r--r--app/src/main/java/es/wolfi/app/passman/autofill/CredentialAutofillService.java34
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/CredentialDisplayFragment.java81
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java8
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/CredentialItemFragment.java59
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java42
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/VaultAddFragment.java161
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/VaultDeleteFragment.java158
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/VaultEditFragment.java146
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/VaultFragment.java56
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/VaultLockScreenFragment.java6
-rw-r--r--app/src/main/java/es/wolfi/passman/API/Core.java185
-rw-r--r--app/src/main/java/es/wolfi/passman/API/Credential.java94
-rw-r--r--app/src/main/java/es/wolfi/passman/API/Vault.java215
-rw-r--r--app/src/main/java/es/wolfi/utils/CredentialLabelSort.java22
-rw-r--r--app/src/main/java/es/wolfi/utils/FileUtils.java30
-rw-r--r--app/src/main/java/es/wolfi/utils/FilterListAsyncTask.java32
-rw-r--r--app/src/main/java/es/wolfi/utils/IconUtils.java22
-rw-r--r--app/src/main/java/es/wolfi/utils/KeyStoreUtils.java323
-rw-r--r--app/src/main/java/es/wolfi/utils/PasswordGenerator.java22
-rw-r--r--app/src/main/java/es/wolfi/utils/ProgressUtils.java22
-rw-r--r--app/src/main/res/layout/fragment_edit_password_text_item.xml3
-rw-r--r--app/src/main/res/layout/fragment_settings.xml29
-rw-r--r--app/src/main/res/layout/fragment_vault.xml22
-rw-r--r--app/src/main/res/layout/fragment_vault_add.xml125
-rw-r--r--app/src/main/res/layout/fragment_vault_delete.xml87
-rw-r--r--app/src/main/res/layout/fragment_vault_edit.xml73
-rw-r--r--app/src/main/res/layout/fragment_vault_list.xml39
-rw-r--r--app/src/main/res/values-bg-rBG/strings.xml74
-rw-r--r--app/src/main/res/values-cs-rCZ/strings.xml9
-rw-r--r--app/src/main/res/values-de/strings.xml9
-rw-r--r--app/src/main/res/values-es/strings.xml35
-rw-r--r--app/src/main/res/values-eu/strings.xml4
-rw-r--r--app/src/main/res/values-fr/strings.xml9
-rw-r--r--app/src/main/res/values-hr/strings.xml74
-rw-r--r--app/src/main/res/values-hu-rHU/strings.xml5
-rw-r--r--app/src/main/res/values-it/strings.xml21
-rw-r--r--app/src/main/res/values-lt-rLT/strings.xml2
-rw-r--r--app/src/main/res/values-nl/strings.xml5
-rw-r--r--app/src/main/res/values-pl/strings.xml9
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--app/src/main/res/values-sk-rSK/strings.xml5
-rw-r--r--app/src/main/res/values-sl/strings.xml2
-rw-r--r--app/src/main/res/values-tr/strings.xml9
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--app/src/main/res/values-zh-rHK/strings.xml9
-rw-r--r--app/src/main/res/values/strings.xml9
-rw-r--r--app/src/main/res/values/styles.xml6
-rw-r--r--build.gradle2
-rw-r--r--fastlane/metadata/android/en-US/changelogs/14.txt9
-rw-r--r--fastlane/metadata/android/en-US/full_description.txt5
-rw-r--r--gradle.properties.example1
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
77 files changed, 3508 insertions, 335 deletions
diff --git a/README.md b/README.md
index 69fd767..a8b93de 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ The passwords will be provided by [Passman](https://github.com/nextcloud/passman
## Current features
- Setup app (enter the nextcloud server settings)
- App start password option based on the android user authentication
-- Display vault list
+- View, add, rename and delete vaults
- Login to vault
- Display credential list
- View, add, edit and delete credentials
@@ -26,6 +26,8 @@ The passwords will be provided by [Passman](https://github.com/nextcloud/passman
- OTP generation
- Basic Android autofill implementation
- Password generator
+- Encrypted offline cache
+- Encrypted stored vault and cloud connection passwords
## FAQ
@@ -36,7 +38,7 @@ The passwords will be provided by [Passman](https://github.com/nextcloud/passman
- 10.0.2.2
- 10.0.2.2:8080
- mycloud.example.com/
-- Fill in your Nextcloud user and password
+- Fill in your Nextcloud user and password (will be stored encrypted)
- Press the connect button
### What is the design (intention) for log out on the vaults?
@@ -71,10 +73,24 @@ The passwords will be provided by [Passman](https://github.com/nextcloud/passman
- You can't access the deleted credentials with the Passman Android app at the moment
### I don't have enough storage on my phone to install Passman Android from an App Store, what can I do?
-- You could try to install the apk from the GithHub release which matches to your phones CPU architecture and is usually smaller than the App Stores version
+- You could try to install the apk from the GitHub release which matches to your phones CPU architecture and is usually smaller than the App Stores version
- https://github.com/nextcloud/passman-android/releases/latest
- The apks that are delivered from the App Stores combines the required files for all supported architectures
+### What means "Encrypted offline cache"?
+- By default vaults and credentials are stored in the offline cache
+- If your device has at least Android 6 / API 23 the offline cache will be stored encrypted
+ - Since credentials are already encrypted with the vault password, they will be encrypted twice
+- It's called cache because it works like a read-only fallback mode in case your cloud is not reachable over the network
+ - that means vaults and credentials can not be edited without a working cloud connection
+
+### How far can I trust the local storage encryption? Is it save to store my vault password on the device?
+- The [Android keystore system](https://developer.android.com/training/articles/keystore) is used to encrypt a random generated password with AES/GCM
+ - The Android keystore system uses special hardware mechanisms to protect the key
+- This random generated password is used to encrypt all locally stored sensitive data (like the offline cache and stored vault passwords) with the AES-256 encryption that is already used to encrypt credentials
+- If you trust the Android keystore system it should be safe to store your vault password on the device
+ - But don't forget that the security of the saved passwords depends on the access protection of your Android phone if you store your vault password on the device!
+
## Build locally
### Required packages
diff --git a/app/build.gradle b/app/build.gradle
index 3f8bf09..b078066 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,12 +26,12 @@ android {
v2SigningEnabled true
}
}
- compileSdkVersion 30
- buildToolsVersion '30.0.2'
+ compileSdkVersion 31
+ buildToolsVersion '30.0.3'
defaultConfig {
applicationId "es.wolfi.app.passman"
minSdkVersion 21
- targetSdkVersion 30
+ targetSdkVersion 31
versionCode 13
versionName "1.2.1"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -108,8 +108,8 @@ dependencies {
// Nextcloud SSO
implementation "com.github.nextcloud:Android-SingleSignOn:0.5.6"
- implementation 'androidx.appcompat:appcompat:1.3.1'
- implementation 'com.google.android.material:material:1.4.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
implementation 'com.jakewharton:butterknife:10.2.3'
implementation 'com.koushikdutta.ion:ion:3.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -118,6 +118,8 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
implementation 'com.loopj.android:android-async-http:1.4.11'
implementation 'com.caverock:androidsvg:1.4'
+ implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
+ implementation 'com.vdurmont:semver4j:3.1.0'
testImplementation 'junit:junit:4.13.2'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d951f8e..50b0a58 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,7 +22,8 @@
<activity
android:name=".activities.PasswordListActivity"
android:label="@string/app_name"
- android:theme="@style/AppTheme.NoActionBar">
+ android:theme="@style/AppTheme.NoActionBar"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -40,7 +41,8 @@
<service
android:name=".autofill.CredentialAutofillService"
android:label="Passman Credential Autofill Service"
- android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+ android:exported="true">
<meta-data
android:name="android.autofill"
android:resource="@xml/autofill_service" />
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java
index 5a2e8d5..da8b1d7 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.content.Context;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java
index 4497c4f..aa7e38f 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java
@@ -1,5 +1,30 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
+import android.os.Handler;
+import android.os.Looper;
+
import com.koushikdutta.async.future.FutureCallback;
import com.loopj.android.http.AsyncHttpResponseHandler;
@@ -21,34 +46,44 @@ public class CoreAPIGETResponseHandler extends AsyncHttpResponseHandler {
@Override
public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) {
String result = new String(responseBody);
- if (statusCode == 200) {
- if (JSONUtils.isJSONObject(result)) {
- try {
- JSONObject o = new JSONObject(result);
- if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
- callback.onCompleted(new Exception("401"), null);
- return;
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ if (statusCode == 200) {
+ if (JSONUtils.isJSONObject(result)) {
+ try {
+ JSONObject o = new JSONObject(result);
+ if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
+ callback.onCompleted(new Exception("401"), null);
+ return;
+ }
+ } catch (JSONException e1) {
+ e1.printStackTrace();
+ }
}
- } catch (JSONException e1) {
- e1.printStackTrace();
}
+ callback.onCompleted(null, result);
}
- }
- callback.onCompleted(null, result);
+ });
}
@Override
public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
- String errorMessage = error.getMessage();
- if (errorMessage == null) {
- error.printStackTrace();
- errorMessage = "Unknown error";
- }
- if (statusCode == 401) {
- callback.onCompleted(new Exception("401"), null);
- } else {
- callback.onCompleted(new Exception(errorMessage), null);
- }
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ String errorMessage = error.getMessage();
+ if (errorMessage == null) {
+ error.printStackTrace();
+ errorMessage = "Unknown error";
+ }
+ if (statusCode == 401) {
+ callback.onCompleted(new Exception("401"), null);
+ } else {
+ callback.onCompleted(new Exception(errorMessage), null);
+ }
+ }
+ });
}
@Override
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java
index e515d51..708aafd 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.app.ProgressDialog;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java
index 12a2c4d..673c666 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.app.ProgressDialog;
@@ -84,12 +106,17 @@ public class CredentialDeleteResponseHandler extends AsyncHttpResponseHandler {
public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
alreadySaving.set(false);
progress.dismiss();
- String response = new String(responseBody);
+ String response = "";
+
+ if (responseBody != null && responseBody.length > 0) {
+ response = new String(responseBody);
+ }
+ final String finalResponse = response;
passwordListActivity.runOnUiThread(() -> {
- if (!response.equals("") && JSONUtils.isJSONObject(response)) {
+ if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) {
try {
- JSONObject o = new JSONObject(response);
+ JSONObject o = new JSONObject(finalResponse);
if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show();
return;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java
new file mode 100644
index 0000000..71a6b3b
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java
@@ -0,0 +1,146 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.ResponseHandlers;
+
+import android.app.ProgressDialog;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import cz.msebera.android.httpclient.Header;
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.JSONUtils;
+
+public class CredentialSaveForNewVaultResponseHandler extends AsyncHttpResponseHandler {
+
+ private final AtomicBoolean alreadySaving;
+ private final Vault vault;
+ private final int keyStrength;
+ private final ProgressDialog progress;
+ private final View view;
+ private final PasswordListActivity passwordListActivity;
+ private final FragmentManager fragmentManager;
+
+ public CredentialSaveForNewVaultResponseHandler(AtomicBoolean alreadySaving, Vault vault, int keyStrength, ProgressDialog progress, View view, PasswordListActivity passwordListActivity, FragmentManager fragmentManager) {
+ super();
+
+ this.alreadySaving = alreadySaving;
+ this.vault = vault;
+ this.keyStrength = keyStrength;
+ this.progress = progress;
+ this.view = view;
+ this.passwordListActivity = passwordListActivity;
+ this.fragmentManager = fragmentManager;
+ }
+
+ @Override
+ public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) {
+ String result = new String(responseBody);
+ if (statusCode == 200) {
+ try {
+ JSONObject credentialObject = new JSONObject(result);
+ if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == vault.vault_id) {
+
+ AsyncHttpResponseHandler createInitialSharingKeysResponseHandler = new AsyncHttpResponseHandler() {
+ @Override
+ public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
+ if (statusCode == 200) {
+ Toast.makeText(view.getContext(), R.string.successfully_saved, Toast.LENGTH_LONG).show();
+ Objects.requireNonNull(passwordListActivity).addVaultToCurrentLocalVaultList(vault);
+ fragmentManager.popBackStack();
+ } else {
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+
+ alreadySaving.set(false);
+ progress.dismiss();
+ }
+
+ @Override
+ public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
+
+ }
+ };
+
+ //create initial sharing keys
+ vault.updateSharingKeys(keyStrength, view.getContext(), createInitialSharingKeysResponseHandler);
+
+ return;
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ alreadySaving.set(false);
+ progress.dismiss();
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
+ alreadySaving.set(false);
+ progress.dismiss();
+ String response = new String(responseBody);
+
+ if (!response.equals("") && JSONUtils.isJSONObject(response)) {
+ try {
+ JSONObject o = new JSONObject(response);
+ if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
+ Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show();
+ return;
+ }
+ } catch (JSONException e1) {
+ e1.printStackTrace();
+ Toast.makeText(view.getContext(),
+ view.getContext().getString(R.string.error_occurred).concat(e1.getMessage() != null ? e1.getMessage() : ""),
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ if (error != null && error.getMessage() != null && statusCode != 302) {
+ error.printStackTrace();
+ Log.e("async http response", new String(responseBody));
+ Toast.makeText(view.getContext(), view.getContext().getString(R.string.error_occurred).concat(error.getMessage()), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void onRetry(int retryNo) {
+ // called when request is retried
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java
index 38863cb..7559005 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.app.ProgressDialog;
@@ -84,12 +106,17 @@ public class CredentialSaveResponseHandler extends AsyncHttpResponseHandler {
public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
alreadySaving.set(false);
progress.dismiss();
- String response = new String(responseBody);
+ String response = "";
+
+ if (responseBody != null && responseBody.length > 0) {
+ response = new String(responseBody);
+ }
+ final String finalResponse = response;
passwordListActivity.runOnUiThread(() -> {
- if (!response.equals("") && JSONUtils.isJSONObject(response)) {
+ if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) {
try {
- JSONObject o = new JSONObject(response);
+ JSONObject o = new JSONObject(finalResponse);
if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show();
return;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java
index f6a862c..638912b 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.app.ProgressDialog;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java
index ba4d9d6..eabea2e 100644
--- a/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.ResponseHandlers;
import android.app.ProgressDialog;
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java
new file mode 100644
index 0000000..17d5ae7
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java
@@ -0,0 +1,133 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.ResponseHandlers;
+
+import android.app.ProgressDialog;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.JSONUtils;
+
+public class VaultDeleteResponseHandler extends AsyncHttpResponseHandler {
+
+ private final AtomicBoolean alreadySaving;
+ private final Vault vault;
+ private final boolean isDeleteVaultContentRequest;
+ private final ProgressDialog progress;
+ private final View view;
+ private final PasswordListActivity passwordListActivity;
+ private final FragmentManager fragmentManager;
+
+ public VaultDeleteResponseHandler(AtomicBoolean alreadySaving, Vault vault, boolean isDeleteVaultContentRequest, ProgressDialog progress, View view, PasswordListActivity passwordListActivity, FragmentManager fragmentManager) {
+ super();
+
+ this.alreadySaving = alreadySaving;
+ this.vault = vault;
+ this.isDeleteVaultContentRequest = isDeleteVaultContentRequest;
+ this.progress = progress;
+ this.view = view;
+ this.passwordListActivity = passwordListActivity;
+ this.fragmentManager = fragmentManager;
+ }
+
+ @Override
+ public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) {
+ String result = new String(responseBody);
+ if (statusCode == 200) {
+ try {
+ JSONObject responseObject = new JSONObject(result);
+ if (responseObject.has("ok") && responseObject.getBoolean("ok")) {
+ if (isDeleteVaultContentRequest) {
+ final AsyncHttpResponseHandler responseHandler = new VaultDeleteResponseHandler(alreadySaving, vault, false, progress, view, passwordListActivity, fragmentManager);
+ vault.delete(view.getContext(), responseHandler);
+ } else {
+ Toast.makeText(view.getContext(), R.string.successfully_deleted, Toast.LENGTH_LONG).show();
+
+ Objects.requireNonNull(passwordListActivity).deleteVaultInCurrentLocalVaultList(vault);
+
+ alreadySaving.set(false);
+ progress.dismiss();
+ fragmentManager.popBackStack();
+ }
+ return;
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ alreadySaving.set(false);
+ progress.dismiss();
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
+ alreadySaving.set(false);
+ progress.dismiss();
+ String response = "";
+
+ if (responseBody != null && responseBody.length > 0) {
+ response = new String(responseBody);
+ }
+
+ if (!response.equals("") && JSONUtils.isJSONObject(response)) {
+ try {
+ JSONObject o = new JSONObject(response);
+ if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
+ Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show();
+ return;
+ }
+ } catch (JSONException e1) {
+ e1.printStackTrace();
+ }
+ }
+
+ if (error != null && error.getMessage() != null && statusCode != 302) {
+ error.printStackTrace();
+ Log.e("async http response", response);
+ Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void onRetry(int retryNo) {
+ // called when request is retried
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java
new file mode 100644
index 0000000..a59e4c4
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java
@@ -0,0 +1,161 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.ResponseHandlers;
+
+import android.app.ProgressDialog;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Credential;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.JSONUtils;
+
+public class VaultSaveResponseHandler extends AsyncHttpResponseHandler {
+
+ public static String labelPrefixForFirstVaultConsistencyCredential = "Test key for vault ";
+ private final AtomicBoolean alreadySaving;
+ private final boolean updateVault;
+ private final Vault vault;
+ private final int keyStrength;
+ private final ProgressDialog progress;
+ private final View view;
+ private final PasswordListActivity passwordListActivity;
+ private final FragmentManager fragmentManager;
+
+ public VaultSaveResponseHandler(AtomicBoolean alreadySaving, boolean updateVault, Vault vault, int keyStrength, ProgressDialog progress, View view, PasswordListActivity passwordListActivity, FragmentManager fragmentManager) {
+ super();
+
+ this.alreadySaving = alreadySaving;
+ this.updateVault = updateVault;
+ this.vault = vault;
+ this.keyStrength = keyStrength;
+ this.progress = progress;
+ this.view = view;
+ this.passwordListActivity = passwordListActivity;
+ this.fragmentManager = fragmentManager;
+ }
+
+ @Override
+ public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) {
+ String result = new String(responseBody);
+ if (statusCode == 200) {
+ try {
+ if (updateVault) {
+ Vault localVaultInstance = Vault.getVaultByGuid(vault.guid);
+ if (localVaultInstance != null) {
+ localVaultInstance.setName(vault.getName());
+ }
+ alreadySaving.set(false);
+ progress.dismiss();
+ fragmentManager.popBackStack();
+ return;
+ } else {
+ JSONObject vaultObject = new JSONObject(result);
+ Vault v = Vault.fromJSON(vaultObject);
+ if (vaultObject.has("vault_id") && vaultObject.has("name") && vaultObject.getString("name").equals(vault.getName())) {
+ v.setEncryptionKey(vault.getEncryptionKey());
+
+ Toast.makeText(view.getContext(), "Vault created", Toast.LENGTH_LONG).show();
+
+ //create test credential
+ Credential testCred = new Credential();
+ testCred.setVault(v);
+
+ testCred.setLabel(labelPrefixForFirstVaultConsistencyCredential + v.getName());
+ testCred.setPassword("lorem ipsum");
+ testCred.setOtp("{}");
+ testCred.setTags("");
+ testCred.setFavicon("");
+ testCred.setUsername("");
+ testCred.setEmail("");
+ testCred.setUrl("");
+ testCred.setDescription("");
+ testCred.setFiles("[]");
+ testCred.setCustomFields("[]");
+ testCred.setCompromised(false);
+ testCred.setHidden(true);
+
+ final AsyncHttpResponseHandler responseHandler = new CredentialSaveForNewVaultResponseHandler(alreadySaving, v, keyStrength, progress, view, passwordListActivity, fragmentManager);
+ testCred.save(view.getContext(), responseHandler);
+
+ return;
+ }
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ alreadySaving.set(false);
+ progress.dismiss();
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) {
+ alreadySaving.set(false);
+ progress.dismiss();
+ String response = "";
+
+ if (responseBody != null && responseBody.length > 0) {
+ response = new String(responseBody);
+ }
+
+ if (!response.equals("") && JSONUtils.isJSONObject(response)) {
+ try {
+ JSONObject o = new JSONObject(response);
+ if (o.has("message") && o.getString("message").equals("Current user is not logged in")) {
+ Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show();
+ return;
+ }
+ } catch (JSONException e1) {
+ e1.printStackTrace();
+ }
+ }
+
+ if (error != null && error.getMessage() != null && statusCode != 302) {
+ error.printStackTrace();
+ Log.e("async http response", response);
+ Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void onRetry(int retryNo) {
+ // called when request is retried
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/EditPasswordTextItem.java b/app/src/main/java/es/wolfi/app/passman/EditPasswordTextItem.java
index 481aca6..5365ea1 100644
--- a/app/src/main/java/es/wolfi/app/passman/EditPasswordTextItem.java
+++ b/app/src/main/java/es/wolfi/app/passman/EditPasswordTextItem.java
@@ -4,6 +4,7 @@
* @copyright Copyright (c) 2017, Andy Scherzinger
* @copyright Copyright (c) 2017, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2017, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -76,7 +77,7 @@ public class EditPasswordTextItem extends LinearLayout {
ButterKnife.bind(this, v);
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
- generate_password_btn.setVisibility(View.VISIBLE);
+ setPasswordGenerationButtonVisibility(true);
}
@Override
@@ -96,6 +97,14 @@ public class EditPasswordTextItem extends LinearLayout {
password.setEnabled(enabled);
}
+ public void setPasswordGenerationButtonVisibility(boolean isVisible) {
+ if (isVisible) {
+ generate_password_btn.setVisibility(View.VISIBLE);
+ } else {
+ generate_password_btn.setVisibility(View.GONE);
+ }
+ }
+
@OnClick(R.id.toggle_password_visibility_btn)
public void toggleVisibility() {
switch (password.getInputType()) {
diff --git a/app/src/main/java/es/wolfi/app/passman/OfflineStorage.java b/app/src/main/java/es/wolfi/app/passman/OfflineStorage.java
new file mode 100644
index 0000000..bbedacc
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/OfflineStorage.java
@@ -0,0 +1,228 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package es.wolfi.app.passman;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import es.wolfi.utils.FileUtils;
+import es.wolfi.utils.KeyStoreUtils;
+
+/**
+ * The OfflineStorage class can be used to store data completely encrypted in Androids SharedPreferences.
+ * This class uses KeyStoreUtils to encrypt its data.
+ * <p>
+ * <b>Use OfflineStorage.getInstance().? instead of direct calls!</b>
+ * <p>
+ * It has getter and setter methods to store and fetch data of type string/object and int.
+ * <p>
+ * It works like a "full" layer between the Androids SharedPreferences storage engine and the running Passman app.
+ * It should be used instead of calling SharedPreferences directly.
+ */
+public class OfflineStorage {
+
+ protected static OfflineStorage offlineStorage;
+ private JSONObject storage;
+ public final static String LOG_TAG = "OfflineStorage";
+ public final static String EMPTY_STORAGE_STRING = "{}";
+
+ /**
+ * Call the initialization of the OfflineStorage at the top of each activity you want to use it.
+ * Example usage: new OfflineStorage(getBaseContext());
+ *
+ * @param context - base context
+ */
+ public OfflineStorage(Context context) {
+ try {
+ storage = new JSONObject(getOfflineStorageString());
+ } catch (JSONException | NullPointerException e) {
+ storage = new JSONObject();
+ }
+ offlineStorage = this;
+ }
+
+ private String getOfflineStorageString() {
+ return KeyStoreUtils.getString(SettingValues.OFFLINE_STORAGE.toString(), EMPTY_STORAGE_STRING);
+ }
+
+ /**
+ * Use OfflineStorage.getInstance().? instead of direct calls!
+ * Replace ? with any other public method of this class.
+ *
+ * @return OfflineStorage
+ */
+ public static OfflineStorage getInstance() {
+ return offlineStorage;
+ }
+
+ /**
+ * Call commit on closing the app to make the changes persistent.
+ * Example usage: OfflineStorage.getInstance().commit();
+ */
+ public void commit() {
+ if (isEnabled() && storage != null) {
+ KeyStoreUtils.putStringAndCommit(SettingValues.OFFLINE_STORAGE.toString(), storage.toString());
+ Log.d(LOG_TAG, "committed");
+ }
+ }
+
+ /**
+ * Clear offline storage persistent.
+ * Should be only used if errors occur or in the app settings.
+ */
+ public void clear() {
+ storage = new JSONObject();
+ KeyStoreUtils.putStringAndCommit(SettingValues.OFFLINE_STORAGE.toString(), storage.toString());
+ Log.d(LOG_TAG, "cleared and committed");
+ }
+
+ /**
+ * Returns true if the offline storage feature is enabled (in the settings).
+ *
+ * @return boolean
+ */
+ public boolean isEnabled() {
+ return SettingsCache.getBoolean(SettingValues.ENABLE_OFFLINE_CACHE.toString(), true);
+ }
+
+ /**
+ * Calculate a human readable output of the current offline storage size.
+ *
+ * @return String
+ */
+ public String getSize() {
+ int bytes = getOfflineStorageString().getBytes().length;
+ if (isEnabled() && bytes <= EMPTY_STORAGE_STRING.length()) {
+ bytes = storage.toString().getBytes().length;
+ }
+ if (bytes >= EMPTY_STORAGE_STRING.length()) {
+ bytes -= EMPTY_STORAGE_STRING.length();
+ }
+ return FileUtils.humanReadableByteCount((Double.valueOf(bytes)).longValue(), true);
+ }
+
+ /**
+ * Checks if a key is already saved in the offline storage.
+ *
+ * @param key to check existence for
+ * @return boolean
+ */
+ public boolean has(String key) {
+ return storage.has(key);
+ }
+
+ /**
+ * Stores an Object / String in the offline storage.
+ * Example usage: OfflineStorage.getInstance().putObject(key, value);
+ *
+ * @param key String
+ * @param value Object
+ */
+ public void putObject(String key, Object value) {
+ try {
+ storage.put(key, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public Object getObject(String key) {
+ return getObject(key, null);
+ }
+
+ /**
+ * Returns an already stored Object from the offline storage.
+ * Example usage: OfflineStorage.getInstance().getObject(key, defaultObject);
+ *
+ * @param key String
+ * @param defaultObject Object - fallback if the key or it's value does not exist
+ * @return Object
+ */
+ public Object getObject(String key, Object defaultObject) {
+ Log.d(LOG_TAG, "getObject " + key);
+ try {
+ return storage.get(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return defaultObject;
+ }
+
+ public String getString(String key) {
+ return getString(key, null);
+ }
+
+ /**
+ * Returns an already stored String from the offline storage.
+ * Example usage: OfflineStorage.getInstance().getString(key, defaultString);
+ *
+ * @param key String
+ * @param defaultString String - fallback if the key or it's value does not exist
+ * @return String
+ */
+ public String getString(String key, String defaultString) {
+ Log.d(LOG_TAG, "getString " + key);
+ try {
+ return storage.getString(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return defaultString;
+ }
+
+ /**
+ * Stores an int in the offline storage.
+ * Example usage: OfflineStorage.getInstance().putInt(key, value);
+ *
+ * @param key String
+ * @param value int
+ */
+ public void putInt(String key, int value) {
+ try {
+ storage.put(key, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ /**
+ * Returns an already stored int from the offline storage.
+ * Example usage: OfflineStorage.getInstance().getInt(key, defaultInt);
+ *
+ * @param key String
+ * @param defaultInt int - fallback if the key or it's value does not exist
+ * @return int
+ */
+ public int getInt(String key, int defaultInt) {
+ Log.d(LOG_TAG, "getInt " + key);
+ try {
+ return storage.getInt(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return defaultInt;
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/OfflineStorageValues.java b/app/src/main/java/es/wolfi/app/passman/OfflineStorageValues.java
new file mode 100644
index 0000000..d41b603
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/OfflineStorageValues.java
@@ -0,0 +1,38 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.passman;
+
+public enum OfflineStorageValues {
+ VAULTS("vaults"),
+ VERSION("version");
+
+ private final String name;
+
+ OfflineStorageValues(final String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/PassmanReceiver.java b/app/src/main/java/es/wolfi/app/passman/PassmanReceiver.java
index 277ff91..3c4a41e 100644
--- a/app/src/main/java/es/wolfi/app/passman/PassmanReceiver.java
+++ b/app/src/main/java/es/wolfi/app/passman/PassmanReceiver.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.passman;
import android.content.BroadcastReceiver;
diff --git a/app/src/main/java/es/wolfi/app/passman/SettingValues.java b/app/src/main/java/es/wolfi/app/passman/SettingValues.java
index d3e95dd..794faab 100644
--- a/app/src/main/java/es/wolfi/app/passman/SettingValues.java
+++ b/app/src/main/java/es/wolfi/app/passman/SettingValues.java
@@ -35,7 +35,11 @@ public enum SettingValues {
REQUEST_RESPONSE_TIMEOUT("request_response_timeout"),
CLEAR_CLIPBOARD_DELAY("clear_clipboard_delay"),
PASSWORD_GENERATOR_SETTINGS("password_generator_settings"),
- ENABLE_PASSWORD_GENERATOR_SHORTCUT("enable_password_generator_shortcut");
+ ENABLE_PASSWORD_GENERATOR_SHORTCUT("enable_password_generator_shortcut"),
+ OFFLINE_STORAGE("offline_storage"),
+ ENABLE_OFFLINE_CACHE("enable_offline_cache"),
+ KEY_STORE_MIGRATION_STATE("key_store_migration_state"),
+ KEY_STORE_ENCRYPTION_KEY("key_store_encryption_key");
private final String name;
diff --git a/app/src/main/java/es/wolfi/app/passman/SettingsCache.java b/app/src/main/java/es/wolfi/app/passman/SettingsCache.java
new file mode 100644
index 0000000..c63da80
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/SettingsCache.java
@@ -0,0 +1,165 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.passman;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * High frequently required SettingValues from SharedPreferences should be requested using the SettingsCache.
+ * It returns/caches data directly from SharedPreferences without checking possible encryption on it.
+ * <p>
+ * Don't forget to call loadSharedPreferences() before the first usage!
+ */
+public class SettingsCache {
+ protected static SharedPreferences sharedPreferences = null;
+ protected static JSONObject cache = new JSONObject();
+
+ /**
+ * Call the initialization of the SettingsCache at the top of each activity you want to use it.
+ * Example usage: SettingsCache().loadSharedPreferences(getBaseContext());
+ *
+ * @param context - base context
+ */
+ public void loadSharedPreferences(Context context) {
+ if (sharedPreferences == null) {
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+ }
+
+ /**
+ * Can be used to get the "default shared preferences" in any activity (or a fragment of it) the SettingsCache was initially loaded.
+ *
+ * @return SharedPreferences
+ */
+ public static SharedPreferences getSharedPreferences() {
+ return sharedPreferences;
+ }
+
+ /**
+ * The SettingsCache needs to be cleared manually after changing already cached data.
+ * SettingsCache.clear() should only be called after changing settings in SharedPreferences
+ * that are accessed through the SettingsCache.
+ */
+ public static void clear() {
+ cache = new JSONObject();
+ }
+
+ /**
+ * Returns a cached String from sharedPreferences.
+ *
+ * @param key String
+ * @param fallback String - fallback if the key or it's value does not exist
+ * @return String
+ */
+ public static String getString(String key, String fallback) {
+ try {
+ if (!cache.has(key)) {
+ cache.put(key, sharedPreferences.getString(key, fallback));
+ }
+
+ return cache.getString(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return fallback;
+ }
+
+ /**
+ * Returns a cached int from sharedPreferences.
+ *
+ * @param key String
+ * @param fallback int - fallback if the key or it's value does not exist
+ * @return int
+ */
+ public static int getInt(String key, int fallback) {
+ try {
+ if (!cache.has(key)) {
+ cache.put(key, sharedPreferences.getInt(key, fallback));
+ }
+
+ return cache.getInt(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return fallback;
+ }
+
+ /**
+ * Returns a cached boolean from sharedPreferences.
+ *
+ * @param key String
+ * @param fallback boolean - fallback if the key or it's value does not exist
+ * @return boolean
+ */
+ public static boolean getBoolean(String key, boolean fallback) {
+ try {
+ if (!cache.has(key)) {
+ cache.put(key, sharedPreferences.getBoolean(key, fallback));
+ }
+
+ return cache.getBoolean(key);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return fallback;
+ }
+
+ /**
+ * Proves that SettingsCache makes things faster :)
+ *
+ * @param context Context
+ */
+ public static void runTimingTest(Context context) {
+ SettingsCache.clear();
+
+ long beforeOldMethod = System.nanoTime();
+ PreferenceManager.getDefaultSharedPreferences(context).getString(SettingValues.PASSWORD_GENERATOR_SETTINGS.toString(), null);
+ long diffOldMethod = System.nanoTime() - beforeOldMethod;
+
+ long beforeNewMethodFirst = System.nanoTime();
+ SettingsCache.getString(SettingValues.PASSWORD_GENERATOR_SETTINGS.toString(), null);
+ long diffNewMethodFirst = System.nanoTime() - beforeNewMethodFirst;
+
+ long beforeNewMethodSecond = System.nanoTime();
+ SettingsCache.getString(SettingValues.PASSWORD_GENERATOR_SETTINGS.toString(), null);
+ long diffNewMethodSecond = System.nanoTime() - beforeNewMethodSecond;
+
+ /*
+ Log.d("diffOldMethod", String.valueOf(diffOldMethod));
+ Log.d("diffNewMethodFirst", String.valueOf(diffNewMethodFirst));
+ Log.d("diffNewMethodSecond", String.valueOf(diffNewMethodSecond));
+ */
+
+ Log.d("First Cache Call", String.format("Speedup %s", (int) (Math.abs(1 - (double) diffNewMethodFirst / (double) diffOldMethod) * 100)) + "%");
+ Log.d("Second Cache Call", String.format("Speedup %s", (int) (Math.abs(1 - (double) diffNewMethodSecond / (double) diffOldMethod) * 100)) + "%");
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java
index 76f4654..e9a840a 100644
--- a/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java
+++ b/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java
@@ -66,6 +66,7 @@ import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.passman.API.Core;
+import es.wolfi.utils.KeyStoreUtils;
public class LoginActivity extends AppCompatActivity {
public final static String LOG_TAG = "LoginActivity";
@@ -119,10 +120,11 @@ public class LoginActivity extends AppCompatActivity {
}
settings = PreferenceManager.getDefaultSharedPreferences(this);
+ KeyStoreUtils.initialize(settings);
ton = SingleTon.getTon();
try {
- String host = settings.getString(SettingValues.HOST.toString(), null);
+ String host = KeyStoreUtils.getString(SettingValues.HOST.toString(), null);
if (host != null) {
URL uri = new URL(host);
@@ -137,8 +139,8 @@ public class LoginActivity extends AppCompatActivity {
Toast.makeText(getApplicationContext(), getString(R.string.wrongNCUrl), Toast.LENGTH_LONG).show();
}
- input_user.setText(settings.getString(SettingValues.USER.toString(), null));
- input_pass.setText(settings.getString(SettingValues.PASSWORD.toString(), null));
+ input_user.setText(KeyStoreUtils.getString(SettingValues.USER.toString(), null));
+ input_pass.setText(KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null));
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -212,11 +214,9 @@ public class LoginActivity extends AppCompatActivity {
@Override
public void onCompleted(Exception e, Boolean loginSuccessful) {
if (loginSuccessful) {
- settings.edit()
- .putString(SettingValues.HOST.toString(), host)
- .putString(SettingValues.USER.toString(), user)
- .putString(SettingValues.PASSWORD.toString(), pass)
- .apply();
+ KeyStoreUtils.putString(SettingValues.HOST.toString(), host);
+ KeyStoreUtils.putString(SettingValues.USER.toString(), user);
+ KeyStoreUtils.putString(SettingValues.PASSWORD.toString(), pass);
setResult(RESULT_OK);
LoginActivity.this.finish();
diff --git a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
index 1b71d94..2e6e71c 100644
--- a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
+++ b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -67,8 +68,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Objects;
+import es.wolfi.app.passman.OfflineStorage;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
+import es.wolfi.app.passman.SettingsCache;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.app.passman.fragments.CredentialAddFragment;
import es.wolfi.app.passman.fragments.CredentialDisplayFragment;
@@ -82,6 +85,7 @@ import es.wolfi.passman.API.Credential;
import es.wolfi.passman.API.File;
import es.wolfi.passman.API.Vault;
import es.wolfi.utils.FileUtils;
+import es.wolfi.utils.KeyStoreUtils;
import es.wolfi.utils.ProgressUtils;
public class PasswordListActivity extends AppCompatActivity implements
@@ -136,6 +140,9 @@ public class PasswordListActivity extends AppCompatActivity implements
checkFragmentPosition(true);
if (running) return;
+ new SettingsCache().loadSharedPreferences(getBaseContext());
+ KeyStoreUtils.initialize(settings);
+ new OfflineStorage(getBaseContext());
initialAuthentication(false);
}
@@ -244,12 +251,7 @@ public class PasswordListActivity extends AppCompatActivity implements
public void showVaults() {
this.VaultLockButton.setVisibility(View.INVISIBLE);
- Core.getAPIVersion(this, new FutureCallback<Integer>() {
- @Override
- public void onCompleted(Exception e, Integer result) {
- }
- });
HashMap<String, Vault> vaults = (HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString());
if (vaults != null) {
getSupportFragmentManager()
@@ -349,7 +351,7 @@ public class PasswordListActivity extends AppCompatActivity implements
activatedBeforeRecreate = "unlockVault";
this.VaultLockButton.setVisibility(View.VISIBLE);
Vault v = (Vault) ton.getExtra(SettingValues.ACTIVE_VAULT.toString());
- if (v.unlock(settings.getString(v.guid, ""))) {
+ if (v.unlock(KeyStoreUtils.getString(v.guid, ""))) {
showActiveVault();
return;
}
@@ -386,6 +388,11 @@ public class PasswordListActivity extends AppCompatActivity implements
((HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString())).put(v.guid, v);
ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), v);
Vault.updateAutofillVault(v, settings);
+ try {
+ OfflineStorage.getInstance().putObject(v.guid, Vault.asJson(v));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
Fragment vaultFragment = getSupportFragmentManager().findFragmentByTag("vault");
@@ -406,6 +413,11 @@ public class PasswordListActivity extends AppCompatActivity implements
ton.removeExtra(SettingValues.ACTIVE_VAULT.toString());
ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), v);
Vault.updateAutofillVault(v, settings);
+ try {
+ OfflineStorage.getInstance().putObject(v.guid, Vault.asJson(v));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
Fragment vaultFragment = getSupportFragmentManager().findFragmentByTag("vault");
@@ -426,6 +438,11 @@ public class PasswordListActivity extends AppCompatActivity implements
ton.removeExtra(SettingValues.ACTIVE_VAULT.toString());
ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), v);
Vault.updateAutofillVault(v, settings);
+ try {
+ OfflineStorage.getInstance().putObject(v.guid, Vault.asJson(v));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
Fragment vaultFragment = getSupportFragmentManager().findFragmentByTag("vault");
@@ -438,6 +455,20 @@ public class PasswordListActivity extends AppCompatActivity implements
}
}
+ public void addVaultToCurrentLocalVaultList(Vault vault) {
+ HashMap<String, Vault> vaults = (HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString());
+ vaults.put(vault.guid, vault);
+ ton.removeExtra(SettingValues.VAULTS.toString());
+ ton.addExtra(SettingValues.VAULTS.toString(), vaults);
+ }
+
+ public void deleteVaultInCurrentLocalVaultList(Vault vault) {
+ HashMap<String, Vault> vaults = (HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString());
+ vaults.remove(vault.guid);
+ ton.removeExtra(SettingValues.VAULTS.toString());
+ ton.addExtra(SettingValues.VAULTS.toString(), vaults);
+ }
+
void refreshVault() {
final Vault vault = (Vault) ton.getExtra(SettingValues.ACTIVE_VAULT.toString());
ProgressDialog progress = ProgressUtils.showLoadingSequence(this);
@@ -860,4 +891,10 @@ public class PasswordListActivity extends AppCompatActivity implements
checkFragmentPosition(false);
super.onBackPressed();
}
+
+ @Override
+ public void onPause() {
+ OfflineStorage.getInstance().commit();
+ super.onPause();
+ }
}
diff --git a/app/src/main/java/es/wolfi/app/passman/adapters/CredentialViewAdapter.java b/app/src/main/java/es/wolfi/app/passman/adapters/CredentialViewAdapter.java
index da927e4..4a49a1d 100644
--- a/app/src/main/java/es/wolfi/app/passman/adapters/CredentialViewAdapter.java
+++ b/app/src/main/java/es/wolfi/app/passman/adapters/CredentialViewAdapter.java
@@ -32,6 +32,7 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
+import es.wolfi.app.ResponseHandlers.VaultSaveResponseHandler;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.fragments.CredentialItemFragment;
@@ -66,6 +67,12 @@ public class CredentialViewAdapter extends RecyclerView.Adapter<CredentialViewAd
holder.mItem = mValues.get(position);
holder.mContentView.setText(holder.mItem.getLabel());
+ // the automatically created test credential must always be there to ensure vault consistency
+ if (holder.mItem.getLabel().startsWith(VaultSaveResponseHandler.labelPrefixForFirstVaultConsistencyCredential)) {
+ holder.itemView.setVisibility(View.GONE);
+ holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(0, 0));
+ }
+
if (holder.mItem != null && holder.mItem.getCompromised() != null && holder.mItem.getCompromised().equals("true")) {
holder.contentLayout.setBackgroundColor(holder.mView.getResources().getColor(R.color.compromised));
} else {
diff --git a/app/src/main/java/es/wolfi/app/passman/adapters/VaultViewAdapter.java b/app/src/main/java/es/wolfi/app/passman/adapters/VaultViewAdapter.java
index eb13198..49d76ed 100644
--- a/app/src/main/java/es/wolfi/app/passman/adapters/VaultViewAdapter.java
+++ b/app/src/main/java/es/wolfi/app/passman/adapters/VaultViewAdapter.java
@@ -1,43 +1,59 @@
/**
- * Passman Android App
+ * Passman Android App
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
- *
+ * <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
- *
+ * <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ * <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
*/
package es.wolfi.app.passman.adapters;
-import androidx.recyclerview.widget.RecyclerView;
+import android.app.ProgressDialog;
+import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.koushikdutta.async.future.FutureCallback;
+import com.vdurmont.semver4j.Semver;
import java.text.DateFormat;
+import java.util.HashMap;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.SettingValues;
+import es.wolfi.app.passman.SingleTon;
+import es.wolfi.app.passman.fragments.VaultDeleteFragment;
+import es.wolfi.app.passman.fragments.VaultEditFragment;
import es.wolfi.app.passman.fragments.VaultFragment.OnListFragmentInteractionListener;
+import es.wolfi.passman.API.Core;
import es.wolfi.passman.API.Vault;
import es.wolfi.utils.ColorUtils;
+import es.wolfi.utils.ProgressUtils;
/**
* {@link RecyclerView.Adapter} that can display a {@link Vault} and makes a call to the
@@ -48,10 +64,12 @@ public class VaultViewAdapter extends RecyclerView.Adapter<VaultViewAdapter.View
private final List<Vault> mValues;
private final OnListFragmentInteractionListener mListener;
+ private final FragmentManager fragmentManager;
- public VaultViewAdapter(List<Vault> items, OnListFragmentInteractionListener listener) {
+ public VaultViewAdapter(List<Vault> items, OnListFragmentInteractionListener listener, FragmentManager fragmentManager) {
mValues = items;
mListener = listener;
+ this.fragmentManager = fragmentManager;
}
@Override
@@ -86,6 +104,82 @@ public class VaultViewAdapter extends RecyclerView.Adapter<VaultViewAdapter.View
}
}
});
+
+ holder.vault_edit_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Context context = view.getContext();
+ final ProgressDialog progress = ProgressUtils.showLoadingSequence(context);
+ Vault.getVault(context, holder.mItem.guid, new FutureCallback<Vault>() {
+ @Override
+ public void onCompleted(Exception e, Vault result) {
+ progress.dismiss();
+ if (e != null) {
+ Log.e(TAG, "Unknown network error", e);
+
+ Toast.makeText(context, context.getString(R.string.net_error), Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ // Update the local vault record
+ ((HashMap<String, Vault>) SingleTon.getTon().getExtra(SettingValues.VAULTS.toString())).put(result.guid, result);
+
+ fragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_out_left, R.anim.slide_out_left)
+ .replace(R.id.content_password_list, VaultEditFragment.newInstance(holder.mItem.guid), "vault")
+ .addToBackStack(null)
+ .commit();
+ }
+ });
+ }
+ });
+
+ Core.getAPIVersion(holder.mView.getContext(), new FutureCallback<String>() {
+ @Override
+ public void onCompleted(Exception e, String result) {
+ if (result != null && new Semver(result).isGreaterThanOrEqualTo("2.4.0")) {
+ holder.vault_delete_button.setColorFilter(holder.mView.getResources().getColor(R.color.danger));
+ holder.vault_delete_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Context context = holder.mView.getContext();
+ final ProgressDialog progress = ProgressUtils.showLoadingSequence(context);
+ progress.show();
+
+ Vault.getVault(context, holder.mItem.guid, new FutureCallback<Vault>() {
+ @Override
+ public void onCompleted(Exception e, Vault result) {
+ progress.dismiss();
+ if (e != null) {
+ Log.e(TAG, "Unknown network error", e);
+
+ Toast.makeText(context, context.getString(R.string.net_error), Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ SingleTon ton = SingleTon.getTon();
+
+ // Update the vault record to avoid future loads
+ ((HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString())).put(result.guid, result);
+
+ ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), result);
+
+ fragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_out_left, R.anim.slide_out_left)
+ .replace(R.id.content_password_list, VaultDeleteFragment.newInstance(holder.mItem.guid), "vault")
+ .addToBackStack(null)
+ .commit();
+ }
+ });
+ }
+ });
+ } else {
+ Log.d(TAG, "vault deletion not supported");
+ }
+ }
+ });
}
@Override
@@ -94,9 +188,16 @@ public class VaultViewAdapter extends RecyclerView.Adapter<VaultViewAdapter.View
}
public class ViewHolder extends RecyclerView.ViewHolder {
- @BindView(R.id.vault_name) TextView name;
- @BindView(R.id.vault_created) TextView created;
- @BindView(R.id.vault_last_access) TextView last_access;
+ @BindView(R.id.vault_name)
+ TextView name;
+ @BindView(R.id.vault_created)
+ TextView created;
+ @BindView(R.id.vault_last_access)
+ TextView last_access;
+ @BindView(R.id.vault_edit_button)
+ ImageView vault_edit_button;
+ @BindView(R.id.vault_delete_button)
+ ImageView vault_delete_button;
public final View mView;
public Vault mItem;
diff --git a/app/src/main/java/es/wolfi/app/passman/autofill/AutofillField.java b/app/src/main/java/es/wolfi/app/passman/autofill/AutofillField.java
index 59e2a75..8e2d5bc 100644
--- a/app/src/main/java/es/wolfi/app/passman/autofill/AutofillField.java
+++ b/app/src/main/java/es/wolfi/app/passman/autofill/AutofillField.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.passman.autofill;
import android.app.assist.AssistStructure;
diff --git a/app/src/main/java/es/wolfi/app/passman/autofill/AutofillFieldCollection.java b/app/src/main/java/es/wolfi/app/passman/autofill/AutofillFieldCollection.java
index ad9934c..e416092 100644
--- a/app/src/main/java/es/wolfi/app/passman/autofill/AutofillFieldCollection.java
+++ b/app/src/main/java/es/wolfi/app/passman/autofill/AutofillFieldCollection.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.passman.autofill;
import java.util.ArrayList;
diff --git a/app/src/main/java/es/wolfi/app/passman/autofill/CredentialAutofillService.java b/app/src/main/java/es/wolfi/app/passman/autofill/CredentialAutofillService.java
index 9ee9aa4..647971b 100644
--- a/app/src/main/java/es/wolfi/app/passman/autofill/CredentialAutofillService.java
+++ b/app/src/main/java/es/wolfi/app/passman/autofill/CredentialAutofillService.java
@@ -1,5 +1,29 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.app.passman.autofill;
+import static android.service.autofill.SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE;
+
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.content.SharedPreferences;
@@ -36,7 +60,6 @@ import org.json.JSONObject;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -47,9 +70,7 @@ import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.passman.API.Credential;
import es.wolfi.passman.API.Vault;
-import es.wolfi.utils.JSONUtils;
-
-import static android.service.autofill.SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE;
+import es.wolfi.utils.KeyStoreUtils;
@RequiresApi(api = Build.VERSION_CODES.O)
public final class CredentialAutofillService extends AutofillService {
@@ -543,13 +564,14 @@ public final class CredentialAutofillService extends AutofillService {
private Vault getAutofillVault(SingleTon ton) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+ KeyStoreUtils.initialize(settings);
Vault activeVault = (Vault) ton.getExtra(SettingValues.ACTIVE_VAULT.toString());
String autofillVaultGuid = settings.getString(SettingValues.AUTOFILL_VAULT_GUID.toString(), null);
if (activeVault != null && autofillVaultGuid != null && !activeVault.guid.equals(autofillVaultGuid) && !autofillVaultGuid.equals("")) {
try {
- Vault requestedVault = Vault.fromJSON(new JSONObject(settings.getString(SettingValues.AUTOFILL_VAULT.toString(), "")));
- requestedVault.unlock(settings.getString(autofillVaultGuid, ""));
+ Vault requestedVault = Vault.fromJSON(new JSONObject(KeyStoreUtils.getString(SettingValues.AUTOFILL_VAULT.toString(), "")));
+ requestedVault.unlock(KeyStoreUtils.getString(autofillVaultGuid, ""));
return requestedVault;
} catch (JSONException e) {
e.printStackTrace();
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialDisplayFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialDisplayFragment.java
index 8613c25..a63f5d9 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialDisplayFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialDisplayFragment.java
@@ -125,6 +125,7 @@ public class CredentialDisplayFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
if (getArguments() != null) {
Vault v = (Vault) SingleTon.getTon().getExtra(SettingValues.ACTIVE_VAULT.toString());
if (v != null) {
@@ -212,51 +213,53 @@ public class CredentialDisplayFragment extends Fragment {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
- FloatingActionButton editCredentialButton = view.findViewById(R.id.editCredentialButton);
- editCredentialButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- getParentFragmentManager()
- .beginTransaction()
- .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
- .replace(R.id.content_password_list, CredentialEditFragment.newInstance(credential.getGuid()), "credentialEdit")
- .addToBackStack(null)
- .commit();
- }
- });
- editCredentialButton.setVisibility(View.VISIBLE);
+ if (credential != null) {
+ FloatingActionButton editCredentialButton = view.findViewById(R.id.editCredentialButton);
+ editCredentialButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getParentFragmentManager()
+ .beginTransaction()
+ .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
+ .replace(R.id.content_password_list, CredentialEditFragment.newInstance(credential.getGuid()), "credentialEdit")
+ .addToBackStack(null)
+ .commit();
+ }
+ });
+ editCredentialButton.setVisibility(View.VISIBLE);
- RecyclerView filesListRecyclerView = (RecyclerView) view.findViewById(R.id.filesList);
- filesListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- filesListRecyclerView.setAdapter(new FileViewAdapter(credential.getFilesList(), filelistListener));
+ RecyclerView filesListRecyclerView = (RecyclerView) view.findViewById(R.id.filesList);
+ filesListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ filesListRecyclerView.setAdapter(new FileViewAdapter(credential.getFilesList(), filelistListener));
- RecyclerView customFieldsListRecyclerView = (RecyclerView) view.findViewById(R.id.customFieldsList);
- customFieldsListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- customFieldsListRecyclerView.setAdapter(new CustomFieldViewAdapter(credential.getCustomFieldsList(), filelistListener));
+ RecyclerView customFieldsListRecyclerView = (RecyclerView) view.findViewById(R.id.customFieldsList);
+ customFieldsListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ customFieldsListRecyclerView.setAdapter(new CustomFieldViewAdapter(credential.getCustomFieldsList(), filelistListener));
- if (credential.getCompromised().equals("true")) {
- TextView passwordLabel = view.findViewById(R.id.credential_password_label);
- passwordLabel.setBackgroundColor(getResources().getColor(R.color.compromised));
- }
+ if (credential.getCompromised().equals("true")) {
+ TextView passwordLabel = view.findViewById(R.id.credential_password_label);
+ passwordLabel.setBackgroundColor(getResources().getColor(R.color.compromised));
+ }
- label.setText(credential.getLabel());
- user.setText(credential.getUsername());
- password.setModePassword();
- password.setText(credential.getPassword());
- email.setModeEmail();
- email.setText(credential.getEmail());
- url.setText(credential.getUrl());
- description.setText(credential.getDescription());
- otp.setEnabled(false);
- IconUtils.loadIconToImageView(credential.getFavicon(), credentialIcon);
-
- if (URLUtil.isValidUrl(credential.getUrl())) {
- url.setModeURL();
- }
+ label.setText(credential.getLabel());
+ user.setText(credential.getUsername());
+ password.setModePassword();
+ password.setText(credential.getPassword());
+ email.setModeEmail();
+ email.setText(credential.getEmail());
+ url.setText(credential.getUrl());
+ description.setText(credential.getDescription());
+ otp.setEnabled(false);
+ IconUtils.loadIconToImageView(credential.getFavicon(), credentialIcon);
+
+ if (URLUtil.isValidUrl(credential.getUrl())) {
+ url.setModeURL();
+ }
- if (otp_refresh == null) {
- otp_progress.setProgress(0);
+ if (otp_refresh == null) {
+ otp_progress.setProgress(0);
+ }
}
}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
index 30ae52f..158200b 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
@@ -161,6 +161,8 @@ public class CredentialEditFragment extends Fragment implements View.OnClickList
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
+ Vault.checkCloudConnectionAndShowHint(view);
+
filesListRecyclerView = (RecyclerView) view.findViewById(R.id.filesList);
filesListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
filesListRecyclerView.setAdapter(fed);
@@ -182,7 +184,11 @@ public class CredentialEditFragment extends Fragment implements View.OnClickList
super.onCreate(savedInstanceState);
if (getArguments() != null) {
Vault v = (Vault) SingleTon.getTon().getExtra(SettingValues.ACTIVE_VAULT.toString());
- credential = v.findCredentialByGUID(getArguments().getString(CREDENTIAL));
+ try {
+ credential = Credential.clone(v.findCredentialByGUID(getArguments().getString(CREDENTIAL)));
+ } catch (JSONException e) {
+ credential = v.findCredentialByGUID(getArguments().getString(CREDENTIAL));
+ }
}
}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialItemFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialItemFragment.java
index 7d1408f..1b9511b 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialItemFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialItemFragment.java
@@ -26,10 +26,12 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextWatcher;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
+import android.widget.Toast;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.fragment.app.Fragment;
@@ -98,36 +100,41 @@ public class CredentialItemFragment extends Fragment {
final EditText searchInput = (EditText) view.findViewById(R.id.search_input);
final AppCompatImageButton toggleSortButton = (AppCompatImageButton) view.findViewById(R.id.toggle_sort_button);
- searchInput.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ if (v != null) {
+ searchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
- }
+ }
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
- applyFilters(v, searchInput);
- }
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ applyFilters(v, searchInput);
+ }
- @Override
- public void afterTextChanged(Editable editable) {
+ @Override
+ public void afterTextChanged(Editable editable) {
- }
- });
- toggleSortButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- sortMethod = (++sortMethod % 3);
- updateToggleSortButtonImage(toggleSortButton);
-
- v.sort(sortMethod);
- applyFilters(v, searchInput);
- }
- });
- v.sort(sortMethod);
- recyclerView.setAdapter(new CredentialViewAdapter(v.getCredentials(), mListener, PreferenceManager.getDefaultSharedPreferences(getContext())));
- scrollToLastPosition();
- updateToggleSortButtonImage(toggleSortButton);
+ }
+ });
+ toggleSortButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ sortMethod = (++sortMethod % 3);
+ updateToggleSortButtonImage(toggleSortButton);
+
+ v.sort(sortMethod);
+ applyFilters(v, searchInput);
+ }
+ });
+ v.sort(sortMethod);
+ recyclerView.setAdapter(new CredentialViewAdapter(v.getCredentials(), mListener, PreferenceManager.getDefaultSharedPreferences(getContext())));
+ scrollToLastPosition();
+ updateToggleSortButtonImage(toggleSortButton);
+ } else {
+ Toast.makeText(getContext(), getString(R.string.error_occurred), Toast.LENGTH_LONG).show();
+ Log.e("CredentialItemFragment", "active vault could not be found");
+ }
}
public void applyFilters(Vault vault, EditText searchInput) {
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java
index c9b2314..8576372 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -61,11 +62,14 @@ import java.util.Objects;
import java.util.Set;
import butterknife.ButterKnife;
+import es.wolfi.app.passman.OfflineStorage;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
+import es.wolfi.app.passman.SettingsCache;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.app.passman.activities.PasswordListActivity;
import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.KeyStoreUtils;
import es.wolfi.utils.PasswordGenerator;
@@ -92,6 +96,7 @@ public class SettingsFragment extends Fragment {
EditText settings_password_generator_length_value;
MaterialCheckBox enable_credential_list_icons_switch;
+ MaterialCheckBox enable_offline_cache_switch;
TextView default_autofill_vault_title;
Spinner default_autofill_vault;
@@ -99,6 +104,7 @@ public class SettingsFragment extends Fragment {
EditText request_connect_timeout_value;
EditText request_response_timeout_value;
+ Button clear_offline_cache_button;
SharedPreferences settings;
SingleSignOnAccount ssoAccount;
@@ -150,6 +156,7 @@ public class SettingsFragment extends Fragment {
settings_password_generator_length_value = view.findViewById(R.id.settings_password_generator_length_value);
enable_credential_list_icons_switch = view.findViewById(R.id.enable_credential_list_icons_switch);
+ enable_offline_cache_switch = view.findViewById(R.id.enable_offline_cache_switch);
default_autofill_vault_title = view.findViewById(R.id.default_autofill_vault_title);
default_autofill_vault = view.findViewById(R.id.default_autofill_vault);
@@ -157,6 +164,8 @@ public class SettingsFragment extends Fragment {
request_connect_timeout_value = view.findViewById(R.id.request_connect_timeout_value);
request_response_timeout_value = view.findViewById(R.id.request_response_timeout_value);
+ clear_offline_cache_button = view.findViewById(R.id.clear_offline_cache_button);
+ clear_offline_cache_button.setOnClickListener(this.getClearOfflineCacheButtonListener());
return view;
}
@@ -189,9 +198,9 @@ public class SettingsFragment extends Fragment {
sso_settings.removeAllViews();
}
- settings_nextcloud_url.setText(settings.getString(SettingValues.HOST.toString(), null));
- settings_nextcloud_user.setText(settings.getString(SettingValues.USER.toString(), null));
- settings_nextcloud_password.setText(settings.getString(SettingValues.PASSWORD.toString(), null));
+ settings_nextcloud_url.setText(KeyStoreUtils.getString(SettingValues.HOST.toString(), null));
+ settings_nextcloud_user.setText(KeyStoreUtils.getString(SettingValues.USER.toString(), null));
+ settings_nextcloud_password.setText(KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null));
settings_app_start_password_switch.setChecked(settings.getBoolean(SettingValues.ENABLE_APP_START_DEVICE_PASSWORD.toString(), false));
@@ -211,6 +220,7 @@ public class SettingsFragment extends Fragment {
}
enable_credential_list_icons_switch.setChecked(settings.getBoolean(SettingValues.ENABLE_CREDENTIAL_LIST_ICONS.toString(), true));
+ enable_offline_cache_switch.setChecked(settings.getBoolean(SettingValues.ENABLE_OFFLINE_CACHE.toString(), true));
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
String last_selected_guid = "";
@@ -242,6 +252,7 @@ public class SettingsFragment extends Fragment {
request_connect_timeout_value.setText(String.valueOf(settings.getInt(SettingValues.REQUEST_CONNECT_TIMEOUT.toString(), 15)));
request_response_timeout_value.setText(String.valueOf(settings.getInt(SettingValues.REQUEST_RESPONSE_TIMEOUT.toString(), 120)));
+ clear_offline_cache_button.setText(String.format("%s (%s)", getString(R.string.clear_offline_cache), OfflineStorage.getInstance().getSize()));
}
private Set<Map.Entry<String, Vault>> getVaultsEntrySet() {
@@ -309,6 +320,7 @@ public class SettingsFragment extends Fragment {
passwordGenerator.applyChanges();
settings.edit().putBoolean(SettingValues.ENABLE_CREDENTIAL_LIST_ICONS.toString(), enable_credential_list_icons_switch.isChecked()).commit();
+ settings.edit().putBoolean(SettingValues.ENABLE_OFFLINE_CACHE.toString(), enable_offline_cache_switch.isChecked()).commit();
settings.edit().putInt(SettingValues.CLEAR_CLIPBOARD_DELAY.toString(), Integer.parseInt(clear_clipboard_delay_value.getText().toString())).commit();
Objects.requireNonNull(((PasswordListActivity) getActivity())).attachClipboardListener();
@@ -343,9 +355,10 @@ public class SettingsFragment extends Fragment {
}
}
- if (ssoAccount == null && (!settings.getString(SettingValues.HOST.toString(), null).equals(settings_nextcloud_url.getText().toString()) ||
- !settings.getString(SettingValues.USER.toString(), null).equals(settings_nextcloud_user.getText().toString()) ||
- !settings.getString(SettingValues.PASSWORD.toString(), null).equals(settings_nextcloud_password.getText().toString()))) {
+ SettingsCache.clear();
+ if (ssoAccount == null && (!KeyStoreUtils.getString(SettingValues.HOST.toString(), null).equals(settings_nextcloud_url.getText().toString()) ||
+ !KeyStoreUtils.getString(SettingValues.USER.toString(), null).equals(settings_nextcloud_user.getText().toString()) ||
+ !KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null).equals(settings_nextcloud_password.getText().toString()))) {
ton.removeString(SettingValues.HOST.toString());
ton.removeString(SettingValues.USER.toString());
ton.removeString(SettingValues.PASSWORD.toString());
@@ -354,9 +367,9 @@ public class SettingsFragment extends Fragment {
ton.addString(SettingValues.USER.toString(), settings_nextcloud_user.getText().toString());
ton.addString(SettingValues.PASSWORD.toString(), settings_nextcloud_password.getText().toString());
- settings.edit().putString(SettingValues.HOST.toString(), settings_nextcloud_url.getText().toString()).commit();
- settings.edit().putString(SettingValues.USER.toString(), settings_nextcloud_user.getText().toString()).commit();
- settings.edit().putString(SettingValues.PASSWORD.toString(), settings_nextcloud_password.getText().toString()).commit();
+ KeyStoreUtils.putStringAndCommit(SettingValues.HOST.toString(), settings_nextcloud_url.getText().toString());
+ KeyStoreUtils.putStringAndCommit(SettingValues.USER.toString(), settings_nextcloud_user.getText().toString());
+ KeyStoreUtils.putStringAndCommit(SettingValues.PASSWORD.toString(), settings_nextcloud_password.getText().toString());
Objects.requireNonNull(((PasswordListActivity) getActivity())).applyNewSettings(true);
} else {
@@ -365,4 +378,15 @@ public class SettingsFragment extends Fragment {
}
};
}
+
+ public View.OnClickListener getClearOfflineCacheButtonListener() {
+ return new View.OnClickListener() {
+ @SuppressLint("ApplySharedPref")
+ @Override
+ public void onClick(View view) {
+ OfflineStorage.getInstance().clear();
+ clear_offline_cache_button.setText(String.format("%s (%s)", getString(R.string.clear_offline_cache), OfflineStorage.getInstance().getSize()));
+ }
+ };
+ }
}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/VaultAddFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/VaultAddFragment.java
new file mode 100644
index 0000000..9f7b66d
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/VaultAddFragment.java
@@ -0,0 +1,161 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.passman.fragments;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import es.wolfi.app.ResponseHandlers.VaultSaveResponseHandler;
+import es.wolfi.app.passman.EditPasswordTextItem;
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.ProgressUtils;
+
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link VaultAddFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class VaultAddFragment extends Fragment implements View.OnClickListener {
+ @BindView(R.id.add_vault_name_header)
+ TextView add_vault_name_header;
+ @BindView(R.id.add_vault_name)
+ EditText add_vault_name;
+
+ @BindView(R.id.add_vault_password_header)
+ TextView add_vault_password_header;
+ @BindView(R.id.add_vault_password)
+ EditPasswordTextItem add_vault_password;
+
+ @BindView(R.id.add_vault_password_repeat_header)
+ TextView add_vault_password_repeat_header;
+ @BindView(R.id.add_vault_password_repeat)
+ EditPasswordTextItem add_vault_password_repeat;
+
+ @BindView(R.id.add_vault_sharing_key_strength)
+ Spinner add_vault_sharing_key_strength;
+
+ private Vault vault;
+ private AtomicBoolean alreadySaving = new AtomicBoolean(false);
+
+ public VaultAddFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * Use this factory method to create a new instance of this fragment.
+ *
+ * @return A new instance of fragment VaultAddFragment.
+ */
+ public static VaultAddFragment newInstance() {
+ return new VaultAddFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_vault_add, container, false);
+
+ FloatingActionButton saveVaultButton = view.findViewById(R.id.SaveVaultButton);
+ saveVaultButton.setOnClickListener(this);
+ saveVaultButton.setVisibility(View.VISIBLE);
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+
+ add_vault_password.setPasswordGenerationButtonVisibility(false);
+ add_vault_password_repeat.setPasswordGenerationButtonVisibility(false);
+
+ ArrayAdapter<Integer> adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, Vault.keyStrengths);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ add_vault_sharing_key_strength.setAdapter(adapter);
+ add_vault_sharing_key_strength.setSelection(1);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.vault = new Vault();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (alreadySaving.get()) {
+ return;
+ }
+
+ if (add_vault_name.getText().toString().equals("")) {
+ add_vault_name_header.setTextColor(getResources().getColor(R.color.danger));
+ return;
+ } else {
+ add_vault_name_header.setTextColor(getResources().getColor(R.color.colorAccent));
+ }
+
+ if (!add_vault_password.getText().toString().equals(add_vault_password_repeat.getText().toString())) {
+ add_vault_password_header.setTextColor(getResources().getColor(R.color.danger));
+ add_vault_password_repeat_header.setTextColor(getResources().getColor(R.color.danger));
+ return;
+ } else {
+ add_vault_password_header.setTextColor(getResources().getColor(R.color.colorAccent));
+ add_vault_password_repeat_header.setTextColor(getResources().getColor(R.color.colorAccent));
+ }
+
+ alreadySaving.set(true);
+
+ this.vault.setName(add_vault_name.getText().toString());
+ this.vault.setEncryptionKey(add_vault_password.getText().toString());
+ int keyStrength = Integer.parseInt(add_vault_sharing_key_strength.getSelectedItem().toString());
+
+ Context context = getContext();
+ final ProgressDialog progress = ProgressUtils.showLoadingSequence(context);
+ final AsyncHttpResponseHandler responseHandler = new VaultSaveResponseHandler(alreadySaving, false, this.vault, keyStrength, progress, view, (PasswordListActivity) getActivity(), getFragmentManager());
+
+ this.vault.save(context, responseHandler);
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/VaultDeleteFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/VaultDeleteFragment.java
new file mode 100644
index 0000000..c7b7104
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/VaultDeleteFragment.java
@@ -0,0 +1,158 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.passman.fragments;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import es.wolfi.app.ResponseHandlers.VaultDeleteResponseHandler;
+import es.wolfi.app.passman.EditPasswordTextItem;
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.ProgressUtils;
+
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link VaultDeleteFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class VaultDeleteFragment extends Fragment implements View.OnClickListener {
+ public static String VAULT = "vault";
+
+ @BindView(R.id.vault_name)
+ TextView vault_name;
+ @BindView(R.id.delete_vault_password_header)
+ TextView delete_vault_password_header;
+ @BindView(R.id.delete_vault_password)
+ EditPasswordTextItem delete_vault_password;
+
+ private Vault vault;
+ private AtomicBoolean alreadySaving = new AtomicBoolean(false);
+
+ public VaultDeleteFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * Use this factory method to create a new instance of this fragment.
+ *
+ * @return A new instance of fragment VaultEditFragment.
+ */
+ public static VaultDeleteFragment newInstance(String vaultGUID) {
+ VaultDeleteFragment fragment = new VaultDeleteFragment();
+
+ Bundle b = new Bundle();
+ b.putString(VAULT, vaultGUID);
+ fragment.setArguments(b);
+
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_vault_delete, container, false);
+
+ FloatingActionButton deleteVaultButton = view.findViewById(R.id.DeleteVaultButton);
+ deleteVaultButton.setOnClickListener(this);
+ deleteVaultButton.setVisibility(View.VISIBLE);
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+
+ vault_name.setText(vault.getName());
+ delete_vault_password.setPasswordGenerationButtonVisibility(false);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null) {
+ try {
+ // copy vault to not lock it if it ws unlocked before, and a wrong password is entered
+ vault = Vault.fromJSON(new JSONObject(Vault.asJson(Vault.getVaultByGuid(getArguments().getString(VAULT)))));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ vault = Vault.getVaultByGuid(getArguments().getString(VAULT));
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (vault.unlock(delete_vault_password.getText().toString())) {
+ delete_vault_password_header.setTextColor(getResources().getColor(R.color.colorAccent));
+
+ Context context = view.getContext();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(view.getContext().getString(R.string.confirm_vault_deletion) + " (" + vault.getName() + ")");
+ builder.setCancelable(false);
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (alreadySaving.get()) {
+ return;
+ }
+ alreadySaving.set(true);
+ final ProgressDialog progress = ProgressUtils.showLoadingSequence(context);
+ final AsyncHttpResponseHandler responseHandler = new VaultDeleteResponseHandler(alreadySaving, vault, true, progress, view, (PasswordListActivity) getActivity(), getFragmentManager());
+ vault.deleteVaultContents(context, responseHandler);
+ dialogInterface.dismiss();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.show();
+ } else {
+ delete_vault_password_header.setTextColor(getResources().getColor(R.color.danger));
+ }
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/VaultEditFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/VaultEditFragment.java
new file mode 100644
index 0000000..55994f7
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/VaultEditFragment.java
@@ -0,0 +1,146 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.app.passman.fragments;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.loopj.android.http.AsyncHttpResponseHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import es.wolfi.app.ResponseHandlers.VaultSaveResponseHandler;
+import es.wolfi.app.passman.R;
+import es.wolfi.app.passman.activities.PasswordListActivity;
+import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.ProgressUtils;
+
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link VaultEditFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class VaultEditFragment extends Fragment implements View.OnClickListener {
+ public static String VAULT = "vault";
+
+ @BindView(R.id.edit_vault_name_header)
+ TextView edit_vault_name_header;
+ @BindView(R.id.edit_vault_name)
+ EditText edit_vault_name;
+
+ private Vault vault;
+ private AtomicBoolean alreadySaving = new AtomicBoolean(false);
+
+ public VaultEditFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * Use this factory method to create a new instance of this fragment.
+ *
+ * @return A new instance of fragment VaultEditFragment.
+ */
+ public static VaultEditFragment newInstance(String vaultGUID) {
+ VaultEditFragment fragment = new VaultEditFragment();
+
+ Bundle b = new Bundle();
+ b.putString(VAULT, vaultGUID);
+ fragment.setArguments(b);
+
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_vault_edit, container, false);
+
+ FloatingActionButton saveVaultButton = view.findViewById(R.id.SaveVaultButton);
+ saveVaultButton.setOnClickListener(this);
+ saveVaultButton.setVisibility(View.VISIBLE);
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+
+ edit_vault_name.setText(vault.getName());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null) {
+ try {
+ vault = Vault.fromJSON(new JSONObject(Vault.asJson(Vault.getVaultByGuid(getArguments().getString(VAULT)))));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ vault = Vault.getVaultByGuid(getArguments().getString(VAULT));
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (alreadySaving.get()) {
+ return;
+ }
+
+ if (edit_vault_name.getText().toString().equals("")) {
+ edit_vault_name_header.setTextColor(getResources().getColor(R.color.danger));
+ return;
+ } else {
+ edit_vault_name_header.setTextColor(getResources().getColor(R.color.colorAccent));
+ }
+
+ alreadySaving.set(true);
+
+ this.vault.setName(edit_vault_name.getText().toString());
+
+ Context context = getContext();
+ final ProgressDialog progress = ProgressUtils.showLoadingSequence(context);
+ final AsyncHttpResponseHandler responseHandler = new VaultSaveResponseHandler(alreadySaving, true, this.vault, 0, progress, view, (PasswordListActivity) getActivity(), getFragmentManager());
+
+ this.vault.edit(context, responseHandler);
+ }
+}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/VaultFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/VaultFragment.java
index e2d6033..ce4e54c 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/VaultFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/VaultFragment.java
@@ -1,36 +1,40 @@
/**
- * Passman Android App
+ * Passman Android App
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
- *
+ * <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
- *
+ * <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ * <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
*/
package es.wolfi.app.passman.fragments;
import android.content.Context;
import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.HashMap;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
@@ -38,9 +42,6 @@ import es.wolfi.app.passman.SingleTon;
import es.wolfi.app.passman.adapters.VaultViewAdapter;
import es.wolfi.passman.API.Vault;
-import java.util.ArrayList;
-import java.util.HashMap;
-
/**
* A fragment representing a list of Items.
* <p/>
@@ -87,19 +88,30 @@ public class VaultFragment extends Fragment {
View view = inflater.inflate(R.layout.fragment_vault_list, container, false);
// Set the adapter
- if (view instanceof RecyclerView) {
- Context context = view.getContext();
- RecyclerView recyclerView = (RecyclerView) view;
- if (mColumnCount <= 1) {
- recyclerView.setLayoutManager(new LinearLayoutManager(context));
- } else {
- recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+ RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
+ Context context = recyclerView.getContext();
+ if (mColumnCount <= 1) {
+ recyclerView.setLayoutManager(new LinearLayoutManager(context));
+ } else {
+ recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+ }
+
+ HashMap<String, Vault> vaults = (HashMap<String, Vault>) SingleTon.getTon().getExtra(SettingValues.VAULTS.toString());
+ ArrayList<Vault> l = new ArrayList<Vault>(vaults.values());
+ recyclerView.setAdapter(new VaultViewAdapter(l, mListener, getParentFragmentManager()));
+
+ view.findViewById(R.id.add_vault_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getParentFragmentManager()
+ .beginTransaction()
+ .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_out_left, R.anim.slide_out_left)
+ .replace(R.id.content_password_list, VaultAddFragment.newInstance(), "vault")
+ .addToBackStack(null)
+ .commit();
}
+ });
- HashMap<String, Vault> vaults = (HashMap<String, Vault>) SingleTon.getTon().getExtra(SettingValues.VAULTS.toString());
- ArrayList<Vault> l = new ArrayList<Vault>(vaults.values());
- recyclerView.setAdapter(new VaultViewAdapter(l, mListener));
- }
return view;
}
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/VaultLockScreenFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/VaultLockScreenFragment.java
index 4ba613c..8a4a612 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/VaultLockScreenFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/VaultLockScreenFragment.java
@@ -22,9 +22,7 @@
package es.wolfi.app.passman.fragments;
import android.content.Context;
-import android.content.SharedPreferences;
import android.os.Bundle;
-import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -45,6 +43,7 @@ import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.passman.API.Vault;
+import es.wolfi.utils.KeyStoreUtils;
/**
@@ -127,8 +126,7 @@ public class VaultLockScreenFragment extends Fragment {
void onBtnUnlockClick() {
if (vault.unlock(vault_password.getText().toString())) {
if (chk_save.isChecked()) {
- SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getContext());
- p.edit().putString(vault.guid, vault_password.getText().toString()).commit();
+ KeyStoreUtils.putStringAndCommit(vault.guid, vault_password.getText().toString());
}
mListener.onVaultUnlock(vault);
return;
diff --git a/app/src/main/java/es/wolfi/passman/API/Core.java b/app/src/main/java/es/wolfi/passman/API/Core.java
index 64290ad..94326a1 100644
--- a/app/src/main/java/es/wolfi/passman/API/Core.java
+++ b/app/src/main/java/es/wolfi/passman/API/Core.java
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -21,12 +22,12 @@
package es.wolfi.passman.API;
+import android.app.AlertDialog;
import android.content.Context;
-import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
-import android.preference.PreferenceManager;
import android.util.Log;
+import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -35,7 +36,6 @@ import com.google.gson.GsonBuilder;
import com.koushikdutta.async.future.FutureCallback;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.AsyncHttpResponseHandler;
-import com.loopj.android.http.RequestParams;
import com.nextcloud.android.sso.aidl.NextcloudRequest;
import com.nextcloud.android.sso.api.AidlNetworkRequest;
import com.nextcloud.android.sso.api.NextcloudAPI;
@@ -45,10 +45,12 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
+import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@@ -62,20 +64,28 @@ import java.util.Map;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.HeaderElement;
import cz.msebera.android.httpclient.ParseException;
+import cz.msebera.android.httpclient.entity.StringEntity;
import es.wolfi.app.ResponseHandlers.CoreAPIGETResponseHandler;
+import es.wolfi.app.passman.OfflineStorage;
+import es.wolfi.app.passman.OfflineStorageValues;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
+import es.wolfi.app.passman.SettingsCache;
import es.wolfi.app.passman.SingleTon;
+import es.wolfi.utils.KeyStoreUtils;
public abstract class Core {
protected static final String LOG_TAG = "API_LIB";
+ protected static final String JSON_CONTENT_TYPE = "application/json";
protected static SingleSignOnAccount ssoAccount;
protected static String host;
+ protected static String host_internal;
protected static String username;
protected static String password;
protected static String version_name;
protected static String API_URL = "/index.php/apps/passman/api/v2/";
+ protected static String API_URL_INTERNAL = "/index.php/apps/passman/api/internal/";
protected static int version_number = 0;
@@ -97,6 +107,7 @@ public abstract class Core {
public static void setAPIHost(String host) {
Core.host = host.concat(API_URL);
+ Core.host_internal = host.concat(API_URL_INTERNAL);
}
public static String getAPIUsername() {
@@ -116,94 +127,163 @@ public abstract class Core {
}
public static int getConnectTimeout(Context c) {
- return PreferenceManager.getDefaultSharedPreferences(c).getInt(SettingValues.REQUEST_CONNECT_TIMEOUT.toString(), 15) * 1000;
+ return SettingsCache.getInt(SettingValues.REQUEST_CONNECT_TIMEOUT.toString(), 15) * 1000;
+ }
+
+ public static int getConnectRetries(Context c) {
+ return 0;
}
public static int getResponseTimeout(Context c) {
- return PreferenceManager.getDefaultSharedPreferences(c).getInt(SettingValues.REQUEST_RESPONSE_TIMEOUT.toString(), 120) * 1000;
+ return SettingsCache.getInt(SettingValues.REQUEST_RESPONSE_TIMEOUT.toString(), 120) * 1000;
+ }
+
+ public static void requestInternalAPIGET(Context c, String endpoint, final FutureCallback<String> callback) {
+ final AsyncHttpResponseHandler responseHandler = new CoreAPIGETResponseHandler(callback);
+ if (ssoAccount != null) {
+ final Map<String, List<String>> header = new HashMap<>();
+ header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE));
+
+ NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder()
+ .setMethod("GET")
+ .setUrl(Uri.encode(API_URL_INTERNAL.concat(endpoint), "/"))
+ .build();
+ new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute();
+ } else {
+ AsyncHttpClient client = new AsyncHttpClient();
+ client.setBasicAuth(username, password);
+ client.setConnectTimeout(getConnectTimeout(c));
+ client.setResponseTimeout(getResponseTimeout(c));
+ client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c));
+ client.addHeader("Content-Type", JSON_CONTENT_TYPE);
+ client.get(host_internal.concat(endpoint), responseHandler);
+ }
}
public static void requestAPIGET(Context c, String endpoint, final FutureCallback<String> callback) {
+ final AsyncHttpResponseHandler responseHandler = new CoreAPIGETResponseHandler(callback);
if (ssoAccount != null) {
final Map<String, List<String>> header = new HashMap<>();
- header.put("Content-Type", Collections.singletonList("application/json"));
+ header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE));
NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder()
.setMethod("GET")
.setUrl(Uri.encode(API_URL.concat(endpoint), "/"))
.build();
- new SyncedRequestTask(nextcloudRequest, ssoAccount, callback, c).execute();
+ new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute();
} else {
- final AsyncHttpResponseHandler responseHandler = new CoreAPIGETResponseHandler(callback);
AsyncHttpClient client = new AsyncHttpClient();
client.setBasicAuth(username, password);
client.setConnectTimeout(getConnectTimeout(c));
client.setResponseTimeout(getResponseTimeout(c));
- client.addHeader("Content-Type", "application/json");
+ client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c));
+ client.addHeader("Content-Type", JSON_CONTENT_TYPE);
client.get(host.concat(endpoint), responseHandler);
}
}
- // for sso requests
- public static void requestAPI(Context c, String endpoint, JSONObject postDataParams, String requestType, final AsyncHttpResponseHandler responseHandler)
- throws MalformedURLException {
+ public static void requestAPI(Context c, String endpoint, JSONObject jsonPostData, String requestType, final AsyncHttpResponseHandler responseHandler)
+ throws MalformedURLException, UnsupportedEncodingException {
+
if (ssoAccount != null) {
final Map<String, List<String>> header = new HashMap<>();
header.put("Accept", Collections.singletonList("application/json, text/plain, */*"));
- //header.put("Content-Type", Collections.singletonList("application/json"));
+ //header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE));
NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder()
.setMethod(requestType)
- .setUrl(API_URL.concat(endpoint))
- .setRequestBody(postDataParams.toString())
+ .setUrl(Uri.encode(API_URL.concat(endpoint), "/"))
+ .setRequestBody(jsonPostData.toString())
.setHeader(header)
.build();
new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute();
- }
- }
+ } else {
+ URL url = new URL(host.concat(endpoint));
+ AsyncHttpClient client = new AsyncHttpClient();
+ client.setBasicAuth(username, password);
+ client.setConnectTimeout(getConnectTimeout(c));
+ client.setResponseTimeout(getResponseTimeout(c));
+ client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c));
+ client.addHeader("Accept", "application/json, text/plain, */*");
+
+ StringEntity entity = new StringEntity(jsonPostData.toString());
- // for legacy requests
- public static void requestAPI(Context c, String endpoint, RequestParams postDataParams, String requestType, final AsyncHttpResponseHandler responseHandler)
- throws MalformedURLException {
-
- URL url = new URL(host.concat(endpoint));
-
- AsyncHttpClient client = new AsyncHttpClient();
- client.setBasicAuth(username, password);
- client.setConnectTimeout(getConnectTimeout(c));
- client.setResponseTimeout(getResponseTimeout(c));
- //client.addHeader("Content-Type", "application/json; utf-8");
- client.addHeader("Accept", "application/json, text/plain, */*");
-
- if (requestType.equals("POST")) {
- client.post(url.toString(), postDataParams, responseHandler);
- } else if (requestType.equals("PATCH")) {
- client.patch(url.toString(), postDataParams, responseHandler);
- } else if (requestType.equals("DELETE")) {
- client.delete(url.toString(), postDataParams, responseHandler);
+ if (requestType.equals("POST")) {
+ client.post(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler);
+ } else if (requestType.equals("PATCH")) {
+ client.patch(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler);
+ } else if (requestType.equals("DELETE")) {
+ client.delete(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler);
+ }
}
}
- // TODO Test this method once the server response works!
- public static void getAPIVersion(final Context c, FutureCallback<Integer> cb) {
- if (version_number != 0) {
- cb.onCompleted(null, version_number);
+ public static void getAPIVersion(final Context c, FutureCallback<String> cb) {
+ if (version_name != null) {
+ cb.onCompleted(null, version_name);
return;
}
- /*
- requestAPIGET(c, "version", new FutureCallback<String>() {
+ requestInternalAPIGET(c, "version", new FutureCallback<String>() {
@Override
public void onCompleted(Exception e, String result) {
- if (result != null) {
+ if (result != null && e == null) {
Log.d("getApiVersion", result);
+ if (applyVersionJSON(result)) {
+ OfflineStorage.getInstance().putObject(OfflineStorageValues.VERSION.toString(), result);
+ OfflineStorage.getInstance().commit();
+ cb.onCompleted(null, version_name);
+ }
} else {
- Log.d("getApiVersion", "Failure while getting api version");
+ Log.d("getApiVersion", "Failure while getting api version, maybe offline?");
+ Log.d("OfflineStorage state", OfflineStorage.getInstance().isEnabled() ? "enabled" : "disabled");
+ Log.d("version stored", OfflineStorage.getInstance().has(OfflineStorageValues.VERSION.toString()) ? "yes" : "no");
+ if (OfflineStorage.getInstance().isEnabled() &&
+ OfflineStorage.getInstance().has(OfflineStorageValues.VERSION.toString()) &&
+ OfflineStorage.getInstance().has(OfflineStorageValues.VAULTS.toString())) {
+ showConnectionErrorHint(c);
+ if (applyVersionJSON(OfflineStorage.getInstance().getString(OfflineStorageValues.VERSION.toString()))) {
+ cb.onCompleted(null, version_name);
+ return;
+ }
+ }
+ cb.onCompleted(e, null);
+ }
+ }
+ });
+ }
+
+ public static boolean applyVersionJSON(String version) {
+ try {
+ JSONObject parsedResult = new JSONObject(version);
+ if (parsedResult.has("version")) {
+ version_name = parsedResult.getString("version");
+ version_number = Integer.parseInt(version_name.replace(".", ""));
+ return true;
+ }
+ } catch (JSONException | NumberFormatException jsonException) {
+ jsonException.printStackTrace();
+ }
+ return false;
+ }
+
+ public static void checkCloudConnectionAndShowHint(View view) {
+ requestInternalAPIGET(view.getContext(), "version", new FutureCallback<String>() {
+ @Override
+ public void onCompleted(Exception e, String result) {
+ if (result == null) {
+ showConnectionErrorHint(view.getContext());
}
}
});
- */
+ }
+
+ private static void showConnectionErrorHint(Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.net_error_dialog_description);
+ builder.setCancelable(true);
+ builder.show();
}
/**
@@ -217,8 +297,7 @@ public abstract class Core {
SingleTon ton = SingleTon.getTon();
if (ton.getString(SettingValues.HOST.toString()) == null) {
- SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(c);
- String url = settings.getString(SettingValues.HOST.toString(), null);
+ String url = KeyStoreUtils.getString(SettingValues.HOST.toString(), null);
// If the url is null app has not yet been configured!
if (url == null) {
@@ -228,8 +307,8 @@ public abstract class Core {
// Load the server settings
ton.addString(SettingValues.HOST.toString(), url);
- ton.addString(SettingValues.USER.toString(), settings.getString(SettingValues.USER.toString(), ""));
- ton.addString(SettingValues.PASSWORD.toString(), settings.getString(SettingValues.PASSWORD.toString(), ""));
+ ton.addString(SettingValues.USER.toString(), KeyStoreUtils.getString(SettingValues.USER.toString(), ""));
+ ton.addString(SettingValues.PASSWORD.toString(), KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), ""));
}
String host = ton.getString(SettingValues.HOST.toString());
@@ -241,10 +320,10 @@ public abstract class Core {
//Log.d(LOG_TAG, "Pass: " + pass);
Log.d(LOG_TAG, "Pass: " + pass.replaceAll("(?s).", "*"));
- Vault.setUpAPI(c, host, user, pass);
- Vault.getVaults(c, new FutureCallback<HashMap<String, Vault>>() {
+ setUpAPI(c, host, user, pass);
+ getAPIVersion(c, new FutureCallback<String>() {
@Override
- public void onCompleted(Exception e, HashMap<String, Vault> result) {
+ public void onCompleted(Exception e, String result) {
boolean ret = true;
if (e != null) {
@@ -273,7 +352,6 @@ public abstract class Core {
}
private static class NCHeader implements Header {
-
String name, value;
public NCHeader(String name, String value) {
@@ -306,7 +384,6 @@ public abstract class Core {
}
private static class SyncedRequestTask extends AsyncTask<Void, Void, Boolean> {
-
private final NextcloudRequest nextcloudRequest;
private final NextcloudAPI mNextcloudAPI;
private final AsyncHttpResponseHandler responseHandler;
diff --git a/app/src/main/java/es/wolfi/passman/API/Credential.java b/app/src/main/java/es/wolfi/passman/API/Credential.java
index a52e7a2..405498b 100644
--- a/app/src/main/java/es/wolfi/passman/API/Credential.java
+++ b/app/src/main/java/es/wolfi/passman/API/Credential.java
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -26,12 +27,12 @@ import android.content.Context;
import android.util.Log;
import com.loopj.android.http.AsyncHttpResponseHandler;
-import com.loopj.android.http.RequestParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
@@ -341,33 +342,26 @@ public class Credential extends Core implements Filterable {
this.sharedKeyDecrypted = null;
}
- public JSONObject getAsJSONObject(boolean forUpdate) throws JSONException {
+ public JSONObject getAsJSONObject() throws JSONException {
JSONObject params = new JSONObject();
JSONObject icon = null;
- if (forUpdate) {
- params.put("credential_id", getId());
- params.put("guid", getGuid());
-
+ try {
if (favicon != null && !favicon.equals("") && !favicon.equals("null")) {
- try {
- icon = new JSONObject(favicon);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- } else {
- try {
+ icon = new JSONObject(favicon);
+ } else {
icon = new JSONObject();
icon.put("type", false);
icon.put("content", "");
- } catch (JSONException e) {
- e.printStackTrace();
}
+ } catch (JSONException e) {
+ e.printStackTrace();
}
params.put("user_id", getUserId());
+ params.put("credential_id", getId());
+ params.put("guid", getGuid());
params.put("shared_key", getSharedKey());
params.put("vault_id", getVaultId());
params.put("label", label);
@@ -392,10 +386,8 @@ public class Credential extends Core implements Filterable {
return params;
}
- public RequestParams getAsRequestParams(boolean forUpdate, boolean useJsonStreamer) {
- RequestParams params = new RequestParams();
- params.setUseJsonStreamer(useJsonStreamer);
-
+ public JSONObject getAsJsonObjectForApiRequest(boolean forUpdate) throws JSONException {
+ JSONObject params = new JSONObject();
JSONObject icon = null;
if (forUpdate) {
@@ -497,70 +489,48 @@ public class Credential extends Core implements Filterable {
return c;
}
+ public static Credential clone(Credential input) throws JSONException {
+ return Credential.fromJSON(input.getAsJSONObject(), input.getVault());
+ }
+
public void save(Context c, final AsyncHttpResponseHandler responseHandler) {
try {
- if (Core.ssoAccount != null) {
- requestAPI(c, "credentials", getAsJSONObject(false), "POST", responseHandler);
- } else {
- requestAPI(c, "credentials", getAsRequestParams(false, true), "POST", responseHandler);
- }
- } catch (MalformedURLException | JSONException e) {
+ requestAPI(c, "credentials", getAsJsonObjectForApiRequest(false), "POST", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public void update(Context c, final AsyncHttpResponseHandler responseHandler) {
try {
- if (Core.ssoAccount != null) {
- requestAPI(c, "credentials/" + getGuid(), getAsJSONObject(true), "PATCH", responseHandler);
- } else {
- requestAPI(c, "credentials/" + getGuid(), getAsRequestParams(true, true), "PATCH", responseHandler);
- }
- } catch (MalformedURLException | JSONException e) {
+ requestAPI(c, "credentials/" + getGuid(), getAsJsonObjectForApiRequest(true), "PATCH", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public void sendFileDeleteRequest(Context c, int file_id, final AsyncHttpResponseHandler responseHandler) {
+ JSONObject params = new JSONObject();
try {
- if (Core.ssoAccount != null) {
- requestAPI(c, "file/" + file_id, new JSONObject(), "DELETE", responseHandler);
- } else {
- requestAPI(c, "file/" + file_id, new RequestParams(), "DELETE", responseHandler);
- }
- } catch (MalformedURLException e) {
+ requestAPI(c, "file/" + file_id, params, "DELETE", responseHandler);
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public void uploadFile(Context c, String encodedFile, String fileName, String mimeType, int fileSize, final AsyncHttpResponseHandler responseHandler, ProgressDialog progress) {
+ JSONObject params = new JSONObject();
+
progress.setMessage(c.getString(R.string.wait_while_encrypting));
try {
- if (Core.ssoAccount != null) {
- JSONObject params = new JSONObject();
-
- params.put("filename", encryptString(fileName));
- params.put("data", encryptRawStringData(encodedFile));
- params.put("mimetype", mimeType);
- params.put("size", fileSize);
-
- progress.setMessage(c.getString(R.string.wait_while_uploading));
- requestAPI(c, "file", params, "POST", responseHandler);
- } else {
- RequestParams params = new RequestParams();
- params.setUseJsonStreamer(true);
-
- params.put("filename", encryptString(fileName));
- params.put("data", encryptRawStringData(encodedFile));
- params.put("mimetype", mimeType);
- params.put("size", fileSize);
-
- progress.setMessage(c.getString(R.string.wait_while_uploading));
- requestAPI(c, "file", params, "POST", responseHandler);
- }
- } catch (MalformedURLException | JSONException e) {
+ params.put("filename", encryptString(fileName));
+ params.put("data", encryptRawStringData(encodedFile));
+ params.put("mimetype", mimeType);
+ params.put("size", fileSize);
+ progress.setMessage(c.getString(R.string.wait_while_uploading));
+ requestAPI(c, "file", params, "POST", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
e.printStackTrace();
- progress.cancel();
}
}
diff --git a/app/src/main/java/es/wolfi/passman/API/Vault.java b/app/src/main/java/es/wolfi/passman/API/Vault.java
index 866b6a0..0358d5c 100644
--- a/app/src/main/java/es/wolfi/passman/API/Vault.java
+++ b/app/src/main/java/es/wolfi/passman/API/Vault.java
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
@@ -23,24 +24,39 @@ package es.wolfi.passman.API;
import android.content.Context;
import android.content.SharedPreferences;
+import android.util.Base64;
import android.util.Log;
+import android.util.Pair;
import com.koushikdutta.async.future.FutureCallback;
+import com.loopj.android.http.AsyncHttpResponseHandler;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import es.wolfi.app.passman.OfflineStorage;
+import es.wolfi.app.passman.OfflineStorageValues;
import es.wolfi.app.passman.SJCLCrypto;
import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.SingleTon;
import es.wolfi.utils.CredentialLabelSort;
import es.wolfi.utils.Filterable;
+import es.wolfi.utils.KeyStoreUtils;
public class Vault extends Core implements Filterable {
public int vault_id;
@@ -50,12 +66,24 @@ public class Vault extends Core implements Filterable {
public String public_sharing_key;
public double last_access;
public String challenge_password;
+ public int sharing_keys_generated;
+ public boolean delete_request_pending;
+ public JSONObject vault_settings = null;
+ public static Integer[] keyStrengths = {1024, 2048, 4096};
ArrayList<Credential> credentials;
HashMap<String, Integer> credential_guid;
private String encryption_key = "";
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
public void setEncryptionKey(String k) {
encryption_key = k;
}
@@ -171,8 +199,15 @@ public class Vault extends Core implements Filterable {
@Override
public void onCompleted(Exception e, String result) {
if (e != null) {
- cb.onCompleted(e, null);
- return;
+ Log.d("vaults cached", OfflineStorage.getInstance().has(OfflineStorageValues.VAULTS.toString()) ? "yes" : "no");
+ if (OfflineStorage.getInstance().isEnabled() && OfflineStorage.getInstance().has(OfflineStorageValues.VAULTS.toString())) {
+ result = OfflineStorage.getInstance().getString(OfflineStorageValues.VAULTS.toString(), null);
+ }
+ if (result == null || !OfflineStorage.getInstance().isEnabled() ||
+ !OfflineStorage.getInstance().has(OfflineStorageValues.VAULTS.toString())) {
+ cb.onCompleted(e, null);
+ return;
+ }
}
// Log.e(Vault.LOG_TAG, result);
@@ -185,7 +220,9 @@ public class Vault extends Core implements Filterable {
l.put(v.guid, v);
}
- cb.onCompleted(e, l);
+ OfflineStorage.getInstance().putObject(OfflineStorageValues.VAULTS.toString(), result);
+ OfflineStorage.getInstance().commit();
+ cb.onCompleted(null, l);
} catch (JSONException ex) {
cb.onCompleted(ex, null);
}
@@ -198,16 +235,21 @@ public class Vault extends Core implements Filterable {
@Override
public void onCompleted(Exception e, String result) {
if (e != null) {
- cb.onCompleted(e, null);
- return;
+ if (OfflineStorage.getInstance().isEnabled() && OfflineStorage.getInstance().has(guid)) {
+ result = OfflineStorage.getInstance().getString(guid, null);
+ }
+ if (result == null || !OfflineStorage.getInstance().isEnabled() ||
+ !OfflineStorage.getInstance().has(guid)) {
+ cb.onCompleted(e, null);
+ return;
+ }
}
try {
JSONObject data = new JSONObject(result);
-
Vault v = Vault.fromJSON(data);
-
- cb.onCompleted(e, v);
+ OfflineStorage.getInstance().putObject(guid, result);
+ cb.onCompleted(null, v);
} catch (JSONException ex) {
cb.onCompleted(ex, null);
}
@@ -225,7 +267,7 @@ public class Vault extends Core implements Filterable {
v.public_sharing_key = o.getString("public_sharing_key");
v.last_access = o.getDouble("last_access");
- if (o.has("credentials")) {
+ if (o.has("credentials") && !o.getString("credentials").equals("null")) {
JSONArray j = o.getJSONArray("credentials");
v.credentials = new ArrayList<Credential>();
v.credential_guid = new HashMap<>();
@@ -238,10 +280,26 @@ public class Vault extends Core implements Filterable {
}
}
v.challenge_password = v.credentials.get(0).password;
- } else {
+ } else if (o.has("challenge_password")) {
v.challenge_password = o.getString("challenge_password");
}
+ if (o.has("vault_settings") && !o.getString("vault_settings").equals("null")) {
+ v.vault_settings = new JSONObject(new String(Base64.decode(o.getString("vault_settings"), Base64.DEFAULT)));
+ } else {
+ v.vault_settings = new JSONObject();
+ }
+
+ if (o.has("delete_request_pending")) {
+ v.delete_request_pending = o.getBoolean("delete_request_pending");
+ } else {
+ v.delete_request_pending = false;
+ }
+
+ if (o.has("sharing_keys_generated")) {
+ v.sharing_keys_generated = o.getInt("sharing_keys_generated");
+ }
+
return v;
}
@@ -305,11 +363,14 @@ public class Vault extends Core implements Filterable {
obj.put("created", vault.created);
obj.put("public_sharing_key", vault.public_sharing_key);
obj.put("last_access", vault.last_access);
+ obj.put("delete_request_pending", vault.delete_request_pending);
+ obj.put("sharing_keys_generated", vault.sharing_keys_generated);
+
if (vault.getCredentials() != null) {
JSONArray credentialArr = new JSONArray();
for (Credential credential : vault.getCredentials()) {
try {
- credentialArr.put(credential.getAsJSONObject(true));
+ credentialArr.put(credential.getAsJSONObject());
} catch (JSONException e) {
e.printStackTrace();
}
@@ -318,14 +379,40 @@ public class Vault extends Core implements Filterable {
} else {
obj.put("challenge_password", vault.challenge_password);
}
+
+ if (vault.vault_settings != null) {
+ obj.put("vault_settings", Base64.encodeToString(vault.vault_settings.toString().getBytes(StandardCharsets.UTF_8), Base64.DEFAULT));
+ }
+
return obj.toString();
}
+ public static JSONObject getAsJsonObjectForApiRequest(Vault vault, boolean forEdit) throws JSONException {
+ JSONObject params = new JSONObject();
+
+ params.put("vault_id", vault.vault_id);
+ params.put("guid", vault.guid);
+ params.put("name", vault.name);
+ params.put("created", vault.created);
+ params.put("public_sharing_key", vault.public_sharing_key);
+ params.put("last_access", vault.last_access);
+
+ if (forEdit) {
+ params.put("delete_request_pending", vault.delete_request_pending);
+ params.put("sharing_keys_generated", vault.sharing_keys_generated);
+ if (vault.vault_settings != null) {
+ params.put("vault_settings", Base64.encodeToString(vault.vault_settings.toString().getBytes(StandardCharsets.UTF_8), Base64.DEFAULT));
+ }
+ }
+
+ return params;
+ }
+
public static void updateAutofillVault(Vault vault, SharedPreferences settings) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (settings.getString(SettingValues.AUTOFILL_VAULT_GUID.toString(), "").equals(vault.guid)) {
try {
- settings.edit().putString(SettingValues.AUTOFILL_VAULT.toString(), Vault.asJson(vault)).apply();
+ KeyStoreUtils.putString(SettingValues.AUTOFILL_VAULT.toString(), Vault.asJson(vault));
} catch (JSONException e) {
e.printStackTrace();
}
@@ -333,6 +420,110 @@ public class Vault extends Core implements Filterable {
}
}
+ public void updateSharingKeys(int keyStrength, Context context, AsyncHttpResponseHandler createInitialSharingKeysResponseHandler) {
+ Pair<String, String> keyPair = getNewPEMKeyPair(keyStrength);
+ if (keyPair != null) {
+ public_sharing_key = keyPair.first;
+
+ try {
+ JSONObject params = getAsJsonObjectForApiRequest(this, false);
+ params.put("private_sharing_key", encryptRawStringData(keyPair.second));
+ Vault.requestAPI(context, "vaults/" + guid + "/sharing-keys", params, "POST", createInitialSharingKeysResponseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static Pair<String, String> getNewPEMKeyPair(int keyStrength) {
+ Pair<String, String> pairPublicPrivatePEM = null;
+ try {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(keyStrength);
+ KeyPair keyPair = kpg.generateKeyPair();
+
+ // Convert PublicKey to PEM format
+ StringWriter publicWriter = new StringWriter();
+ JcaPEMWriter publicPemWriter = new JcaPEMWriter(publicWriter);
+ publicPemWriter.writeObject(keyPair.getPublic());
+ publicPemWriter.flush();
+ publicPemWriter.close();
+ String publicPem = publicWriter.toString();
+
+ // Convert PrivateKey to PEM format
+ StringWriter privateWriter = new StringWriter();
+ JcaPEMWriter privatePemWriter = new JcaPEMWriter(privateWriter);
+ privatePemWriter.writeObject(keyPair.getPrivate());
+ privatePemWriter.flush();
+ privatePemWriter.close();
+ String privatePem = privateWriter.toString();
+
+ pairPublicPrivatePEM = new Pair<>(publicPem, privatePem);
+ } catch (NoSuchAlgorithmException | IOException e) {
+ e.printStackTrace();
+ }
+
+ return pairPublicPrivatePEM;
+ }
+
+ public void save(Context c, final AsyncHttpResponseHandler responseHandler) {
+ JSONObject params = new JSONObject();
+
+ try {
+ params.put("vault_name", this.name);
+ requestAPI(c, "vaults", params, "POST", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void edit(Context c, final AsyncHttpResponseHandler responseHandler) {
+ try {
+ JSONObject params = getAsJsonObjectForApiRequest(this, true);
+ requestAPI(c, "vaults/" + this.guid, params, "PATCH", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * deleteVaultContents() should be called before delete() to remove vaults credentials and files
+ *
+ * @param context
+ * @param responseHandler
+ */
+ public void deleteVaultContents(Context context, final AsyncHttpResponseHandler responseHandler) {
+ JSONObject collectionToDelete = new JSONObject();
+ JSONArray fileIds = new JSONArray();
+
+ for (Credential c : this.getCredentials()) {
+ for (File f : c.getFilesList()) {
+ fileIds.put(f.getFileId());
+ }
+ }
+
+ try {
+ collectionToDelete.put("file_ids", fileIds);
+ requestAPI(context, "files/delete", collectionToDelete, "POST", responseHandler);
+ } catch (MalformedURLException | JSONException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * delete() is automatically called by the VaultDeleteResponseHandler when calling deleteVaultContents() with passing the isDeleteVaultContentRequest as true
+ *
+ * @param context
+ * @param responseHandler
+ */
+ public void delete(Context context, final AsyncHttpResponseHandler responseHandler) {
+ try {
+ requestAPI(context, "vaults/" + this.guid, new JSONObject(), "DELETE", responseHandler);
+ } catch (MalformedURLException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
@Override
public String getFilterableAttribute() {
return this.name.toLowerCase();
diff --git a/app/src/main/java/es/wolfi/utils/CredentialLabelSort.java b/app/src/main/java/es/wolfi/utils/CredentialLabelSort.java
index b26c71c..664b93d 100644
--- a/app/src/main/java/es/wolfi/utils/CredentialLabelSort.java
+++ b/app/src/main/java/es/wolfi/utils/CredentialLabelSort.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.utils;
import java.util.Comparator;
diff --git a/app/src/main/java/es/wolfi/utils/FileUtils.java b/app/src/main/java/es/wolfi/utils/FileUtils.java
index 738828e..7ee4bb3 100644
--- a/app/src/main/java/es/wolfi/utils/FileUtils.java
+++ b/app/src/main/java/es/wolfi/utils/FileUtils.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.utils;
import android.annotation.SuppressLint;
@@ -150,8 +172,7 @@ public class FileUtils {
selectionArgs = new String[]{split[1]};
- return getDataColumn(context, contentUri, selection,
- selectionArgs);
+ return getDataColumn(context, contentUri, selection, selectionArgs);
} else if (isGoogleDriveUri(uri)) {
return getDriveFilePath(uri, context);
}
@@ -332,9 +353,12 @@ public class FileUtils {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
+ } catch (Exception e) {
+ e.printStackTrace();
} finally {
- if (cursor != null)
+ if (cursor != null) {
cursor.close();
+ }
}
return null;
diff --git a/app/src/main/java/es/wolfi/utils/FilterListAsyncTask.java b/app/src/main/java/es/wolfi/utils/FilterListAsyncTask.java
index 01adf7c..6fc9a8b 100644
--- a/app/src/main/java/es/wolfi/utils/FilterListAsyncTask.java
+++ b/app/src/main/java/es/wolfi/utils/FilterListAsyncTask.java
@@ -1,23 +1,23 @@
/**
- * Passman Android App
+ * Passman Android App
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
- *
+ * <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
- *
+ * <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ * <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
*/
package es.wolfi.utils;
@@ -25,36 +25,39 @@ package es.wolfi.utils;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
+import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
-import es.wolfi.app.passman.fragments.CredentialItemFragment;
import es.wolfi.app.passman.adapters.CredentialViewAdapter;
-import es.wolfi.app.passman.fragments.VaultFragment;
import es.wolfi.app.passman.adapters.VaultViewAdapter;
+import es.wolfi.app.passman.fragments.CredentialItemFragment;
+import es.wolfi.app.passman.fragments.VaultFragment;
import es.wolfi.passman.API.Credential;
import es.wolfi.passman.API.Vault;
-public class FilterListAsyncTask <T extends Filterable> extends AsyncTask<ArrayList<T>, Integer, ArrayList<T>>{
+public class FilterListAsyncTask<T extends Filterable> extends AsyncTask<ArrayList<T>, Integer, ArrayList<T>> {
private String filter;
RecyclerView recyclerView;
CredentialItemFragment.OnListFragmentInteractionListener credentialMListener = null;
VaultFragment.OnListFragmentInteractionListener vaultMListener = null;
+ FragmentManager fragmentManager = null;
Boolean isVaultFragment;
- public FilterListAsyncTask(String filter, RecyclerView recyclerView, CredentialItemFragment.OnListFragmentInteractionListener mListener){
+ public FilterListAsyncTask(String filter, RecyclerView recyclerView, CredentialItemFragment.OnListFragmentInteractionListener mListener) {
this.filter = filter;
this.recyclerView = recyclerView;
this.credentialMListener = mListener;
this.isVaultFragment = false;
}
- public FilterListAsyncTask(String filter, RecyclerView recyclerView, VaultFragment.OnListFragmentInteractionListener mListener){
+ public FilterListAsyncTask(String filter, RecyclerView recyclerView, VaultFragment.OnListFragmentInteractionListener mListener, FragmentManager fragmentManager) {
this.filter = filter;
this.recyclerView = recyclerView;
this.vaultMListener = mListener;
+ this.fragmentManager = fragmentManager;
this.isVaultFragment = true;
}
@@ -64,11 +67,10 @@ public class FilterListAsyncTask <T extends Filterable> extends AsyncTask<ArrayL
}
@Override
- protected void onPostExecute(ArrayList<T> filteredList){
- if(isVaultFragment){
- recyclerView.setAdapter(new VaultViewAdapter((ArrayList<Vault>)filteredList, vaultMListener));
- }
- else {
+ protected void onPostExecute(ArrayList<T> filteredList) {
+ if (isVaultFragment) {
+ recyclerView.setAdapter(new VaultViewAdapter((ArrayList<Vault>) filteredList, vaultMListener, fragmentManager));
+ } else {
recyclerView.setAdapter(new CredentialViewAdapter((ArrayList<Credential>) filteredList, credentialMListener, PreferenceManager.getDefaultSharedPreferences(recyclerView.getContext())));
}
}
diff --git a/app/src/main/java/es/wolfi/utils/IconUtils.java b/app/src/main/java/es/wolfi/utils/IconUtils.java
index 6ac5735..c1a0ac7 100644
--- a/app/src/main/java/es/wolfi/utils/IconUtils.java
+++ b/app/src/main/java/es/wolfi/utils/IconUtils.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.utils;
import android.graphics.Bitmap;
diff --git a/app/src/main/java/es/wolfi/utils/KeyStoreUtils.java b/app/src/main/java/es/wolfi/utils/KeyStoreUtils.java
new file mode 100644
index 0000000..abf7a49
--- /dev/null
+++ b/app/src/main/java/es/wolfi/utils/KeyStoreUtils.java
@@ -0,0 +1,323 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package es.wolfi.utils;
+
+import android.content.SharedPreferences;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.util.Log;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+import es.wolfi.app.passman.OfflineStorage;
+import es.wolfi.app.passman.SJCLCrypto;
+import es.wolfi.app.passman.SettingValues;
+import es.wolfi.app.passman.SettingsCache;
+
+/**
+ * Takes care of data encryption in SharedPreferences.
+ * This is an optional feature, but should be used for all user data.
+ * <p>
+ * Use this class directly only if you don't want to make use of the OfflineStorage class!
+ */
+public class KeyStoreUtils {
+
+ private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+ private static final String AES_MODE = "AES/GCM/NoPadding";
+ private static final String RANDOM_ALGORITHM = "SHA1PRNG";
+ private static final String KEY_ALIAS = "PassmanAndroidDefaultKey";
+ private static final int IV_LENGTH = 12;
+ private static final int TAG_LENGTH = 128;
+ private static KeyStore keyStore = null;
+ private static SharedPreferences settings = null;
+
+ /**
+ * Call initialize() at the top of each activity you want to use encrypted data stored in Androids SharedPreferences.
+ * Example usage: KeyStoreUtils.initialize(SharedPreferences settings);
+ * <p>
+ * If the Android KeyStore does not contain the required KEY_ALIAS (usually only at the first app start) an encryption key
+ * for AES/GCM will be generated and stored in the KeyStore (which also protects it from any direct access).
+ * This AES/GCM key is the encryption key for a random generated password which is used to encrypt the user data with the known SJCL.cpp lib.
+ * This is much faster than using any java crypto implementation to encrypt/decrypt user data like data from the OfflineStorage class.
+ *
+ * @param sharedPreferences SharedPreferences
+ */
+ public static void initialize(SharedPreferences sharedPreferences) {
+ Log.d("KeyStoreUtils", "initialize");
+ settings = sharedPreferences;
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ if (keyStore == null) {
+ Log.d("KeyStoreUtils", "load KeyStore");
+ keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
+ keyStore.load(null);
+
+ // KEY_STORE_MIGRATION_STATE == 0 check prevents creating a KeyStore after the first app start and making already stored data unusable
+ if (!keyStore.containsAlias(KEY_ALIAS) && settings.getInt(SettingValues.KEY_STORE_MIGRATION_STATE.toString(), 0) == 0) {
+ Log.d("KeyStoreUtils", "generate new encryption key");
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
+ keyGenerator.init(
+ new KeyGenParameterSpec.Builder(KEY_ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setRandomizedEncryptionRequired(false)
+ .build());
+ SecretKey key = keyGenerator.generateKey();
+ keyStore.setKeyEntry(KEY_ALIAS, key, null, null);
+
+ SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
+ byte[] encryptionKeyBytes = new byte[4096];
+ random.nextBytes(encryptionKeyBytes);
+
+ String encryptionKeyString = new String(Hex.encodeHex(DigestUtils.sha512(encryptionKeyBytes)));
+ String encryptedEncryptionKeyString = encryptKey(encryptionKeyString);
+ settings.edit().putString(SettingValues.KEY_STORE_ENCRYPTION_KEY.toString(), encryptedEncryptionKeyString).commit();
+ }
+ migrateSharedPreferences();
+ }
+ } else {
+ Log.d("KeyStoreUtils", "not supported");
+
+ // since offline cache is enabled by default this code disables it for devices with Android < API 23
+ boolean enableOfflineCache = settings.getBoolean(SettingValues.ENABLE_OFFLINE_CACHE.toString(), false);
+ if (!enableOfflineCache) {
+ settings.edit().putBoolean(SettingValues.ENABLE_OFFLINE_CACHE.toString(), false).commit();
+ SettingsCache.clear();
+ }
+ }
+ } catch (KeyStoreException | IOException | NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | CertificateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Called with any KeyStoreUtils.initialize().
+ * Used to automatically encrypt unencrypted stored data from older Passman versions.
+ */
+ private static void migrateSharedPreferences() {
+ int originalMigrationState = settings.getInt(SettingValues.KEY_STORE_MIGRATION_STATE.toString(), 0);
+ int currentMigrationState = originalMigrationState;
+
+ if (currentMigrationState < 1) {
+ // first app start and first KeyStoreUtils usage migration
+ // already saved vault password will not be migrated and have to be reentered
+ Log.d("KeyStoreUtils", "run initial local storage encryption migration");
+
+ KeyStoreUtils.putStringAndCommit(SettingValues.HOST.toString(), settings.getString(SettingValues.HOST.toString(), null));
+ KeyStoreUtils.putStringAndCommit(SettingValues.USER.toString(), settings.getString(SettingValues.USER.toString(), null));
+ KeyStoreUtils.putStringAndCommit(SettingValues.PASSWORD.toString(), settings.getString(SettingValues.PASSWORD.toString(), null));
+ KeyStoreUtils.putStringAndCommit(SettingValues.AUTOFILL_VAULT.toString(), settings.getString(SettingValues.AUTOFILL_VAULT.toString(), ""));
+ KeyStoreUtils.putStringAndCommit(SettingValues.OFFLINE_STORAGE.toString(), settings.getString(SettingValues.OFFLINE_STORAGE.toString(), OfflineStorage.EMPTY_STORAGE_STRING));
+
+ currentMigrationState++;
+ }
+
+ if (originalMigrationState != currentMigrationState) {
+ settings.edit().putInt(SettingValues.KEY_STORE_MIGRATION_STATE.toString(), currentMigrationState).commit();
+ }
+ }
+
+ /**
+ * Generates the initialisation vector for the encryption of the user data's encryption key.
+ *
+ * @return byte[]
+ * @throws NoSuchAlgorithmException
+ */
+ private static byte[] generateIv() throws NoSuchAlgorithmException {
+ SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
+ byte[] iv = new byte[IV_LENGTH];
+ random.nextBytes(iv);
+ return iv;
+ }
+
+ /**
+ * Returns a Key instance to encrypt/decrypt the data encryption key.
+ *
+ * @return java.security.Key instance from Android KeyStore
+ * @throws UnrecoverableKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ */
+ private static java.security.Key getSecretKey() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
+ return keyStore.getKey(KEY_ALIAS, null);
+ }
+
+ /**
+ * Encrypts the user data's encryption key.
+ * Should be called only once from initialize().
+ *
+ * @param input String plain encryption key
+ * @return String|null encrypted encryption key or null if the encryption failed or the used KeyStore was not initialized
+ */
+ private static String encryptKey(String input) {
+ try {
+ if (input != null && keyStore != null && keyStore.containsAlias(KEY_ALIAS)) {
+ Cipher c = Cipher.getInstance(AES_MODE);
+ byte[] iv = generateIv();
+ c.init(Cipher.ENCRYPT_MODE, getSecretKey(), new GCMParameterSpec(TAG_LENGTH, iv));
+ byte[] encryptedBytes = c.doFinal(input.getBytes(StandardCharsets.UTF_8));
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ outputStream.write(iv);
+ outputStream.write(encryptedBytes);
+ return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Decrypts the user data's encryption key.
+ *
+ * @param encrypted String encrypted encryption key
+ * @return String|null plain encryption key or null if the decryption failed or the used KeyStore was not initialized
+ */
+ private static String decryptKey(String encrypted) {
+ try {
+ if (encrypted != null && keyStore != null && keyStore.containsAlias(KEY_ALIAS) && encrypted.length() >= IV_LENGTH) {
+ byte[] decoded = Base64.decode(encrypted, Base64.DEFAULT);
+ byte[] iv = Arrays.copyOfRange(decoded, 0, IV_LENGTH);
+ Cipher c = Cipher.getInstance(AES_MODE);
+ c.init(Cipher.DECRYPT_MODE, getSecretKey(), new GCMParameterSpec(TAG_LENGTH, iv));
+ byte[] decrypted = c.doFinal(decoded, IV_LENGTH, decoded.length - IV_LENGTH);
+
+ return new String(decrypted);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Encrypt data using the SJCLCrypto library (to store it in SharedPreferences).
+ *
+ * @param input String
+ * @return String encrypted data or original input data if encryption is not enabled or failed
+ */
+ public static String encrypt(String input) {
+ if (input != null && keyStore != null) {
+ String encryptedEncryptionKey = settings.getString(SettingValues.KEY_STORE_ENCRYPTION_KEY.toString(), null);
+ String encryptionKey = decryptKey(encryptedEncryptionKey);
+
+ if (encryptionKey != null) {
+ try {
+ return SJCLCrypto.encryptString(input, encryptionKey, true);
+ } catch (Exception e) {
+ Log.e("KeyStoreUtils encrypt", e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // seems like KeyStore is not enabled / supported (KeyStore requires at least Android API 23)
+ return input;
+ }
+
+ /**
+ * Decrypt data using the SJCLCrypto library (to load encrypted data from SharedPreferences).
+ *
+ * @param encrypted String
+ * @return String decrypted data or original input data if decryption is not enabled or failed
+ */
+ public static String decrypt(String encrypted) {
+ if (encrypted != null && keyStore != null) {
+ String encryptedEncryptionKey = settings.getString(SettingValues.KEY_STORE_ENCRYPTION_KEY.toString(), null);
+ String encryptionKey = decryptKey(encryptedEncryptionKey);
+
+ if (encryptionKey != null) {
+ try {
+ return SJCLCrypto.decryptString(encrypted, encryptionKey);
+ } catch (Exception e) {
+ Log.e("KeyStoreUtils decrypt", e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // seems like KeyStore is not enabled / supported (KeyStore requires at least Android API 23)
+ return encrypted;
+ }
+
+ /**
+ * Decrypt data from SharedPreferences and return it as String.
+ * Replace settings.getString() with KeyStoreUtils.getString().
+ *
+ * @param key String
+ * @param fallback String
+ * @return String
+ */
+ public static String getString(String key, String fallback) {
+ return decrypt(settings.getString(key, fallback));
+ }
+
+ /**
+ * Encrypt data and store it in SharedPreferences.
+ * Replace settings.edit().putString() with KeyStoreUtils.putString().
+ * Without the explicit commit() call, the backend will take care of storing the data asynchronously (recommended).
+ *
+ * @param key String
+ * @param value String
+ */
+ public static void putString(String key, String value) {
+ settings.edit().putString(key, encrypt(value)).apply();
+ }
+
+ /**
+ * Encrypt data and store it in SharedPreferences.
+ * Replace settings.edit().putString().commit() with KeyStoreUtils.putStringAndCommit().
+ * With the explicit commit() call, data will be stored synchronously (not recommended for the most cases).
+ *
+ * @param key String
+ * @param value String
+ * @return boolean returns true if the value was successfully written to persistent storage
+ */
+ public static boolean putStringAndCommit(String key, String value) {
+ return settings.edit().putString(key, encrypt(value)).commit();
+ }
+}
diff --git a/app/src/main/java/es/wolfi/utils/PasswordGenerator.java b/app/src/main/java/es/wolfi/utils/PasswordGenerator.java
index 359804d..9d5859a 100644
--- a/app/src/main/java/es/wolfi/utils/PasswordGenerator.java
+++ b/app/src/main/java/es/wolfi/utils/PasswordGenerator.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.utils;
import android.content.Context;
diff --git a/app/src/main/java/es/wolfi/utils/ProgressUtils.java b/app/src/main/java/es/wolfi/utils/ProgressUtils.java
index f8a47a8..647b660 100644
--- a/app/src/main/java/es/wolfi/utils/ProgressUtils.java
+++ b/app/src/main/java/es/wolfi/utils/ProgressUtils.java
@@ -1,3 +1,25 @@
+/**
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2021, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2021, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package es.wolfi.utils;
import android.app.ProgressDialog;
diff --git a/app/src/main/res/layout/fragment_edit_password_text_item.xml b/app/src/main/res/layout/fragment_edit_password_text_item.xml
index cc04580..f64a89b 100644
--- a/app/src/main/res/layout/fragment_edit_password_text_item.xml
+++ b/app/src/main/res/layout/fragment_edit_password_text_item.xml
@@ -3,6 +3,7 @@
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
@@ -39,6 +40,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
+ android:layout_marginEnd="3dp"
app:srcCompat="@drawable/ic_eye_grey"/>
<ImageButton
@@ -47,6 +49,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
+ android:layout_marginEnd="3dp"
app:srcCompat="@drawable/ic_baseline_refresh_24"/>
</merge>
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index c6cfd22..e63a722 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -170,6 +170,26 @@
android:text="@string/enable_credential_list_icons" />
<TextView
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/offline_cache" />
+
+ <TextView
+ style="@style/SettingsDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/offline_cache_description" />
+
+ <com.google.android.material.checkbox.MaterialCheckBox
+ android:id="@+id/enable_offline_cache_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:checked="false"
+ android:text="@string/enable_offline_cache" />
+
+ <TextView
android:id="@+id/default_autofill_vault_title"
style="@style/Label"
android:layout_width="match_parent"
@@ -242,6 +262,15 @@
android:inputType="number"
tools:ignore="LabelFor" />
+ <Button
+ android:id="@+id/clear_offline_cache_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_gravity="start"
+ android:text="@string/clear_offline_cache"
+ android:theme="@style/Button.Danger" />
+
</LinearLayout>
</ScrollView>
diff --git a/app/src/main/res/layout/fragment_vault.xml b/app/src/main/res/layout/fragment_vault.xml
index 5f3c3ab..034ab21 100644
--- a/app/src/main/res/layout/fragment_vault.xml
+++ b/app/src/main/res/layout/fragment_vault.xml
@@ -5,6 +5,7 @@
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
* @copyright Copyright (c) 2017, Andy Scherzinger
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
@@ -90,4 +91,25 @@
</TableLayout>
</LinearLayout>
+
+ <ImageView
+ android:id="@+id/vault_delete_button"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_marginTop="12dp"
+ android:layout_marginEnd="8dp"
+ android:src="@drawable/ic_baseline_delete_24"
+ card_view:tint="@color/secondary_button_background_color" />
+
+ <ImageView
+ android:id="@+id/vault_edit_button"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_marginTop="12dp"
+ android:layout_marginEnd="50dp"
+ android:src="@drawable/ic_baseline_edit_24"
+ card_view:tint="@android:color/darker_gray" />
+
</androidx.cardview.widget.CardView>
diff --git a/app/src/main/res/layout/fragment_vault_add.xml b/app/src/main/res/layout/fragment_vault_add.xml
new file mode 100644
index 0000000..84ef19d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_vault_add.xml
@@ -0,0 +1,125 @@
+<!--
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/floating_button_parent_padding">
+
+ <TextView
+ android:id="@+id/add_vault_name_header"
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="0dp"
+ android:text="@string/vault_name" />
+
+ <EditText
+ android:id="@+id/add_vault_name"
+ style="@style/FormText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/add_vault_password_header"
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/vault_password" />
+
+ <es.wolfi.app.passman.EditPasswordTextItem
+ android:id="@+id/add_vault_password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingRight="-10dp" />
+
+ <TextView
+ android:id="@+id/add_vault_password_repeat_header"
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/vault_password" />
+
+ <es.wolfi.app.passman.EditPasswordTextItem
+ android:id="@+id/add_vault_password_repeat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingRight="-10dp" />
+
+ <TextView
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sharing_key_strength" />
+
+ <TableLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TableRow>
+
+ <Spinner
+ android:id="@+id/add_vault_sharing_key_strength"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="Bit" />
+ </TableRow>
+ </TableLayout>
+
+ </LinearLayout>
+ </ScrollView>
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom|end">
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/SaveVaultButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:backgroundTint="?attr/colorPrimary"
+ app:srcCompat="@drawable/ic_baseline_save_24_white" />
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_vault_delete.xml b/app/src/main/res/layout/fragment_vault_delete.xml
new file mode 100644
index 0000000..0a3b86c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_vault_delete.xml
@@ -0,0 +1,87 @@
+<!--
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/floating_button_parent_padding">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="0dp"
+ android:text="@string/confirm_vault_deletion" />
+
+ <TextView
+ android:id="@+id/vault_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:textColor="@color/danger"
+ android:textSize="15sp" />
+
+ <TextView
+ android:id="@+id/delete_vault_password_header"
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="25dp"
+ android:text="@string/vault_password" />
+
+ <es.wolfi.app.passman.EditPasswordTextItem
+ android:id="@+id/delete_vault_password"
+ style="@style/FormText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+ </ScrollView>
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom|end">
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/DeleteVaultButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:backgroundTint="?attr/colorPrimary"
+ app:srcCompat="@drawable/ic_baseline_delete_24_white" />
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_vault_edit.xml b/app/src/main/res/layout/fragment_vault_edit.xml
new file mode 100644
index 0000000..a3f0769
--- /dev/null
+++ b/app/src/main/res/layout/fragment_vault_edit.xml
@@ -0,0 +1,73 @@
+<!--
+ * Passman Android App
+ *
+ * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
+ * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/floating_button_parent_padding">
+
+ <TextView
+ android:id="@+id/edit_vault_name_header"
+ style="@style/Label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="0dp"
+ android:text="@string/new_vault_name" />
+
+ <EditText
+ android:id="@+id/edit_vault_name"
+ style="@style/FormText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+ </ScrollView>
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom|end">
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/SaveVaultButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:backgroundTint="?attr/colorPrimary"
+ app:srcCompat="@drawable/ic_baseline_save_24_white" />
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_vault_list.xml b/app/src/main/res/layout/fragment_vault_list.xml
index e4b4df6..6dd2169 100644
--- a/app/src/main/res/layout/fragment_vault_list.xml
+++ b/app/src/main/res/layout/fragment_vault_list.xml
@@ -1,9 +1,9 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
* Passman Android App
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
+ * @copyright Copyright (c) 2021, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
@@ -20,15 +20,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
-->
-<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/list"
- android:name="es.wolfi.app.passman.VaultFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginLeft="0dp"
- android:layout_marginRight="0dp"
- app:layoutManager="LinearLayoutManager"
- tools:context="es.wolfi.app.passman.fragments.VaultFragment"
- tools:listitem="@layout/fragment_vault" />
+ android:orientation="vertical">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:name="es.wolfi.app.passman.VaultFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="0dp"
+ android:layout_marginRight="0dp"
+ android:layout_weight="1"
+ app:layoutManager="LinearLayoutManager"
+ tools:context="es.wolfi.app.passman.fragments.VaultFragment"
+ tools:listitem="@layout/fragment_vault">
+
+ </androidx.recyclerview.widget.RecyclerView>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/add_vault_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:backgroundTint="?attr/colorPrimary"
+ app:srcCompat="@drawable/ic_plus_white" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
new file mode 100644
index 0000000..e98a688
--- /dev/null
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -0,0 +1,74 @@
+<resources>
+ <string name="action_settings">Настройки</string>
+ <string name="action_refresh">Опресняване</string>
+ <string name="nchost">Адрес на сървъра</string>
+ <string name="username">Потребител</string>
+ <string name="password">Парола</string>
+ <string name="connect">Свързване</string>
+ <string name="wrongNCUrl">URL адреса на Nextcloud сървъра е грешен или не може да се осъществи връзка</string>
+ <string name="wrongNCSettings">Настройките са грешни</string>
+ <string name="net_error">Мрежова грешка</string>
+ <string name="unlock">Отключване</string>
+ <string name="wrong_vault_pw">Грешна парола за хранилището</string>
+ <string name="label">Етикет</string>
+ <string name="email">Имейл</string>
+ <string name="URL">URL</string>
+ <string name="description">Описание</string>
+ <string name="copied_to_clipboard">Текста е копиран</string>
+ <string name="vault_password">Парола на хранилището</string>
+ <string name="vault_password_save">Запомни паролата</string>
+ <string name="not_implemented_yet">Все още не е внедрен</string>
+ <string name="created">Създадено:</string>
+ <string name="last_accessed">Последно вписване:</string>
+ <string name="search">Търсене</string>
+ <string name="save">Запиши</string>
+ <string name="successfully_saved">Успешно записан</string>
+ <string name="error_occurred">Възникна грешка</string>
+ <string name="update">Обновяване</string>
+ <string name="successfully_updated">Успешно обновен</string>
+ <string name="delete">Изтриване</string>
+ <string name="successfully_deleted">Успешно изтрит</string>
+ <string name="nextcloud_connection_settings">Настройки за връзка с Nextcloud</string>
+ <string name="app_settings">Настройки на приложението</string>
+ <string name="wait_while_loading">Изчакайте, докато се зарежда … </string>
+ <string name="loading">Зареждане</string>
+ <string name="error_downloading_file">Грешка при изтегляне на файл</string>
+ <string name="error_writing_file">Грешка при записването на файла</string>
+ <string name="files">Файлове</string>
+ <string name="custom_fields">Персонализирани полета</string>
+ <string name="add_file">Добавяне на файл</string>
+ <string name="add_custom_field">Добавяне на персонализирано поле</string>
+ <string name="unlock_passman">Отключване на Passman</string>
+ <string name="unlock_passman_message_device_auth">Моля, удостоверете се за достъп до Passman</string>
+ <string name="app_start_password">Парола за стартиране на приложението</string>
+ <string name="app_start_password_android_auth_description">Активиране на удостоверяване на потребител на Android при стартиране на приложението</string>
+ <string name="wait_while_encrypting">Изчакайте, докато се криптира …</string>
+ <string name="wait_while_decrypting">Изчакайте, докато се дешифрира …</string>
+ <string name="wait_while_uploading">Изчакайте, докато се качва …</string>
+ <string name="wait_while_downloading">Изчакайте, докато се изтегля …</string>
+ <string name="autofill_noactivevault">Няма активно хранилище</string>
+ <string name="autofill_vaultlocked">Хранилището трябва да бъде отключено</string>
+ <string name="autofill_vaultempty">Няма идентификационни данни в хранилището</string>
+ <string name="autofill_createdbyautofillservice">Създадено от услугата за автоматично попълване</string>
+ <string name="default_autofill_vault">Хранилище за автоматично попълване по подразбиране</string>
+ <string name="automatically">автоматично</string>
+ <string name="request_connect_timeout">Време за изчакване на свързване (в секунди)</string>
+ <string name="request_response_timeout">Време за изчакване на отговор на заявката (в секунди)</string>
+ <string name="expert_settings">Експертни настройки</string>
+ <string name="confirm_credential_deletion">Сигурен ли сте, че искате да изтриете този идентификационен номер?</string>
+ <string name="yes">Да</string>
+ <string name="cancel">Отказ</string>
+ <string name="enable_credential_list_icons">Активиране на иконите на списъка с идентификационни данни</string>
+ <string name="credential_icon">Икона за идентификационни данни</string>
+ <string name="clear_clipboard_delay">Изчистване на клипборда (след секунди)</string>
+ <string name="generate_password">Генериране на парола</string>
+ <string name="generate_password_to_clipboard">Генериране на произволна парола и копиране в клипборда</string>
+ <string name="enable_password_generator_shortcut_description">Активиране на пряк път към приложението за генериране на пароли</string>
+ <string name="password_generator_require_every_char_type">Изискване за всеки тип символ</string>
+ <string name="password_generator_avoid_ambiguous_chars">Избягване на двусмислени знаци</string>
+ <string name="password_generator_use_special_chars">Използване на специални знаци</string>
+ <string name="password_generator_use_digits">Използване на цифри</string>
+ <string name="password_generator_use_lowercase">Използване на малки букви</string>
+ <string name="password_generator_use_uppercase">Използване на главни букви</string>
+ <string name="password_length">Дължина на паролата</string>
+ </resources>
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index 3fda2e4..6fecfc1 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Nesprávná URL Nextcloud podpůrné vrstvy nebo se nedaří spojit</string>
<string name="wrongNCSettings">Nesprávná nastavení</string>
<string name="net_error">Chyba sítě</string>
+ <string name="net_error_dialog_description">Nedaří se připojit k serveru!\nBez funkčního připojení k serveru, nemohou být změny uloženy.</string>
<string name="unlock">Odemknout</string>
<string name="wrong_vault_pw">Nesprávné heslo k trezoru</string>
<string name="label">Štítek</string>
@@ -56,6 +57,7 @@
<string name="request_response_timeout">Časový limit požadavku na odpověď (sekundy)</string>
<string name="expert_settings">Nastavení pro pokročilé uživatele</string>
<string name="confirm_credential_deletion">Opravdu chcete tento přihlašovací údaj smazat?</string>
+ <string name="confirm_vault_deletion">Opravdu chcete tento trezor smazat?</string>
<string name="yes">Ano</string>
<string name="cancel">Storno</string>
<string name="enable_credential_list_icons">Zapnout ikony v seznamu přihlašovacích údajů</string>
@@ -71,4 +73,11 @@
<string name="password_generator_use_lowercase">Použít malá písmena</string>
<string name="password_generator_use_uppercase">Použít velká písmena</string>
<string name="password_length">Délka hesla</string>
+ <string name="vault_name">Název trezoru</string>
+ <string name="sharing_key_strength">Odolnost klíče pro sdílení</string>
+ <string name="new_vault_name">Název pro nový trezor</string>
+ <string name="offline_cache">Mezipaměť offline režimu</string>
+ <string name="offline_cache_description">Uchovává zašifrované trezory, pokud byly načteny ručně, v mezipaměti</string>
+ <string name="enable_offline_cache">Zapnout mezipaměť offline režimu</string>
+ <string name="clear_offline_cache">Vymazat mezipaměť offline režimu</string>
</resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 83a6f4d..4ae64dc 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Falsche Nextcloud-Backend-URL oder keine Verbindung möglich</string>
<string name="wrongNCSettings">Einstellungen falsch</string>
<string name="net_error">Netzwerkfehler</string>
+ <string name="net_error_dialog_description">Es konnte keine Verbindung zum Server hergestellt werden!\nÄnderungen können ohne eine funktionierende Serververbindung nicht gespeichert werden.</string>
<string name="unlock">Öffnen</string>
<string name="wrong_vault_pw">Falsches Tresor-Passwort</string>
<string name="label">Beschriftung</string>
@@ -56,6 +57,7 @@
<string name="request_response_timeout">Zeitüberschreitung bei der Beantwortung von Anfragen (in Sekunden)</string>
<string name="expert_settings">Experteneinstellungen</string>
<string name="confirm_credential_deletion">Möchten Sie diese Zugangsdaten wirklisch löschen?</string>
+ <string name="confirm_vault_deletion">Möchten Sie diesen Tresor wirklich löschen?</string>
<string name="yes">Ja</string>
<string name="cancel">Abbrechen</string>
<string name="enable_credential_list_icons">Symbole für Anmeldeinformationen aktivieren</string>
@@ -71,4 +73,11 @@
<string name="password_generator_use_lowercase">Kleinbuchstaben verwenden</string>
<string name="password_generator_use_uppercase">Großbuchstaben verwenden</string>
<string name="password_length">Passwortlänge</string>
+ <string name="vault_name">Tresorname</string>
+ <string name="sharing_key_strength">Schlüsselstärke teilen</string>
+ <string name="new_vault_name">Neuer Tresorname</string>
+ <string name="offline_cache">Offline-Cache</string>
+ <string name="offline_cache_description">Die verschlüsselten Tresore werden zwischengespeichert, wenn sie manuell geladen wurden.</string>
+ <string name="enable_offline_cache">Offline-Cache aktivieren</string>
+ <string name="clear_offline_cache">Offline-Cache leeren</string>
</resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index c478410..156b51c 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">URL del backend Nextcloud incorrecto o incapaz de conectar</string>
<string name="wrongNCSettings">Configuraciones incorrectas</string>
<string name="net_error">Error de red</string>
+ <string name="net_error_dialog_description">No se ha podido conectar al servidor.\nLos cambios no se pueden guardar sin una conexión al servidor funcionando.</string>
<string name="unlock">Desbloquear</string>
<string name="wrong_vault_pw">Contraseña errónea de la caja fuerte</string>
<string name="label">Etiqueta</string>
@@ -40,7 +41,39 @@
<string name="add_custom_field">Añadir un campo personalizado</string>
<string name="unlock_passman">Desbloquea Passman</string>
<string name="unlock_passman_message_device_auth">Por favor, identifícate para acceder a Passman</string>
+ <string name="app_start_password">Contraseña al iniciar la aplicación</string>
+ <string name="app_start_password_android_auth_description">Habilitar autenticación del usuario Android al iniciar la aplicación</string>
+ <string name="wait_while_encrypting">Espera mientras se encripta ...</string>
+ <string name="wait_while_decrypting">Espera mientras se desencripta ...</string>
+ <string name="wait_while_uploading">Espera mientras se sube ...</string>
+ <string name="wait_while_downloading">Espera mientras se descarga ...</string>
+ <string name="autofill_noactivevault">No hay una caja fuerte activa</string>
+ <string name="autofill_vaultlocked">Hay que desbloquear la caja fuerte</string>
+ <string name="autofill_vaultempty">No hay credenciales en la caja fuerte</string>
+ <string name="autofill_createdbyautofillservice">Creado por el servicio de autocompletado</string>
+ <string name="default_autofill_vault">Caja fuerte por defecto para autocompletar</string>
+ <string name="automatically">automáticamente</string>
+ <string name="request_connect_timeout">Límite de tiempo de la solicitud (en segundos)</string>
+ <string name="request_response_timeout">Límite de tiempo de la respuesta (en segundos)</string>
<string name="expert_settings">Configuración avanzada</string>
+ <string name="confirm_credential_deletion">¿Estás seguro de querer borrar esta credencial?</string>
<string name="yes">Sí</string>
<string name="cancel">Cancelar</string>
- </resources>
+ <string name="enable_credential_list_icons">Habilitar los iconos en la lista de credenciales</string>
+ <string name="credential_icon">Icono del credencial</string>
+ <string name="clear_clipboard_delay">Limpiar el portapapeles (tras ... segundos)</string>
+ <string name="generate_password">Generar contraseña</string>
+ <string name="generate_password_to_clipboard">Generar una contraseña aleatoria y copiar al portapapeles</string>
+ <string name="enable_password_generator_shortcut_description">Habilitar acceso directo al generador de contraseñas</string>
+ <string name="password_generator_require_every_char_type">Requerir todos los tipos de caracter</string>
+ <string name="password_generator_avoid_ambiguous_chars">Evitar caracteres ambiguos</string>
+ <string name="password_generator_use_special_chars">Usar caracteres especiales</string>
+ <string name="password_generator_use_digits">Usar dígitos</string>
+ <string name="password_generator_use_lowercase">Usar letras minúsculas</string>
+ <string name="password_generator_use_uppercase">Usar letras mayúsculas</string>
+ <string name="password_length">Longitud de la contraseña</string>
+ <string name="offline_cache">Caché sin conexión</string>
+ <string name="offline_cache_description">Guarda en caché las bóvedas cifradas si se han cargado manualmente</string>
+ <string name="enable_offline_cache">Activar caché sin conexión</string>
+ <string name="clear_offline_cache">Limpiar caché sin conexión</string>
+</resources>
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 55b6f60..f041ce0 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -71,4 +71,8 @@
<string name="password_generator_use_lowercase">Erabili karaktere minuskulak</string>
<string name="password_generator_use_uppercase">Erabili karaktere maiuskulak</string>
<string name="password_length">Pasahitzaren luzera</string>
+ <string name="new_vault_name">Gordailu berriaren izena</string>
+ <string name="offline_cache">Lineaz kanpoko cachea</string>
+ <string name="enable_offline_cache">Gaitu lineaz kanpoko cachea</string>
+ <string name="clear_offline_cache">Garbitu lineaz kanpoko cachea</string>
</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 8d03d38..ed6babb 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -36,6 +36,15 @@
<string name="error_writing_file">Erreur à l\'écriture du fichier</string>
<string name="files">Fichiers</string>
<string name="custom_fields">Champs personnalisés</string>
+ <string name="add_file">Ajouter un fichier</string>
+ <string name="add_custom_field">Ajouter un champ personnalisé</string>
+ <string name="app_start_password">Mot de passe de démarrage de l\'appli</string>
<string name="wait_while_uploading">Patienter pendant le téléchargement</string>
<string name="automatically">automatiquement</string>
+ <string name="yes">Oui</string>
+ <string name="cancel">Annuler</string>
+ <string name="generate_password">Générer un mot de passe</string>
+ <string name="password_generator_use_lowercase">Utiliser des lettres minuscules</string>
+ <string name="password_generator_use_uppercase">Utiliser des lettres majuscules</string>
+ <string name="password_length">Longueur du mot de passe</string>
</resources>
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000..e266328
--- /dev/null
+++ b/app/src/main/res/values-hr/strings.xml
@@ -0,0 +1,74 @@
+<resources>
+ <string name="action_settings">Postavke</string>
+ <string name="action_refresh">Osvježi</string>
+ <string name="nchost">Adresa poslužitelja</string>
+ <string name="username">Korisničko ime</string>
+ <string name="password">Zaporka</string>
+ <string name="connect">Poveži</string>
+ <string name="wrongNCUrl">Netočan pozadinski URL Nextclouda ili se ne može uspostaviti veza</string>
+ <string name="wrongNCSettings">Postavke nisu ispravne</string>
+ <string name="net_error">Pogreška mreže</string>
+ <string name="unlock">Otključaj</string>
+ <string name="wrong_vault_pw">Pogrešna zaporka trezora</string>
+ <string name="label">Oznaka</string>
+ <string name="email">E-pošta</string>
+ <string name="URL">URL</string>
+ <string name="description">Opis</string>
+ <string name="copied_to_clipboard">Tekst kopiran u međuspremnik</string>
+ <string name="vault_password">Zaporka trezora</string>
+ <string name="vault_password_save">Spremi zaporku trezora</string>
+ <string name="not_implemented_yet">Još nije implementirano</string>
+ <string name="created">Stvoreno:</string>
+ <string name="last_accessed">Posljednji pristup:</string>
+ <string name="search">Traži</string>
+ <string name="save">Spremi</string>
+ <string name="successfully_saved">Uspješno spremljeno</string>
+ <string name="error_occurred">Došlo je do pogreške</string>
+ <string name="update">Ažuriraj</string>
+ <string name="successfully_updated">Uspješno ažurirano</string>
+ <string name="delete">Izbriši</string>
+ <string name="successfully_deleted">Uspješno izbrisano</string>
+ <string name="nextcloud_connection_settings">Postavke veze Nextclouda</string>
+ <string name="app_settings">Postavke aplikacije</string>
+ <string name="wait_while_loading">Pričekajte dok se učitava…</string>
+ <string name="loading">Učitavanje</string>
+ <string name="error_downloading_file">Pogreška pri preuzimanju datoteke</string>
+ <string name="error_writing_file">Pogreška pisanja datoteke</string>
+ <string name="files">Datoteke</string>
+ <string name="custom_fields">Prilagodljiva polja</string>
+ <string name="add_file">Dodaj datoteku</string>
+ <string name="add_custom_field">Dodaj prilagodljivo polje</string>
+ <string name="unlock_passman">Otključaj Passman</string>
+ <string name="unlock_passman_message_device_auth">Autorizirajte se za pristup Passmanu</string>
+ <string name="app_start_password">Zaporka za pokretanje aplikacije</string>
+ <string name="app_start_password_android_auth_description">Omogućite autorizaciju Android korisnika pri pokretanju aplikacije</string>
+ <string name="wait_while_encrypting">Pričekajte dok se šifrira...</string>
+ <string name="wait_while_decrypting">Pričekajte dok se dešifrira...</string>
+ <string name="wait_while_uploading">Pričekajte dok se otprema...</string>
+ <string name="wait_while_downloading">Pričekajte dok se preuzima...</string>
+ <string name="autofill_noactivevault">Nema aktivnog trezora</string>
+ <string name="autofill_vaultlocked">Trezor mora biti otključan</string>
+ <string name="autofill_vaultempty">Nema vjerodajnica u trezoru</string>
+ <string name="autofill_createdbyautofillservice">Stvorila usluga automatskog popunjavanja</string>
+ <string name="default_autofill_vault">Zadan trezor automatskog popunjavanja</string>
+ <string name="automatically">automatsko</string>
+ <string name="request_connect_timeout">Istek zahtjeva za povezivanje (u sekundama)</string>
+ <string name="request_response_timeout">Istek zahtjeva za odgovorom (u sekundama)</string>
+ <string name="expert_settings">Napredne postavke</string>
+ <string name="confirm_credential_deletion">Jeste li sigurni da želite izbrisati ovu vjerodajnicu?</string>
+ <string name="yes">Da</string>
+ <string name="cancel">Odustani</string>
+ <string name="enable_credential_list_icons">Omogući ikone popisa vjerodajnica</string>
+ <string name="credential_icon">Ikona vjerodajnice</string>
+ <string name="clear_clipboard_delay">Izbriši sadržaj međuspremnika (nakon sekundi)</string>
+ <string name="generate_password">Generiraj zaporku</string>
+ <string name="generate_password_to_clipboard">Generiraj nasumičnu zaporku i kopiraj je u međuspremnik</string>
+ <string name="enable_password_generator_shortcut_description">Omogući prečac aplikacije za generiranje zaporki</string>
+ <string name="password_generator_require_every_char_type">Koristiti svaku vrstu znakova</string>
+ <string name="password_generator_avoid_ambiguous_chars">Izbjegavaj dvosmislene znakove</string>
+ <string name="password_generator_use_special_chars">Koristi posebne znakove</string>
+ <string name="password_generator_use_digits">Koristi brojke</string>
+ <string name="password_generator_use_lowercase">Koristi mala slova</string>
+ <string name="password_generator_use_uppercase">Koristi velika slova</string>
+ <string name="password_length">Dužina zaporke</string>
+ </resources>
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 6693cce..0729ea9 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Hibás Nextcloud háttérszolgáltatás URL vagy sikertelen kapcsolódás</string>
<string name="wrongNCSettings">Hibás beállítások</string>
<string name="net_error">Hálózati hiba</string>
+ <string name="net_error_dialog_description">Nem sikerült kapcsolódni a kiszolgálóhoz!\nA módosítások nem menthetők működő kiszolgálókapcsolat nélkül.</string>
<string name="unlock">Feloldás</string>
<string name="wrong_vault_pw">Hibás széf jelszó</string>
<string name="label">Címke</string>
@@ -71,4 +72,8 @@
<string name="password_generator_use_lowercase">Kisbetűk használata</string>
<string name="password_generator_use_uppercase">Nagybetűk használata</string>
<string name="password_length">Jelszó hossza</string>
+ <string name="offline_cache">Offline gyorsítótár </string>
+ <string name="offline_cache_description">Gyorsítótárazza a titkosított széfeket, ha kézileg lettek betöltve</string>
+ <string name="enable_offline_cache">Offline gyorsítótár engedélyezése</string>
+ <string name="clear_offline_cache">Offline gyorsítótár törlése</string>
</resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index c891677..dccf11b 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">URL del motore di Nextcloud non corretto o impossibile connettersi</string>
<string name="wrongNCSettings">Impostazioni non corrette</string>
<string name="net_error">Errore di rete</string>
+ <string name="net_error_dialog_description">Impossibile connettersi al server!\nLe modifiche non possono essere salvate senza una connessione funzionante al server.</string>
<string name="unlock">Sblocca</string>
<string name="wrong_vault_pw">Password della cassaforte errata</string>
<string name="label">Etichetta</string>
@@ -41,6 +42,7 @@
<string name="unlock_passman">Sblocca Passman</string>
<string name="unlock_passman_message_device_auth">Autenticati per accedere a Passman</string>
<string name="app_start_password">Password di avvio dell\'app</string>
+ <string name="app_start_password_android_auth_description">Attivare l\'autenticazione utente di Android all\'avvio dell\'app</string>
<string name="wait_while_encrypting">Attendi la crittografia ...</string>
<string name="wait_while_decrypting">Attendi la decifrazione ...</string>
<string name="wait_while_uploading">Attendi l\'invio ...</string>
@@ -57,4 +59,21 @@
<string name="confirm_credential_deletion">Vuoi davvero eliminare questa credenziale?</string>
<string name="yes">Sì</string>
<string name="cancel">Annulla</string>
- </resources>
+ <string name="enable_credential_list_icons">Abilita le icone elenco credenziali</string>
+ <string name="credential_icon">Icona delle credenziali</string>
+ <string name="clear_clipboard_delay">Cancella appunti (dopo secondi)</string>
+ <string name="generate_password">Genera password</string>
+ <string name="generate_password_to_clipboard">Genera una password casuale e copiala negli appunti</string>
+ <string name="enable_password_generator_shortcut_description">Abilita collegamento all\'applicazione generatore di password</string>
+ <string name="password_generator_require_every_char_type">Richiedi ogni tipo di carattere</string>
+ <string name="password_generator_avoid_ambiguous_chars">Evita caratteri ambigui</string>
+ <string name="password_generator_use_special_chars">Utilizza caratteri speciali</string>
+ <string name="password_generator_use_digits">Usa cifre</string>
+ <string name="password_generator_use_lowercase">Utilizza caratteri minuscole</string>
+ <string name="password_generator_use_uppercase">Utilizza caratteri maiuscole</string>
+ <string name="password_length">Lunghezza della password</string>
+ <string name="offline_cache">Cache offline</string>
+ <string name="offline_cache_description">Memorizza nella cache i depositi crittografati se sono stati caricati manualmente</string>
+ <string name="enable_offline_cache">Abilita cache offline</string>
+ <string name="clear_offline_cache">Svuota la cache offline</string>
+</resources>
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index 477d59a..58b81ee 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -41,4 +41,4 @@
<string name="password_generator_use_lowercase">Naudoti mažąsias raides</string>
<string name="password_generator_use_uppercase">Naudoti didžiąsias raides</string>
<string name="password_length">Slaptažodžio ilgis</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 1883fd4..e7f6552 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Verkeerde Nextcloud link of niet in staat te verbinden</string>
<string name="wrongNCSettings">Onjuiste configuratie</string>
<string name="net_error">Netwerk fout</string>
+ <string name="net_error_dialog_description">Kan geen verbinding maken met de server!\nWijzigingen kunnen niet worden opgeslagen zonder een werkende serververbinding.</string>
<string name="unlock">Deblokkeren</string>
<string name="wrong_vault_pw">Onjuist kluiswachtwoord</string>
<string name="label">Label</string>
@@ -71,4 +72,8 @@
<string name="password_generator_use_lowercase">Gebruik kleine letters</string>
<string name="password_generator_use_uppercase">Gebruik hoofdletters</string>
<string name="password_length">Wachtwoordlengte</string>
+ <string name="offline_cache">Off-line cache</string>
+ <string name="offline_cache_description">De gecodeerde kluizen worden in de cache opgeslagen als ze handmatig zijn geladen</string>
+ <string name="enable_offline_cache">Inschakelen off-line cache</string>
+ <string name="clear_offline_cache">Leesgmaken off-line cache</string>
</resources>
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index a49e89e..a33ab32 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Nieprawidłowy URL serwera Nextcloud lub brak połączenia</string>
<string name="wrongNCSettings">Błędne ustawienia</string>
<string name="net_error">Błąd sieci</string>
+ <string name="net_error_dialog_description">Nie można połączyć się z serwerem!\nZmiany nie mogą zostać zapisane bez działającego połączenia z serwerem.</string>
<string name="unlock">Odblokuj</string>
<string name="wrong_vault_pw">Nieprawidłowe hasło do sejfu</string>
<string name="label">Etykieta</string>
@@ -56,6 +57,7 @@
<string name="request_response_timeout">Czas oczekiwania na odpowiedź (w sekundach)</string>
<string name="expert_settings">Ustawienia eksperckie</string>
<string name="confirm_credential_deletion">Czy na pewno chcesz usunąć to poświadczenie?</string>
+ <string name="confirm_vault_deletion">Czy na pewno chcesz usunąć ten sejf?</string>
<string name="yes">Tak</string>
<string name="cancel">Anuluj</string>
<string name="enable_credential_list_icons">Włącz ikony listy poświadczeń</string>
@@ -71,4 +73,11 @@
<string name="password_generator_use_lowercase">Użyj małych liter</string>
<string name="password_generator_use_uppercase">Użyj wielkich liter</string>
<string name="password_length">Długość hasła</string>
+ <string name="vault_name">Nazwa sejfu</string>
+ <string name="sharing_key_strength">Udostępnianie siły klucza</string>
+ <string name="new_vault_name">Nowa nazwa sejfu</string>
+ <string name="offline_cache">Pamięć podręczna offline</string>
+ <string name="offline_cache_description">Buforuje zaszyfrowane sejfy, jeśli zostały wczytane ręcznie</string>
+ <string name="enable_offline_cache">Włącz pamięć podręczną offline</string>
+ <string name="clear_offline_cache">Wyczyść pamięć podręczną offline</string>
</resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 8b13c87..3217475 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">URL da plataforma de serviço Nextcloud incorreta ou impossível conectar</string>
<string name="wrongNCSettings">Configurações incorretas</string>
<string name="net_error">Erro de rede</string>
+ <string name="net_error_dialog_description">Não foi possível conectar-se ao servidor!\nAs alterações não podem ser salvas sem uma conexão de servidor de funcionando.</string>
<string name="unlock">Desbloquear</string>
<string name="wrong_vault_pw">Senha do cofre incorreta</string>
<string name="label">Etiqueta</string>
@@ -71,4 +72,8 @@
<string name="password_generator_use_lowercase">Use caracteres minúsculos</string>
<string name="password_generator_use_uppercase">Use letras maiúsculas</string>
<string name="password_length">Comprimento da senha</string>
+ <string name="offline_cache">Cache offline</string>
+ <string name="offline_cache_description">Armazena em cache os cofres criptografados se eles foram carregados manualmente</string>
+ <string name="enable_offline_cache">Habilitar cache offline</string>
+ <string name="clear_offline_cache">Limpar cache offline</string>
</resources>
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index 166a967..63c5e78 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Nesprávna URL Nextcloud backend servera alebo sa nie je možné pripojiť</string>
<string name="wrongNCSettings">Nesprávne nastavenia</string>
<string name="net_error">Chyba siete</string>
+ <string name="net_error_dialog_description">Nepodarilo sa pripojiť k serveru!\nZmeny nie je možné uložiť bez funkčného pripojenia k serveru.</string>
<string name="unlock">Odomknúť</string>
<string name="wrong_vault_pw">Nesprávne heslo k trezoru</string>
<string name="label">Štítok</string>
@@ -71,4 +72,8 @@
<string name="password_generator_use_lowercase">Použiť malé znaky</string>
<string name="password_generator_use_uppercase">Použiť veľké znaky</string>
<string name="password_length">Dĺžka hesla</string>
+ <string name="offline_cache">Offline vyrovnávacia pamäť</string>
+ <string name="offline_cache_description">Ukladá do vyrovnávacej pamäte šifrované trezory, ak boli načítané manuálne</string>
+ <string name="enable_offline_cache">Zapnúť offline vyrovnávaciu pamäť</string>
+ <string name="clear_offline_cache">Vyčistiť offline vyrovnávaciu pamäť</string>
</resources>
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 041f5b5..2ed0e29 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -70,4 +70,4 @@
<string name="password_generator_use_lowercase">Uporabi male črke</string>
<string name="password_generator_use_uppercase">Uporabi velike črke</string>
<string name="password_length">Dolžina gesla</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 91b33e4..722f9b4 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">Nextcloud yönetim bölümü adresi yanlış ya da bağlanılamıyor</string>
<string name="wrongNCSettings">Ayarlar hatalı</string>
<string name="net_error">Ağ sorunu</string>
+ <string name="net_error_dialog_description">Sunucu ile bağlantı kurulamadı!\nDeğişiklikler, çalışan bir sunucu bağlantısı olmadan kaydedilemez.</string>
<string name="unlock">Kilidi aç</string>
<string name="wrong_vault_pw">Kasa parolası yanlış</string>
<string name="label">Etiket</string>
@@ -56,6 +57,7 @@
<string name="request_response_timeout">İstek yanıtı zaman aşımı (saniye)</string>
<string name="expert_settings">Uzman ayarları</string>
<string name="confirm_credential_deletion">Bu kimlik doğrulama bilgisini silmek istediğinize emin misiniz?</string>
+ <string name="confirm_vault_deletion">Bu kasayı silmek istediğinize emin misiniz?</string>
<string name="yes">Evet</string>
<string name="cancel">İptal</string>
<string name="enable_credential_list_icons">Kimlik doğrulama listesi simgeleri kullanılsın</string>
@@ -71,4 +73,11 @@
<string name="password_generator_use_lowercase">Küçük harf karakterler kullanılsın</string>
<string name="password_generator_use_uppercase">Büyük harf karakterler kullanılsın</string>
<string name="password_length">Parola uzunluğu</string>
+ <string name="vault_name">Kasa adı</string>
+ <string name="sharing_key_strength">Paylaşılan anahtar güçlüğü</string>
+ <string name="new_vault_name">Yeni kasa adı</string>
+ <string name="offline_cache">Çevrimdışı ön bellek</string>
+ <string name="offline_cache_description">El ile olarak yüklenmişlerse şifrelenmiş kasaları ön belleğe alır</string>
+ <string name="enable_offline_cache">Çevrimdışı ön bellek kullanılsın</string>
+ <string name="clear_offline_cache">Çevrimdışı ön belleği temizle</string>
</resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 9b07a7e..0355ff5 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">无效 Nextcloud 后端 URL 或无法访问</string>
<string name="wrongNCSettings">配置错误</string>
<string name="net_error">网络错误</string>
+ <string name="net_error_dialog_description">无法连接到服务器!\n 没有正常工作的服务器连接,将无法保存更改。</string>
<string name="unlock">解锁</string>
<string name="wrong_vault_pw">保险箱密码错误</string>
<string name="label">标签</string>
@@ -71,4 +72,8 @@
<string name="password_generator_use_lowercase">使用小写字符</string>
<string name="password_generator_use_uppercase">使用大写字符</string>
<string name="password_length">密码长度</string>
+ <string name="offline_cache">离线缓存</string>
+ <string name="offline_cache_description">缓存手动加载的密码库</string>
+ <string name="enable_offline_cache">启用离线缓存</string>
+ <string name="clear_offline_cache">清除离线缓存</string>
</resources>
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 8dfd80c..31d1a6a 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -8,6 +8,7 @@
<string name="wrongNCUrl">不正確的Nextcloud後端URL或無法連接</string>
<string name="wrongNCSettings">設定不正確</string>
<string name="net_error">網絡異常</string>
+ <string name="net_error_dialog_description">無法連接到伺服器!\n如果沒有有效的伺服器連接,則無法保存更改。</string>
<string name="unlock">解鎖</string>
<string name="wrong_vault_pw">保險庫密碼錯誤</string>
<string name="label">標籤</string>
@@ -56,6 +57,7 @@
<string name="request_response_timeout">請求回應超時(秒)</string>
<string name="expert_settings">專家設定</string>
<string name="confirm_credential_deletion">您確定要刪除此身份驗證嗎?</string>
+ <string name="confirm_vault_deletion">您確定要刪除此保險庫?</string>
<string name="yes">是</string>
<string name="cancel">取消</string>
<string name="enable_credential_list_icons">啟用身份驗證清單圖示</string>
@@ -71,4 +73,11 @@
<string name="password_generator_use_lowercase">使用小寫字母</string>
<string name="password_generator_use_uppercase">使用大寫字母 </string>
<string name="password_length">密碼長度</string>
+ <string name="vault_name">保險庫名稱</string>
+ <string name="sharing_key_strength">分享密鑰強度</string>
+ <string name="new_vault_name">新保險庫名稱</string>
+ <string name="offline_cache">離線缓存</string>
+ <string name="offline_cache_description">緩存手動加載的已加密保管庫</string>
+ <string name="enable_offline_cache">啟用離線缓存</string>
+ <string name="clear_offline_cache">清除離線缓存</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 80d41cc..bc0d6ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
<string name="wrongNCUrl">Incorrect Nextcloud backend URL or unable to connect</string>
<string name="wrongNCSettings">Settings incorrect</string>
<string name="net_error">Network error</string>
+ <string name="net_error_dialog_description">Could not connect to the server!\nChanges cannot be saved without a working server connection.</string>
<string name="unlock">Unlock</string>
<string name="wrong_vault_pw">Wrong vault password</string>
<string name="label">Label</string>
@@ -60,6 +61,7 @@
<string name="request_response_timeout">Request response timeout (in seconds)</string>
<string name="expert_settings">Expert settings</string>
<string name="confirm_credential_deletion">Are you sure you want to delete this credential?</string>
+ <string name="confirm_vault_deletion">Are you sure you want to delete this vault?</string>
<string name="yes">Yes</string>
<string name="cancel">Cancel</string>
<string name="enable_credential_list_icons">Enable credential list icons</string>
@@ -75,6 +77,13 @@
<string name="password_generator_use_lowercase">Use lowercase characters</string>
<string name="password_generator_use_uppercase">Use uppercase characters</string>
<string name="password_length">Password length</string>
+ <string name="vault_name">Vault name</string>
+ <string name="sharing_key_strength">Sharing key strength</string>
+ <string name="new_vault_name">New vault name</string>
+ <string name="offline_cache">Offline cache</string>
+ <string name="offline_cache_description">Caches the encrypted vaults if they were loaded manually</string>
+ <string name="enable_offline_cache">Enable offline cache</string>
+ <string name="clear_offline_cache">Clear offline cache</string>
<string name="nextcloud_single_sign_on">Nextcloud Single Sign On</string>
<string name="manual_login">Manual Login</string>
<string name="confirm_account_logout">Are you sure you want to log out?</string>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d11a361..7babe0e 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -58,6 +58,12 @@
<item name="android:textSize">14sp</item>
</style>
+ <style name="SettingsDescription">
+ <item name="android:layout_marginTop">3dp</item>
+ <item name="android:layout_marginStart">6dp</item>
+ <item name="android:textSize">13sp</item>
+ </style>
+
<style name="FormText">
<item name="android:textColor">#333333</item>
<item name="android:textSize">16sp</item>
diff --git a/build.gradle b/build.gradle
index b1b4f06..016e46a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
apply plugin: 'maven-publish'
- classpath 'com.android.tools.build:gradle:7.0.2'
+ classpath 'com.android.tools.build:gradle:7.1.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt
new file mode 100644
index 0000000..bddf4f0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/14.txt
@@ -0,0 +1,9 @@
+- add offline cache (can be disabled in settings)
+- add local storage encryption for sensitive data (requires at least Android 6 / API 23)
+ - encrypts the offline cache
+ - stored vault passwords have to be re-entered
+ - cloud connection settings will be migrated automatically
+- add an internal settings cache to speed up some things a bit
+- add vault actions (add, rename, delete)
+ - delete action requires at least Passman v2.4.0 (server side)
+- update dependencies (gradle/appcompat)
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index bec1ed0..cf40167 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -6,13 +6,16 @@ This app is only compatible with Passman V2.x or higher.
* Setup app (enter the nextcloud server settings)
* App start password option based on the android user authentication
-* Display vault list
+* View, add, rename and delete vaults
* Login to vault
* Display credential list
* View, add, edit and delete credentials
* Add, download and delete files
* OTP generation
* Basic Android autofill implementation
+* Password generator
+* Encrypted offline cache
+* Encrypted stored vault and cloud connection passwords
<b>Requirements</b>
diff --git a/gradle.properties.example b/gradle.properties.example
index b233f8c..32427f1 100644
--- a/gradle.properties.example
+++ b/gradle.properties.example
@@ -25,3 +25,4 @@ RELEASE_KEY_ALIAS=release
RELEASE_KEY_PASSWORD=release store key
android.useAndroidX=true
android.enableJetifier=true
+android.jetifier.ignorelist=bcprov-jdk15on-1.70.jar
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cc3da69..bcb0351 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Oct 16 22:19:00 CEST 2020
+#Tue Mar 22 14:20:29 GMT 2022
distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME