diff options
author | binsky08 <timo@binsky.org> | 2022-03-27 21:53:51 +0300 |
---|---|---|
committer | binsky08 <timo@binsky.org> | 2022-03-27 21:53:51 +0300 |
commit | b0a025bb5657998300939a6ede9603fbfe441f37 (patch) | |
tree | e8d6fb6fb65efd34ab9dfa2a1e50f98412610b15 | |
parent | dcc2bb2ab8ffbb2198e15a7a55754e63f7d851f8 (diff) | |
parent | 982fc25fc67c2c6267ad00d68235fc0ccdc08bdf (diff) |
Merge branch 'upstream-master' into sso-3-backport
Signed-off-by: binsky08 <timo@binsky.org>
77 files changed, 3508 insertions, 335 deletions
@@ -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 |