Add {otp} placeholder for generating TOTP codes

This commit is contained in:
Artem Chepurnoy 2024-01-07 23:08:38 +02:00
parent d06292c071
commit 458fbcdcc4
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
12 changed files with 227 additions and 35 deletions

View File

@ -1,6 +1,5 @@
package com.artemchep.keyguard.common.service.placeholder
import arrow.core.Option
import com.artemchep.keyguard.common.io.IO
// See:
@ -9,6 +8,15 @@ interface Placeholder {
operator fun get(key: String): IO<String?>?
interface Factory {
fun createOrNull(
scope: PlaceholderScope,
): Placeholder?
}
}
fun List<Placeholder.Factory>.create(
scope: PlaceholderScope,
): List<Placeholder> = this
.mapNotNull { placeholder ->
placeholder.createOrNull(scope)
}

View File

@ -4,6 +4,7 @@ import com.artemchep.keyguard.common.model.DSecret
import kotlinx.datetime.Instant
data class PlaceholderScope(
val cipher: DSecret,
val now: Instant,
val cipher: DSecret,
val url: String? = null,
)

View File

@ -2,34 +2,49 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.io.map
import com.artemchep.keyguard.common.io.toIO
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.usecase.GetTotpCode
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import com.artemchep.keyguard.common.service.totp.TotpService
import kotlinx.coroutines.Dispatchers
import kotlinx.datetime.Instant
import org.kodein.di.DirectDI
import org.kodein.di.instance
class CipherPlaceholder(
// private val getTotpCode: GetTotpCode,
private val totpService: TotpService,
private val cipher: DSecret,
private val now: Instant,
) : Placeholder {
override fun get(
key: String,
): IO<String?>? = when {
key.equals("uuid", ignoreCase = true) ->
cipher.service.remote?.id.let(::io)
key.equals("title", ignoreCase = true) ->
cipher.name.let(::io)
key.equals("username", ignoreCase = true) ->
cipher.login?.username.let(::io)
key.equals("password", ignoreCase = true) ->
cipher.login?.password.let(::io)
// key.equals("otp", ignoreCase = true) -> run {
// val token = cipher.login?.totp?.token
// ?: return@run null.let(::io)
// getTotpCode(token)
// .toIO()
// .map { code -> code.code }
// }
key.equals("otp", ignoreCase = true) -> run {
val token = cipher.login?.totp?.token
?: return@run null.let(::io)
ioEffect(Dispatchers.Default) {
totpService
.generate(
token = token,
timestamp = now,
)
.code
}
}
key.equals("notes", ignoreCase = true) ->
cipher.notes.let(::io)
// extras
@ -39,7 +54,21 @@ class CipherPlaceholder(
else -> null
}
class Factory {
class Factory(
private val totpService: TotpService,
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
totpService = directDI.instance(),
)
override fun createOrNull(
scope: PlaceholderScope,
) = CipherPlaceholder(
totpService = totpService,
cipher = scope.cipher,
now = scope.now,
)
}
}

View File

@ -3,6 +3,8 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import org.kodein.di.DirectDI
class CommentPlaceholder(
) : Placeholder {
@ -15,4 +17,16 @@ class CommentPlaceholder(
// unknown
else -> null
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = CommentPlaceholder()
}
}

View File

@ -4,6 +4,8 @@ import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import org.kodein.di.DirectDI
class CustomPlaceholder(
private val cipher: DSecret,
@ -26,4 +28,18 @@ class CustomPlaceholder(
// unknown
else -> null
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = CustomPlaceholder(
cipher = scope.cipher,
)
}
}

View File

@ -1,21 +1,19 @@
package com.artemchep.keyguard.common.service.placeholder.impl
import arrow.core.None
import arrow.core.Option
import arrow.core.some
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import kotlinx.datetime.Clock
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import org.kodein.di.DirectDI
import java.time.format.DateTimeFormatter
class DateTimePlaceholder(
private val now: Instant,
) : Placeholder {
private val now = Clock.System.now()
private val localDateTime by lazy {
val tz = TimeZone.currentSystemDefault()
now.toLocalDateTime(tz)
@ -105,4 +103,18 @@ class DateTimePlaceholder(
// unknown
else -> null
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = DateTimePlaceholder(
now = scope.now,
)
}
}

View File

@ -3,6 +3,8 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import org.kodein.di.DirectDI
class EnvironmentPlaceholder(
) : Placeholder {
@ -22,4 +24,16 @@ class EnvironmentPlaceholder(
null
}
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = EnvironmentPlaceholder()
}
}

View File

@ -3,7 +3,11 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import com.artemchep.keyguard.common.service.placeholder.util.Parser
import com.artemchep.keyguard.common.usecase.GetTotpCode
import org.kodein.di.DirectDI
import org.kodein.di.instance
class TextReplaceRegexPlaceholder(
) : Placeholder {
@ -31,4 +35,16 @@ class TextReplaceRegexPlaceholder(
rg.replace(params.value, replacement)
}
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = TextReplaceRegexPlaceholder()
}
}

View File

@ -3,8 +3,10 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import com.artemchep.keyguard.common.service.placeholder.util.Parser
import io.ktor.util.*
import org.kodein.di.DirectDI
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.Locale
@ -85,4 +87,16 @@ class TextTransformPlaceholder(
): String {
return URLDecoder.decode(value, "UTF-8")
}
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = TextTransformPlaceholder()
}
}

