mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8baae8b801 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
cb59cef1b8 | ||
|
|
d9b600466c | ||
|
|
4edf2f8cd1 | ||
|
|
c60dfdf0d7 | ||
|
|
006afc6841 | ||
|
|
f70879581d | ||
|
|
db1d71af9f | ||
|
|
6b03ef35a6 | ||
|
|
2afd02d86f | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b |
11
CHANGELOG
11
CHANGELOG
@@ -1,3 +1,14 @@
|
||||
KeePassDX(3.0.0)
|
||||
* Fix search with non-latin chars #971
|
||||
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||
* Fix timeout with 0s #974
|
||||
|
||||
KeePassDX(2.9.19)
|
||||
* Fix search slowdown #964
|
||||
* Fix closing notification after lock request #965
|
||||
* Better temp advanced unlocking code implementation #965
|
||||
* Fix OTP token generation #967
|
||||
|
||||
KeePassDX(2.9.18)
|
||||
* Move groups #658
|
||||
* Improve autofill recognition #960
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 72
|
||||
versionName = "2.9.18"
|
||||
versionCode = 75
|
||||
versionName = "2.9.20"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import junit.framework.TestCase
|
||||
import java.util.*
|
||||
|
||||
class UUIDTest: TestCase() {
|
||||
|
||||
fun testUUID() {
|
||||
val randomUUID = UUID.randomUUID()
|
||||
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||
assertEquals(randomUUID, retrievedUUID)
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,7 @@ class GroupActivity : LockingActivity(),
|
||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||
setIntent(intentNotNull)
|
||||
if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||
finishNodeAction()
|
||||
// only one instance of search in backstack
|
||||
deletePreviousSearchGroup()
|
||||
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
||||
|
||||
@@ -106,7 +106,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
|
||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
return database.createEntry()?.apply {
|
||||
database.startManageEntry(this)
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||
{ standardIconId ->
|
||||
@@ -127,7 +126,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
}
|
||||
)
|
||||
}
|
||||
database.stopManageEntry(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +148,14 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
if (searchGroup != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||
database.startManageEntry(entry)
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
cursorKDBX?.addEntry(it)
|
||||
}
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
@@ -42,23 +39,35 @@ class CipherDatabaseAction(context: Context) {
|
||||
// Temp DAO to easily remove content if object no longer in memory
|
||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
|
||||
private val mIntentAdvancedUnlockService = Intent(applicationContext,
|
||||
AdvancedUnlockNotificationService::class.java)
|
||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||
private var mServiceConnection: ServiceConnection? = null
|
||||
|
||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
||||
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
||||
deleteAll()
|
||||
removeAllDataAndDetach()
|
||||
}
|
||||
|
||||
fun reloadPreferences() {
|
||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun attachService(performedAction: () -> Unit) {
|
||||
// Check if a service is currently running else do nothing
|
||||
if (mBinder != null) {
|
||||
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
|
||||
// Check if a service is currently running else call action without info
|
||||
if (startService && mServiceConnection == null) {
|
||||
attachService(performedAction)
|
||||
} else {
|
||||
performedAction.invoke()
|
||||
} else if (mServiceConnection == null) {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun attachService(performedAction: () -> Unit) {
|
||||
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||
})
|
||||
|
||||
mServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
||||
@@ -66,42 +75,59 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
try {
|
||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||
mServiceConnection!!,
|
||||
Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to start cipher action", e)
|
||||
performedAction.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun detachService() {
|
||||
try {
|
||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||
} catch (e: Exception) {}
|
||||
|
||||
mServiceConnection?.let {
|
||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAllDataAndDetach() {
|
||||
detachService()
|
||||
onClear()
|
||||
}
|
||||
|
||||
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||
mDatabaseListeners.add(listenerCipher)
|
||||
}
|
||||
|
||||
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||
mDatabaseListeners.remove(listenerCipher)
|
||||
}
|
||||
|
||||
private fun onClear() {
|
||||
mBinder = null
|
||||
mServiceConnection = null
|
||||
mDatabaseListeners.forEach {
|
||||
it.onDatabaseCleared()
|
||||
}
|
||||
}
|
||||
}
|
||||
applicationContext.bindService(mIntentAdvancedUnlockService,
|
||||
mServiceConnection!!,
|
||||
Context.BIND_ABOVE_CLIENT)
|
||||
if (mBinder == null) {
|
||||
try {
|
||||
applicationContext.startService(mIntentAdvancedUnlockService)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to start cipher action", e)
|
||||
}
|
||||
}
|
||||
it.onCipherDatabaseCleared()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerDatabaseListener(listener: DatabaseListener) {
|
||||
mDatabaseListeners.add(listener)
|
||||
}
|
||||
|
||||
fun unregisterDatabaseListener(listener: DatabaseListener) {
|
||||
mDatabaseListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface DatabaseListener {
|
||||
fun onDatabaseCleared()
|
||||
interface CipherDatabaseListener {
|
||||
fun onCipherDatabaseCleared()
|
||||
}
|
||||
|
||||
fun getCipherDatabase(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
serviceActionTask {
|
||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||
}
|
||||
} else {
|
||||
@@ -126,7 +152,8 @@ class CipherDatabaseAction(context: Context) {
|
||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
// The only case to create service (not needed to get an info)
|
||||
serviceActionTask(true) {
|
||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
@@ -151,7 +178,7 @@ class CipherDatabaseAction(context: Context) {
|
||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
serviceActionTask {
|
||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
@@ -168,14 +195,19 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
attachService {
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
mBinder?.deleteAll()
|
||||
}
|
||||
}
|
||||
// To erase the residues
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
}
|
||||
).execute()
|
||||
// Unbind
|
||||
removeAllDataAndDetach()
|
||||
}
|
||||
|
||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||
|
||||
@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
|
||||
@@ -68,7 +67,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
@@ -402,9 +401,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
fun connect(databaseUri: Uri) {
|
||||
showViews(true)
|
||||
this.databaseFileUri = databaseUri
|
||||
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
||||
override fun onDatabaseCleared() {
|
||||
deleteEncryptedDatabaseKey()
|
||||
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||
override fun onCipherDatabaseCleared() {
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
cipherDatabaseAction.apply {
|
||||
@@ -435,14 +435,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
allowOpenBiometricPrompt = false
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
} ?: checkUnlockAvailability()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
@@ -479,7 +477,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
||||
advancedUnlockManager?.encryptData(credential)
|
||||
}
|
||||
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
|
||||
}
|
||||
Mode.EXTRACT_CREDENTIAL -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
|
||||
@@ -557,7 +557,6 @@ class Database {
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
ignoreCase = true
|
||||
}, omitBackup, max)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
@@ -75,6 +76,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||
|
||||
var kdbxVersion = UnsignedInt(0)
|
||||
var name = ""
|
||||
@@ -333,6 +335,19 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return customData.isNotEmpty()
|
||||
}
|
||||
|
||||
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||
return entryIndexes.values.find { entry ->
|
||||
entry.customData.containsValue(customDataValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of a field reference
|
||||
*/
|
||||
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
|
||||
return mFieldReferenceEngine.compile(textReference, recursionLevel)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
@@ -654,9 +669,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
this.deletedObjects.add(deletedObject)
|
||||
}
|
||||
|
||||
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.addEntryTo(newEntry, parent)
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun updateEntry(entry: EntryKDBX) {
|
||||
super.updateEntry(entry)
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.removeEntryFrom(entryToRemove, parent)
|
||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
||||
@@ -727,6 +753,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
override fun clearCache() {
|
||||
try {
|
||||
super.clearCache()
|
||||
mFieldReferenceEngine.clear()
|
||||
attachmentPool.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear cache", e)
|
||||
|
||||
@@ -67,7 +67,7 @@ abstract class DatabaseVersioned<
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
@@ -271,6 +271,26 @@ abstract class DatabaseVersioned<
|
||||
return this.entryIndexes[id]
|
||||
}
|
||||
|
||||
fun getEntryByTitle(title: String): Entry? {
|
||||
return this.entryIndexes.values.find { entry -> entry.title.equals(title, true) }
|
||||
}
|
||||
|
||||
fun getEntryByUsername(username: String): Entry? {
|
||||
return this.entryIndexes.values.find { entry -> entry.username.equals(username, true) }
|
||||
}
|
||||
|
||||
fun getEntryByURL(url: String): Entry? {
|
||||
return this.entryIndexes.values.find { entry -> entry.url.equals(url, true) }
|
||||
}
|
||||
|
||||
fun getEntryByPassword(password: String): Entry? {
|
||||
return this.entryIndexes.values.find { entry -> entry.password.equals(password, true) }
|
||||
}
|
||||
|
||||
fun getEntryByNotes(notes: String): Entry? {
|
||||
return this.entryIndexes.values.find { entry -> entry.notes.equals(notes, true) }
|
||||
}
|
||||
|
||||
fun addEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
@@ -336,14 +356,14 @@ abstract class DatabaseVersioned<
|
||||
removeGroupIndex(groupToRemove)
|
||||
}
|
||||
|
||||
fun addEntryTo(newEntry: Entry, parent: Group?) {
|
||||
open fun addEntryTo(newEntry: Entry, parent: Group?) {
|
||||
// Add entry to parent
|
||||
parent?.addChildEntry(newEntry)
|
||||
newEntry.parent = parent
|
||||
addEntryIndex(newEntry)
|
||||
}
|
||||
|
||||
fun updateEntry(entry: Entry) {
|
||||
open fun updateEntry(entry: Entry) {
|
||||
updateEntryIndex(entry)
|
||||
}
|
||||
|
||||
|
||||
@@ -146,53 +146,73 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
return NodeIdUUID(nodeId.id)
|
||||
}
|
||||
|
||||
override val type: Type
|
||||
get() = Type.ENTRY
|
||||
|
||||
/**
|
||||
* Decode a reference key with the FieldReferencesEngine
|
||||
* @param decodeRef
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
||||
private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
|
||||
return fields[key]?.toString()?.let { text ->
|
||||
return if (decodeRef) {
|
||||
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!)
|
||||
mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
|
||||
} else text
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
fun decodeTitleKey(recursionLevel: Int): String {
|
||||
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
|
||||
}
|
||||
|
||||
override var title: String
|
||||
get() = decodeRefKey(mDecodeRef, STR_TITLE)
|
||||
get() = decodeTitleKey(0)
|
||||
set(value) {
|
||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
|
||||
fields[STR_TITLE] = ProtectedString(protect, value)
|
||||
}
|
||||
|
||||
override val type: Type
|
||||
get() = Type.ENTRY
|
||||
fun decodeUsernameKey(recursionLevel: Int): String {
|
||||
return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
|
||||
}
|
||||
|
||||
override var username: String
|
||||
get() = decodeRefKey(mDecodeRef, STR_USERNAME)
|
||||
get() = decodeUsernameKey(0)
|
||||
set(value) {
|
||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
|
||||
fields[STR_USERNAME] = ProtectedString(protect, value)
|
||||
}
|
||||
|
||||
fun decodePasswordKey(recursionLevel: Int): String {
|
||||
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
|
||||
}
|
||||
|
||||
override var password: String
|
||||
get() = decodeRefKey(mDecodeRef, STR_PASSWORD)
|
||||
get() = decodePasswordKey(0)
|
||||
set(value) {
|
||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
|
||||
fields[STR_PASSWORD] = ProtectedString(protect, value)
|
||||
}
|
||||
|
||||
fun decodeUrlKey(recursionLevel: Int): String {
|
||||
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
|
||||
}
|
||||
|
||||
override var url
|
||||
get() = decodeRefKey(mDecodeRef, STR_URL)
|
||||
get() = decodeUrlKey(0)
|
||||
set(value) {
|
||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
|
||||
fields[STR_URL] = ProtectedString(protect, value)
|
||||
}
|
||||
|
||||
fun decodeNotesKey(recursionLevel: Int): String {
|
||||
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
|
||||
}
|
||||
|
||||
override var notes: String
|
||||
get() = decodeRefKey(mDecodeRef, STR_NOTES)
|
||||
get() = decodeNotesKey(0)
|
||||
set(value) {
|
||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
|
||||
fields[STR_NOTES] = ProtectedString(protect, value)
|
||||
@@ -245,7 +265,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
field.clear()
|
||||
for ((key, value) in fields) {
|
||||
if (!isStandardField(key)) {
|
||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key, 0))
|
||||
}
|
||||
}
|
||||
return field
|
||||
|
||||
@@ -19,344 +19,124 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.entry
|
||||
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import java.util.*
|
||||
|
||||
class FieldReferencesEngine {
|
||||
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||
|
||||
fun compile(text: String, entry: EntryKDBX, database: DatabaseKDBX): String {
|
||||
return compileInternal(text, SprContextKDBX(database, entry), 0)
|
||||
// Key : <WantedField>@<SearchIn>:<Text>
|
||||
// Value : content
|
||||
private var refsCache: MutableMap<String, String?> = HashMap()
|
||||
|
||||
fun clear() {
|
||||
refsCache.clear()
|
||||
}
|
||||
|
||||
private fun compileInternal(text: String?, sprContextKDBX: SprContextKDBX?, recursionLevel: Int): String {
|
||||
if (text == null) {
|
||||
return ""
|
||||
}
|
||||
if (sprContextKDBX == null) {
|
||||
return ""
|
||||
}
|
||||
fun compile(textReference: String, recursionLevel: Int): String {
|
||||
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
||||
""
|
||||
} else fillRefPlaceholders(text, sprContextKDBX, recursionLevel)
|
||||
|
||||
} else
|
||||
fillReferencesPlaceholders(textReference, recursionLevel)
|
||||
}
|
||||
|
||||
private fun fillRefPlaceholders(textReference: String, contextKDBX: SprContextKDBX, recursionLevel: Int): String {
|
||||
var text = textReference
|
||||
|
||||
if (contextKDBX.databaseKDBX == null) {
|
||||
return text
|
||||
}
|
||||
/**
|
||||
* Manage placeholders with {REF:<WantedField>@<SearchIn>:<Text>}
|
||||
*/
|
||||
private fun fillReferencesPlaceholders(textReference: String, recursionLevel: Int): String {
|
||||
var textValue = textReference
|
||||
|
||||
var offset = 0
|
||||
for (i in 0..19) {
|
||||
text = fillRefsUsingCache(text, contextKDBX)
|
||||
var numberInlineRef = 0
|
||||
while (textValue.contains(STR_REF_START)
|
||||
&& numberInlineRef <= MAX_INLINE_REF) {
|
||||
numberInlineRef++
|
||||
|
||||
val start = text.indexOf(STR_REF_START, offset, true)
|
||||
textValue = fillReferencesUsingCache(textValue)
|
||||
|
||||
val start = textValue.indexOf(STR_REF_START, offset, true)
|
||||
if (start < 0) {
|
||||
break
|
||||
}
|
||||
val end = text.indexOf(STR_REF_END, start + 1, true)
|
||||
val end = textValue.indexOf(STR_REF_END, offset, true)
|
||||
if (end <= start) {
|
||||
break
|
||||
}
|
||||
|
||||
val fullRef = text.substring(start, end + 1)
|
||||
val result = findRefTarget(fullRef, contextKDBX)
|
||||
val reference = textValue.substring(start + STR_REF_START.length, end)
|
||||
val fullReference = "$STR_REF_START$reference$STR_REF_END"
|
||||
|
||||
if (result != null) {
|
||||
val found = result.entry
|
||||
found?.stopToManageFieldReferences()
|
||||
val wanted = result.wanted
|
||||
|
||||
var data: String? = null
|
||||
when (wanted) {
|
||||
'T' -> data = found?.title
|
||||
'U' -> data = found?.username
|
||||
'A' -> data = found?.url
|
||||
'P' -> data = found?.password
|
||||
'N' -> data = found?.notes
|
||||
'I' -> data = found?.nodeId.toString()
|
||||
if (!refsCache.containsKey(fullReference)) {
|
||||
val result = findReferenceTarget(reference)
|
||||
val entryFound = result.entry
|
||||
val newRecursionLevel = recursionLevel + 1
|
||||
val data: String? = when (result.wanted) {
|
||||
'T' -> entryFound?.decodeTitleKey(newRecursionLevel)
|
||||
'U' -> entryFound?.decodeUsernameKey(newRecursionLevel)
|
||||
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
||||
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
||||
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
||||
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
|
||||
else -> null
|
||||
}
|
||||
refsCache[fullReference] = data
|
||||
textValue = fillReferencesUsingCache(textValue)
|
||||
}
|
||||
|
||||
if (data != null && found != null) {
|
||||
val subCtx = SprContextKDBX(contextKDBX)
|
||||
subCtx.entryKDBX = found
|
||||
|
||||
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
|
||||
addRefsToCache(fullRef, innerContent, contextKDBX)
|
||||
text = fillRefsUsingCache(text, contextKDBX)
|
||||
} else {
|
||||
offset = start + 1
|
||||
offset = end
|
||||
}
|
||||
return textValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun findRefTarget(fullReference: String?, contextKDBX: SprContextKDBX): TargetResult? {
|
||||
var fullRef: String? = fullReference ?: return null
|
||||
|
||||
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
|
||||
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
|
||||
if (ref.length <= 4) {
|
||||
return null
|
||||
}
|
||||
if (ref[1] != '@') {
|
||||
return null
|
||||
}
|
||||
if (ref[3] != ':') {
|
||||
return null
|
||||
}
|
||||
|
||||
val scan = Character.toUpperCase(ref[2])
|
||||
val wanted = Character.toUpperCase(ref[0])
|
||||
|
||||
val searchParameters = SearchParameters()
|
||||
searchParameters.setupNone()
|
||||
|
||||
searchParameters.searchQuery = ref.substring(4)
|
||||
when (scan) {
|
||||
'T' -> searchParameters.searchInTitles = true
|
||||
'U' -> searchParameters.searchInUserNames = true
|
||||
'A' -> searchParameters.searchInUrls = true
|
||||
'P' -> searchParameters.searchInPasswords = true
|
||||
'N' -> searchParameters.searchInNotes = true
|
||||
'I' -> searchParameters.searchInUUIDs = true
|
||||
'O' -> searchParameters.searchInOther = true
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val list = ArrayList<EntryKDBX>()
|
||||
searchEntries(contextKDBX, searchParameters, list)
|
||||
|
||||
return if (list.size > 0) {
|
||||
TargetResult(list[0], wanted)
|
||||
} else null
|
||||
|
||||
}
|
||||
|
||||
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextKDBX?) {
|
||||
if (ref == null) {
|
||||
return
|
||||
}
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
if (ctx == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.refsCache.containsKey(ref)) {
|
||||
ctx.refsCache[ref] = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillRefsUsingCache(text: String, sprContextKDBX: SprContextKDBX): String {
|
||||
private fun fillReferencesUsingCache(text: String): String {
|
||||
var newText = text
|
||||
for ((key, value) in sprContextKDBX.refsCache) {
|
||||
newText = text.replace(key, value, true)
|
||||
for ((key, value) in refsCache) {
|
||||
// Replace by key if value not found
|
||||
newText = newText.replace(key, value ?: key, true)
|
||||
}
|
||||
return newText
|
||||
}
|
||||
|
||||
private fun searchEntries(contextKDBX: SprContextKDBX,
|
||||
searchParameters: SearchParameters?,
|
||||
listStorage: MutableList<EntryKDBX>?) {
|
||||
private fun findReferenceTarget(reference: String): TargetResult {
|
||||
|
||||
val root = contextKDBX.databaseKDBX?.rootGroup
|
||||
if (searchParameters == null) {
|
||||
return
|
||||
val targetResult = TargetResult(null, 'J')
|
||||
|
||||
if (reference.length <= 4) {
|
||||
return targetResult
|
||||
}
|
||||
if (listStorage == null) {
|
||||
return
|
||||
if (reference[1] != '@') {
|
||||
return targetResult
|
||||
}
|
||||
if (reference[3] != ':') {
|
||||
return targetResult
|
||||
}
|
||||
|
||||
val terms = splitStringTerms(searchParameters.searchQuery)
|
||||
if (terms.size <= 1 || searchParameters.regularExpression) {
|
||||
root!!.doForEachChild(EntryKDBXSearchHandler(searchParameters, listStorage), null)
|
||||
return
|
||||
targetResult.wanted = Character.toUpperCase(reference[0])
|
||||
val searchIn = Character.toUpperCase(reference[2])
|
||||
val searchQuery = reference.substring(4)
|
||||
targetResult.entry = when (searchIn) {
|
||||
'T' -> mDatabase.getEntryByTitle(searchQuery)
|
||||
'U' -> mDatabase.getEntryByUsername(searchQuery)
|
||||
'A' -> mDatabase.getEntryByURL(searchQuery)
|
||||
'P' -> mDatabase.getEntryByPassword(searchQuery)
|
||||
'N' -> mDatabase.getEntryByNotes(searchQuery)
|
||||
'I' -> {
|
||||
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
|
||||
mDatabase.getEntryById(NodeIdUUID(uuid))
|
||||
}
|
||||
}
|
||||
'O' -> mDatabase.getEntryByCustomData(searchQuery)
|
||||
else -> null
|
||||
}
|
||||
return targetResult
|
||||
}
|
||||
|
||||
// Search longest term first
|
||||
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
|
||||
Collections.sort(terms, stringLengthComparator)
|
||||
|
||||
val fullSearch = searchParameters.searchQuery
|
||||
var childEntries: List<EntryKDBX>? = root!!.getChildEntries()
|
||||
for (i in terms.indices) {
|
||||
val pgNew = ArrayList<EntryKDBX>()
|
||||
|
||||
searchParameters.searchQuery = terms[i]
|
||||
|
||||
var negate = false
|
||||
if (searchParameters.searchQuery.startsWith("-")) {
|
||||
searchParameters.searchQuery = searchParameters.searchQuery.substring(1)
|
||||
negate = searchParameters.searchQuery.isNotEmpty()
|
||||
}
|
||||
|
||||
if (!root.doForEachChild(EntryKDBXSearchHandler(searchParameters, pgNew), null)) {
|
||||
childEntries = null
|
||||
break
|
||||
}
|
||||
|
||||
childEntries = if (negate) {
|
||||
val complement = ArrayList<EntryKDBX>()
|
||||
for (entry in childEntries!!) {
|
||||
if (!pgNew.contains(entry)) {
|
||||
complement.add(entry)
|
||||
}
|
||||
}
|
||||
complement
|
||||
} else {
|
||||
pgNew
|
||||
}
|
||||
}
|
||||
|
||||
if (childEntries != null) {
|
||||
listStorage.addAll(childEntries)
|
||||
}
|
||||
searchParameters.searchQuery = fullSearch
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a list of String by split text when ' ', '\t', '\r' or '\n' is found
|
||||
*/
|
||||
private fun splitStringTerms(text: String?): List<String> {
|
||||
val list = ArrayList<String>()
|
||||
if (text == null) {
|
||||
return list
|
||||
}
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
var quoted = false
|
||||
|
||||
for (element in text) {
|
||||
|
||||
if ((element == ' ' || element == '\t' || element == '\r' || element == '\n') && !quoted) {
|
||||
|
||||
val len = stringBuilder.length
|
||||
when {
|
||||
len > 0 -> {
|
||||
list.add(stringBuilder.toString())
|
||||
stringBuilder.delete(0, len)
|
||||
}
|
||||
element == '\"' -> quoted = !quoted
|
||||
else -> stringBuilder.append(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stringBuilder.isNotEmpty()) {
|
||||
list.add(stringBuilder.toString())
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
inner class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
||||
|
||||
private inner class SprContextKDBX {
|
||||
|
||||
var databaseKDBX: DatabaseKDBX? = null
|
||||
var entryKDBX: EntryKDBX
|
||||
var refsCache: MutableMap<String, String> = HashMap()
|
||||
|
||||
constructor(databaseKDBX: DatabaseKDBX, entry: EntryKDBX) {
|
||||
this.databaseKDBX = databaseKDBX
|
||||
this.entryKDBX = entry
|
||||
}
|
||||
|
||||
constructor(source: SprContextKDBX) {
|
||||
this.databaseKDBX = source.databaseKDBX
|
||||
this.entryKDBX = source.entryKDBX
|
||||
this.refsCache = source.refsCache
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryKDBXSearchHandler(private val mSearchParametersKDBX: SearchParameters,
|
||||
private val mListStorage: MutableList<EntryKDBX>)
|
||||
: NodeHandler<EntryKDBX>() {
|
||||
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
if (mSearchParametersKDBX.excludeExpired
|
||||
&& node.isCurrentlyExpires) {
|
||||
return true
|
||||
}
|
||||
if (searchStrings(node)) {
|
||||
mListStorage.add(node)
|
||||
return true
|
||||
}
|
||||
if (searchInGroupNames(node)) {
|
||||
mListStorage.add(node)
|
||||
return true
|
||||
}
|
||||
if (searchInUUID(node)) {
|
||||
mListStorage.add(node)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun searchStrings(entry: EntryKDBX): Boolean {
|
||||
var searchFound = false
|
||||
// Search all strings in the KDBX entry
|
||||
EntryFieldsLoop@ for((key, value) in entry.fields) {
|
||||
if (entryKDBXKeyIsAllowedToSearch(key, mSearchParametersKDBX)) {
|
||||
val currentString = value.toString()
|
||||
if (SearchHelper.checkSearchQuery(currentString, mSearchParametersKDBX)) {
|
||||
searchFound = true
|
||||
break@EntryFieldsLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
return searchFound
|
||||
}
|
||||
|
||||
private fun entryKDBXKeyIsAllowedToSearch(key: String, searchParameters: SearchParameters): Boolean {
|
||||
return when (key) {
|
||||
EntryKDBX.STR_TITLE -> searchParameters.searchInTitles
|
||||
EntryKDBX.STR_USERNAME -> searchParameters.searchInUserNames
|
||||
EntryKDBX.STR_PASSWORD -> searchParameters.searchInPasswords
|
||||
EntryKDBX.STR_URL -> searchParameters.searchInUrls
|
||||
EntryKDBX.STR_NOTES -> searchParameters.searchInNotes
|
||||
else -> searchParameters.searchInOther
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchInGroupNames(entry: EntryKDBX): Boolean {
|
||||
if (mSearchParametersKDBX.searchInGroupNames) {
|
||||
val parent = entry.parent
|
||||
if (parent != null) {
|
||||
return parent.title
|
||||
.contains(mSearchParametersKDBX.searchQuery,
|
||||
mSearchParametersKDBX.ignoreCase)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun searchInUUID(entry: EntryKDBX): Boolean {
|
||||
if (mSearchParametersKDBX.searchInUUIDs) {
|
||||
return UuidUtil.toHexString(entry.id)
|
||||
.contains(mSearchParametersKDBX.searchQuery, true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
private data class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECURSION_DEPTH = 12
|
||||
private const val MAX_RECURSION_DEPTH = 10
|
||||
private const val MAX_INLINE_REF = 10
|
||||
private const val STR_REF_START = "{REF:"
|
||||
private const val STR_REF_END = "}"
|
||||
}
|
||||
|
||||
@@ -67,6 +67,26 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
||||
return true
|
||||
}
|
||||
|
||||
fun searchChildEntry(criteria: (entry: Entry) -> Boolean): Entry? {
|
||||
return searchChildEntry(this, criteria)
|
||||
}
|
||||
|
||||
private fun searchChildEntry(rootGroup: GroupVersionedInterface<Group, Entry>,
|
||||
criteria: (entry: Entry) -> Boolean): Entry? {
|
||||
for (childEntry in rootGroup.getChildEntries()) {
|
||||
if (criteria.invoke(childEntry)) {
|
||||
return childEntry
|
||||
}
|
||||
}
|
||||
for (group in rootGroup.getChildGroups()) {
|
||||
val searchChildEntry = searchChildEntry(group, criteria)
|
||||
if (searchChildEntry != null) {
|
||||
return searchChildEntry
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun searchChildGroup(criteria: (group: Group) -> Boolean): Group? {
|
||||
return searchChildGroup(this, criteria)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeDiacriticalMarks
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
|
||||
class SearchHelper {
|
||||
|
||||
@@ -76,11 +76,7 @@ class SearchHelper {
|
||||
private fun entryContainsString(database: Database,
|
||||
entry: Entry,
|
||||
searchParameters: SearchParameters): Boolean {
|
||||
val searchQuery = searchParameters.searchQuery
|
||||
// Entry don't contains string if the search string is empty
|
||||
if (searchQuery.isEmpty())
|
||||
return false
|
||||
|
||||
// To search in field references
|
||||
database.startManageEntry(entry)
|
||||
// Search all strings in the entry
|
||||
val searchFound = searchInEntry(entry, searchParameters)
|
||||
@@ -89,41 +85,6 @@ class SearchHelper {
|
||||
return searchFound
|
||||
}
|
||||
|
||||
private fun searchInEntry(entry: Entry,
|
||||
searchParameters: SearchParameters): Boolean {
|
||||
// Search all strings in the KDBX entry
|
||||
if (searchParameters.searchInTitles) {
|
||||
if (checkSearchQuery(entry.title, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUserNames) {
|
||||
if (checkSearchQuery(entry.username, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInPasswords) {
|
||||
if (checkSearchQuery(entry.password, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUrls) {
|
||||
if (checkSearchQuery(entry.url, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInNotes) {
|
||||
if (checkSearchQuery(entry.notes, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInOther) {
|
||||
entry.getExtraFields().forEach { field ->
|
||||
if (field.name != OTP_FIELD
|
||||
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
||||
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_SEARCH_ENTRY = 10
|
||||
|
||||
@@ -162,16 +123,66 @@ class SearchHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
|
||||
if (stringToCheck.isNotEmpty()
|
||||
&& stringToCheck
|
||||
.removeDiacriticalMarks()
|
||||
.contains(searchParameters.searchQuery
|
||||
.removeDiacriticalMarks(),
|
||||
searchParameters.ignoreCase)) {
|
||||
/**
|
||||
* Return true if the search query in search parameters is found in available parameters
|
||||
*/
|
||||
fun searchInEntry(entry: Entry,
|
||||
searchParameters: SearchParameters): Boolean {
|
||||
val searchQuery = searchParameters.searchQuery
|
||||
// Entry don't contains string if the search string is empty
|
||||
if (searchQuery.isEmpty())
|
||||
return false
|
||||
|
||||
// Search all strings in the KDBX entry
|
||||
if (searchParameters.searchInTitles) {
|
||||
if (checkSearchQuery(entry.title, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUserNames) {
|
||||
if (checkSearchQuery(entry.username, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInPasswords) {
|
||||
if (checkSearchQuery(entry.password, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUrls) {
|
||||
if (checkSearchQuery(entry.url, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInNotes) {
|
||||
if (checkSearchQuery(entry.notes, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUUIDs) {
|
||||
val hexString = UuidUtil.toHexString(entry.nodeId.id)
|
||||
if (hexString != null && hexString.contains(searchQuery, true))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInOther) {
|
||||
entry.getExtraFields().forEach { field ->
|
||||
if (field.name != OTP_FIELD
|
||||
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
||||
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
|
||||
/*
|
||||
// TODO Search settings
|
||||
var regularExpression = false
|
||||
var ignoreCase = true
|
||||
var removeAccents = true <- Too much time, to study
|
||||
var excludeExpired = false
|
||||
var searchOnlyInCurrentGroup = false
|
||||
*/
|
||||
return stringToCheck.isNotEmpty()
|
||||
&& stringToCheck.contains(
|
||||
searchParameters.searchQuery, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,57 +23,15 @@ package com.kunzisoft.keepass.database.search
|
||||
* Parameters for searching strings in the database.
|
||||
*/
|
||||
class SearchParameters {
|
||||
|
||||
var searchQuery: String = ""
|
||||
|
||||
var regularExpression = false
|
||||
var searchInTitles = true
|
||||
var searchInUserNames = true
|
||||
var searchInPasswords = false
|
||||
var searchInUrls = true
|
||||
var searchInGroupNames = false
|
||||
var searchInNotes = true
|
||||
var searchInOTP = false
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = true
|
||||
var ignoreCase = true
|
||||
var ignoreExpired = false
|
||||
var excludeExpired = false
|
||||
|
||||
constructor()
|
||||
|
||||
/**
|
||||
* Copy search parameters
|
||||
* @param source
|
||||
*/
|
||||
constructor(source: SearchParameters) {
|
||||
this.regularExpression = source.regularExpression
|
||||
this.searchInTitles = source.searchInTitles
|
||||
this.searchInUserNames = source.searchInUserNames
|
||||
this.searchInPasswords = source.searchInPasswords
|
||||
this.searchInUrls = source.searchInUrls
|
||||
this.searchInGroupNames = source.searchInGroupNames
|
||||
this.searchInNotes = source.searchInNotes
|
||||
this.searchInOTP = source.searchInOTP
|
||||
this.searchInOther = source.searchInOther
|
||||
this.searchInUUIDs = source.searchInUUIDs
|
||||
this.searchInTags = source.searchInTags
|
||||
this.ignoreCase = source.ignoreCase
|
||||
this.ignoreExpired = source.ignoreExpired
|
||||
this.excludeExpired = source.excludeExpired
|
||||
}
|
||||
|
||||
fun setupNone() {
|
||||
searchInTitles = false
|
||||
searchInUserNames = false
|
||||
searchInPasswords = false
|
||||
searchInUrls = false
|
||||
searchInGroupNames = false
|
||||
searchInNotes = false
|
||||
searchInOTP = false
|
||||
searchInOther = false
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
||||
value
|
||||
} else {
|
||||
TokenCalculator.HOTP_INITIAL_COUNTER
|
||||
throw IllegalArgumentException()
|
||||
throw NumberFormatException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_HOTP_COUNTER = 1
|
||||
const val MIN_HOTP_COUNTER = 0
|
||||
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
|
||||
|
||||
const val MIN_TOTP_PERIOD = 1
|
||||
|
||||
@@ -295,22 +295,30 @@ object OtpEntryFields {
|
||||
secretHexField != null -> otpElement.setHexSecret(secretHexField)
|
||||
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
|
||||
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
||||
lengthField != null -> otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
periodField != null -> otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
algorithmField != null -> otpElement.algorithm =
|
||||
else -> return false
|
||||
}
|
||||
otpElement.type = OtpType.TOTP
|
||||
if (lengthField != null) {
|
||||
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
}
|
||||
if (lengthField != null) {
|
||||
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
}
|
||||
if (periodField != null) {
|
||||
otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
}
|
||||
if (algorithmField != null) {
|
||||
otpElement.algorithm =
|
||||
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
|
||||
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
|
||||
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
|
||||
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
|
||||
else -> HashAlgorithm.SHA1
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
}
|
||||
|
||||
otpElement.type = OtpType.TOTP
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -321,10 +329,10 @@ object OtpEntryFields {
|
||||
return try {
|
||||
// KeeOtp string format
|
||||
val query = breakDownKeyValuePairs(plainText)
|
||||
otpElement.type = OtpType.TOTP
|
||||
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
|
||||
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
otpElement.type = OtpType.TOTP
|
||||
true
|
||||
} catch (exception: Exception) {
|
||||
false
|
||||
@@ -351,6 +359,7 @@ object OtpEntryFields {
|
||||
// malformed
|
||||
return false
|
||||
}
|
||||
otpElement.type = OtpType.TOTP
|
||||
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
matcher.group(2)?.let { secondMatcher ->
|
||||
try {
|
||||
@@ -365,7 +374,6 @@ object OtpEntryFields {
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
}
|
||||
otpElement.type = OtpType.TOTP
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -374,6 +382,7 @@ object OtpEntryFields {
|
||||
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
|
||||
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
|
||||
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
|
||||
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
|
||||
try {
|
||||
when {
|
||||
secretField != null -> otpElement.setUTF8Secret(secretField)
|
||||
@@ -382,16 +391,13 @@ object OtpEntryFields {
|
||||
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
||||
else -> return false
|
||||
}
|
||||
|
||||
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
|
||||
otpElement.type = OtpType.HOTP
|
||||
if (secretCounterField != null) {
|
||||
otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
}
|
||||
|
||||
otpElement.type = OtpType.HOTP
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
@@ -46,18 +45,16 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
return getString(R.string.advanced_unlock)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
mTempCipherDao = ArrayList()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return mActionTaskBinder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply {
|
||||
action = ACTION_REMOVE_KEYS
|
||||
}
|
||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0)
|
||||
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(if (biometricUnlockEnabled) {
|
||||
@@ -65,39 +62,29 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
} else {
|
||||
R.drawable.notification_ic_device_unlock_24dp
|
||||
})
|
||||
intent?.let {
|
||||
setContentTitle(getString(R.string.advanced_unlock))
|
||||
}
|
||||
setContentText(getString(R.string.advanced_unlock_tap_delete))
|
||||
setContentIntent(pendingDeleteIntent)
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
}
|
||||
|
||||
when (intent?.action) {
|
||||
ACTION_TIMEOUT -> {
|
||||
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
||||
// Not necessarily a foreground service
|
||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
|
||||
stopSelf()
|
||||
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
||||
}
|
||||
} else {
|
||||
startForeground(notificationId, notificationBuilder.build())
|
||||
}
|
||||
|
||||
return mActionTaskBinder
|
||||
}
|
||||
ACTION_REMOVE_KEYS -> {
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
stopSelf()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
mTempCipherDao = ArrayList()
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -105,22 +92,32 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
|
||||
removeKeyAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
||||
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
|
||||
|
||||
private const val ACTION_TIMEOUT = "ACTION_TIMEOUT"
|
||||
private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS"
|
||||
|
||||
fun startServiceForTimeout(context: Context) {
|
||||
if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) {
|
||||
context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply {
|
||||
action = ACTION_TIMEOUT
|
||||
})
|
||||
}
|
||||
// Only one service connection
|
||||
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
|
||||
context.bindService(Intent(context,
|
||||
AdvancedUnlockNotificationService::class.java),
|
||||
serviceConnection,
|
||||
flags)
|
||||
}
|
||||
|
||||
fun stopService(context: Context) {
|
||||
context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java))
|
||||
fun unbindService(context: Context, serviceConnection: ServiceConnection) {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
override val notificationId = 485
|
||||
private var mEntryInfo: EntryInfo? = null
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var mNotificationTimeoutMilliSecs: Long = 0
|
||||
|
||||
override fun retrieveChannelId(): String {
|
||||
return CHANNEL_CLIPBOARD_ID
|
||||
@@ -68,9 +67,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
// Get entry info from intent
|
||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||
|
||||
//Get settings
|
||||
mNotificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||
|
||||
when {
|
||||
intent == null -> Log.w(TAG, "null intent")
|
||||
ACTION_NEW_NOTIFICATION == intent.action -> {
|
||||
@@ -169,8 +165,10 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
builder.setDeleteIntent(cleanPendingIntent)
|
||||
|
||||
if (mNotificationTimeoutMilliSecs != NEVER) {
|
||||
defineTimerJob(builder, mNotificationTimeoutMilliSecs, {
|
||||
//Get settings
|
||||
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||
if (notificationTimeoutMilliSecs != NEVER) {
|
||||
defineTimerJob(builder, notificationTimeoutMilliSecs, {
|
||||
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||
// New auto generated value
|
||||
if (generatedValue != newGeneratedValue) {
|
||||
|
||||
@@ -200,10 +200,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
if (intentAction == null && !mDatabase.loaded) {
|
||||
stopSelf()
|
||||
}
|
||||
if (intentAction == ACTION_DATABASE_CLOSE) {
|
||||
// Send lock action
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
||||
@@ -378,10 +374,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
ReadOnlyHelper.putReadOnlyInIntent(this, mDatabase.isReadOnly)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
|
||||
action = ACTION_DATABASE_CLOSE
|
||||
}
|
||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||
4576, Intent(LOCK_ACTION), 0)
|
||||
// Add actions in notifications
|
||||
notificationBuilder.apply {
|
||||
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
|
||||
@@ -877,7 +871,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
||||
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
||||
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
|
||||
|
||||
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
||||
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
||||
|
||||
@@ -57,8 +57,8 @@ abstract class LockNotificationService : NotificationService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
mLockReceiver = null
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ abstract class NotificationService : Service() {
|
||||
actionEnd: () -> Unit) {
|
||||
mTimerJob?.cancel()
|
||||
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
if (timeoutMilliseconds > 0) {
|
||||
val timeoutInSeconds = timeoutMilliseconds / 1000L
|
||||
for (currentTime in timeoutInSeconds downTo 0) {
|
||||
actionAfterASecond?.invoke()
|
||||
@@ -92,6 +93,10 @@ abstract class NotificationService : Service() {
|
||||
actionEnd()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If timeout is 0, run action once
|
||||
actionEnd()
|
||||
}
|
||||
notificationManager?.cancel(notificationId)
|
||||
mTimerJob = null
|
||||
cancel()
|
||||
|
||||
@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
@@ -374,7 +373,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
})
|
||||
}
|
||||
AdvancedUnlockNotificationService.stopService(activity.applicationContext)
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||
|
||||
@@ -12,10 +12,5 @@ object StringUtil {
|
||||
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
||||
}
|
||||
|
||||
fun String.removeDiacriticalMarks(): String {
|
||||
return "\\p{InCombiningDiacriticalMarks}+".toRegex()
|
||||
.replace(Normalizer.normalize(this, Normalizer.Form.NFD), "")
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
}
|
||||
@@ -19,19 +19,23 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
||||
|
||||
public class UuidUtil {
|
||||
|
||||
public static String toHexString(UUID uuid) {
|
||||
public static @Nullable String toHexString(@Nullable UUID uuid) {
|
||||
if (uuid == null) { return null; }
|
||||
|
||||
try {
|
||||
byte[] buf = uuidTo16Bytes(uuid);
|
||||
|
||||
int len = buf.length;
|
||||
if (len == 0) { return ""; }
|
||||
if (len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -46,6 +50,43 @@ public class UuidUtil {
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable UUID fromHexString(@Nullable String hexString) {
|
||||
if (hexString == null)
|
||||
return null;
|
||||
|
||||
if (hexString.length() != 32)
|
||||
return null;
|
||||
|
||||
char[] charArray = hexString.toLowerCase().toCharArray();
|
||||
char[] leastSignificantChars = new char[16];
|
||||
char[] mostSignificantChars = new char[16];
|
||||
|
||||
for (int i = 31; i >= 0; i = i-2) {
|
||||
if (i >= 16) {
|
||||
mostSignificantChars[32-i] = charArray[i];
|
||||
mostSignificantChars[31-i] = charArray[i-1];
|
||||
} else {
|
||||
leastSignificantChars[16-i] = charArray[i];
|
||||
leastSignificantChars[15-i] = charArray[i-1];
|
||||
}
|
||||
}
|
||||
StringBuilder standardUUIDString = new StringBuilder();
|
||||
standardUUIDString.append(leastSignificantChars);
|
||||
standardUUIDString.append(mostSignificantChars);
|
||||
standardUUIDString.insert(8, '-');
|
||||
standardUUIDString.insert(13, '-');
|
||||
standardUUIDString.insert(18, '-');
|
||||
standardUUIDString.insert(23, '-');
|
||||
try {
|
||||
return UUID.fromString(standardUUIDString.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use short to represent unsigned byte
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
<string name="never">Nie</string>
|
||||
<string name="no_results">Keine Suchergebnisse</string>
|
||||
<string name="no_url_handler">Bitte einen Webbrowser installieren, um diese URL zu öffnen.</string>
|
||||
<string name="omit_backup_search_title">Recycle bin und Backup nicht durchsuchen</string>
|
||||
<string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Recycle bin“ werden bei der Suche nicht berücksichtigt</string>
|
||||
<string name="omit_backup_search_title">Papierkorb und Backup nicht durchsuchen</string>
|
||||
<string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt</string>
|
||||
<string name="auto_focus_search_title">Schnellsuche</string>
|
||||
<string name="auto_focus_search_summary">Beim Öffnen einer Datenbank eine Suche veranlassen</string>
|
||||
<string name="progress_create">Neue Datenbank anlegen …</string>
|
||||
@@ -167,7 +167,7 @@
|
||||
<string name="file_name">Dateiname</string>
|
||||
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</string>
|
||||
<string name="biometric_unlock_enable_summary">Ermöglicht Ihre Biometrie zu scannen, um die Datenbank zu öffnen.</string>
|
||||
<string name="advanced_unlock">Erweiterte Entsperrung</string>
|
||||
<string name="advanced_unlock">Moderne Entsperrung</string>
|
||||
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
|
||||
<string name="lock">Sperren</string>
|
||||
<string name="list_password_generator_options_summary">Erlaubte Zeichen für Passwortgenerator festlegen</string>
|
||||
@@ -353,12 +353,12 @@
|
||||
<string name="content_description_update_from_list">Aktualisieren</string>
|
||||
<string name="content_description_keyboard_close_fields">Felder schließen</string>
|
||||
<string name="error_create_database_file">Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen.</string>
|
||||
<string name="menu_advanced_unlock_settings">Erweitertes Entsperren</string>
|
||||
<string name="menu_advanced_unlock_settings">Modernes Entsperren</string>
|
||||
<string name="biometric">Biometrisch</string>
|
||||
<string name="enable">Aktivieren</string>
|
||||
<string name="disable">Deaktivieren</string>
|
||||
<string name="biometric_auto_open_prompt_title">Abfrage automatisch öffnen</string>
|
||||
<string name="biometric_auto_open_prompt_summary">Automatisch nach der erweiterten Entsperrung fragen, wenn die Datenbank dafür eingerichtet ist</string>
|
||||
<string name="biometric_auto_open_prompt_summary">Automatisch moderne Entsperrung abfragen, wenn die Datenbank dafür eingerichtet ist</string>
|
||||
<string name="master_key">Hauptschlüssel</string>
|
||||
<string name="security">Sicherheit</string>
|
||||
<string name="entry_history">Verlauf</string>
|
||||
@@ -385,7 +385,7 @@
|
||||
<string name="contains_duplicate_uuid_procedure">Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\?</string>
|
||||
<string name="database_opened">Datenbank geöffnet</string>
|
||||
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</string>
|
||||
<string name="advanced_unlock_explanation_summary">Erweitertes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
|
||||
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
|
||||
<string name="database_data_compression_title">Datenkompression</string>
|
||||
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
|
||||
<string name="max_history_items_title">Maximale Anzahl</string>
|
||||
@@ -506,22 +506,22 @@
|
||||
<string name="keyboard_save_search_info_title">Gemeinsam genutzte Informationen speichern</string>
|
||||
<string name="warning_empty_recycle_bin">Sollen alle ausgewählten Knoten wirklich aus dem Papierkorb gelöscht werden\?</string>
|
||||
<string name="error_field_name_already_exists">Der Feldname existiert bereits.</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Warnung: Sie müssen sich immer noch an Ihr Masterpasswort erinnern, wenn Sie die erweiterte Entsperrerkennung verwenden.</string>
|
||||
<string name="open_advanced_unlock_prompt_store_credential">Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen</string>
|
||||
<string name="open_advanced_unlock_prompt_unlock_database">Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Warnung: Sie müssen sich immer noch an Ihr Masterpasswort erinnern, wenn Sie die moderne Entsperrerkennung verwenden.</string>
|
||||
<string name="open_advanced_unlock_prompt_store_credential">Zum Speichern der Anmeldeinformationen Dialog zum modernen Entsperren öffnen</string>
|
||||
<string name="open_advanced_unlock_prompt_unlock_database">Dialog zum modernen Entsperren der Datenbank öffnen</string>
|
||||
<string name="menu_keystore_remove_key">Schlüssel für modernes Entsperren löschen</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Erweiterte Entsperrerkennung</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Moderne Entsperrerkennung</string>
|
||||
<string name="education_advanced_unlock_summary">Verknüpfen Sie Ihr Passwort mit Ihren gescannten Biometriedaten oder Daten zur Geräteanmeldung, um schnell Ihre Datenbank zu entsperren.</string>
|
||||
<string name="education_advanced_unlock_title">Erweiterte Entsperrung der Datenbank</string>
|
||||
<string name="advanced_unlock_timeout">Verfallzeit der erweiterten Entsperrung</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">Dauer der erweiterten Entsperrung bevor sein Inhalt gelöscht wird</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">Verfall der erweiterten Entsperrung</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">Keinen verschlüsselten Inhalt speichern, um erweiterte Entsperrung zu benutzen</string>
|
||||
<string name="temp_advanced_unlock_enable_title">Temporäre erweiterte Entsperrung</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">Laufzeit der modernen Entsperrung bevor ihr Inhalt gelöscht wird</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">Verfall der modernen Entsperrung</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">Keinen verschlüsselten Inhalt speichern, um moderne Entsperrung zu benutzen</string>
|
||||
<string name="temp_advanced_unlock_enable_title">Temporär moderne Entsperrung</string>
|
||||
<string name="device_credential_unlock_enable_summary">Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden</string>
|
||||
<string name="advanced_unlock_tap_delete">Drücken, um erweiterte Entsperrschlüssel zu löschen</string>
|
||||
<string name="advanced_unlock_tap_delete">Drücken, um Schlüssel für modernes Entsperren zu löschen</string>
|
||||
<string name="content">Inhalt</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Öffne Datenbank mit erweiterter Entsperrerkennung</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Datenbank mit moderner Entsperrerkennung öffnen</string>
|
||||
<string name="enter">Eingabetaste</string>
|
||||
<string name="backspace">Rücktaste</string>
|
||||
<string name="select_entry">Wähle Eintrag</string>
|
||||
@@ -531,11 +531,11 @@
|
||||
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
|
||||
<string name="device_credential">Geräteanmeldedaten</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Geben Sie das Passwort ein und klicken Sie dann auf diesen Knopf.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen.</string>
|
||||
<string name="advanced_unlock_scanning_error">Erweitertes Entsperren Fehler: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Konnte den Abdruck des erweiterten Entsperrens nicht erkennen</string>
|
||||
<string name="advanced_unlock_invalid_key">Kann den Schlüssel zum erweiterten Entsperren nicht lesen. Bitte löschen sie ihn und wiederholen sie Prozedur zum Erkennen des Entsperrens.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
|
||||
<string name="advanced_unlock_scanning_error">Fehler beim modernen Entsperren: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Abdruck zum modernen Entsperren nicht erkannt</string>
|
||||
<string name="advanced_unlock_invalid_key">Schlüssel zum modernen Entsperren nicht lesbar. Bitte löschen Sie ihn und wiederholen Sie die Prozedur zur Entsperrerkennung.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Datenbankanmeldedaten mit Daten aus moderner Entsperrung extrahieren</string>
|
||||
<string name="error_rebuild_list">Die Liste kann nicht ordnungsgemäß neu erstellt werden.</string>
|
||||
<string name="error_database_uri_null">Datenbank-URI kann nicht abgerufen werden.</string>
|
||||
<string name="menu_reload_database">Datenbank neu laden</string>
|
||||
@@ -549,4 +549,13 @@
|
||||
<string name="error_start_database_action">Beim Ausführen einer Aktion in der Datenbank ist ein Fehler aufgetreten.</string>
|
||||
<string name="error_otp_type">Der vorhandene Einmalpassworttyp wird von diesem Formular nicht erkannt, seine Validierung erzeugt das Token möglicherweise nicht mehr korrekt.</string>
|
||||
<string name="content_description_otp_information">Informationen zu Einmalpasswörtern</string>
|
||||
<string name="warning_database_revoked">Auf die Datei kann nicht zugegriffen werden. Schließen Sie die Datenbank und öffnen Sie die Datei erneut.</string>
|
||||
<string name="error_export_app_properties">Fehler beim Exportieren der App-Eigenschaften</string>
|
||||
<string name="success_export_app_properties">App-Eigenschaften exportiert</string>
|
||||
<string name="error_import_app_properties">Fehler beim Importieren der App-Eigenschaften</string>
|
||||
<string name="success_import_app_properties">App-Eigenschaften importiert</string>
|
||||
<string name="export_app_properties_summary">Erstellen einer Datei zum Exportieren von App-Eigenschaften</string>
|
||||
<string name="export_app_properties_title">App-Eigenschaften exportieren</string>
|
||||
<string name="import_app_properties_summary">Wählen Sie eine Datei zum Importieren von App-Eigenschaften</string>
|
||||
<string name="error_move_group_here">Sie können hier keine Gruppe verschieben.</string>
|
||||
</resources>
|
||||
@@ -560,4 +560,6 @@
|
||||
<string name="import_app_properties_summary">Επιλέξτε ένα αρχείο για εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||
<string name="import_app_properties_title">Εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||
<string name="error_start_database_action">Παρουσιάστηκε σφάλμα κατά την εκτέλεση μιας ενέργειας στη βάση δεδομένων.</string>
|
||||
<string name="error_move_group_here">Δεν μπορείτε να μετακινήσετε μια ομάδα εδώ.</string>
|
||||
<string name="error_word_reserved">Αυτή η λέξη είναι δεσμευμένη και δεν μπορεί να χρησιμοποιηθεί.</string>
|
||||
</resources>
|
||||
@@ -568,4 +568,6 @@
|
||||
<string name="import_app_properties_summary">Sélectionner un fichier pour importer les propriétés de l\'application</string>
|
||||
<string name="import_app_properties_title">Importation des propriétés de l\'appli</string>
|
||||
<string name="error_start_database_action">Une erreur s\'est produite lors de l\'exécution d\'une action sur la base de données.</string>
|
||||
<string name="error_move_group_here">Vous ne pouvez pas déplacer un groupe ici.</string>
|
||||
<string name="error_word_reserved">Ce mot est réservé et ne peut pas être utilisé.</string>
|
||||
</resources>
|
||||
@@ -68,7 +68,7 @@
|
||||
<string name="error_invalid_path">Assicurati che il percorso sia corretto.</string>
|
||||
<string name="error_no_name">Inserisci un nome.</string>
|
||||
<string name="error_out_of_memory">Memoria insufficiente per caricare l\'intero database.</string>
|
||||
<string name="error_pass_gen_type">Deve essere selezionato almeno un tipo di generazione password.</string>
|
||||
<string name="error_pass_gen_type">Selezionare almeno un tipo di generazione della password.</string>
|
||||
<string name="error_pass_match">Le password non corrispondono.</string>
|
||||
<string name="error_rounds_too_large">«Livello» troppo alto. Impostato a 2147483648.</string>
|
||||
<string name="error_string_key">Ogni stringa deve avere un nome.</string>
|
||||
@@ -563,4 +563,5 @@
|
||||
<string name="export_app_properties_title">Esporta le proprietà dell\'app</string>
|
||||
<string name="import_app_properties_summary">Seleziona un file da cui importare le proprietà dell\'app</string>
|
||||
<string name="import_app_properties_title">Importa le proprietà dell\'app</string>
|
||||
<string name="error_word_reserved">Questa parola è riservata e non può essere usata.</string>
|
||||
</resources>
|
||||
@@ -562,4 +562,6 @@
|
||||
<string name="import_app_properties_summary">Selecteer een bestand om app-eigenschappen te importeren</string>
|
||||
<string name="import_app_properties_title">App-eigenschappen importeren</string>
|
||||
<string name="error_start_database_action">Er is een fout opgetreden bij het uitvoeren van een actie op de database.</string>
|
||||
<string name="error_move_group_here">Je kunt hier geen groep verplaatsen.</string>
|
||||
<string name="error_word_reserved">Dit woord is gereserveerd en kan niet worden gebruikt.</string>
|
||||
</resources>
|
||||
@@ -550,14 +550,16 @@
|
||||
<string name="error_duplicate_file">Данные файла уже существует.</string>
|
||||
<string name="error_remove_file">Ошибка при удалении данных файла.</string>
|
||||
<string name="properties">Свойства</string>
|
||||
<string name="error_export_app_properties">Ошибка при экспорте свойств приложения</string>
|
||||
<string name="success_export_app_properties">Свойства приложения экспортированы</string>
|
||||
<string name="error_import_app_properties">Ошибка при импорте свойств приложения</string>
|
||||
<string name="success_import_app_properties">Свойства приложения импортированы</string>
|
||||
<string name="description_app_properties">Свойства KeePassDX для управления настройками приложения</string>
|
||||
<string name="export_app_properties_summary">Создайте файл для экспорта свойств приложения</string>
|
||||
<string name="export_app_properties_title">Экспорт свойств приложения</string>
|
||||
<string name="import_app_properties_summary">Выберите файл для импорта свойств приложения</string>
|
||||
<string name="import_app_properties_title">Импортировать свойства приложения</string>
|
||||
<string name="error_export_app_properties">Ошибка при экспорте настроек приложения</string>
|
||||
<string name="success_export_app_properties">Настройки приложения экспортированы</string>
|
||||
<string name="error_import_app_properties">Ошибка при импорте настроек приложения</string>
|
||||
<string name="success_import_app_properties">Настройки приложения импортированы</string>
|
||||
<string name="description_app_properties">Управление настройками приложения KeePassDX</string>
|
||||
<string name="export_app_properties_summary">Создать файл настроек приложения</string>
|
||||
<string name="export_app_properties_title">Экспорт настроек</string>
|
||||
<string name="import_app_properties_summary">Импортировать настройки приложения из файла</string>
|
||||
<string name="import_app_properties_title">Импорт настроек</string>
|
||||
<string name="error_start_database_action">Произошла ошибка при выполнении действия с базой.</string>
|
||||
<string name="error_move_group_here">Сюда группу переместить невозможно.</string>
|
||||
<string name="error_word_reserved">Это слово зарезервировано и не может быть использовано.</string>
|
||||
</resources>
|
||||
@@ -555,4 +555,6 @@
|
||||
<string name="import_app_properties_summary">Uygulama özelliklerini içe aktarmak için bir dosya seçin</string>
|
||||
<string name="import_app_properties_title">Uygulama özelliklerini içe aktar</string>
|
||||
<string name="error_start_database_action">Veri tabanında bir eylem gerçekleştirilirken bir hata oluştu.</string>
|
||||
<string name="error_move_group_here">Bir grubu buraya taşıyamazsınız.</string>
|
||||
<string name="error_word_reserved">Bu sözcük ayrılmıştır ve kullanılamaz.</string>
|
||||
</resources>
|
||||
@@ -560,4 +560,6 @@
|
||||
<string name="import_app_properties_summary">Виберіть файл для імпорту властивостей застосунку</string>
|
||||
<string name="import_app_properties_title">Імпорт властивостей застосунку</string>
|
||||
<string name="error_start_database_action">Під час виконання дії з базою даних сталася помилка.</string>
|
||||
<string name="error_move_group_here">Ви не можете перемістити групу сюди.</string>
|
||||
<string name="error_word_reserved">Це слово зарезервоване, його не можна використовувати.</string>
|
||||
</resources>
|
||||
@@ -560,4 +560,6 @@
|
||||
<string name="import_app_properties_summary">选择一个文件来导入应用属性</string>
|
||||
<string name="import_app_properties_title">导入应用属性</string>
|
||||
<string name="error_start_database_action">对数据库执行操作时发生了一个错误。</string>
|
||||
<string name="error_move_group_here">你不能把一个组移动到此处。</string>
|
||||
<string name="error_word_reserved">这个单词是保留的,不能使用。</string>
|
||||
</resources>
|
||||
@@ -49,9 +49,9 @@ platform :android do
|
||||
lane :deploy_beta_free do
|
||||
upload_to_play_store(
|
||||
track: "beta",
|
||||
skip_upload_metadata: "false",
|
||||
skip_upload_images: "false",
|
||||
skip_upload_screenshots: "false",
|
||||
skip_upload_metadata: "true",
|
||||
skip_upload_images: "true",
|
||||
skip_upload_screenshots: "true",
|
||||
apk: "./app/build/outputs/apk/free/release/app-free-release.apk",
|
||||
validate_only: "false",
|
||||
)
|
||||
@@ -62,9 +62,9 @@ platform :android do
|
||||
sh("cp", "-a", "./pro/.", "./")
|
||||
upload_to_play_store(
|
||||
track: "beta",
|
||||
skip_upload_metadata: "false",
|
||||
skip_upload_images: "false",
|
||||
skip_upload_screenshots: "false",
|
||||
skip_upload_metadata: "true",
|
||||
skip_upload_images: "true",
|
||||
skip_upload_screenshots: "true",
|
||||
apk: "./app/build/outputs/apk/pro/release/app-pro-release.apk",
|
||||
validate_only: "false",
|
||||
)
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/73.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/73.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
* Fix search slowdown #964
|
||||
* Fix closing notification after lock request #965
|
||||
* Better temp advanced unlocking code implementation #965
|
||||
* Fix OTP token generation #967
|
||||
3
fastlane/metadata/android/en-US/changelogs/74.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/74.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
* Fix search with non-latin chars #971
|
||||
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||
* Fix timeout with 0s #974
|
||||
4
fastlane/metadata/android/fr-FR/changelogs/73.txt
Normal file
4
fastlane/metadata/android/fr-FR/changelogs/73.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
* Correction du ralentissement de la recherche #964
|
||||
* Correction de la fermeture de notification après une requête de verrouillage #965
|
||||
* Meilleure implémentation du déverrouillage avancé temporaire #965
|
||||
* Correction de la génération des jetons de mots de passe uniques #967
|
||||
3
fastlane/metadata/android/fr-FR/changelogs/74.txt
Normal file
3
fastlane/metadata/android/fr-FR/changelogs/74.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
* Correction de la recherche avec des caractères non latin #971
|
||||
* Correction du mode action avec la recherche #972 (retour arrière des accents ignorés #945)
|
||||
* Correction de l'expiration de temps à 0s #974
|
||||
Reference in New Issue
Block a user