@@ -32,6 +32,7 @@ import to.bitkit.models.PubkyProfile
3232import to.bitkit.models.PubkyProfileData
3333import to.bitkit.models.PubkyProfileLink
3434import to.bitkit.services.PubkyService
35+ import to.bitkit.utils.AppError
3536import to.bitkit.utils.Logger
3637import java.io.ByteArrayOutputStream
3738import javax.inject.Inject
@@ -40,6 +41,11 @@ import kotlin.math.min
4041
4142enum class PubkyAuthState { Idle , Authenticating , Authenticated }
4243
44+ sealed class PubkyContactError (message : String ) : AppError(message) {
45+ data object CannotAddSelf : PubkyContactError (" Cannot add your own pubky as a contact" )
46+ data object InvalidFormat : PubkyContactError (" Invalid pubky key format" )
47+ }
48+
4349@Suppress(" TooManyFunctions" )
4450@Singleton
4551class PubkyRepo @Inject constructor(
@@ -56,6 +62,8 @@ class PubkyRepo @Inject constructor(
5662 private const val PUBKY_SCHEME = " pubky://"
5763 private const val AVATAR_MAX_SIZE = 400
5864 private const val AVATAR_QUALITY = 80
65+ private const val PUBKY_KEY_LENGTH = 52
66+ private val Z_BASE_32_REGEX = Regex (" ^[ybndrfg8ejkmcpqxot1uwisza345h769]+$" )
5967 }
6068
6169 private val scope = CoroutineScope (ioDispatcher + SupervisorJob ())
@@ -137,7 +145,7 @@ class PubkyRepo @Inject constructor(
137145 }.getOrNull() ? : return
138146
139147 when (result) {
140- is InitResult .NoSession -> Logger .debug(" No saved paykit session found " , context = TAG )
148+ is InitResult .NoSession -> Logger .debug(" Found no saved paykit session" , context = TAG )
141149 is InitResult .Restored -> {
142150 _publicKey .update { result.publicKey }
143151 _authState .update { PubkyAuthState .Authenticated }
@@ -157,7 +165,7 @@ class PubkyRepo @Inject constructor(
157165 }.getOrNull()
158166
159167 if (secretKeyHex.isNullOrEmpty()) {
160- Logger .warn(" No secret key available for re-sign-in recovery" , context = TAG )
168+ Logger .warn(" Skipped re-sign-in recovery, no secret key available " , context = TAG )
161169 return InitResult .RestorationFailed
162170 }
163171
@@ -264,7 +272,7 @@ class PubkyRepo @Inject constructor(
264272 val json = pubkyService.fetchFileString(uri)
265273 PubkyProfileData .decode(json).toPubkyProfile(publicKey)
266274 }.onFailure {
267- Logger .debug(" No bitkit profile found, falling back to FFI " , context = TAG )
275+ Logger .debug(" Falling back to FFI, no bitkit profile found " , context = TAG )
268276 }.getOrNull()
269277
270278 suspend fun fetchRemoteProfile (publicKey : String ): Result <PubkyProfile ?> = runCatching {
@@ -373,7 +381,7 @@ class PubkyRepo @Inject constructor(
373381 " No session available"
374382 }
375383 writeProfile(session, name, bio, links, tags, imageUrl)
376- val pk = requireNotNull(_publicKey .value)
384+ val pk = requireNotNull(_publicKey .value) { " No public key available " }
377385 val profile = PubkyProfile (
378386 publicKey = pk,
379387 name = name,
@@ -500,10 +508,14 @@ class PubkyRepo @Inject constructor(
500508 }
501509
502510 suspend fun fetchContactProfile (publicKey : String ): Result <PubkyProfile > {
503- val prefixedKey = publicKey.ensurePubkyPrefix()
511+ val prefixedKey = runCatching { requireAddableContactPublicKey(publicKey) }
512+ .getOrElse { return Result .failure(it) }
504513 return fetchRemoteProfile(prefixedKey)
505514 .map { it ? : PubkyProfile .placeholder(prefixedKey) }
506- .recover {
515+ .recoverCatching {
516+ if (! it.isMissingPubkyData()) {
517+ throw it
518+ }
507519 Logger .warn(" Falling back to placeholder contact '$prefixedKey '" , it, context = TAG )
508520 PubkyProfile .placeholder(prefixedKey)
509521 }
@@ -514,7 +526,7 @@ class PubkyRepo @Inject constructor(
514526 val session = requireNotNull(keychain.loadString(Keychain .Key .PAYKIT_SESSION .name)) {
515527 " No session available"
516528 }
517- val prefixedKey = publicKey.ensurePubkyPrefix( )
529+ val prefixedKey = requireAddableContactPublicKey(publicKey )
518530 val profile = existingProfile?.copy(publicKey = prefixedKey) ? : run {
519531 val ffiProfile = pubkyService.getProfile(prefixedKey)
520532 PubkyProfile .fromFfi(prefixedKey, ffiProfile)
@@ -734,20 +746,45 @@ class PubkyRepo @Inject constructor(
734746 _authState .update { PubkyAuthState .Idle }
735747 }
736748
749+ private fun requireAddableContactPublicKey (publicKey : String ): String {
750+ val prefixedKey = publicKey.trim().ensurePubkyPrefix()
751+ val strippedKey = prefixedKey.removePrefix(PUBKY_PREFIX )
752+ if (strippedKey.length != PUBKY_KEY_LENGTH || ! Z_BASE_32_REGEX .matches(strippedKey)) {
753+ throw PubkyContactError .InvalidFormat
754+ }
755+ if (_publicKey .value == prefixedKey) {
756+ throw PubkyContactError .CannotAddSelf
757+ }
758+ return prefixedKey
759+ }
760+
737761 private fun String.ensurePubkyPrefix (): String =
738762 if (startsWith(PUBKY_PREFIX )) this else " $PUBKY_PREFIX$this "
739763
740764 private fun Throwable.isMissingPubkyDirectory (): Boolean {
741- val fullMessage = buildString {
765+ if (isMissingPubkyData()) {
766+ return true
767+ }
768+
769+ val fullMessage = buildErrorMessage()
770+ return fullMessage.contains(" directory not found" , ignoreCase = true )
771+ }
772+
773+ private fun Throwable.isMissingPubkyData (): Boolean {
774+ val fullMessage = buildErrorMessage()
775+ return fullMessage.contains(" 404" ) ||
776+ fullMessage.contains(" not found" , ignoreCase = true ) ||
777+ fullMessage.contains(" missing" , ignoreCase = true )
778+ }
779+
780+ private fun Throwable.buildErrorMessage (): String =
781+ buildString {
742782 append(message.orEmpty())
743783 cause?.message?.takeIf { it.isNotBlank() }?.let {
744784 append(" " )
745785 append(it)
746786 }
747787 }
748- return fullMessage.contains(" directory not found" , ignoreCase = true ) ||
749- (fullMessage.contains(" 404" ) && fullMessage.contains(" not found" , ignoreCase = true ))
750- }
751788
752789 // endregion
753790}
0 commit comments