View File

@ -3,7 +3,9 @@ package com.artemchep.keyguard.common.service.placeholder.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import io.ktor.http.*
import org.kodein.di.DirectDI
class UrlPlaceholder(
private val url: String,
@ -90,7 +92,15 @@ class UrlPlaceholder(
else -> null
}
class Factory {
class Factory(
) : Placeholder.Factory {
constructor(
directDI: DirectDI,
) : this(
)
override fun createOrNull(
scope: PlaceholderScope,
) = scope.url?.let(::UrlPlaceholder)
}
}

View File

@ -80,6 +80,9 @@ import com.artemchep.keyguard.common.service.execute.ExecuteCommand
import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor
import com.artemchep.keyguard.common.service.extract.LinkInfoRegistry
import com.artemchep.keyguard.common.service.passkey.PassKeyService
import com.artemchep.keyguard.common.service.placeholder.Placeholder
import com.artemchep.keyguard.common.service.placeholder.PlaceholderScope
import com.artemchep.keyguard.common.service.placeholder.create
import com.artemchep.keyguard.common.service.placeholder.impl.CipherPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.CommentPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.CustomPlaceholder
@ -266,6 +269,7 @@ fun vaultViewScreenState(
addCipherOpenedHistory = instance(),
getJustDeleteMeByUrl = instance(),
windowCoroutineScope = instance(),
placeholderFactories = allInstances(),
linkInfoExtractors = allInstances(),
mode = mode,
contentColor = contentColor,
@ -340,6 +344,7 @@ fun vaultViewScreenState(
addCipherOpenedHistory: AddCipherOpenedHistory,
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
windowCoroutineScope: WindowCoroutineScope,
placeholderFactories: List<Placeholder.Factory>,
linkInfoExtractors: List<LinkInfoExtractor<LinkInfo, LinkInfo>>,
itemId: String,
accountId: String,
@ -490,15 +495,8 @@ fun vaultViewScreenState(
val canEdit = canAddSecret && secretOrNull.canEdit() && !hasCanNotWriteCiphers
val canDelete = canAddSecret && secretOrNull.canDelete() && !hasCanNotWriteCiphers
val placeholders = listOf(
CipherPlaceholder(secretOrNull),
CommentPlaceholder(),
CustomPlaceholder(secretOrNull),
DateTimePlaceholder(),
TextReplaceRegexPlaceholder(),
TextTransformPlaceholder(),
EnvironmentPlaceholder(),
)
val now = Clock.System.now()
val extractors = LinkInfoRegistry(linkInfoExtractors)
val cipherUris = secretOrNull
.uris
@ -516,16 +514,28 @@ fun vaultViewScreenState(
}
else -> {
val newUriPlaceholders = placeholderFactories
.create(
scope = PlaceholderScope(
now = now,
cipher = secretOrNull,
),
)
val newUriString = kotlin.runCatching {
uri.uri.placeholderFormat(placeholders)
uri.uri.placeholderFormat(newUriPlaceholders)
}.getOrElse { uri.uri }
val newUri = uri.copy(uri = newUriString)
// Process URL overrides
val urlOverridePlaceholders by lazy {
placeholders + listOf(
UrlPlaceholder(newUriString),
)
placeholderFactories
.create(
scope = PlaceholderScope(
now = now,
cipher = secretOrNull,
url = newUriString,
),
)
}
val urlOverrideList = urlOverrides
.filter { override ->

View File

@ -28,6 +28,14 @@ import com.artemchep.keyguard.common.service.license.impl.LicenseServiceImpl
import com.artemchep.keyguard.common.service.logging.LogRepository
import com.artemchep.keyguard.common.service.passkey.PassKeyService
import com.artemchep.keyguard.common.service.passkey.impl.PassKeyServiceImpl
import com.artemchep.keyguard.common.service.placeholder.impl.CipherPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.CommentPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.CustomPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.DateTimePlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.EnvironmentPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.TextReplaceRegexPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.TextTransformPlaceholder
import com.artemchep.keyguard.common.service.placeholder.impl.UrlPlaceholder
import com.artemchep.keyguard.common.service.relays.di.emailRelayDiModule
import com.artemchep.keyguard.common.service.review.ReviewLog
import com.artemchep.keyguard.common.service.review.impl.ReviewLogImpl
@ -952,6 +960,46 @@ fun globalModuleJvm() = DI.Module(
)
}
import(emailRelayDiModule())
bindSingleton<CipherPlaceholder.Factory> {
CipherPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<CommentPlaceholder.Factory> {
CommentPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<CustomPlaceholder.Factory> {
CustomPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<DateTimePlaceholder.Factory> {
DateTimePlaceholder.Factory(
directDI = this,
)
}
bindSingleton<EnvironmentPlaceholder.Factory> {
EnvironmentPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<TextReplaceRegexPlaceholder.Factory> {
TextReplaceRegexPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<TextTransformPlaceholder.Factory> {
TextTransformPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<UrlPlaceholder.Factory> {
UrlPlaceholder.Factory(
directDI = this,
)
}
bindSingleton<DeeplinkService> {
DeeplinkServiceImpl(
directDI = this,