mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: First validation pass
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
namespace 'com.kunzisoft.keepass.database'
|
||||
@@ -41,7 +42,7 @@ dependencies {
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
|
||||
implementation project(path: ':crypto')
|
||||
api project(path: ':crypto')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||
@@ -885,29 +886,15 @@ open class Database {
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(
|
||||
searchInfoString: String,
|
||||
searchInfoByDomain: Boolean,
|
||||
searchInfo: SearchInfo,
|
||||
max: Int = Integer.MAX_VALUE
|
||||
): Group? {
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
searchQuery = searchInfoString
|
||||
allowEmptyQuery = false
|
||||
searchInTitles = true
|
||||
searchInUsernames = false
|
||||
searchInPasswords = false
|
||||
searchInUrls = true
|
||||
searchByDomain = searchInfoByDomain
|
||||
searchInNotes = true
|
||||
searchInOTP = false
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
searchInCurrentGroup = false
|
||||
searchInSearchableGroup = true
|
||||
searchInRecycleBin = false
|
||||
searchInTemplates = false
|
||||
}, null, max)
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(
|
||||
database = this,
|
||||
searchParameters = searchInfo.buildSearchParameters(),
|
||||
fromGroup = null,
|
||||
max = max
|
||||
)
|
||||
}
|
||||
|
||||
val tagPool: Tags
|
||||
|
||||
@@ -33,12 +33,16 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.AppOriginEntryField
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Passkey
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorString
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import java.util.UUID
|
||||
|
||||
class Entry : Node, EntryVersionedInterface<Group> {
|
||||
@@ -354,6 +358,24 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getPasskey(): Passkey? {
|
||||
entryKDBX?.let {
|
||||
return PasskeyEntryFields.parseFields { key ->
|
||||
it.getFieldValue(key)?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getAppOrigin(): AppOrigin? {
|
||||
entryKDBX?.let {
|
||||
return AppOriginEntryField.parseFields { key ->
|
||||
it.getFieldValue(key)?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||
entryKDBX?.startToManageFieldReferences(database)
|
||||
}
|
||||
@@ -470,9 +492,13 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryInfo.customFields = getExtraFields().toMutableList()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
// Add Passkey
|
||||
entryInfo.passkey = getPasskey()
|
||||
entryInfo.appOrigin = getAppOrigin()
|
||||
if (!raw) {
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
entryInfo.customFields = PasskeyEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
}
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
|
||||
|
||||
@@ -49,6 +49,10 @@ class Tags: Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(tag: String): Boolean {
|
||||
return mTags.contains(tag)
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return mTags.isEmpty()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ package com.kunzisoft.keepass.database.element.entry
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||
@@ -79,7 +80,7 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
||||
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
||||
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
||||
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
|
||||
'I' -> entryFound?.nodeId?.id?.asHexString()
|
||||
else -> null
|
||||
}
|
||||
refsCache[fullReference] = data
|
||||
@@ -127,7 +128,7 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||
'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel)
|
||||
'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel)
|
||||
'I' -> {
|
||||
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
|
||||
searchQuery.asUUID()?.let { uuid ->
|
||||
mDatabase.getEntryById(NodeIdUUID(uuid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.database.element.node
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
class NodeIdUUID : NodeId<UUID> {
|
||||
|
||||
@@ -62,7 +62,7 @@ class NodeIdUUID : NodeId<UUID> {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return UuidUtil.toHexString(id) ?: id.toString()
|
||||
return id.asHexString() ?: id.toString()
|
||||
}
|
||||
|
||||
override fun toVisualString(): String {
|
||||
|
||||
@@ -24,7 +24,8 @@ import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||
|
||||
class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database) {
|
||||
|
||||
@@ -33,7 +34,7 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
}
|
||||
|
||||
override fun getTemplate(entryKDBX: EntryKDBX): Template? {
|
||||
UuidUtil.fromHexString(entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID))?.let { templateUUID ->
|
||||
entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID).asUUID()?.let { templateUUID ->
|
||||
return getTemplateByCache(templateUUID)
|
||||
}
|
||||
return null
|
||||
@@ -48,7 +49,7 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
}
|
||||
|
||||
private fun getTemplateUUIDField(template: Template): Field? {
|
||||
UuidUtil.toHexString(template.uuid)?.let { uuidString ->
|
||||
template.uuid.asHexString()?.let { uuidString ->
|
||||
return Field(TEMPLATE_ENTRY_UUID,
|
||||
ProtectedString(false, uuidString))
|
||||
}
|
||||
|
||||
@@ -24,8 +24,11 @@ import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.isOtpExclusion
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||
|
||||
class SearchHelper {
|
||||
@@ -163,18 +166,31 @@ class SearchHelper {
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUUIDs) {
|
||||
val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: ""
|
||||
val hexString = entry.nodeId.id.asHexString() ?: ""
|
||||
if (checkSearchQuery(hexString, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInOTP) {
|
||||
if(entry.getExtraFields().any { field ->
|
||||
field.name == OTP_FIELD
|
||||
&& checkSearchQuery(field.protectedValue.stringValue, searchParameters)
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInRelyingParty) {
|
||||
if(entry.getExtraFields().any { field ->
|
||||
field.name == FIELD_RELYING_PARTY
|
||||
&& checkSearchQuery(field.protectedValue.stringValue, 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
|
||||
}
|
||||
}
|
||||
if(entry.getExtraFields().any { field ->
|
||||
field.isOtpExclusion()
|
||||
&& field.isPasskeyExclusion()
|
||||
&& checkSearchQuery(field.protectedValue.toString(), searchParameters)
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInTags) {
|
||||
if (checkSearchQuery(entry.tags.toString(), searchParameters))
|
||||
|
||||
@@ -38,6 +38,7 @@ class SearchParameters() : Parcelable{
|
||||
var searchInExpired = false
|
||||
var searchInNotes = true
|
||||
var searchInOTP = false
|
||||
var searchInRelyingParty = false
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = false
|
||||
@@ -60,6 +61,7 @@ class SearchParameters() : Parcelable{
|
||||
searchInExpired = parcel.readByte() != 0.toByte()
|
||||
searchInNotes = parcel.readByte() != 0.toByte()
|
||||
searchInOTP = parcel.readByte() != 0.toByte()
|
||||
searchInRelyingParty = parcel.readByte() != 0.toByte()
|
||||
searchInOther = parcel.readByte() != 0.toByte()
|
||||
searchInUUIDs = parcel.readByte() != 0.toByte()
|
||||
searchInTags = parcel.readByte() != 0.toByte()
|
||||
@@ -81,6 +83,7 @@ class SearchParameters() : Parcelable{
|
||||
parcel.writeByte(if (searchInExpired) 1 else 0)
|
||||
parcel.writeByte(if (searchInNotes) 1 else 0)
|
||||
parcel.writeByte(if (searchInOTP) 1 else 0)
|
||||
parcel.writeByte(if (searchInRelyingParty) 1 else 0)
|
||||
parcel.writeByte(if (searchInOther) 1 else 0)
|
||||
parcel.writeByte(if (searchInUUIDs) 1 else 0)
|
||||
parcel.writeByte(if (searchInTags) 1 else 0)
|
||||
|
||||
143
database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt
Normal file
143
database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
|
||||
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AppOrigin(
|
||||
val verified: Boolean,
|
||||
val androidOrigins: MutableList<AndroidOrigin> = mutableListOf(),
|
||||
val webOrigins: MutableList<WebOrigin> = mutableListOf(),
|
||||
) : Parcelable {
|
||||
|
||||
fun addAndroidOrigin(androidOrigin: AndroidOrigin) {
|
||||
androidOrigins.add(androidOrigin)
|
||||
}
|
||||
|
||||
fun addWebOrigin(webOrigin: WebOrigin) {
|
||||
this.webOrigins.add(webOrigin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the app origin by comparing it to the list of android origins,
|
||||
* return the first verified origin or throw an exception if none is found
|
||||
*/
|
||||
fun checkAppOrigin(compare: AppOrigin): String {
|
||||
return androidOrigins.firstOrNull { androidOrigin ->
|
||||
compare.androidOrigins.any {
|
||||
it.packageName == androidOrigin.packageName
|
||||
&& it.fingerprint == androidOrigin.fingerprint
|
||||
}
|
||||
}?.let {
|
||||
AndroidOrigin(
|
||||
packageName = it.packageName,
|
||||
fingerprint = it.fingerprint
|
||||
).toAndroidOrigin()
|
||||
} ?: throw SecurityException("Wrong signature for ${toName()}")
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
androidOrigins.clear()
|
||||
webOrigins.clear()
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return androidOrigins.isEmpty() && webOrigins.isEmpty()
|
||||
}
|
||||
|
||||
fun toName(): String? {
|
||||
return if (androidOrigins.isNotEmpty()) {
|
||||
androidOrigins.first().packageName
|
||||
} else if (webOrigins.isNotEmpty()){
|
||||
webOrigins.first().origin
|
||||
} else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = AppOrigin::class.java.simpleName
|
||||
|
||||
fun fromOrigin(origin: String, androidOrigin: AndroidOrigin, verified: Boolean): AppOrigin {
|
||||
val appOrigin = AppOrigin(verified)
|
||||
if (origin.startsWith(RELYING_PARTY_DEFAULT_PROTOCOL)) {
|
||||
appOrigin.apply {
|
||||
addWebOrigin(WebOrigin(origin))
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Unknown verified origin $origin")
|
||||
appOrigin.apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
}
|
||||
}
|
||||
return appOrigin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AndroidOrigin(
|
||||
val packageName: String,
|
||||
val fingerprint: String?
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Creates an Android App Origin string of the form "android:apk-key-hash:<base64_urlsafe_hash>"
|
||||
* from a colon-separated hex fingerprint string.
|
||||
*
|
||||
* The input fingerprint is assumed to be the SHA-256 hash of the app's signing certificate.
|
||||
*
|
||||
* @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:...").
|
||||
* @return The Android App Origin string.
|
||||
* @throws IllegalArgumentException if the hex string (after removing colons) has an odd length
|
||||
* or contains non-hex characters.
|
||||
*/
|
||||
fun toAndroidOrigin(): String {
|
||||
if (fingerprint == null) {
|
||||
throw IllegalArgumentException("Fingerprint $fingerprint cannot be null")
|
||||
}
|
||||
return "android:apk-key-hash:${fingerprintToUrlSafeBase64(fingerprint)}"
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class WebOrigin(
|
||||
val origin: String
|
||||
) : Parcelable {
|
||||
|
||||
fun toWebOrigin(): String {
|
||||
return origin
|
||||
}
|
||||
|
||||
fun defaultAssetLinks(): String {
|
||||
return "${origin}/.well-known/assetlinks.json"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
|
||||
fun fromRelyingParty(relyingParty: String): WebOrigin = WebOrigin(
|
||||
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryInfo.Companion.suffixFieldNamePosition
|
||||
|
||||
object AppOriginEntryField {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
|
||||
const val APPLICATION_SIGNATURE_FIELD_NAME = "AndroidApp Signature"
|
||||
|
||||
/**
|
||||
* Parse fields of an entry to retrieve a an AppOrigin
|
||||
*/
|
||||
fun parseFields(getField: (id: String) -> String?): AppOrigin {
|
||||
val appOrigin = AppOrigin(verified = true)
|
||||
// Get Application identifiers
|
||||
generateSequence(0) { it + 1 }
|
||||
.map { position ->
|
||||
val appId = getField(APPLICATION_ID_FIELD_NAME + suffixFieldNamePosition(position))
|
||||
val appSignature = getField(APPLICATION_SIGNATURE_FIELD_NAME + suffixFieldNamePosition(position))
|
||||
// Pair them up, if appId is null, we stop
|
||||
if (appId != null) {
|
||||
appId to (appSignature ?: "")
|
||||
} else {
|
||||
// Stop
|
||||
null
|
||||
}
|
||||
}.takeWhile { it != null }
|
||||
.forEach { pair ->
|
||||
appOrigin.addAndroidOrigin(
|
||||
AndroidOrigin(pair!!.first, pair.second)
|
||||
)
|
||||
}
|
||||
// Get Domains
|
||||
var domainFieldPosition = 0
|
||||
while (true) {
|
||||
val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition)
|
||||
val domainValue = getField(domainKey)
|
||||
if (domainValue != null) {
|
||||
appOrigin.addWebOrigin(WebOrigin(origin = domainValue))
|
||||
domainFieldPosition++
|
||||
} else {
|
||||
break // No more domain found
|
||||
}
|
||||
}
|
||||
|
||||
return appOrigin
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to detect if an other KeePass compatibility app already add a web domain or an app id
|
||||
*/
|
||||
private fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean {
|
||||
if (url.contains(search))
|
||||
return true
|
||||
return customFields.find {
|
||||
it.protectedValue.stringValue.contains(search)
|
||||
} != null
|
||||
}
|
||||
|
||||
fun EntryInfo.setWebDomain(webDomain: String?, scheme: String?, customFieldsAllowed: Boolean) {
|
||||
// If unable to save web domain in custom field or URL not populated, save in URL
|
||||
webDomain?.let {
|
||||
val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
|
||||
val webDomainToStore = if (webDomain.contains("://")) {
|
||||
webDomain
|
||||
} else {
|
||||
"$webScheme://$webDomain"
|
||||
}
|
||||
if (!containsDomainOrApplicationId(webDomain)) {
|
||||
if (!customFieldsAllowed || url.isEmpty()) {
|
||||
url = webDomainToStore
|
||||
} else {
|
||||
// Save web domain in custom field
|
||||
addUniqueField(
|
||||
Field(
|
||||
WEB_DOMAIN_FIELD_NAME,
|
||||
ProtectedString(false, webDomainToStore)
|
||||
),
|
||||
1 // Start to one because URL is a standard field name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save application id in custom field and the application signature if provided
|
||||
*/
|
||||
fun EntryInfo.setApplicationId(applicationId: String?, signature: String? = null) {
|
||||
// Save application id in custom field
|
||||
applicationId?.let {
|
||||
// Check compatibility with other KeePass client unless a signature need to be saved
|
||||
if (!containsDomainOrApplicationId(applicationId) || signature != null) {
|
||||
val position = addUniqueField(
|
||||
Field(
|
||||
APPLICATION_ID_FIELD_NAME,
|
||||
ProtectedString(false, applicationId)
|
||||
)
|
||||
).first
|
||||
signature?.let {
|
||||
addOrReplaceFieldWithSuffix(
|
||||
Field(
|
||||
APPLICATION_SIGNATURE_FIELD_NAME,
|
||||
ProtectedString(true, signature)
|
||||
),
|
||||
position
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an AppOrigin to an EntryInfo,
|
||||
* Only if [customFieldsAllowed] is true
|
||||
*/
|
||||
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
|
||||
appOrigin?.androidOrigins?.forEach { appIdentifier ->
|
||||
setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint)
|
||||
}
|
||||
appOrigin?.webOrigins?.forEach { webOrigin ->
|
||||
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_CVV
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_HOLDER
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_NUMBER
|
||||
import org.joda.time.DateTime
|
||||
|
||||
object CreditCardEntryFields {
|
||||
|
||||
const val CREDIT_CARD_TAG = "Credit Card"
|
||||
|
||||
/**
|
||||
* Parse fields of an entry to retrieve a Passkey
|
||||
*/
|
||||
fun parseFields(getField: (id: String) -> String?): CreditCard? {
|
||||
val cardHolderField = getField(LABEL_HOLDER)
|
||||
val cardNumberField = getField(LABEL_NUMBER)
|
||||
val cardExpiration = DateTime() // TODO Expiration
|
||||
val cardCVVField = getField(LABEL_CVV)
|
||||
if (cardHolderField == null
|
||||
|| cardNumberField == null)
|
||||
return null
|
||||
return CreditCard(
|
||||
cardholder = cardHolderField,
|
||||
number = cardNumberField,
|
||||
expiration = cardExpiration,
|
||||
cvv = cardCVVField
|
||||
)
|
||||
}
|
||||
|
||||
fun EntryInfo.setCreditCard(creditCard: CreditCard?) {
|
||||
if (creditCard != null) {
|
||||
tags.put(CREDIT_CARD_TAG)
|
||||
creditCard.cardholder?.let {
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
LABEL_HOLDER,
|
||||
ProtectedString(enableProtection = false, it)
|
||||
)
|
||||
)
|
||||
}
|
||||
creditCard.number?.let {
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
LABEL_NUMBER,
|
||||
ProtectedString(enableProtection = false, it)
|
||||
)
|
||||
)
|
||||
}
|
||||
creditCard.expiration?.let {
|
||||
expires = true
|
||||
expiryTime = DateInstant(creditCard.expiration.toInstant())
|
||||
}
|
||||
creditCard.cvv?.let {
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
LABEL_CVV,
|
||||
ProtectedString(enableProtection = true, it)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,18 +22,27 @@ package com.kunzisoft.keepass.model
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
import com.kunzisoft.keepass.database.element.entry.AutoType
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin
|
||||
import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId
|
||||
import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain
|
||||
import com.kunzisoft.keepass.model.CreditCardEntryFields.setCreditCard
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.setPasskey
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.isOtpExclusion
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.setOtp
|
||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||
import com.kunzisoft.keepass.utils.readListCompat
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
class EntryInfo : NodeInfo {
|
||||
|
||||
@@ -49,6 +58,8 @@ class EntryInfo : NodeInfo {
|
||||
var attachments: MutableList<Attachment> = mutableListOf()
|
||||
var autoType: AutoType = AutoType()
|
||||
var otpModel: OtpModel? = null
|
||||
var passkey: Passkey? = null
|
||||
var appOrigin: AppOrigin? = null
|
||||
var isTemplate: Boolean = false
|
||||
|
||||
constructor() : super()
|
||||
@@ -68,6 +79,8 @@ class EntryInfo : NodeInfo {
|
||||
parcel.readListCompat(attachments)
|
||||
autoType = parcel.readParcelableCompat() ?: autoType
|
||||
otpModel = parcel.readParcelableCompat() ?: otpModel
|
||||
passkey = parcel.readParcelableCompat() ?: passkey
|
||||
appOrigin = parcel.readParcelableCompat() ?: appOrigin
|
||||
isTemplate = parcel.readBooleanCompat()
|
||||
}
|
||||
|
||||
@@ -89,15 +102,16 @@ class EntryInfo : NodeInfo {
|
||||
parcel.writeList(attachments)
|
||||
parcel.writeParcelable(autoType, flags)
|
||||
parcel.writeParcelable(otpModel, flags)
|
||||
parcel.writeParcelable(passkey, flags)
|
||||
parcel.writeParcelable(appOrigin, flags)
|
||||
parcel.writeBooleanCompat(isTemplate)
|
||||
}
|
||||
|
||||
fun containsCustomFieldsProtected(): Boolean {
|
||||
return customFields.any { it.protectedValue.isProtected }
|
||||
}
|
||||
|
||||
fun containsCustomFieldsNotProtected(): Boolean {
|
||||
return customFields.any { !it.protectedValue.isProtected }
|
||||
fun getCustomFieldsForFilling(): List<Field> {
|
||||
return customFields.filter {
|
||||
!it.isOtpExclusion()
|
||||
&& !it.isPasskeyExclusion()
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCustomField(label: String): Boolean {
|
||||
@@ -113,133 +127,98 @@ class EntryInfo : NodeInfo {
|
||||
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
||||
}
|
||||
|
||||
// Return true if modified
|
||||
private fun addUniqueField(field: Field, number: Int = 0) {
|
||||
var sameName = false
|
||||
var sameValue = false
|
||||
val suffix = if (number > 0) "_$number" else ""
|
||||
customFields.forEach { currentField ->
|
||||
// Not write the same data again
|
||||
if (currentField.protectedValue.stringValue == field.protectedValue.stringValue) {
|
||||
sameValue = true
|
||||
return
|
||||
/**
|
||||
* Add a field to the custom fields list, replace if name already exists
|
||||
*/
|
||||
fun addOrReplaceField(field: Field) {
|
||||
customFields.lastOrNull { it.name == field.name }?.let {
|
||||
it.apply {
|
||||
protectedValue = field.protectedValue
|
||||
}
|
||||
// Same name but new value, create a new suffix
|
||||
if (currentField.name == field.name + suffix) {
|
||||
sameName = true
|
||||
addUniqueField(field, number + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!sameName && !sameValue)
|
||||
(customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue))
|
||||
}
|
||||
|
||||
private fun containsDomainOrApplicationId(search: String): Boolean {
|
||||
if (url.contains(search))
|
||||
return true
|
||||
return customFields.find {
|
||||
it.protectedValue.stringValue.contains(search)
|
||||
} != null
|
||||
} ?: customFields.add(field)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add searchInfo to current EntryInfo, return true if new data, false if no modification
|
||||
* Add a field to the custom fields list with a suffix position,
|
||||
* replace if name already exists
|
||||
*/
|
||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo): Boolean {
|
||||
var modification = false
|
||||
fun addOrReplaceFieldWithSuffix(field: Field, position: Int) {
|
||||
addOrReplaceField(Field(
|
||||
field.name + suffixFieldNamePosition(position),
|
||||
field.protectedValue)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an unique field to the custom fields list with a suffix
|
||||
* if name already exists and value not the same
|
||||
* @param field the field to add
|
||||
* @param position the number to add to the suffix
|
||||
* @return the increment number and the custom field created
|
||||
*/
|
||||
fun addUniqueField(field: Field, position: Int = 0): Pair<Int, Field> {
|
||||
val suffix = suffixFieldNamePosition(position)
|
||||
if (customFields.any { currentField -> currentField.name == field.name + suffix }) {
|
||||
val fieldFound = customFields.find {
|
||||
it.name == field.name + suffix
|
||||
&& it.protectedValue.stringValue == field.protectedValue.stringValue
|
||||
}
|
||||
return if (fieldFound != null) {
|
||||
Pair(position, fieldFound)
|
||||
} else {
|
||||
addUniqueField(field, position + 1)
|
||||
}
|
||||
} else {
|
||||
val field = Field(field.name + suffix, field.protectedValue)
|
||||
customFields.add(field)
|
||||
return Pair(position, field)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize and remove suffix of a title
|
||||
*/
|
||||
fun String.toTitle(): String {
|
||||
return this.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add searchInfo to current EntryInfo
|
||||
*/
|
||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
||||
searchInfo.otpString?.let { otpString ->
|
||||
// Replace the OTP field
|
||||
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
|
||||
if (title.isEmpty())
|
||||
title = otpElement.issuer
|
||||
if (username.isEmpty())
|
||||
username = otpElement.name
|
||||
// Add OTP field
|
||||
val mutableCustomFields = customFields as ArrayList<Field>
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
||||
if (mutableCustomFields.contains(otpField)) {
|
||||
mutableCustomFields.remove(otpField)
|
||||
}
|
||||
mutableCustomFields.add(otpField)
|
||||
modification = true
|
||||
}
|
||||
setOtp(otpString)
|
||||
} ?: searchInfo.webDomain?.let { webDomain ->
|
||||
// If unable to save web domain in custom field or URL not populated, save in URL
|
||||
val scheme = searchInfo.webScheme
|
||||
val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
|
||||
val webDomainToStore = "$webScheme://$webDomain"
|
||||
if (!containsDomainOrApplicationId(webDomain)) {
|
||||
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
|
||||
url = webDomainToStore
|
||||
} else {
|
||||
// Save web domain in custom field
|
||||
addUniqueField(
|
||||
Field(
|
||||
WEB_DOMAIN_FIELD_NAME,
|
||||
ProtectedString(false, webDomainToStore)
|
||||
),
|
||||
1 // Start to one because URL is a standard field name
|
||||
)
|
||||
}
|
||||
modification = true
|
||||
}
|
||||
setWebDomain(
|
||||
webDomain,
|
||||
searchInfo.webScheme,
|
||||
database?.allowEntryCustomFields() == true
|
||||
)
|
||||
} ?: searchInfo.applicationId?.let { applicationId ->
|
||||
// Save application id in custom field
|
||||
if (database?.allowEntryCustomFields() == true) {
|
||||
if (!containsDomainOrApplicationId(applicationId)) {
|
||||
addUniqueField(
|
||||
Field(
|
||||
APPLICATION_ID_FIELD_NAME,
|
||||
ProtectedString(false, applicationId)
|
||||
)
|
||||
)
|
||||
modification = true
|
||||
}
|
||||
}
|
||||
setApplicationId(applicationId)
|
||||
}
|
||||
if (title.isEmpty()) {
|
||||
title = searchInfoToTitle(searchInfo)
|
||||
title = searchInfo.toString().toTitle()
|
||||
}
|
||||
return modification
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize and remove suffix of web domain to create a title
|
||||
* Add registerInfo to current EntryInfo
|
||||
*/
|
||||
private fun searchInfoToTitle(searchInfo: SearchInfo): String {
|
||||
val webDomain = searchInfo.webDomain
|
||||
return webDomain?.substring(0, webDomain.lastIndexOf('.'))?.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||
} ?: searchInfo.toString()
|
||||
}
|
||||
|
||||
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
||||
registerInfo.searchInfo.let {
|
||||
title = searchInfoToTitle(it)
|
||||
}
|
||||
registerInfo.username?.let {
|
||||
username = it
|
||||
}
|
||||
registerInfo.password?.let {
|
||||
password = it
|
||||
}
|
||||
|
||||
if (database?.allowEntryCustomFields() == true) {
|
||||
val creditCard: CreditCard? = registerInfo.creditCard
|
||||
creditCard?.cardholder?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
||||
}
|
||||
creditCard?.expiration?.let {
|
||||
expires = true
|
||||
expiryTime = DateInstant(creditCard.expiration.toInstant())
|
||||
}
|
||||
creditCard?.number?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))
|
||||
}
|
||||
creditCard?.cvv?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
|
||||
}
|
||||
saveSearchInfo(database, registerInfo.searchInfo)
|
||||
registerInfo.username?.let { username = it }
|
||||
registerInfo.password?.let { password = it }
|
||||
setCreditCard(registerInfo.creditCard)
|
||||
setPasskey(registerInfo.passkey)
|
||||
setAppOrigin(
|
||||
registerInfo.appOrigin,
|
||||
database?.allowEntryCustomFields() == true
|
||||
)
|
||||
if (title.isEmpty()) {
|
||||
title = registerInfo.toString().toTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +247,8 @@ class EntryInfo : NodeInfo {
|
||||
if (attachments != other.attachments) return false
|
||||
if (autoType != other.autoType) return false
|
||||
if (otpModel != other.otpModel) return false
|
||||
if (passkey != other.passkey) return false
|
||||
if (appOrigin != other.appOrigin) return false
|
||||
if (isTemplate != other.isTemplate) return false
|
||||
|
||||
return true
|
||||
@@ -287,6 +268,8 @@ class EntryInfo : NodeInfo {
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + autoType.hashCode()
|
||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||
result = 31 * result + (passkey?.hashCode() ?: 0)
|
||||
result = 31 * result + (appOrigin?.hashCode() ?: 0)
|
||||
result = 31 * result + isTemplate.hashCode()
|
||||
return result
|
||||
}
|
||||
@@ -294,8 +277,12 @@ class EntryInfo : NodeInfo {
|
||||
|
||||
companion object {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
|
||||
/**
|
||||
* Create a field name suffix depending on the field position
|
||||
*/
|
||||
fun suffixFieldNamePosition(position: Int): String {
|
||||
return if (position > 0) "_$position" else ""
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Passkey(
|
||||
val username: String,
|
||||
val privateKeyPem: String,
|
||||
val credentialId: String,
|
||||
val userHandle: String,
|
||||
val relyingParty: String
|
||||
): Parcelable
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
|
||||
object PasskeyEntryFields {
|
||||
|
||||
// field names from KeypassXC are used
|
||||
const val FIELD_USERNAME = "KPEX_PASSKEY_USERNAME"
|
||||
|
||||
const val FIELD_PRIVATE_KEY = "KPEX_PASSKEY_PRIVATE_KEY_PEM"
|
||||
const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
|
||||
const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
|
||||
const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
|
||||
|
||||
const val PASSKEY_FIELD = "Passkey"
|
||||
const val PASSKEY_TAG = "Passkey"
|
||||
|
||||
/**
|
||||
* Parse fields of an entry to retrieve a Passkey
|
||||
*/
|
||||
fun parseFields(getField: (id: String) -> String?): Passkey? {
|
||||
val usernameField = getField(FIELD_USERNAME)
|
||||
val privateKeyField = getField(FIELD_PRIVATE_KEY)
|
||||
val credentialIdField = getField(FIELD_CREDENTIAL_ID)
|
||||
val userHandleField = getField(FIELD_USER_HANDLE)
|
||||
val relyingPartyField = getField(FIELD_RELYING_PARTY)
|
||||
if (usernameField == null
|
||||
|| privateKeyField == null
|
||||
|| credentialIdField == null
|
||||
|| userHandleField == null
|
||||
|| relyingPartyField == null)
|
||||
return null
|
||||
return Passkey(
|
||||
username = usernameField,
|
||||
privateKeyPem = privateKeyField,
|
||||
credentialId = credentialIdField,
|
||||
userHandle = userHandleField,
|
||||
relyingParty = relyingPartyField
|
||||
)
|
||||
}
|
||||
|
||||
fun EntryInfo.setPasskey(passkey: Passkey?) {
|
||||
if (passkey != null) {
|
||||
tags.put(PASSKEY_TAG)
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
FIELD_USERNAME,
|
||||
ProtectedString(enableProtection = false, passkey.username)
|
||||
)
|
||||
)
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
FIELD_PRIVATE_KEY,
|
||||
ProtectedString(enableProtection = true, passkey.privateKeyPem)
|
||||
)
|
||||
)
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
FIELD_CREDENTIAL_ID,
|
||||
ProtectedString(enableProtection = true, passkey.credentialId)
|
||||
)
|
||||
)
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
FIELD_USER_HANDLE,
|
||||
ProtectedString(enableProtection = true, passkey.userHandle)
|
||||
)
|
||||
)
|
||||
addOrReplaceField(
|
||||
Field(
|
||||
FIELD_RELYING_PARTY,
|
||||
ProtectedString(enableProtection = false, passkey.relyingParty)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Passkey field from a Passkey
|
||||
*/
|
||||
fun buildPasskeyField(passkey: Passkey): Field {
|
||||
return Field(
|
||||
name = PASSKEY_FIELD,
|
||||
value = ProtectedString(
|
||||
enableProtection = false,
|
||||
string = passkey.relyingParty
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||
* Remove parameters fields use to generate auto fields
|
||||
*/
|
||||
fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
|
||||
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
|
||||
// Remove parameter fields
|
||||
val usernameField = Field(FIELD_USERNAME)
|
||||
val privateKeyField = Field(FIELD_PRIVATE_KEY)
|
||||
val credentialIdField = Field(FIELD_CREDENTIAL_ID)
|
||||
val userHandleField = Field(FIELD_USER_HANDLE)
|
||||
val relyingPartyField = Field(FIELD_RELYING_PARTY)
|
||||
newCustomFields.remove(usernameField)
|
||||
newCustomFields.remove(privateKeyField)
|
||||
newCustomFields.remove(credentialIdField)
|
||||
newCustomFields.remove(userHandleField)
|
||||
newCustomFields.remove(relyingPartyField)
|
||||
// Empty auto generated OTP Token field
|
||||
if (fieldsToParse.contains(usernameField)
|
||||
|| fieldsToParse.contains(privateKeyField)
|
||||
|| fieldsToParse.contains(credentialIdField)
|
||||
|| fieldsToParse.contains(userHandleField)
|
||||
|| fieldsToParse.contains(relyingPartyField)
|
||||
)
|
||||
newCustomFields.add(
|
||||
Field(
|
||||
name = PASSKEY_FIELD,
|
||||
value = ProtectedString(enableProtection = false)
|
||||
)
|
||||
)
|
||||
return newCustomFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Field ignored for a search or a form filling
|
||||
*/
|
||||
fun Field.isPasskeyExclusion(): Boolean {
|
||||
return when(name) {
|
||||
PASSKEY_FIELD -> true
|
||||
FIELD_USERNAME -> true
|
||||
FIELD_PRIVATE_KEY -> true
|
||||
FIELD_CREDENTIAL_ID -> true
|
||||
FIELD_USER_HANDLE -> true
|
||||
FIELD_RELYING_PARTY -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,56 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
|
||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
||||
val username: String?,
|
||||
val password: String?,
|
||||
val creditCard: CreditCard?): Parcelable {
|
||||
data class RegisterInfo(
|
||||
val searchInfo: SearchInfo,
|
||||
val username: String? = null,
|
||||
val password: String? = null,
|
||||
val creditCard: CreditCard? = null,
|
||||
val passkey: Passkey? = null,
|
||||
val appOrigin: AppOrigin? = null
|
||||
) : ObjectNameResource, Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelableCompat() ?: SearchInfo(),
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelableCompat()) {
|
||||
}
|
||||
searchInfo = parcel.readParcelableCompat() ?: SearchInfo(),
|
||||
username = parcel.readString(),
|
||||
password = parcel.readString(),
|
||||
creditCard = parcel.readParcelableCompat(),
|
||||
passkey = parcel.readParcelableCompat(),
|
||||
appOrigin = parcel.readParcelableCompat()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(searchInfo, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeParcelable(creditCard, flags)
|
||||
parcel.writeParcelable(passkey, flags)
|
||||
parcel.writeParcelable(appOrigin, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getName(resources: Resources): String {
|
||||
return username
|
||||
?: passkey?.relyingParty
|
||||
?: appOrigin?.toName()
|
||||
?: searchInfo.getName(resources)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return username
|
||||
?: passkey?.relyingParty
|
||||
?: appOrigin?.toName()
|
||||
?: searchInfo.toString()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): RegisterInfo {
|
||||
return RegisterInfo(parcel)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||
@@ -11,6 +12,7 @@ import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||
|
||||
class SearchInfo : ObjectNameResource, Parcelable {
|
||||
var manualSelection: Boolean = false
|
||||
var tag: String? = null
|
||||
var applicationId: String? = null
|
||||
set(value) {
|
||||
field = when {
|
||||
@@ -33,26 +35,33 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
get() {
|
||||
return if (webDomain == null) null else field
|
||||
}
|
||||
var relyingParty: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(toCopy: SearchInfo?) {
|
||||
manualSelection = toCopy?.manualSelection ?: manualSelection
|
||||
tag = toCopy?.tag
|
||||
applicationId = toCopy?.applicationId
|
||||
webDomain = toCopy?.webDomain
|
||||
webScheme = toCopy?.webScheme
|
||||
relyingParty = toCopy?.relyingParty
|
||||
otpString = toCopy?.otpString
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
manualSelection = parcel.readBooleanCompat()
|
||||
val readTag = parcel.readString()
|
||||
tag = if (readTag.isNullOrEmpty()) null else readTag
|
||||
val readAppId = parcel.readString()
|
||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||
val readDomain = parcel.readString()
|
||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||
val readScheme = parcel.readString()
|
||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||
val readRelyingParty = parcel.readString()
|
||||
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
||||
val readOtp = parcel.readString()
|
||||
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||
}
|
||||
@@ -63,9 +72,11 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeBooleanCompat(manualSelection)
|
||||
parcel.writeString(tag ?: "")
|
||||
parcel.writeString(applicationId ?: "")
|
||||
parcel.writeString(webDomain ?: "")
|
||||
parcel.writeString(webScheme ?: "")
|
||||
parcel.writeString(relyingParty ?: "")
|
||||
parcel.writeString(otpString ?: "")
|
||||
}
|
||||
|
||||
@@ -79,24 +90,54 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
fun containsOnlyNullValues(): Boolean {
|
||||
return applicationId == null
|
||||
return tag == null
|
||||
&& applicationId == null
|
||||
&& webDomain == null
|
||||
&& webScheme == null
|
||||
&& relyingParty == null
|
||||
&& otpString == null
|
||||
}
|
||||
|
||||
fun isASearchByDomain(): Boolean {
|
||||
private fun isADomainSearch(): Boolean {
|
||||
return toString() == webDomain && webDomain != null
|
||||
}
|
||||
}
|
||||
|
||||
var isAPasskeySearch: Boolean = false
|
||||
|
||||
var query: String? = null
|
||||
|
||||
fun buildSearchParameters(): SearchParameters {
|
||||
return SearchParameters().apply {
|
||||
searchQuery = query ?: this@SearchInfo.toString()
|
||||
allowEmptyQuery = false
|
||||
searchInTitles = !isAPasskeySearch
|
||||
searchInUsernames = false
|
||||
searchInPasswords = false
|
||||
searchInUrls = !isAPasskeySearch
|
||||
searchByDomain = isADomainSearch()
|
||||
searchInNotes = false
|
||||
searchInOTP = false
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
searchInRelyingParty = isAPasskeySearch
|
||||
searchInCurrentGroup = false
|
||||
searchInSearchableGroup = true
|
||||
searchInRecycleBin = false
|
||||
searchInTemplates = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SearchInfo) return false
|
||||
|
||||
if (manualSelection != other.manualSelection) return false
|
||||
if (tag != other.tag) return false
|
||||
if (applicationId != other.applicationId) return false
|
||||
if (webDomain != other.webDomain) return false
|
||||
if (webScheme != other.webScheme) return false
|
||||
if (relyingParty != other.relyingParty) return false
|
||||
if (otpString != other.otpString) return false
|
||||
|
||||
return true
|
||||
@@ -104,15 +145,17 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manualSelection.hashCode()
|
||||
result = 31 * result + (tag?.hashCode() ?: 0)
|
||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return otpString ?: webDomain ?: applicationId ?: ""
|
||||
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.HOTP_INITIAL_COUNTER
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.HashAlgorithm
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_DIGITS
|
||||
@@ -434,6 +435,25 @@ object OtpEntryFields {
|
||||
buildOtpUri(otpElement, title, username).toString()))
|
||||
}
|
||||
|
||||
fun EntryInfo.setOtp(otpString: String): Boolean {
|
||||
// Replace the OTP field
|
||||
parseOTPUri(otpString)?.let { otpElement ->
|
||||
if (title.isEmpty())
|
||||
title = otpElement.issuer
|
||||
if (username.isEmpty())
|
||||
username = otpElement.name
|
||||
// Add OTP field
|
||||
val mutableCustomFields = customFields as ArrayList<Field>
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
||||
if (mutableCustomFields.contains(otpField)) {
|
||||
mutableCustomFields.remove(otpField)
|
||||
}
|
||||
mutableCustomFields.add(otpField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||
* Remove parameters fields use to generate auto fields
|
||||
@@ -486,4 +506,28 @@ object OtpEntryFields {
|
||||
newCustomFields.add(Field(OTP_TOKEN_FIELD))
|
||||
return newCustomFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Field ignored for a search or a form filling
|
||||
*/
|
||||
fun Field.isOtpExclusion(): Boolean {
|
||||
return when(name) {
|
||||
OTP_FIELD -> true
|
||||
TOTP_SEED_FIELD -> true
|
||||
TOTP_SETTING_FIELD -> true
|
||||
HMACOTP_SECRET_FIELD -> true
|
||||
HMACOTP_SECRET_HEX_FIELD -> true
|
||||
HMACOTP_SECRET_BASE32_FIELD -> true
|
||||
HMACOTP_SECRET_BASE64_FIELD -> true
|
||||
HMACOTP_SECRET_COUNTER_FIELD -> true
|
||||
TIMEOTP_SECRET_FIELD -> true
|
||||
TIMEOTP_SECRET_HEX_FIELD -> true
|
||||
TIMEOTP_SECRET_BASE32_FIELD -> true
|
||||
TIMEOTP_SECRET_BASE64_FIELD -> true
|
||||
TIMEOTP_LENGTH_FIELD -> true
|
||||
TIMEOTP_PERIOD_FIELD -> true
|
||||
TIMEOTP_ALGORITHM_FIELD -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
database/src/main/java/com/kunzisoft/keepass/utils/UUIDUtils.kt
Normal file
117
database/src/main/java/com/kunzisoft/keepass/utils/UUIDUtils.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
object UUIDUtils {
|
||||
|
||||
/**
|
||||
* Specific UUID string format for KeePass database
|
||||
*/
|
||||
fun UUID.asHexString(): String? {
|
||||
try {
|
||||
val buf = uuidTo16Bytes(this)
|
||||
|
||||
val len = buf.size
|
||||
if (len == 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
|
||||
var bt: Short
|
||||
var high: Char
|
||||
var low: Char
|
||||
for (b in buf) {
|
||||
bt = (b.toInt() and 0xFF).toShort()
|
||||
high = (bt.toInt() ushr 4).toChar()
|
||||
low = (bt.toInt() and 0x0F).toChar()
|
||||
sb.append(byteToChar(high))
|
||||
sb.append(byteToChar(low))
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From a specific UUID KeePass database string format,
|
||||
* Note : For a standard UUID string format, use UUID.fromString()
|
||||
*/
|
||||
fun String.asUUID(): UUID? {
|
||||
if (this.length != 32) return null
|
||||
|
||||
val charArray = this.lowercase(Locale.getDefault()).toCharArray()
|
||||
val leastSignificantChars = CharArray(16)
|
||||
val mostSignificantChars = CharArray(16)
|
||||
|
||||
var i = 31
|
||||
while (i >= 0) {
|
||||
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]
|
||||
}
|
||||
i = i - 2
|
||||
}
|
||||
val standardUUIDString = StringBuilder()
|
||||
standardUUIDString.append(leastSignificantChars)
|
||||
standardUUIDString.append(mostSignificantChars)
|
||||
standardUUIDString.insert(8, '-')
|
||||
standardUUIDString.insert(13, '-')
|
||||
standardUUIDString.insert(18, '-')
|
||||
standardUUIDString.insert(23, '-')
|
||||
return try {
|
||||
UUID.fromString(standardUUIDString.toString())
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.asUUID(): UUID {
|
||||
val bb = ByteBuffer.wrap(this)
|
||||
val firstLong = bb.getLong()
|
||||
val secondLong = bb.getLong()
|
||||
return UUID(firstLong, secondLong)
|
||||
}
|
||||
|
||||
fun UUID.asBytes(): ByteArray {
|
||||
return ByteBuffer.allocate(16).apply {
|
||||
putLong(mostSignificantBits)
|
||||
putLong(leastSignificantBits)
|
||||
}.array()
|
||||
}
|
||||
|
||||
// Use short to represent unsigned byte
|
||||
private fun byteToChar(bt: Char): Char {
|
||||
return if (bt.code >= 10) {
|
||||
('A'.code + bt.code - 10).toChar()
|
||||
} else {
|
||||
('0'.code + bt.code).toChar()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class UuidUtil {
|
||||
|
||||
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 "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
short bt;
|
||||
char high, low;
|
||||
for (byte b : buf) {
|
||||
bt = (short) (b & 0xFF);
|
||||
high = (char) (bt >>> 4);
|
||||
low = (char) (bt & 0x0F);
|
||||
sb.append(byteToChar(high));
|
||||
sb.append(byteToChar(low));
|
||||
}
|
||||
|
||||
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
|
||||
private static char byteToChar(char bt) {
|
||||
if (bt >= 10) {
|
||||
return (char)('A' + bt - 10);
|
||||
}
|
||||
else {
|
||||
return (char)('0' + bt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||
import junit.framework.TestCase
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
class UUIDTest: TestCase() {
|
||||
|
||||
fun testUUID() {
|
||||
fun testUUIDHexString() {
|
||||
val randomUUID = UUID.randomUUID()
|
||||
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||
val hexStringUUID = randomUUID.asHexString()
|
||||
val retrievedUUID = hexStringUUID?.asUUID()
|
||||
assertEquals(randomUUID, retrievedUUID)
|
||||
}
|
||||
|
||||
fun testUUIDString() {
|
||||
val staticUUID = "4be0643f-1d98-573b-97cd-ca98a65347dd"
|
||||
val stringUUID = UUID.fromString(staticUUID).asBytes().asUUID().toString()
|
||||
assertEquals(staticUUID, stringUUID)
|
||||
}
|
||||
|
||||
fun testUUIDBytes() {
|
||||
val randomUUID = UUID.randomUUID()
|
||||
val byteArrayUUID = randomUUID.asBytes()
|
||||
val retrievedUUID = byteArrayUUID.asUUID()
|
||||
assertEquals(randomUUID, retrievedUUID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user