diff --git a/.gitignore b/.gitignore
index 4ff14c001..57157a1b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
+.DS_Store
+/build
+/app/build
+/projectFilesBackup
+
# Built application files
*.apk
*.ap_
@@ -34,6 +39,7 @@ captures/
# Intellij
*.iml
+<<<<<<< HEAD
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
@@ -43,12 +49,19 @@ captures/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
+=======
+.idea/*
+
+# Keystore files
+*.jks
+>>>>>>> master
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json
+<<<<<<< HEAD
# Freeline
freeline.py
@@ -56,4 +69,6 @@ freeline/
freeline_project_description.json
# Iml Files
-app/app.iml
\ No newline at end of file
+app/app.iml
+=======
+>>>>>>> master
diff --git a/CHANGELOG b/CHANGELOG
index 92a92cb2f..9ce5b9df1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,12 @@
+KeePassDroid (2.2.0.0)
+ * Add Fingerprint integration
+
+KeePassDroid (2.1.0.0)
+ * Add support for KDBXv4 (used in mainline KeePass >= 2.35)
* Updated Lithuanian translations
+ * Updated Russian translations
+ * Updated French translations
+ * Add Samsung multi-window support
KeePassDroid (2.0.6.4)
* Expose Storage Access Framework option
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 2d2127473..48c436e57 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -12,13 +12,15 @@ Nicholas FitzRoy-Dale - auto launch intents
yulin2 - responsiveness improvements
Tadashi Saito
vhschlenker
+bumper314 - Samsung multiwindow support
+Hans Cappelle - fingerprint sensor integration
Jeremy Jamet - Material Design - Patches
Translations:
Diego Pierotto - Italian
-Laurent, Norman Obry, Nam, Bruno Parmentier - French
+Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French
Maciej Bieniek, cod3r - Polish
-Максим Сёмочкин, i.nedoboy, filimonic - Russian
+Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian
MaWi, rvs2008, meviox, MaDill - German
yslandro - Norwegian Nynorsk
王科峰 - Chinese
diff --git a/LICENSE b/LICENSE
index 1ca99d67b..4ee074247 100644
--- a/LICENSE
+++ b/LICENSE
@@ -381,3 +381,22 @@ from Tavmjong Bah. For further information, contact: tavmjong @ free
$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $
+--
+ChaCha7539Engine.java
+
+Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/app/app.iml b/app/app.iml
deleted file mode 100644
index dde05b350..000000000
--- a/app/app.iml
+++ /dev/null
@@ -1,143 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- generateDebugSources
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index f08d7bc10..44aab6c28 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,36 +2,37 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion = 25
- buildToolsVersion = "25.0.3"
-
+ buildToolsVersion = "26.0.2"
defaultConfig {
applicationId = "com.android.keepass"
minSdkVersion 11
targetSdkVersion 25
- versionCode = 154
+ versionCode = 1
versionName = "2.5.0.0"
testApplicationId = "com.keepassdroid.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
}
-
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
}
}
-
buildTypes {
release {
minifyEnabled = false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ compileSdkVersion 25
+ buildToolsVersion '26.0.2'
+ dexOptions {
+ }
}
def supportVersion = "25.4.0"
-def spongycastleVersion = "1.54.0.0"
+def spongycastleVersion = "1.58.0.0"
dependencies {
androidTestCompile files('libs/junit4.jar')
@@ -40,5 +41,5 @@ dependencies {
compile "com.android.support:preference-v7:$supportVersion"
compile "com.madgag.spongycastle:core:$spongycastleVersion"
compile "com.madgag.spongycastle:prov:$spongycastleVersion"
- compile "joda-time:joda-time:2.9.9"
+ compile 'joda-time:joda-time:2.9.9'
}
diff --git a/app/src/androidTest/java/com/keepassdroid/tests/database/Kdb4.java b/app/src/androidTest/java/com/keepassdroid/tests/database/Kdb4.java
index 3bc9a50c0..580a816ac 100644
--- a/app/src/androidTest/java/com/keepassdroid/tests/database/Kdb4.java
+++ b/app/src/androidTest/java/com/keepassdroid/tests/database/Kdb4.java
@@ -69,14 +69,22 @@ public class Kdb4 extends AndroidTestCase {
}
- public void testSaving() throws IOException, InvalidDBException, PwDbOutputException {
+ public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
+ testSaving("test.kdbx", "12345", "test-out.kdbx");
+ }
+
+ public void testSavingKDBXV4() throws IOException, InvalidDBException, PwDbOutputException {
+ testSaving("test-kdbxv4.kdbx", "1", "test-kdbxv4-out.kdbx");
+ }
+
+ private void testSaving(String inputFile, String password, String outputFile) throws IOException, InvalidDBException, PwDbOutputException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
- InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
+ InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
ImporterV4 importer = new ImporterV4();
- PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
+ PwDatabaseV4 db = importer.openDatabase(is, password, null);
is.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -86,12 +94,12 @@ public class Kdb4 extends AndroidTestCase {
byte[] data = bos.toByteArray();
- FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath("test-out.kdbx"), false);
+ FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath(outputFile), false);
InputStream bis = new ByteArrayInputStream(data);
bis = new CopyInputStream(bis, fos);
importer = new ImporterV4();
- db = importer.openDatabase(bis, "12345", null);
+ db = importer.openDatabase(bis, password, null);
bis.close();
fos.close();
diff --git a/app/src/androidTest/res/values-fr/strings.xml b/app/src/androidTest/res/values-fr/strings.xml
new file mode 100644
index 000000000..1768da0f9
--- /dev/null
+++ b/app/src/androidTest/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Salut le Monde, Fantôme
+ KeePassDroid
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a324ef1fc..6d7b102b6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,9 +8,10 @@
android:largeScreens="true"
android:anyDensity="true"
/>
+
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/assets/test-kdbxv4.kdbx b/app/src/main/assets/test-kdbxv4.kdbx
new file mode 100644
index 000000000..092d9beeb
Binary files /dev/null and b/app/src/main/assets/test-kdbxv4.kdbx differ
diff --git a/app/src/main/java/com/keepassdroid/EntryActivity.java b/app/src/main/java/com/keepassdroid/EntryActivity.java
index 3de4a4729..d0cb4cc63 100644
--- a/app/src/main/java/com/keepassdroid/EntryActivity.java
+++ b/app/src/main/java/com/keepassdroid/EntryActivity.java
@@ -37,6 +37,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v7.widget.Toolbar;
+import android.support.v4.app.NotificationCompat;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.method.PasswordTransformationMethod;
@@ -236,32 +237,20 @@ public class EntryActivity extends LockCloseHideActivity {
}
private Notification getNotification(String intentText, int descResId) {
- String description = getString(descResId);
+
+ String desc = getString(descResId);
Intent intent = new Intent(intentText);
PendingIntent pending = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- Notification notification;
+ // no longer supported for api level >22
+ // notify.setLatestEventInfo(this, getString(R.string.app_name), desc, pending);
+ // so instead using compat builder and create new notification
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ Notification notify = builder.setContentIntent(pending).setContentText(desc).setContentTitle(getString(R.string.app_name))
+ .setSmallIcon(R.drawable.notify).setTicker(desc).setWhen(System.currentTimeMillis()).build();
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
- notification = new Notification(R.drawable.notify, description, System.currentTimeMillis());
- try {
- Method deprecatedMethod = notification.getClass().getMethod("setLatestEventInfo", Context.class, CharSequence.class, CharSequence.class, PendingIntent.class);
- deprecatedMethod.invoke(notification, this, getString(R.string.app_name), description, pending);
- } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
- | InvocationTargetException e) {
- Log.w("EntryActivity", "Method not found", e);
- }
- } else {
- // Use new API
- Notification.Builder builder = new Notification.Builder(this)
- .setContentIntent(pending)
- .setSmallIcon(R.drawable.notify)
- .setContentTitle(getString(R.string.app_name));
- notification = builder.getNotification();
- }
-
- return notification;
+ return notify;
}
private String getDateTime(Date dt) {
diff --git a/app/src/main/java/com/keepassdroid/PasswordActivity.java b/app/src/main/java/com/keepassdroid/PasswordActivity.java
index aa72f68a0..cce97e410 100644
--- a/app/src/main/java/com/keepassdroid/PasswordActivity.java
+++ b/app/src/main/java/com/keepassdroid/PasswordActivity.java
@@ -27,11 +27,15 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
import android.support.v7.widget.Toolbar;
+import android.text.Editable;
import android.text.InputType;
+import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -40,7 +44,6 @@ import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
-import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
@@ -55,6 +58,7 @@ import com.keepassdroid.database.edit.LoadDB;
import com.keepassdroid.database.edit.OnFinish;
import com.keepassdroid.dialog.PasswordEncodingDialogHelper;
import com.keepassdroid.fileselect.BrowserDialog;
+import com.keepassdroid.fingerprint.FingerPrintHelper;
import com.keepassdroid.intents.Intents;
import com.keepassdroid.settings.AppSettingsActivity;
import com.keepassdroid.utils.EmptyUtils;
@@ -65,7 +69,9 @@ import com.keepassdroid.utils.Util;
import java.io.File;
import java.io.FileNotFoundException;
-public class PasswordActivity extends LockingActivity {
+import javax.crypto.Cipher;
+
+public class PasswordActivity extends LockingActivity implements FingerPrintHelper.FingerPrintCallback {
public static final String KEY_DEFAULT_FILENAME = "defaultFileName";
private static final String KEY_FILENAME = "fileName";
@@ -84,12 +90,25 @@ public class PasswordActivity extends LockingActivity {
SharedPreferences prefs;
private boolean isShowPasswordChecked = false;
+ private FingerPrintHelper fingerPrintHelper;
+ private int mode;
+ private static final String PREF_KEY_VALUE_PREFIX = "valueFor_"; // key is a combination of db file name and this prefix
+ private static final String PREF_KEY_IV_PREFIX = "ivFor_"; // key is a combination of db file name and this prefix
+ private View fingerprintView;
+ private TextView confirmationView;
+ private EditText passwordView;
+ private Button confirmButton;
- public static void Launch(Activity act, String fileName) throws FileNotFoundException {
- Launch(act,fileName,"");
+ public static void Launch(
+ Activity act,
+ String fileName) throws FileNotFoundException {
+ Launch(act, fileName, "");
}
- public static void Launch(Activity act, String fileName, String keyFile) throws FileNotFoundException {
+ public static void Launch(
+ Activity act,
+ String fileName,
+ String keyFile) throws FileNotFoundException {
if (EmptyUtils.isNullOrEmpty(fileName)) {
throw new FileNotFoundException();
}
@@ -113,52 +132,55 @@ public class PasswordActivity extends LockingActivity {
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ protected void onActivityResult(
+ int requestCode,
+ int resultCode,
+ Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
- case KeePass.EXIT_NORMAL:
- setEditText(R.id.password, "");
- App.getDB().clear();
- break;
+ case KeePass.EXIT_NORMAL:
+ setEditText(R.id.password, "");
+ App.getDB().clear();
+ break;
- case KeePass.EXIT_LOCK:
- setResult(KeePass.EXIT_LOCK);
- setEditText(R.id.password, "");
- finish();
- App.getDB().clear();
- break;
- case FILE_BROWSE:
- if (resultCode == RESULT_OK) {
- String filename = data.getDataString();
- if (filename != null) {
- EditText fn = (EditText) findViewById(R.id.pass_keyfile);
- fn.setText(filename);
- mKeyUri = UriUtil.parseDefaultFile(filename);
- }
- }
- break;
- case GET_CONTENT:
- case OPEN_DOC:
- if (resultCode == RESULT_OK) {
- if (data != null) {
- Uri uri = data.getData();
- if (uri != null) {
- if (requestCode==GET_CONTENT) {
- uri = UriUtil.translate(this, uri);
- }
- String path = uri.toString();
- if (path != null) {
- EditText fn = (EditText) findViewById(R.id.pass_keyfile);
- fn.setText(path);
-
- }
- mKeyUri = uri;
+ case KeePass.EXIT_LOCK:
+ setResult(KeePass.EXIT_LOCK);
+ setEditText(R.id.password, "");
+ finish();
+ App.getDB().clear();
+ break;
+ case FILE_BROWSE:
+ if (resultCode == RESULT_OK) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ EditText fn = (EditText) findViewById(R.id.pass_keyfile);
+ fn.setText(filename);
+ mKeyUri = UriUtil.parseDefaultFile(filename);
}
}
- }
- break;
+ break;
+ case GET_CONTENT:
+ case OPEN_DOC:
+ if (resultCode == RESULT_OK) {
+ if (data != null) {
+ Uri uri = data.getData();
+ if (uri != null) {
+ if (requestCode == GET_CONTENT) {
+ uri = UriUtil.translate(this, uri);
+ }
+ String path = uri.toString();
+ if (path != null) {
+ EditText fn = (EditText) findViewById(R.id.pass_keyfile);
+ fn.setText(path);
+
+ }
+ mKeyUri = uri;
+ }
+ }
+ }
+ break;
}
}
@@ -179,7 +201,14 @@ public class PasswordActivity extends LockingActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
+ confirmButton = (Button) findViewById(R.id.pass_ok);
+ fingerprintView = findViewById(R.id.fingerprint);
+ confirmationView = (TextView) findViewById(R.id.fingerprint_label);
+ passwordView = (EditText) findViewById(R.id.password);
+
new InitTask().execute(i);
+
+ initForFingerprint();
}
@Override
@@ -195,6 +224,9 @@ public class PasswordActivity extends LockingActivity {
// Clear the shutdown flag
App.clearShutdown();
+
+ // checks if fingerprint is available, will also start listening for fingerprints when available
+ checkAvailability();
}
private void retrieveSettings() {
@@ -206,7 +238,7 @@ public class PasswordActivity extends LockingActivity {
}
private Uri getKeyFile(Uri dbUri) {
- if ( mRememberKeyfile ) {
+ if (mRememberKeyfile) {
return App.getFileHistory().getFileByName(dbUri);
} else {
@@ -229,15 +261,215 @@ public class PasswordActivity extends LockingActivity {
}
*/
- private void errorMessage(int resId)
- {
+ private void errorMessage(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
}
+ // fingerprint related code here
+
+ private void initForFingerprint() {
+ fingerPrintHelper = new FingerPrintHelper(this, this);
+ if (fingerPrintHelper.isFingerprintSupported()) {
+
+ // when text entered we can enable the logon/purchase button and if required update encryption/decryption mode
+ passwordView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(
+ final CharSequence s,
+ final int start,
+ final int count,
+ final int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(
+ final CharSequence s,
+ final int start,
+ final int before,
+ final int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ final boolean validInput = s.length() > 0;
+ // encrypt or decrypt mode based on how much input or not
+ confirmationView.setText(validInput ? R.string.store_with_fingerprint : R.string.scanning_fingerprint);
+ mode = validInput ? toggleMode(Cipher.ENCRYPT_MODE) : toggleMode(Cipher.DECRYPT_MODE);
+
+ }
+ });
+
+ // callback for fingerprint findings
+ fingerPrintHelper.setAuthenticationCallback(new FingerprintManagerCompat.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(
+ final int errorCode,
+ final CharSequence errString) {
+
+ // this is triggered on stop/start listening done by helper to switch between modes so don't restart here
+ // errorCode = 5
+ // errString = "Fingerprint operation canceled."
+ //onException();
+ //confirmationView.setText(errString);
+ // true false fingerprint readings are handled otherwise with the toast messages, see below in code
+ }
+
+ @Override
+ public void onAuthenticationHelp(
+ final int helpCode,
+ final CharSequence helpString) {
+
+ onException();
+ confirmationView.setText(helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(final FingerprintManagerCompat.AuthenticationResult result) {
+
+ if (mode == Cipher.ENCRYPT_MODE) {
+
+ // newly store the entered password in encrypted way
+ final String password = passwordView.getText().toString();
+ fingerPrintHelper.encryptData(password);
+
+ } else if (mode == Cipher.DECRYPT_MODE) {
+
+ // retrieve the encrypted value from preferences
+ final String encryptedValue = prefs.getString(getPreferenceKeyValue(), null);
+ if (encryptedValue != null) {
+ fingerPrintHelper.decryptData(encryptedValue);
+ }
+ }
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ onException();
+ }
+ });
+ }
+ }
+
+ private String getPreferenceKeyValue() {
+ // makes it possible to store passwords uniqly per database
+ return PREF_KEY_VALUE_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
+ }
+
+ private String getPreferenceKeyIvSpec() {
+ return PREF_KEY_IV_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
+ }
+
+ private int toggleMode(final int newMode) {
+ // check if mode is different so we can update fingerprint helper
+ if (mode != newMode) {
+ mode = newMode;
+ switch (mode) {
+ case Cipher.ENCRYPT_MODE:
+ fingerPrintHelper.initEncryptData();
+ break;
+ case Cipher.DECRYPT_MODE:
+ final String ivSpecValue = prefs.getString(getPreferenceKeyIvSpec(), null);
+ fingerPrintHelper.initDecryptData(ivSpecValue);
+ break;
+ }
+ return newMode;
+ }
+ // remains in current mode
+ return mode;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // stop listening when we go in background
+ if (fingerPrintHelper != null) {
+ fingerPrintHelper.stopListening();
+ }
+ }
+
+ private void checkAvailability() {
+ // fingerprint not supported (by API level or hardware) so keep option hidden
+ if (!fingerPrintHelper.isFingerprintSupported()) {
+
+ fingerprintView.setVisibility(View.GONE);
+ // since this is a fingerprint example inform user, don't do this in your app
+ //confirmationView.setText(R.string.fingerprint_not_supported);
+ confirmationView.setVisibility(View.GONE);
+ }
+ // fingerprint is available but not configured show icon but in disabled state with some information
+ else if (!fingerPrintHelper.hasEnrolledFingerprints()) {
+
+ fingerprintView.setVisibility(View.VISIBLE);
+ confirmationView.setVisibility(View.VISIBLE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ fingerprintView.setAlpha(0.3f);
+ }
+ // This happens when no fingerprints are registered. Listening won't start
+ confirmationView.setText(R.string.configure_fingerprint);
+ }
+ // finally fingerprint available and configured so we can use it
+ else {
+
+ fingerprintView.setVisibility(View.VISIBLE);
+ confirmationView.setVisibility(View.VISIBLE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ fingerprintView.setAlpha(1f);
+ }
+ // fingerprint available but no stored password found yet for this DB so show info don't listen
+ if (prefs.getString(getPreferenceKeyValue(), null) == null) {
+
+ confirmationView.setText(R.string.no_password_stored);
+ }
+ // all is set here so we can confirm to user and start listening for fingerprints
+ else {
+
+ confirmationView.setText(R.string.scanning_fingerprint);
+ // listen for decryption by default
+ toggleMode(Cipher.DECRYPT_MODE);
+ }
+ }
+ }
+
+ @Override
+ public void handleEncryptedResult(
+ final String value,
+ final String ivSpec) {
+
+ prefs.edit()
+ .putString(getPreferenceKeyValue(), value)
+ .putString(getPreferenceKeyIvSpec(), ivSpec)
+ .commit();
+ // and remove visual input to reset UI
+ passwordView.setText("");
+ confirmationView.setText(R.string.encrypted_value_stored);
+ }
+
+ @Override
+ public void handleDecryptedResult(final String value) {
+ // on decrypt enter it for the purchase/login action
+ passwordView.setText(value);
+ confirmButton.performClick();
+ }
+
+ @Override
+ public void onInvalidKeyException() {
+ Toast.makeText(this, R.string.fingerprint_invalid_key, Toast.LENGTH_SHORT).show();
+ checkAvailability(); // restarts listening
+ }
+
+ @Override
+ public void onException() {
+ Toast.makeText(this, R.string.fingerprint_error, Toast.LENGTH_SHORT).show();
+ checkAvailability(); // restarts listening
+ }
+
private class DefaultCheckChange implements CompoundButton.OnCheckedChangeListener {
@Override
- public void onCheckedChanged(CompoundButton buttonView,
+ public void onCheckedChanged(
+ CompoundButton buttonView,
boolean isChecked) {
String newDefaultFileName;
@@ -268,13 +500,16 @@ public class PasswordActivity extends LockingActivity {
}
}
- private void loadDatabase(String pass, String keyfile) {
+ private void loadDatabase(
+ String pass,
+ String keyfile) {
loadDatabase(pass, UriUtil.parseDefaultFile(keyfile));
}
- private void loadDatabase(String pass, Uri keyfile)
- {
- if ( pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) {
+ private void loadDatabase(
+ String pass,
+ Uri keyfile) {
+ if (pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) {
errorMessage(R.string.error_nopass);
return;
}
@@ -296,9 +531,11 @@ public class PasswordActivity extends LockingActivity {
return Util.getEditText(this, resId);
}
- private void setEditText(int resId, String str) {
- TextView te = (TextView) findViewById(resId);
- assert(te == null);
+ private void setEditText(
+ int resId,
+ String str) {
+ TextView te = (TextView) findViewById(resId);
+ assert (te == null);
if (te != null) {
te.setText(str);
@@ -317,7 +554,8 @@ public class PasswordActivity extends LockingActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch ( item.getItemId() ) {
+
+ switch (item.getItemId()) {
case R.id.menu_about:
AboutDialog dialog = new AboutDialog(this);
dialog.show();
@@ -336,9 +574,12 @@ public class PasswordActivity extends LockingActivity {
}
private final class AfterLoad extends OnFinish {
+
private Database db;
- public AfterLoad(Handler handler, Database db) {
+ public AfterLoad(
+ Handler handler,
+ Database db) {
super(handler);
this.db = db;
@@ -346,17 +587,19 @@ public class PasswordActivity extends LockingActivity {
@Override
public void run() {
- if ( db.passwordEncodingError) {
+ if (db.passwordEncodingError) {
PasswordEncodingDialogHelper dialog = new PasswordEncodingDialogHelper();
dialog.show(PasswordActivity.this, new OnClickListener() {
@Override
- public void onClick(DialogInterface dialog, int which) {
+ public void onClick(
+ DialogInterface dialog,
+ int which) {
GroupActivity.Launch(PasswordActivity.this);
}
});
- } else if ( mSuccess ) {
+ } else if (mSuccess) {
GroupActivity.Launch(PasswordActivity.this);
} else {
displayMessage(PasswordActivity.this);
@@ -365,23 +608,23 @@ public class PasswordActivity extends LockingActivity {
}
private class InitTask extends AsyncTask {
+
String password = "";
boolean launch_immediately = false;
@Override
protected Integer doInBackground(Intent... args) {
Intent i = args[0];
- String action = i.getAction();;
- if ( action != null && action.equals(VIEW_INTENT) ) {
+ String action = i.getAction();
+ if (action != null && action.equals(VIEW_INTENT)) {
Uri incoming = i.getData();
mDbUri = incoming;
- mKeyUri = ClipDataCompat.getUriFromIntent(i, KEY_KEYFILE);
+ mKeyUri = ClipDataCompat.getUriFromIntent(i, KEY_KEYFILE);
if (incoming == null) {
return R.string.error_can_not_handle_uri;
- }
- else if (incoming.getScheme().equals("file")) {
+ } else if (incoming.getScheme().equals("file")) {
String fileName = incoming.getPath();
if (fileName.length() == 0) {
@@ -395,14 +638,14 @@ public class PasswordActivity extends LockingActivity {
return R.string.FileNotFound;
}
- if(mKeyUri == null)
- mKeyUri = getKeyFile(mDbUri);
- }
- else if (incoming.getScheme().equals("content")) {
- if(mKeyUri == null)
- mKeyUri = getKeyFile(mDbUri);
- }
- else {
+ if (mKeyUri == null) {
+ mKeyUri = getKeyFile(mDbUri);
+ }
+ } else if (incoming.getScheme().equals("content")) {
+ if (mKeyUri == null) {
+ mKeyUri = getKeyFile(mDbUri);
+ }
+ } else {
return R.string.error_can_not_handle_uri;
}
password = i.getStringExtra(KEY_PASSWORD);
@@ -414,7 +657,7 @@ public class PasswordActivity extends LockingActivity {
password = i.getStringExtra(KEY_PASSWORD);
launch_immediately = i.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false);
- if ( mKeyUri == null || mKeyUri.toString().length() == 0) {
+ if (mKeyUri == null || mKeyUri.toString().length() == 0) {
mKeyUri = getKeyFile(mDbUri);
}
}
@@ -422,7 +665,7 @@ public class PasswordActivity extends LockingActivity {
}
public void onPostExecute(Integer result) {
- if(result != null) {
+ if (result != null) {
Toast.makeText(PasswordActivity.this, result, Toast.LENGTH_LONG).show();
finish();
return;
@@ -467,8 +710,7 @@ public class PasswordActivity extends LockingActivity {
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
startActivityForResult(i, OPEN_DOC);
- }
- else {
+ } else {
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
@@ -518,8 +760,9 @@ public class PasswordActivity extends LockingActivity {
retrieveSettings();
- if (launch_immediately)
+ if (launch_immediately) {
loadDatabase(password, mKeyUri);
+ }
}
}
}
diff --git a/app/src/main/java/com/keepassdroid/collections/VariantDictionary.java b/app/src/main/java/com/keepassdroid/collections/VariantDictionary.java
index be0f7af27..5887cb00e 100644
--- a/app/src/main/java/com/keepassdroid/collections/VariantDictionary.java
+++ b/app/src/main/java/com/keepassdroid/collections/VariantDictionary.java
@@ -20,8 +20,10 @@
package com.keepassdroid.collections;
import com.keepassdroid.stream.LEDataInputStream;
+import com.keepassdroid.stream.LEDataOutputStream;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -31,64 +33,60 @@ public class VariantDictionary {
private static final int VdmCritical = 0xFF00;
private static final int VdmInfo = 0x00FF;
- private Map dict = new HashMap();
+ private Map dict = new HashMap();
- private enum VdType {
- None(0x00),
- UInt32(0x04),
- UInt64(0x05),
- Bool(0x08),
- Int32(0x0C),
- Int64(0x0D),
- String(0x18),
- ByteArray(0x42);
+ private class VdType {
+ public static final byte None = 0x00;
+ public static final byte UInt32 = 0x04;
+ public static final byte UInt64 =0x05;
+ public static final byte Bool =0x08;
+ public static final byte Int32 =0x0C;
+ public static final byte Int64 =0x0D;
+ public static final byte String =0x18;
+ public static final byte ByteArray =0x42;
+ public final byte type;
+ public final Object value;
- private final byte value;
-
- VdType(int value) {
- this.value = (byte) value;
+ VdType(byte type, Object value) {
+ this.type = type;
+ this.value = value;
}
- boolean equals(byte type) {
- return type == value;
+ }
+
+ private Object getValue(String name) {
+ VdType val = dict.get(name);
+ if (val == null) {
+ return null;
}
- byte getValue() {
- return value;
- }
+ return val.value;
+ }
+ private void putType(byte type, String name, Object value) {
+ dict.put(name, new VdType(type, value));
}
- public void setUInt32(String name, long value) {
- dict.put(name, value);
- }
- public long getUInt32(String name) { return (long)dict.get(name); }
+ public void setUInt32(String name, long value) { putType(VdType.UInt32, name, value); }
+ public long getUInt32(String name) { return (long)dict.get(name).value; }
- public void setUInt64(String name, long value) {
- dict.put(name, value);
- }
- public long getUInt64(String name) { return (long)dict.get(name); }
+ public void setUInt64(String name, long value) { putType(VdType.UInt64, name, value); }
+ public long getUInt64(String name) { return (long)dict.get(name).value; }
- public void setBool(String name, boolean value) {
- dict.put(name, value);
- }
+ public void setBool(String name, boolean value) { putType(VdType.Bool, name, value); }
+ public boolean getBool(String name) { return (boolean)dict.get(name).value; }
- public void setInt32(String name, int value) {
- dict.put(name, value);
- }
+ public void setInt32(String name, int value) { putType(VdType.Int32 ,name, value); }
+ public int getInt32(String name) { return (int)dict.get(name).value; }
- public void setInt64(String name, long value) {
- dict.put(name, value);
- }
+ public void setInt64(String name, long value) { putType(VdType.Int64 ,name, value); }
+ public long getInt64(String name) { return (long)dict.get(name).value; }
- public void setString(String name, String value) {
- dict.put(name, value);
- }
+ public void setString(String name, String value) { putType(VdType.String ,name, value); }
+ public String getString(String name) { return (String)getValue(name); }
- public void setByteArray(String name, byte[] value) {
- dict.put(name, value);
- }
- public byte[] getByteArray(String name) { return (byte[])dict.get(name); }
+ public void setByteArray(String name, byte[] value) { putType(VdType.ByteArray, name, value); }
+ public byte[] getByteArray(String name) { return (byte[])getValue(name); }
public static VariantDictionary deserialize(LEDataInputStream lis) throws IOException {
VariantDictionary d = new VariantDictionary();
@@ -105,7 +103,7 @@ public class VariantDictionary {
}
byte bType = (byte)type;
- if (VdType.None.equals(bType)) {
+ if (bType == VdType.None) {
break;
}
@@ -122,51 +120,125 @@ public class VariantDictionary {
throw new IOException("Invalid format");
}
- if (VdType.UInt32.equals(bType)) {
- if (valueLen == 4) {
- d.setUInt32(name, LEDataInputStream.readUInt(valueBuf, 0));
- }
- }
- else if (VdType.UInt64.equals(bType)) {
- if (valueLen == 8) {
- d.setUInt64(name, LEDataInputStream.readLong(valueBuf, 0));
- }
- }
- else if (VdType.Bool.equals(bType)) {
- if (valueLen == 1) {
- d.setBool(name, valueBuf[0] != 0);
- }
- }
- else if (VdType.Int32.equals(bType)) {
- if (valueLen == 4) {
- d.setInt32(name, LEDataInputStream.readInt(valueBuf, 0));
- }
- }
- else if (VdType.Int64.equals(bType)) {
- if (valueLen == 8) {
- d.setInt64(name, LEDataInputStream.readLong(valueBuf, 0));
- }
- }
- else if (VdType.String.equals(bType)) {
- d.setString(name, new String(valueBuf, "UTF-8"));
- }
- else if (VdType.ByteArray.equals(bType)) {
- d.setByteArray(name, valueBuf);
- }
- else {
- assert(false);
+ switch (bType) {
+ case VdType.UInt32:
+ if (valueLen == 4) {
+ d.setUInt32(name, LEDataInputStream.readUInt(valueBuf, 0));
+ }
+ break;
+ case VdType.UInt64:
+ if (valueLen == 8) {
+ d.setUInt64(name, LEDataInputStream.readLong(valueBuf, 0));
+ }
+ break;
+ case VdType.Bool:
+ if (valueLen == 1) {
+ d.setBool(name, valueBuf[0] != 0);
+ }
+ break;
+ case VdType.Int32:
+ if (valueLen == 4) {
+ d.setInt32(name, LEDataInputStream.readInt(valueBuf, 0));
+ }
+ break;
+ case VdType.Int64:
+ if (valueLen == 8) {
+ d.setInt64(name, LEDataInputStream.readLong(valueBuf, 0));
+ }
+ break;
+ case VdType.String:
+ d.setString(name, new String(valueBuf, "UTF-8"));
+ break;
+ case VdType.ByteArray:
+ d.setByteArray(name, valueBuf);
+ break;
+ default:
+ assert (false);
+ break;
}
}
return d;
}
+ public static void serialize(VariantDictionary d, LEDataOutputStream los) throws IOException{
+ if (los == null) {
+ assert(false);
+ return;
+ }
+
+ los.writeUShort(VdVersion);
+
+ for (Map.Entry entry: d.dict.entrySet()) {
+ String name = entry.getKey();
+ byte[] nameBuf = null;
+ try {
+ nameBuf = name.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ assert(false);
+ throw new IOException("Couldn't encode parameter name.");
+ }
+
+ VdType vd = entry.getValue();
+
+ los.write(vd.type);
+ los.writeInt(nameBuf.length);
+ los.write(nameBuf);
+
+ byte[] buf;
+ switch (vd.type) {
+ case VdType.UInt32:
+ los.writeInt(4);
+ los.writeUInt((long)vd.value);
+ break;
+ case VdType.UInt64:
+ los.writeInt(8);
+ los.writeLong((long)vd.value);
+ break;
+ case VdType.Bool:
+ los.writeInt(1);
+ byte bool = (boolean)vd.value ? (byte)1 : (byte)0;
+ los.write(bool);
+ break;
+ case VdType.Int32:
+ los.writeInt(4);
+ los.writeInt((int)vd.value);
+ break;
+ case VdType.Int64:
+ los.writeInt(8);
+ los.writeLong((long)vd.value);
+ break;
+ case VdType.String:
+ String value = (String)vd.value;
+ buf = value.getBytes("UTF-8");
+ los.writeInt(buf.length);
+ los.write(buf);
+ break;
+ case VdType.ByteArray:
+ buf = (byte[])vd.value;
+ los.writeInt(buf.length);
+ los.write(buf);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ }
+
+ los.write(VdType.None);
+
+ }
+
public void copyTo(VariantDictionary d) {
- for (Map.Entry entry : d.dict.entrySet()) {
+ for (Map.Entry entry : d.dict.entrySet()) {
String key = entry.getKey();
- Object value = entry.getValue();
+ VdType value = entry.getValue();
dict.put(key, value);
}
}
+;
+ public int size() {
+ return dict.size();
+ }
}
diff --git a/app/src/main/java/com/keepassdroid/compat/BuildCompat.java b/app/src/main/java/com/keepassdroid/compat/BuildCompat.java
index cca28c29b..f94d70d22 100644
--- a/app/src/main/java/com/keepassdroid/compat/BuildCompat.java
+++ b/app/src/main/java/com/keepassdroid/compat/BuildCompat.java
@@ -32,6 +32,7 @@ public class BuildCompat {
public static final int VERSION_CODE_JELLY_BEAN = 16;
public static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
public static final int VERSION_KITKAT = 19;
+ public static final int VERSION_CODE_M = 23;
private static Field versionSDK;
private static int versionInt;
diff --git a/app/src/main/java/com/keepassdroid/crypto/CipherFactory.java b/app/src/main/java/com/keepassdroid/crypto/CipherFactory.java
index 7b3ce1045..594ff897b 100644
--- a/app/src/main/java/com/keepassdroid/crypto/CipherFactory.java
+++ b/app/src/main/java/com/keepassdroid/crypto/CipherFactory.java
@@ -33,6 +33,7 @@ import javax.crypto.spec.SecretKeySpec;
import android.os.Build;
import com.keepassdroid.crypto.engine.AesEngine;
+import com.keepassdroid.crypto.engine.ChaCha20Engine;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.crypto.engine.TwofishEngine;
import com.keepassdroid.utils.Types;
@@ -88,6 +89,8 @@ public class CipherFactory {
return new AesEngine();
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
return new TwofishEngine();
+ } else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) {
+ return new ChaCha20Engine();
}
throw new NoSuchAlgorithmException("UUID unrecognized.");
diff --git a/app/src/main/java/com/keepassdroid/crypto/PwStreamCipherFactory.java b/app/src/main/java/com/keepassdroid/crypto/PwStreamCipherFactory.java
index 1c690accb..e553803f7 100644
--- a/app/src/main/java/com/keepassdroid/crypto/PwStreamCipherFactory.java
+++ b/app/src/main/java/com/keepassdroid/crypto/PwStreamCipherFactory.java
@@ -20,7 +20,7 @@
package com.keepassdroid.crypto;
import org.spongycastle.crypto.StreamCipher;
-import org.spongycastle.crypto.engines.ChaChaEngine;
+import org.spongycastle.crypto.engines.ChaCha7539Engine;
import org.spongycastle.crypto.engines.Salsa20Engine;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
@@ -65,9 +65,9 @@ public class PwStreamCipherFactory {
System.arraycopy(hash, 32, iv, 0, 12);
KeyParameter keyParam = new KeyParameter(key32);
- ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV);
+ ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv);
- StreamCipher cipher = new ChaChaEngine();
+ StreamCipher cipher = new ChaCha7539Engine();
cipher.init(true, ivParam);
return cipher;
diff --git a/app/src/main/java/com/keepassdroid/crypto/engine/ChaCha20Engine.java b/app/src/main/java/com/keepassdroid/crypto/engine/ChaCha20Engine.java
index b962d6553..d661f87a5 100644
--- a/app/src/main/java/com/keepassdroid/crypto/engine/ChaCha20Engine.java
+++ b/app/src/main/java/com/keepassdroid/crypto/engine/ChaCha20Engine.java
@@ -19,9 +19,14 @@
*/
package com.keepassdroid.crypto.engine;
+import com.keepassdroid.utils.Types;
+
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
@@ -29,6 +34,11 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ChaCha20Engine extends CipherEngine {
+ public static final UUID CIPHER_UUID = Types.bytestoUUID(
+ new byte[]{(byte)0xD6, (byte)0x03, (byte)0x8A, (byte)0x2B, (byte)0x8B, (byte)0x6F,
+ (byte)0x4C, (byte)0xB5, (byte)0xA5, (byte)0x24, (byte)0x33, (byte)0x9A, (byte)0x31,
+ (byte)0xDB, (byte)0xB5, (byte)0x9A});
+
@Override
public int ivLength() {
return 12;
@@ -36,8 +46,8 @@ public class ChaCha20Engine extends CipherEngine {
@Override
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
- Cipher cipher = Cipher.getInstance("Chacha");
- cipher.init(opmode, new SecretKeySpec(key, "ChaCha"), new IvParameterSpec(IV));
+ Cipher cipher = Cipher.getInstance("Chacha7539", new BouncyCastleProvider());
+ cipher.init(opmode, new SecretKeySpec(key, "ChaCha7539"), new IvParameterSpec(IV));
return cipher;
}
}
diff --git a/app/src/main/java/com/keepassdroid/crypto/keyDerivation/KdfParameters.java b/app/src/main/java/com/keepassdroid/crypto/keyDerivation/KdfParameters.java
index 034bb0494..242f49e7d 100644
--- a/app/src/main/java/com/keepassdroid/crypto/keyDerivation/KdfParameters.java
+++ b/app/src/main/java/com/keepassdroid/crypto/keyDerivation/KdfParameters.java
@@ -21,9 +21,11 @@ package com.keepassdroid.crypto.keyDerivation;
import com.keepassdroid.collections.VariantDictionary;
import com.keepassdroid.stream.LEDataInputStream;
+import com.keepassdroid.stream.LEDataOutputStream;
import com.keepassdroid.utils.Types;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;
@@ -59,4 +61,13 @@ public class KdfParameters extends VariantDictionary {
}
+ public static byte[] serialize(KdfParameters kdf) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ LEDataOutputStream los = new LEDataOutputStream(bos);
+
+ KdfParameters.serialize(kdf, los);
+
+ return bos.toByteArray();
+ }
+
}
diff --git a/app/src/main/java/com/keepassdroid/database/PwDatabaseV4.java b/app/src/main/java/com/keepassdroid/database/PwDatabaseV4.java
index 6fb704451..11afb386f 100644
--- a/app/src/main/java/com/keepassdroid/database/PwDatabaseV4.java
+++ b/app/src/main/java/com/keepassdroid/database/PwDatabaseV4.java
@@ -24,6 +24,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@@ -101,7 +102,7 @@ public class PwDatabaseV4 extends PwDatabase {
public List customIcons = new ArrayList();
public Map customData = new HashMap();
public KdfParameters kdfParameters = KdfFactory.getDefaultParameters();
- public VariantDictionary publicCustomData;
+ public VariantDictionary publicCustomData = new VariantDictionary();
public String localizedAppName = "KeePassDroid";
@@ -459,4 +460,67 @@ public class PwDatabaseV4 extends PwDatabase {
return filename.substring(0, lastExtDot);
}
+
+ private class GroupHasCustomData extends GroupHandler {
+
+ public boolean hasCustomData = false;
+
+ @Override
+ public boolean operate(PwGroup group) {
+ if (group == null) {
+ return true;
+ }
+ PwGroupV4 g4 = (PwGroupV4) group;
+ if (g4.customData.size() > 0) {
+ hasCustomData = true;
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ private class EntryHasCustomData extends EntryHandler {
+
+ public boolean hasCustomData = false;
+
+ @Override
+ public boolean operate(PwEntry entry) {
+ if (entry == null) {
+ return true;
+ }
+
+ PwEntryV4 e4 = (PwEntryV4)entry;
+ if (e4.customData.size() > 0) {
+ hasCustomData = true;
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ public int getMinKdbxVersion() {
+ if (!AesKdf.CIPHER_UUID.equals(kdfParameters.kdfUUID)) {
+ return PwDbHeaderV4.FILE_VERSION_32;
+ }
+
+ if (publicCustomData.size() > 0) {
+ return PwDbHeaderV4.FILE_VERSION_32;
+ }
+
+ EntryHasCustomData entryHandler = new EntryHasCustomData();
+ GroupHasCustomData groupHandler = new GroupHasCustomData();
+
+ if (rootGroup == null ) {
+ return PwDbHeaderV4.FILE_VERSION_32_3;
+ }
+ rootGroup.preOrderTraverseTree(groupHandler, entryHandler);
+ if (groupHandler.hasCustomData || entryHandler.hasCustomData) {
+ return PwDbHeaderV4.FILE_VERSION_32;
+ }
+
+ return PwDbHeaderV4.FILE_VERSION_32_3;
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/keepassdroid/database/PwDbHeaderV4.java b/app/src/main/java/com/keepassdroid/database/PwDbHeaderV4.java
index 3f9095689..53e131140 100644
--- a/app/src/main/java/com/keepassdroid/database/PwDbHeaderV4.java
+++ b/app/src/main/java/com/keepassdroid/database/PwDbHeaderV4.java
@@ -22,14 +22,18 @@ package com.keepassdroid.database;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.security.DigestInputStream;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
import com.keepassdroid.crypto.keyDerivation.AesKdf;
import com.keepassdroid.crypto.keyDerivation.KdfParameters;
import com.keepassdroid.database.exception.InvalidDBVersionException;
+import com.keepassdroid.database.security.ProtectedBinary;
import com.keepassdroid.stream.CopyInputStream;
import com.keepassdroid.stream.HmacBlockStream;
import com.keepassdroid.stream.LEDataInputStream;
@@ -87,14 +91,15 @@ public class PwDbHeaderV4 extends PwDbHeader {
}
private PwDatabaseV4 db;
- public byte[] protectedStreamKey = new byte[32];
+ public byte[] innerRandomStreamKey = new byte[32];
public byte[] streamStartBytes = new byte[32];
public CrsAlgorithm innerRandomStream;
public long version;
+ public List binaries = new ArrayList();
public PwDbHeaderV4(PwDatabaseV4 d) {
db = d;
-
+ version = d.getMinKdbxVersion();
masterSeed = new byte[32];
}
@@ -198,7 +203,7 @@ public class PwDbHeaderV4 extends PwDbHeader {
case PwDbHeaderV4Fields.InnerRandomstreamKey:
assert(version < PwDbHeaderV4.FILE_VERSION_32_4);
- protectedStreamKey = fieldData;
+ innerRandomStreamKey = fieldData;
break;
case PwDbHeaderV4Fields.StreamStartBytes:
@@ -310,10 +315,6 @@ public class PwDbHeaderV4 extends PwDbHeader {
return hmac.doFinal(header);
}
- public void setMinimumVersion() {
-
- }
-
public byte[] getTransformSeed() {
assert(version < FILE_VERSION_32_4);
diff --git a/app/src/main/java/com/keepassdroid/database/load/ImporterV4.java b/app/src/main/java/com/keepassdroid/database/load/ImporterV4.java
index 760c686d2..b9a8d196c 100644
--- a/app/src/main/java/com/keepassdroid/database/load/ImporterV4.java
+++ b/app/src/main/java/com/keepassdroid/database/load/ImporterV4.java
@@ -40,7 +40,6 @@ import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import org.spongycastle.crypto.StreamCipher;
-import org.spongycastle.util.encoders.Base64Encoder;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@@ -52,7 +51,6 @@ import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.database.BinaryPool;
-import com.keepassdroid.database.CrsAlgorithm;
import com.keepassdroid.database.ITimeLogger;
import com.keepassdroid.database.PwCompressionAlgorithm;
import com.keepassdroid.database.PwDatabaseV4;
@@ -111,6 +109,7 @@ public class ImporterV4 extends Importer {
db = createDB();
PwDbHeaderV4 header = new PwDbHeaderV4(db);
+ header.binaries.clear();
PwDbHeaderV4.HeaderAndHash hh = header.loadFromFile(inStream);
version = header.version;
@@ -192,12 +191,12 @@ public class ImporterV4 extends Importer {
LoadInnerHeader(isXml, header);
}
- if ( header.protectedStreamKey == null ) {
+ if ( header.innerRandomStreamKey == null ) {
assert(false);
throw new IOException("Invalid stream key.");
}
- randomStream = PwStreamCipherFactory.getInstance(header.innerRandomStream, header.protectedStreamKey);
+ randomStream = PwStreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey);
if ( randomStream == null ) {
throw new ArcFourException();
@@ -243,7 +242,7 @@ public class ImporterV4 extends Importer {
header.setRandomStreamID(data);
break;
case PwDbHeaderV4.PwDbInnerHeaderV4Fields.InnerRandomstreamKey:
- header.protectedStreamKey = data;
+ header.innerRandomStreamKey = data;
break;
case PwDbHeaderV4.PwDbInnerHeaderV4Fields.Binary:
if (data.length < 1) throw new IOException("Invalid binary format");
diff --git a/app/src/main/java/com/keepassdroid/database/save/PwDbHeaderOutputV4.java b/app/src/main/java/com/keepassdroid/database/save/PwDbHeaderOutputV4.java
index fb427820a..1c5c82331 100644
--- a/app/src/main/java/com/keepassdroid/database/save/PwDbHeaderOutputV4.java
+++ b/app/src/main/java/com/keepassdroid/database/save/PwDbHeaderOutputV4.java
@@ -19,44 +19,74 @@
*/
package com.keepassdroid.database.save;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.DigestOutputStream;
+import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import com.keepassdroid.collections.VariantDictionary;
+import com.keepassdroid.crypto.keyDerivation.KdfParameters;
import com.keepassdroid.database.PwDatabaseV4;
import com.keepassdroid.database.PwDbHeader;
import com.keepassdroid.database.PwDbHeaderV4;
import com.keepassdroid.database.PwDbHeaderV4.PwDbHeaderV4Fields;
import com.keepassdroid.database.exception.PwDbOutputException;
+import com.keepassdroid.stream.HmacBlockStream;
import com.keepassdroid.stream.LEDataOutputStream;
+import com.keepassdroid.stream.MacOutputStream;
import com.keepassdroid.utils.Types;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
public class PwDbHeaderOutputV4 extends PwDbHeaderOutput {
private PwDbHeaderV4 header;
private LEDataOutputStream los;
+ private MacOutputStream mos;
private DigestOutputStream dos;
private PwDatabaseV4 db;
+ public byte[] headerHmac;
private static byte[] EndHeaderValue = {'\r', '\n', '\r', '\n'};
public PwDbHeaderOutputV4(PwDatabaseV4 d, PwDbHeaderV4 h, OutputStream os) throws PwDbOutputException {
db = d;
header = h;
-
+
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new PwDbOutputException("SHA-256 not implemented here.");
}
-
+
+ try {
+ d.makeFinalKey(header.masterSeed, d.kdfParameters);
+ } catch (IOException e) {
+ throw new PwDbOutputException(e);
+ }
+
+ Mac hmac;
+ try {
+ hmac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec signingKey = new SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256");
+ hmac.init(signingKey);
+ } catch (NoSuchAlgorithmException e) {
+ throw new PwDbOutputException(e);
+ } catch (InvalidKeyException e) {
+ throw new PwDbOutputException(e);
+ }
+
dos = new DigestOutputStream(os, md);
- los = new LEDataOutputStream(dos);
+ mos = new MacOutputStream(dos, hmac);
+ los = new LEDataOutputStream(mos);
}
public void output() throws IOException {
+
los.writeUInt(PwDbHeader.PWM_DBSIG_1);
los.writeUInt(PwDbHeaderV4.DBSIG_2);
los.writeUInt(header.version);
@@ -65,16 +95,36 @@ public class PwDbHeaderOutputV4 extends PwDbHeaderOutput {
writeHeaderField(PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher));
writeHeaderField(PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(db.compressionAlgorithm.id));
writeHeaderField(PwDbHeaderV4Fields.MasterSeed, header.masterSeed);
- writeHeaderField(PwDbHeaderV4Fields.TransformSeed, header.getTransformSeed());
- writeHeaderField(PwDbHeaderV4Fields.TransformRounds, LEDataOutputStream.writeLongBuf(db.numKeyEncRounds));
- writeHeaderField(PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV);
- writeHeaderField(PwDbHeaderV4Fields.InnerRandomstreamKey, header.protectedStreamKey);
- writeHeaderField(PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes);
- writeHeaderField(PwDbHeaderV4Fields.InnerRandomStreamID, LEDataOutputStream.writeIntBuf(header.innerRandomStream.id));
+
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ writeHeaderField(PwDbHeaderV4Fields.TransformSeed, header.getTransformSeed());
+ writeHeaderField(PwDbHeaderV4Fields.TransformRounds, LEDataOutputStream.writeLongBuf(db.numKeyEncRounds));
+ } else {
+ writeHeaderField(PwDbHeaderV4Fields.KdfParameters, KdfParameters.serialize(db.kdfParameters));
+ }
+
+ if (header.encryptionIV.length > 0) {
+ writeHeaderField(PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV);
+ }
+
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ writeHeaderField(PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey);
+ writeHeaderField(PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes);
+ writeHeaderField(PwDbHeaderV4Fields.InnerRandomStreamID, LEDataOutputStream.writeIntBuf(header.innerRandomStream.id));
+ }
+
+ if (db.publicCustomData.size() > 0) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ LEDataOutputStream los = new LEDataOutputStream(bos);
+ VariantDictionary.serialize(db.publicCustomData, los);
+ writeHeaderField(PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray());
+ }
+
writeHeaderField(PwDbHeaderV4Fields.EndOfHeader, EndHeaderValue);
los.flush();
hashOfHeader = dos.getMessageDigest().digest();
+ headerHmac = mos.getMac();
}
private void writeHeaderField(byte fieldId, byte[] pbData) throws IOException {
@@ -82,11 +132,19 @@ public class PwDbHeaderOutputV4 extends PwDbHeaderOutput {
los.write(fieldId);
if (pbData != null) {
- los.writeUShort(pbData.length);
+ writeHeaderFieldSize(pbData.length);
los.write(pbData);
} else {
- los.writeUShort(0);
+ writeHeaderFieldSize(0);
}
}
-
+
+ private void writeHeaderFieldSize(int size) throws IOException {
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ los.writeUShort(size);
+ } else {
+ los.writeInt(size);
+ }
+
+ }
}
diff --git a/app/src/main/java/com/keepassdroid/database/save/PwDbInnerHeaderOutputV4.java b/app/src/main/java/com/keepassdroid/database/save/PwDbInnerHeaderOutputV4.java
new file mode 100644
index 000000000..19a90b608
--- /dev/null
+++ b/app/src/main/java/com/keepassdroid/database/save/PwDbInnerHeaderOutputV4.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Brian Pellin.
+ *
+ * This file is part of KeePassDroid.
+ *
+ * KeePassDroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDroid 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDroid. If not, see .
+ *
+ */
+package com.keepassdroid.database.save;
+
+import com.keepassdroid.database.PwDatabaseV4;
+import com.keepassdroid.database.PwDbHeaderV4;
+import com.keepassdroid.database.PwDbHeaderV4.PwDbInnerHeaderV4Fields;
+import com.keepassdroid.database.PwDbHeaderV4.KdbxBinaryFlags;
+import com.keepassdroid.database.security.ProtectedBinary;
+import com.keepassdroid.stream.LEDataOutputStream;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+public class PwDbInnerHeaderOutputV4 {
+ private PwDatabaseV4 db;
+ private PwDbHeaderV4 header;
+ private LEDataOutputStream los;
+
+ public PwDbInnerHeaderOutputV4(PwDatabaseV4 db, PwDbHeaderV4 header, OutputStream os) {
+ this.db = db;
+ this.header = header;
+
+ this.los = new LEDataOutputStream(os);
+ }
+
+ public void output() throws IOException {
+ los.write(PwDbInnerHeaderV4Fields.InnerRandomStreamID);
+ los.writeInt(4);
+ los.writeInt(header.innerRandomStream.id);
+
+ int streamKeySize = header.innerRandomStreamKey.length;
+ los.write(PwDbInnerHeaderV4Fields.InnerRandomstreamKey);
+ los.writeInt(streamKeySize);
+ los.write(header.innerRandomStreamKey);
+
+ for (ProtectedBinary bin : header.binaries) {
+ byte flag = KdbxBinaryFlags.None;
+ if (bin.isProtected()) {
+ flag |= KdbxBinaryFlags.Protected;
+ }
+
+ byte[] binData = bin.getData();
+ los.write(PwDbInnerHeaderV4Fields.Binary);
+ los.writeInt(bin.length() + 1);
+ los.write(flag);
+ los.write(binData);
+
+ Arrays.fill(binData, (byte)0);
+ }
+
+ los.write(PwDbInnerHeaderV4Fields.EndOfHeader);
+ los.writeInt(0);
+ }
+
+}
diff --git a/app/src/main/java/com/keepassdroid/database/save/PwDbV4Output.java b/app/src/main/java/com/keepassdroid/database/save/PwDbV4Output.java
index ab71e1ca7..e8e399960 100644
--- a/app/src/main/java/com/keepassdroid/database/save/PwDbV4Output.java
+++ b/app/src/main/java/com/keepassdroid/database/save/PwDbV4Output.java
@@ -23,6 +23,7 @@ import static com.keepassdroid.database.PwDatabaseV4XML.*;
import java.io.IOException;
import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.List;
@@ -35,6 +36,7 @@ import java.util.zip.GZIPOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
+import org.joda.time.DateTime;
import org.spongycastle.crypto.StreamCipher;
import org.xmlpull.v1.XmlSerializer;
@@ -69,6 +71,9 @@ import com.keepassdroid.database.exception.PwDbOutputException;
import com.keepassdroid.database.security.ProtectedBinary;
import com.keepassdroid.database.security.ProtectedString;
import com.keepassdroid.stream.HashedBlockOutputStream;
+import com.keepassdroid.stream.HmacBlockOutputStream;
+import com.keepassdroid.stream.LEDataOutputStream;
+import com.keepassdroid.utils.DateUtil;
import com.keepassdroid.utils.EmptyUtils;
import com.keepassdroid.utils.MemUtil;
import com.keepassdroid.utils.Types;
@@ -81,7 +86,9 @@ public class PwDbV4Output extends PwDbOutput {
private XmlSerializer xml;
private PwDbHeaderV4 header;
private byte[] hashOfHeader;
-
+ private byte[] headerHmac;
+ private CipherEngine engine = null;
+
protected PwDbV4Output(PwDatabaseV4 pm, OutputStream os) {
super(os);
@@ -91,30 +98,53 @@ public class PwDbV4Output extends PwDbOutput {
@Override
public void output() throws PwDbOutputException {
-
- header = (PwDbHeaderV4) outputHeader(mOS);
-
- CipherOutputStream cos = attachStreamEncryptor(header, mOS);
-
- OutputStream compressed;
- try {
- cos.write(header.streamStartBytes);
-
- HashedBlockOutputStream hashed = new HashedBlockOutputStream(cos);
-
- if ( mPM.compressionAlgorithm == PwCompressionAlgorithm.Gzip ) {
- compressed = new GZIPOutputStream(hashed);
- } else {
- compressed = hashed;
+ try {
+ try {
+ engine = CipherFactory.getInstance(mPM.dataCipher);
+ } catch (NoSuchAlgorithmException e) {
+ throw new PwDbOutputException("No such cipher", e);
}
-
- outputDatabase(compressed);
- compressed.close();
- } catch (IllegalArgumentException e) {
- throw new PwDbOutputException(e);
- } catch (IllegalStateException e) {
- throw new PwDbOutputException(e);
+ header = (PwDbHeaderV4) outputHeader(mOS);
+
+ OutputStream osPlain;
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ CipherOutputStream cos = attachStreamEncryptor(header, mOS);
+ cos.write(header.streamStartBytes);
+
+ HashedBlockOutputStream hashed = new HashedBlockOutputStream(cos);
+ osPlain = hashed;
+ } else {
+ mOS.write(hashOfHeader);
+ mOS.write(headerHmac);
+
+ HmacBlockOutputStream hbos = new HmacBlockOutputStream(mOS, mPM.hmacKey);
+ osPlain = attachStreamEncryptor(header, hbos);
+ }
+
+ OutputStream osXml;
+ try {
+
+
+ if (mPM.compressionAlgorithm == PwCompressionAlgorithm.Gzip) {
+ osXml = new GZIPOutputStream(osPlain);
+ } else {
+ osXml = osPlain;
+ }
+
+ if (header.version >= PwDbHeaderV4.FILE_VERSION_32_4) {
+ PwDbInnerHeaderOutputV4 ihOut = new PwDbInnerHeaderOutputV4((PwDatabaseV4)mPM, header, osXml);
+ ihOut.output();
+ }
+
+
+ outputDatabase(osXml);
+ osXml.close();
+ } catch (IllegalArgumentException e) {
+ throw new PwDbOutputException(e);
+ } catch (IllegalStateException e) {
+ throw new PwDbOutputException(e);
+ }
} catch (IOException e) {
throw new PwDbOutputException(e);
}
@@ -241,8 +271,10 @@ public class PwDbV4Output extends PwDbOutput {
writeObject(ElemHistoryMaxSize, mPM.historyMaxSize);
writeObject(ElemLastSelectedGroup, mPM.lastSelectedGroup);
writeObject(ElemLastTopVisibleGroup, mPM.lastTopVisibleGroup);
-
- writeBinPool();
+
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ writeBinPool();
+ }
writeList(ElemCustomData, mPM.customData);
xml.endTag(null, ElemMeta);
@@ -251,11 +283,9 @@ public class PwDbV4Output extends PwDbOutput {
private CipherOutputStream attachStreamEncryptor(PwDbHeaderV4 header, OutputStream os) throws PwDbOutputException {
Cipher cipher;
- CipherEngine engine;
try {
- mPM.makeFinalKey(header.masterSeed, header.getTransformSeed(), (int)mPM.numKeyEncRounds);
+ //mPM.makeFinalKey(header.masterSeed, mPM.kdfParameters);
- engine = CipherFactory.getInstance(mPM.dataCipher);
cipher = engine.getCipher(Cipher.ENCRYPT_MODE, mPM.finalKey, header.encryptionIV);
} catch (Exception e) {
throw new PwDbOutputException("Invalid algorithm.", e);
@@ -272,19 +302,34 @@ public class PwDbV4Output extends PwDbOutput {
PwDbHeaderV4 h = (PwDbHeaderV4) header;
random.nextBytes(h.masterSeed);
+
+ int ivLength = engine.ivLength();
+ if (ivLength != h.encryptionIV.length) {
+ h.encryptionIV = new byte[ivLength];
+ }
random.nextBytes(h.encryptionIV);
UUID kdfUUID = mPM.kdfParameters.kdfUUID;
KdfEngine kdf = KdfFactory.get(kdfUUID);
kdf.randomize(mPM.kdfParameters);
- random.nextBytes(h.protectedStreamKey);
- h.innerRandomStream = CrsAlgorithm.Salsa20;
- randomStream = PwStreamCipherFactory.getInstance(h.innerRandomStream, h.protectedStreamKey);
+ if (h.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ h.innerRandomStream = CrsAlgorithm.Salsa20;
+ h.innerRandomStreamKey = new byte[32];
+ } else {
+ h.innerRandomStream = CrsAlgorithm.ChaCha20;
+ h.innerRandomStreamKey = new byte[64];
+ }
+ random.nextBytes(h.innerRandomStreamKey);
+
+ randomStream = PwStreamCipherFactory.getInstance(h.innerRandomStream, h.innerRandomStreamKey);
if (randomStream == null) {
throw new PwDbOutputException("Invalid random cipher");
}
- random.nextBytes(h.streamStartBytes);
+
+ if ( h.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ random.nextBytes(h.streamStartBytes);
+ }
return random;
}
@@ -293,7 +338,7 @@ public class PwDbV4Output extends PwDbOutput {
public PwDbHeader outputHeader(OutputStream os) throws PwDbOutputException {
PwDbHeaderV4 header = new PwDbHeaderV4(mPM);
setIVs(header);
-
+
PwDbHeaderOutputV4 pho = new PwDbHeaderOutputV4(mPM, header, os);
try {
pho.output();
@@ -302,6 +347,7 @@ public class PwDbV4Output extends PwDbOutput {
}
hashOfHeader = pho.getHashOfHeader();
+ headerHmac = pho.headerHmac;
return header;
}
@@ -433,7 +479,16 @@ public class PwDbV4Output extends PwDbOutput {
}
private void writeObject(String name, Date value) throws IllegalArgumentException, IllegalStateException, IOException {
- writeObject(name, PwDatabaseV4XML.dateFormat.format(value));
+ if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ writeObject(name, PwDatabaseV4XML.dateFormat.format(value));
+ } else {
+ DateTime dt = new DateTime(value);
+ long seconds = DateUtil.convertDateToKDBX4Time(dt);
+ byte[] buf = LEDataOutputStream.writeLongBuf(seconds);
+ String b64 = new String(Base64Coder.encode(buf));
+ writeObject(name, b64);
+ }
+
}
private void writeObject(String name, long value) throws IllegalArgumentException, IllegalStateException, IOException {
diff --git a/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java b/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java
index 79d9f3fd7..0fea33122 100644
--- a/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java
+++ b/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java
@@ -19,17 +19,24 @@
*/
package com.keepassdroid.fileselect;
+
+import android.Manifest;
+import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
@@ -75,6 +82,7 @@ import java.net.URLDecoder;
public class FileSelectActivity extends AppCompatActivity {
+ private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 111;
private ListView mList;
private ListAdapter mAdapter;
@@ -223,7 +231,12 @@ public class FileSelectActivity extends AppCompatActivity {
startActivityForResult(i, OPEN_DOC);
}
else {
- Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ Intent i;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ } else {
+ i = new Intent(Intent.ACTION_GET_CONTENT);
+ }
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
@@ -413,6 +426,9 @@ public class FileSelectActivity extends AppCompatActivity {
@Override
protected void onResume() {
super.onResume();
+
+ // check for storage permission
+ checkStoragePermission();
// Check to see if we need to change modes
if ( fileHistory.hasRecentFiles() != recentMode ) {
@@ -426,6 +442,60 @@ public class FileSelectActivity extends AppCompatActivity {
fnv.updateExternalStorageWarning();
}
+ private void checkStoragePermission() {
+ // Here, thisActivity is the current activity
+ if (ContextCompat.checkSelfPermission(FileSelectActivity.this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ // Should we show an explanation?
+ //if (ActivityCompat.shouldShowRequestPermissionRationale(FileSelectActivity.this,
+ // Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+
+ // Show an explanation to the user *asynchronously* -- don't block
+ // this thread waiting for the user's response! After the user
+ // sees the explanation, try again to request the permission.
+
+ //} else {
+
+ // No explanation needed, we can request the permission.
+
+ ActivityCompat.requestPermissions(FileSelectActivity.this,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
+
+ // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
+ // app-defined int constant. The callback method gets the
+ // result of the request.
+ //}
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ switch (requestCode) {
+ case MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+
+ // permission was granted, yay! Do the
+ // contacts-related task you need to do.
+
+ } else {
+
+ // permission denied, boo! Disable the
+ // functionality that depends on this permission.
+ }
+ return;
+ }
+
+ // other 'case' lines to check for other
+ // permissions this app might request
+ }
+ }
+
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
diff --git a/app/src/main/java/com/keepassdroid/fingerprint/FingerPrintHelper.java b/app/src/main/java/com/keepassdroid/fingerprint/FingerPrintHelper.java
new file mode 100644
index 000000000..12030e02a
--- /dev/null
+++ b/app/src/main/java/com/keepassdroid/fingerprint/FingerPrintHelper.java
@@ -0,0 +1,286 @@
+package com.keepassdroid.fingerprint;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.os.CancellationSignal;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
+import android.util.Base64;
+
+import com.keepassdroid.compat.BuildCompat;
+
+import java.security.KeyStore;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+public class FingerPrintHelper {
+
+ private static final String ALIAS_KEY = "example-key";
+
+ private FingerprintManagerCompat fingerprintManager;
+ private KeyStore keyStore = null;
+ private KeyGenerator keyGenerator = null;
+ private Cipher cipher = null;
+ private KeyguardManager keyguardManager = null;
+ private FingerprintManagerCompat.CryptoObject cryptoObject = null;
+
+ private boolean initOk = false;
+ private FingerPrintCallback fingerPrintCallback;
+ private CancellationSignal cancellationSignal;
+ private FingerprintManagerCompat.AuthenticationCallback authenticationCallback;
+
+ public void setAuthenticationCallback(final FingerprintManagerCompat.AuthenticationCallback authenticationCallback) {
+ this.authenticationCallback = authenticationCallback;
+ }
+
+ @SuppressLint("NewApi")
+ public void startListening() {
+ // no need to start listening when not initialised
+ if (!isFingerprintInitialized()) {
+ if (fingerPrintCallback != null) {
+ fingerPrintCallback.onException();
+ }
+ return;
+ }
+ // starts listening for fingerprints with the initialised crypto object
+ cancellationSignal = new CancellationSignal();
+ fingerprintManager.authenticate(
+ cryptoObject,
+ 0 /* flags */,
+ cancellationSignal,
+ authenticationCallback,
+ null);
+ }
+
+ @SuppressLint("NewApi")
+ public void stopListening() {
+ if (!isFingerprintInitialized()) {
+ return;
+ }
+ if (cancellationSignal != null) {
+ cancellationSignal.cancel();
+ cancellationSignal = null;
+ }
+ }
+
+ public interface FingerPrintCallback {
+
+ void handleEncryptedResult(String value, String ivSpec);
+
+ void handleDecryptedResult(String value);
+
+ void onInvalidKeyException();
+
+ void onException();
+
+ }
+
+ @TargetApi(BuildCompat.VERSION_CODE_M)
+ public FingerPrintHelper(
+ final Context context,
+ final FingerPrintCallback fingerPrintCallback) {
+
+ if (!isFingerprintSupported()) {
+ // really not much to do when no fingerprint support found
+ setInitOk(false);
+ return;
+ }
+ this.fingerprintManager = FingerprintManagerCompat.from(context);
+ this.keyguardManager = (KeyguardManager)context.getSystemService(Context.KEYGUARD_SERVICE);
+ this.fingerPrintCallback = fingerPrintCallback;
+
+ if (hasEnrolledFingerprints()) {
+ try {
+ this.keyStore = KeyStore.getInstance("AndroidKeyStore");
+ this.keyGenerator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES,
+ "AndroidKeyStore");
+ this.cipher = Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_CBC + "/"
+ + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+ this.cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher);
+ setInitOk(true);
+ } catch (final Exception e) {
+ setInitOk(false);
+ fingerPrintCallback.onException();
+ }
+ }
+ }
+
+ public boolean isFingerprintInitialized() {
+ return hasEnrolledFingerprints() && initOk;
+ }
+
+ @SuppressWarnings("NewApi")
+ public void initEncryptData() {
+
+ if (!isFingerprintInitialized()) {
+ if (fingerPrintCallback != null) {
+ fingerPrintCallback.onException();
+ }
+ return;
+ }
+ try {
+ createNewKeyIfNeeded(false); // no need to keep deleting existing keys
+ keyStore.load(null);
+ final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+
+ stopListening();
+ startListening();
+
+ } catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
+ fingerPrintCallback.onInvalidKeyException();
+ } catch (final Exception e) {
+ fingerPrintCallback.onException();
+ }
+ }
+
+ @SuppressWarnings("NewApi")
+ public void encryptData(final String value) {
+
+ if (!isFingerprintInitialized()) {
+ if (fingerPrintCallback != null) {
+ fingerPrintCallback.onException();
+ }
+ return;
+ }
+ try {
+ // actual do encryption here
+ byte[] encrypted = cipher.doFinal(value.getBytes());
+ final String encryptedValue = Base64.encodeToString(encrypted, 0 /* flags */);
+
+ // passes updated iv spec on to callback so this can be stored for decryption
+ final IvParameterSpec spec = cipher.getParameters().getParameterSpec(IvParameterSpec.class);
+ final String ivSpecValue = Base64.encodeToString(spec.getIV(), Base64.DEFAULT);
+ fingerPrintCallback.handleEncryptedResult(encryptedValue, ivSpecValue);
+
+ } catch (final Exception e) {
+ fingerPrintCallback.onException();
+ }
+
+ }
+
+ @SuppressWarnings("NewApi")
+ public void initDecryptData(final String ivSpecValue) {
+
+ if (!isFingerprintInitialized()) {
+ if (fingerPrintCallback != null) {
+ fingerPrintCallback.onException();
+ }
+ return;
+ }
+ try {
+ createNewKeyIfNeeded(false);
+ keyStore.load(null);
+ final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
+
+ // important to restore spec here that was used for decryption
+ final byte[] iv = Base64.decode(ivSpecValue, Base64.DEFAULT);
+ final IvParameterSpec spec = new IvParameterSpec(iv);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+
+ stopListening();
+ startListening();
+
+ } catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
+ fingerPrintCallback.onInvalidKeyException();
+ } catch (final Exception e) {
+ fingerPrintCallback.onException();
+ }
+ }
+
+ @SuppressWarnings("NewApi")
+ public void decryptData(final String encryptedValue) {
+
+ if (!isFingerprintInitialized()) {
+ if (fingerPrintCallback != null) {
+ fingerPrintCallback.onException();
+ }
+ return;
+ }
+ try {
+ // actual decryption here
+ final byte[] encrypted = Base64.decode(encryptedValue, 0);
+ byte[] decrypted = cipher.doFinal(encrypted);
+ final String decryptedString = new String(decrypted);
+
+ //final String encryptedString = Base64.encodeToString(encrypted, 0 /* flags */);
+ fingerPrintCallback.handleDecryptedResult(decryptedString);
+
+ } catch (final Exception e) {
+ fingerPrintCallback.onException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private void createNewKeyIfNeeded(final boolean allowDeleteExisting) {
+ if (!isFingerprintInitialized()) {
+ return;
+ }
+ try {
+ keyStore.load(null);
+ if (allowDeleteExisting
+ && keyStore.containsAlias(ALIAS_KEY)) {
+
+ keyStore.deleteEntry(ALIAS_KEY);
+ }
+
+ // Create new key if needed
+ if (!keyStore.containsAlias(ALIAS_KEY)) {
+ // Set the alias of the entry in Android KeyStore where the key will appear
+ // and the constrains (purposes) in the constructor of the Builder
+ keyGenerator.init(
+ new KeyGenParameterSpec.Builder(
+ ALIAS_KEY,
+ KeyProperties.PURPOSE_ENCRYPT |
+ KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ // Require the user to authenticate with a fingerprint to authorize every use
+ // of the key
+ .setUserAuthenticationRequired(true)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ .build());
+ keyGenerator.generateKey();
+ }
+ } catch (final Exception e) {
+ fingerPrintCallback.onException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public boolean isHardwareDetected() {
+ return isFingerprintSupported()
+ && fingerprintManager != null
+ && fingerprintManager.isHardwareDetected();
+ }
+
+ @SuppressLint("NewApi")
+ public boolean hasEnrolledFingerprints() {
+ // fingerprint hardware supported and api level OK
+ return isHardwareDetected()
+ // fingerprints enrolled
+ && fingerprintManager != null
+ && fingerprintManager.hasEnrolledFingerprints()
+ // and lockscreen configured
+ && keyguardManager.isKeyguardSecure();
+ }
+
+ void setInitOk(final boolean initOk) {
+ this.initOk = initOk;
+ }
+
+ public boolean isFingerprintSupported() {
+ return Build.VERSION.SDK_INT >= BuildCompat.VERSION_CODE_M;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keepassdroid/stream/HmacBlockOutputStream.java b/app/src/main/java/com/keepassdroid/stream/HmacBlockOutputStream.java
new file mode 100644
index 000000000..b115ce5fc
--- /dev/null
+++ b/app/src/main/java/com/keepassdroid/stream/HmacBlockOutputStream.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 Brian Pellin.
+ *
+ * This file is part of KeePassDroid.
+ *
+ * KeePassDroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDroid 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDroid. If not, see .
+ *
+ */
+package com.keepassdroid.stream;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class HmacBlockOutputStream extends OutputStream {
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 1024;
+ private LEDataOutputStream baseStream;
+ private byte[] key;
+
+ private byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ private int bufferPos = 0;
+ private long blockIndex = 0;
+
+ public HmacBlockOutputStream(OutputStream os, byte[] key) {
+ this.baseStream = new LEDataOutputStream(os);
+ this.key = key;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (bufferPos == 0) {
+ WriteSafeBlock();
+ } else {
+ WriteSafeBlock();
+ WriteSafeBlock();
+ }
+
+ baseStream.flush();;
+ baseStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ baseStream.flush();
+ }
+
+ @Override
+ public void write(byte[] outBuffer) throws IOException {
+ write(outBuffer, 0, outBuffer.length);
+ }
+
+ @Override
+ public void write(byte[] outBuffer, int offset, int count) throws IOException {
+ while (count > 0) {
+ if (bufferPos == buffer.length) {
+ WriteSafeBlock();
+ }
+
+ int copy = Math.min(buffer.length - bufferPos, count);
+ assert(copy > 0);
+
+ System.arraycopy(outBuffer, offset, buffer, bufferPos, copy);
+ offset += copy;
+ bufferPos += copy;
+
+ count -= copy;
+ }
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ byte[] outByte = new byte[1];
+ write(outByte, 0, 1);
+ }
+
+ private void WriteSafeBlock() throws IOException {
+ byte[] bufBlockIndex = LEDataOutputStream.writeLongBuf(blockIndex);
+ byte[] blockSizeBuf = LEDataOutputStream.writeIntBuf(bufferPos);
+
+ byte[] blockHmac;
+ byte[] blockKey = HmacBlockStream.GetHmacKey64(key, blockIndex);
+
+ Mac hmac;
+ try {
+ hmac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec signingKey = new SecretKeySpec(blockKey, "HmacSHA256");
+ hmac.init(signingKey);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException("Invalid Hmac");
+ } catch (InvalidKeyException e) {
+ throw new IOException("Invalid HMAC");
+ }
+
+ hmac.update(bufBlockIndex);
+ hmac.update(blockSizeBuf);
+
+ if (bufferPos > 0) {
+ hmac.update(buffer, 0, bufferPos);
+ }
+
+ blockHmac = hmac.doFinal();
+
+ baseStream.write(blockHmac);
+ baseStream.write(blockSizeBuf);
+
+ if (bufferPos > 0) {
+ baseStream.write(buffer, 0, bufferPos);
+ }
+
+ blockIndex++;
+ bufferPos = 0;
+ }
+}
diff --git a/app/src/main/java/com/keepassdroid/stream/MacOutputStream.java b/app/src/main/java/com/keepassdroid/stream/MacOutputStream.java
new file mode 100644
index 000000000..2d51fcfab
--- /dev/null
+++ b/app/src/main/java/com/keepassdroid/stream/MacOutputStream.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Brian Pellin.
+ *
+ * This file is part of KeePassDroid.
+ *
+ * KeePassDroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDroid 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDroid. If not, see .
+ *
+ */
+package com.keepassdroid.stream;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.crypto.Mac;
+
+public class MacOutputStream extends OutputStream {
+ private Mac mac;
+ private OutputStream os;
+
+ public MacOutputStream(OutputStream os, Mac mac) {
+ this.mac = mac;
+ this.os = os;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ os.close();
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ mac.update((byte) oneByte);
+ os.write(oneByte);
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int count) throws IOException {
+ mac.update(buffer, offset, count);
+ os.write(buffer, offset, count);
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ mac.update(buffer, 0, buffer.length);
+ os.write(buffer);
+ }
+
+ public byte[] getMac() {
+ return mac.doFinal();
+ }
+}
diff --git a/app/src/main/java/com/keepassdroid/utils/DateUtil.java b/app/src/main/java/com/keepassdroid/utils/DateUtil.java
index 25c6faedb..a2b0e52cf 100644
--- a/app/src/main/java/com/keepassdroid/utils/DateUtil.java
+++ b/app/src/main/java/com/keepassdroid/utils/DateUtil.java
@@ -30,4 +30,8 @@ public class DateUtil {
public static Date convertKDBX4Time(long seconds) {
return dotNetEpoch.plus(seconds).toDate();
}
+
+ public static long convertDateToKDBX4Time(DateTime dt) {
+ return (dt.getMillis() / 1000) - (dotNetEpoch.getMillis() / 1000);
+ }
}
diff --git a/app/src/main/res/drawable-hdpi/ic_fp_40px.png b/app/src/main/res/drawable-hdpi/ic_fp_40px.png
new file mode 100644
index 000000000..48ebd8ad7
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_fp_40px.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_fp_40px.png b/app/src/main/res/drawable-xhdpi/ic_fp_40px.png
new file mode 100644
index 000000000..e1c9590bb
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fp_40px.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png b/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png
new file mode 100644
index 000000000..f7e87240e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png differ
diff --git a/app/src/main/res/layout/password.xml b/app/src/main/res/layout/password.xml
index d1c0393ac..98803b181 100644
--- a/app/src/main/res/layout/password.xml
+++ b/app/src/main/res/layout/password.xml
@@ -18,6 +18,7 @@
along with KeePass DX. If not, see .
-->
-
-
+
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:src="@drawable/ic_fp_40px"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+
+
@@ -103,7 +121,7 @@
android:layout_above="@+id/browse_button"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
- android:layout_below="@+id/password_label"
+ android:layout_below="@+id/fingerprint"
android:src="@drawable/ic_visibility_white_24dp"
android:tint="@android:color/darker_gray" />
@@ -119,21 +137,12 @@
android:src="@drawable/ic_folder_white_24dp"
android:tint="?attr/colorAccentCompat" />
-
-
@@ -163,4 +172,99 @@
android:layout_alignParentEnd="true"
android:text="@android:string/ok" />
+
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 13c99ac1a..7a0ea5d63 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -13,23 +13,23 @@
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
-
+
You should have received a copy of the GNU General Public License
along with KeePass DX. If not, see .
-
- French translation by Laurent, Norman
-
-->
-
- Signaler une anomalie\u00A0:
- Site web\u00A0:
+
+ Signaler une anomalie\\u00A0:
+ Site web\\u00A0:
KeePass DX est une implémentation sur Android du gestionnaire de mots de passe KeePass.
Accepter
Ajouter une entrée
Ajouter un groupe
Ajouter un groupe
+ Ajouter une chaîne
Algorithme
- Algorithme\u00A0:
+ Algorithme\\u00A0:
Application timeout
Temps avant le verrouillage de la base de données lorsque l\'application est inactive.
Application
@@ -38,29 +38,29 @@
Enregistrer des modifications dans les fichiers kdbx est EXPÉRIMENTAL. Faites des sauvegardes de votre base de données avant toute modification.
Crochets
File browsing requires the Open Intents File Manager, click below to install it. Due to some quirks in the file manager, browsing may not work correctly, the first time you browse.
- Reconstruction de l\'index de recherche…
+ Reconstruction de l\'index de recherche…
Annuler
Presse-papier vidé
Erreur de presse-papier
- Certains appareils Android Samsung ont un bug dans l\'implémentation du presse-papier qui empêche la copie depuis des applications. Pour plus de détails, visitez\u00A0:
+ Certains appareils Android Samsung ont un bug dans l\'implémentation du presse-papier qui empêche la copie depuis des applications. Pour plus de détails, visitez\\u00A0:
Le vidage du presse-papier a échoué
Clipboard timeout
Temps avant le vidage du presse-papier après copie du nom d\'utilisateur ou du mot de passe
Copier le nom d\'utilisateur dans le presse-papier
Copier le mot de passe dans le presse-papier
- Création de la clé de base de données…
- Groupe actuel\u00A0:
- Groupe actuel\u00A0: Racine
+ Création de la clé de base de données…
+ Groupe actuel\\u00A0:
+ Groupe actuel\\u00A0: Racine
Base de données
- Déchiffrement du contenu de la base de données…
+ Déchiffrement du contenu de la base de données…
Déchiffrement de l\'entrée
Utiliser comme base de données par défaut
Nombres
KeePass DX Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft n\'offre ABSOLUMENT AUCUNE GARANTIE; il s\'agit d\'un logiciel libre, vous pouvez le redistribuer sous les conditions de la licence GPL v2 ou ultérieure.
-
- Sélectionnez la base de données\u00A0:
+ \\u2026
+ Sélectionnez la base de données\\u00A0:
Dernier accès
- Entrez un mot de passe et/ou un fichier de clé pour ouvrir la base de données\u00A0:
+ Entrez un mot de passe et/ou un fichier de clé pour ouvrir la base de données\\u00A0:
Annuler
Commentaires
Confirmer mot de passe
@@ -82,7 +82,7 @@
Impossible de déterminer les paramètres de la base de données.
Échec lors de l\'ouverture du lien.
Le nom de fichier est obligatoire.
- Impossible de créer le fichier\u00A0:
+ Impossible de créer le fichier\\u00A0:
Base de données invalide.
Chemin invalide.
Le nom est obligatoire.
@@ -98,6 +98,7 @@
Nom du champ
Valeur
Fichier non trouvé.
+ Fichier non trouvé. Essayer de l\'ouvrir à nouveau à partir de votre fournisseur de contenu.
Navigateur de fichiers
Générer mot de passe
Groupe
@@ -122,9 +123,9 @@
Longueur
Taille de la liste des groupes
Taille de la police de caractères utilisée pour la liste des groupes
- Ouverture de la base de données…
+ Ouverture de la base de données…
Minuscule
-
+ *****
Afficher le mot de passe
Par défaut, masquer le mot de passe
À propos
@@ -150,15 +151,16 @@
Aucun élément.
Aucun résultat pour cette recherche.
Impossible d\'ouvrir cette URL.
- Bases de données utilisées récemment\u00A0:
+ Bases de données utilisées récemment\\u00A0:
Ignorer les sauvegardes
Ignorer le groupe Sauvegardes des résultats de recherche (uniquement pour .kdb)
Fichier de base de données KeePass
Entrez le mot de passe de la base de données
- Création d\'une nouvelle base de données…
- Veuillez patienter…
+ Création d\'une nouvelle base de données…
+ Veuillez patienter…
Protection
- KeePass DX n\'a pas la permission d\'écrire dans le répertoire de la base de données. Celle-ci sera ouverte en lecture seule.
+ Lecture seule
+ KeePassDroid n\'a pas la permission d\'écrire dans le répertoire de la base de données. Celle-ci sera ouverte en lecture seule.
À partir d\'Android KitKat, certains appareils n\'autorisent plus les applications à écrire sur la carte SD.
Historique de fichiers récents
Mémoriser les fichiers récemment utilisés
@@ -170,7 +172,7 @@
Niveau du chiffrement
Un niveau de chiffrement supérieur assure une protection supplémentaire contre les attaques de force brute, mais peut considérablement ralentir l\'ouverture et l\'enregistrement.
niveaux
- Enregistrement de la base de données…
+ Enregistrement de la base de données…
Espace
Rechercher
Afficher le mot de passe
@@ -183,19 +185,23 @@
Souligné
Version de la base de données non supportée.
Majuscule
+ Utilisez l\'Environnement d\'Accès au Stockage d\'Android pour naviguer dans les fichiers (KitKat ou plus récent)
+ Environnement d\'Accès au Stockage
+ Alerte
+ Le format .kdb ne supporte que le jeu de caractère Latin1. Votre mot de passe doit contenir des caractères en dehors de ce jeu. Tous les caractères non-Latin1 sont convertis en un même caractère, ce qui diminue la sécurité de votre mot de passe. Le changement de votre mot de passe est recommandé.
Votre carte SD est actuellement en lecture seule. Vous ne pourrez pas enregistrer les changements dans la base de données.
Votre carte SD n\'est actuellement pas montée sur votre appareil. Vous ne pourrez pas charger ou créer votre base de données.
- Version\u00A0:
-
+ Version\\u00A0:
+
- - 30 secondes
- - 1 minute
- - 5 minutes
- - Jamais
+ - 30 secondes
+ - 1 minute
+ - 5 minutes
+ - Jamais
- - Petit
- - Moyen
- - Grand
+ - Petit
+ - Moyen
+ - Grand
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index ac5182214..08bf3bec3 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -56,7 +56,7 @@
"KeePass DX © 2009–2013
Разработчик Brian Pellin
-Программа предоставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Распространяется свободно по лицензии GPL v3 или новее"
+Программа предоставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Распространяется свободно по лицензии GPL v2 или новее"
…
Путь к базе KeePass:
Доступ:
@@ -183,6 +183,10 @@
_Подчёркивание_
Неподдерживаемая версия базы
ЗАГЛАВНЫЕ
+ Storage Access Framework для обзора файлов (KK+)
+ Обзор через SAF
+ Внимание
+ Формат .kdb поддерживает только кодировку Latin1. Пароль может содержать символы вне этой кодировки. Все не-Latin1 символы будут преобразованы в одинаковый символ, что снизит надёжность пароля. Рекомендуется изменить пароль.
Запись на карту памяти невозможна. Изменения не будут сохранены
Карта памяти не подключена. Работа с базой невозможна
Версия:
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 60c194b8c..921cfe65c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -29,7 +29,7 @@
#388e3c
#cccccc
#424242
- #bdbdbd
+ #eeeeee
#43a047
#bdbdbd
#388e3c
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 5222bb186..42f9fa9b3 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -19,4 +19,9 @@
12dp
5dp
24dp
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ee5e112e9..e9a6223e7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -194,7 +194,14 @@
Your sd card is currently read-only. You may not be able to save changes to your database.
Your sd card is not currently mounted on your device. You will not be able to load or create your database.
Version:
-
+ Fingerprint supported but not configured for device
+ Listening for fingerprints
+ Encrypted password stored
+ Invalid Key problem
+ Fingerprint problem
+ Use fingerprint to store this password
+ No password stored yet for this database
+
- 30 seconds
- 1 minute
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 1dc494c26..019d7cab5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -36,7 +36,7 @@
- @color/colorAccent
- @color/colorAccent
- - @color/white
+ - @color/colorTextInverse
- @style/KeepassoidStyle.TextAppearance
- @style/KeepassoidStyle.TextAppearance
@@ -52,63 +52,62 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 4f693b6a7..a7a037ab2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ buildscript {
}
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.3.3'
+ classpath 'com.android.tools.build:gradle:3.0.0'
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 05ca384fa..ad47fc2ff 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,4 +1,4 @@
-#Wed Oct 25 20:52:47 CEST 2017
+#Sat Oct 28 10:13:14 CDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME