Skip to content

Commit

Permalink
Use static instances for Base64 encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Sep 1, 2023
1 parent 2332129 commit 11f14a8
Show file tree
Hide file tree
Showing 15 changed files with 113 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package at.asitplus.wallet.lib.msg

import at.asitplus.wallet.lib.aries.jsonSerializer
import at.asitplus.wallet.lib.data.Base64Strict
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.SerialName
Expand All @@ -30,7 +30,7 @@ data class JwmAttachment(

fun decodeString(): String? {
if (data.base64 != null)
return data.base64.decodeToByteArrayOrNull(Base64(strict = true))?.decodeToString()
return data.base64.decodeToByteArrayOrNull(Base64Strict)?.decodeToString()
if (data.jws != null)
return data.jws
return null
Expand All @@ -39,7 +39,7 @@ data class JwmAttachment(

fun decodeBinary(): ByteArray? {
if (data.base64 != null)
return data.base64.decodeToByteArrayOrNull(Base64(strict = true))
return data.base64.decodeToByteArrayOrNull(Base64Strict)
return null
.also { Napier.w("Could not binary decode JWM attachment") }
}
Expand All @@ -57,15 +57,15 @@ data class JwmAttachment(
id = uuid4().toString(),
mediaType = "application/base64",
data = JwmAttachmentData(
base64 = data.encodeToByteArray().encodeToString(Base64(strict = true))
base64 = data.encodeToByteArray().encodeToString(Base64Strict)
)
)

fun encodeBase64(data: ByteArray) = JwmAttachment(
id = uuid4().toString(),
mediaType = "application/base64",
data = JwmAttachmentData(
base64 = data.encodeToString(Base64(strict = true))
base64 = data.encodeToString(Base64Strict)
)
)

Expand All @@ -75,7 +75,7 @@ data class JwmAttachment(
filename = filename,
parent = parent,
data = JwmAttachmentData(
base64 = data.encodeToString(Base64(strict = true))
base64 = data.encodeToString(Base64Strict)
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import at.asitplus.wallet.lib.agent.Issuer
import at.asitplus.wallet.lib.cbor.CoseEllipticCurve
import at.asitplus.wallet.lib.cbor.CoseKey
import at.asitplus.wallet.lib.cbor.CoseKeyType
import at.asitplus.wallet.lib.data.Base64Url
import at.asitplus.wallet.lib.data.Base64UrlStrict
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_CREDENTIAL
import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DOC_TYPE_MDL
Expand All @@ -24,7 +24,6 @@ import at.asitplus.wallet.lib.oidc.OpenIdConstants.TOKEN_TYPE_BEARER
import at.asitplus.wallet.lib.oidc.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import io.ktor.http.URLBuilder
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlin.coroutines.cancellation.CancellationException

Expand Down Expand Up @@ -168,7 +167,7 @@ class IssuerService(
return when (val issuedCredential = issuedCredentialResult.successful.first()) {
is Issuer.IssuedCredential.Iso -> CredentialResponseParameters(
format = CredentialFormatEnum.MSO_MDOC,
credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64Url)
credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64UrlStrict)
)

is Issuer.IssuedCredential.Vc -> CredentialResponseParameters(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import at.asitplus.wallet.lib.cbor.CoseHeader
import at.asitplus.wallet.lib.cbor.CoseKey
import at.asitplus.wallet.lib.cbor.CoseService
import at.asitplus.wallet.lib.cbor.DefaultCoseService
import at.asitplus.wallet.lib.data.Base64Strict
import at.asitplus.wallet.lib.data.CredentialStatus
import at.asitplus.wallet.lib.data.RevocationListSubject
import at.asitplus.wallet.lib.data.VcDataModelConstants.REVOCATION_LIST_MIN_SIZE
Expand Down Expand Up @@ -218,7 +219,7 @@ class IssuerAgent(
issuerCredentialStore.getRevokedStatusListIndexList(timePeriod)
.forEach { bitset[it] = true }
val input = bitset.toByteArray()
return zlibService.compress(input)?.encodeToString(Base64(strict = true))
return zlibService.compress(input)?.encodeToString(Base64Strict)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import at.asitplus.wallet.lib.ZlibService
import at.asitplus.wallet.lib.cbor.CoseKey
import at.asitplus.wallet.lib.cbor.DefaultVerifierCoseService
import at.asitplus.wallet.lib.cbor.VerifierCoseService
import at.asitplus.wallet.lib.data.Base64Strict
import at.asitplus.wallet.lib.data.IsoDocumentParsed
import at.asitplus.wallet.lib.data.RevocationListSubject
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
Expand All @@ -25,7 +26,6 @@ import at.asitplus.wallet.lib.jws.VerifierJwsService
import at.asitplus.wallet.lib.toBitSet
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base16.Base16
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.cbor.ByteStringWrapper
Expand Down Expand Up @@ -88,7 +88,7 @@ class Validator(
return false
.also { Napier.d("credentialSubject invalid") }
val encodedList = parsedVc.jws.vc.credentialSubject.encodedList
this.revocationList = encodedList.decodeToByteArrayOrNull(Base64(strict = true))?.let {
this.revocationList = encodedList.decodeToByteArrayOrNull(Base64Strict)?.let {
zlibService.decompress(it)?.toBitSet() ?: return false.also { Napier.d("Invalid ZLIB") }
} ?: return false.also { Napier.d("Invalid Base64") }
Napier.d("Revocation list is valid")
Expand Down Expand Up @@ -188,56 +188,80 @@ class Validator(
*/
fun verifyDocument(doc: Document, challenge: String): Verifier.VerifyPresentationResult {
if (doc.docType != DOC_TYPE_MDL)
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("Invalid docType: ${doc.docType}") }
if (doc.errors != null) {
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("Document has errors: ${doc.errors}") }
}
val issuerSigned = doc.issuerSigned
val issuerAuth = issuerSigned.issuerAuth

val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let {
CryptoUtils.extractPublicKeyFromX509Cert(it)?.toCoseKey()
} ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
} ?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("Got no issuer key in $issuerAuth") }

if (verifierCoseService.verifyCose(issuerAuth, issuerKey).getOrNull() != true) {
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("IssuerAuth not verified: $issuerAuth") }
}

val mso = issuerSigned.getIssuerAuthPayloadAsMso()
?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") }
if (mso.docType != DOC_TYPE_MDL) {
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("Invalid docType in MSO: ${mso.docType}") }
}
val mdlItems = mso.valueDigests[NAMESPACE_MDL]
?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("mdlItems are null in MSO: ${mso.valueDigests}") }

val walletKey = mso.deviceKeyInfo.deviceKey
val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature
?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") }

if (verifierCoseService.verifyCose(deviceSignature, walletKey).getOrNull() != true) {
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("DeviceSignature not verified") }
}

val deviceSignaturePayload = deviceSignature.payload
?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("DeviceSignature does not contain challenge") }
if (!deviceSignaturePayload.contentEquals(challenge.encodeToByteArray())) {
return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("DeviceSignature does not contain correct challenge") }
}

val issuerSignedItems = issuerSigned.namespaces?.get(NAMESPACE_MDL)
?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true)))
?: return Verifier.VerifyPresentationResult.InvalidStructure(
doc.serialize().encodeToString(Base16(strict = true))
)
.also { Napier.w("No issuer signed items in ${issuerSigned.namespaces}") }

val validatedItems = issuerSignedItems.entries.associateWith { it.verify(mdlItems) }
Expand All @@ -253,7 +277,9 @@ class Validator(
val issuerHash = mdlItems.entries.first { it.key == value.digestId }
// TODO analyze usages of tag wrapping
val verifierHash = serialized.wrapInCborTag(24).sha256()
if (!verifierHash.encodeToString(Base16(strict = true)).contentEquals(issuerHash.value.encodeToString(Base16(strict = true)))) {
if (!verifierHash.encodeToString(Base16(strict = true))
.contentEquals(issuerHash.value.encodeToString(Base16(strict = true)))
) {
Napier.w("Could not verify hash of value for ${value.elementIdentifier}")
return false
}
Expand Down Expand Up @@ -305,12 +331,16 @@ class Validator(
Napier.d("Verifying ISO Cred $it")
if (issuerKey == null) {
Napier.w("ISO: No issuer key")
return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyCredentialResult.InvalidStructure(
it.serialize().encodeToString(Base16(strict = true))
)
}
val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey)
if (result.getOrNull() != true) {
Napier.w("ISO: Could not verify credential", result.exceptionOrNull())
return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true)))
return Verifier.VerifyCredentialResult.InvalidStructure(
it.serialize().encodeToString(Base16(strict = true))
)
}
return Verifier.VerifyCredentialResult.SuccessIso(it)
}
Expand Down
10 changes: 9 additions & 1 deletion vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ val jsonSerializer by lazy {
}
}

val Base64Url = Base64(config = Base64ConfigBuilder().apply {
val Base64UrlStrict = Base64(config = Base64ConfigBuilder().apply {
lineBreakInterval = 0
encodeToUrlSafe = true
isLenient = true
padEncoded = false
}.build())


val Base64Strict = Base64(config = Base64ConfigBuilder().apply {
lineBreakInterval = 0
encodeToUrlSafe = false
isLenient = true
padEncoded = true
}.build())
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package at.asitplus.wallet.lib.jws

import io.matthewnelson.encoding.base64.Base64
import at.asitplus.wallet.lib.data.Base64Strict
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.KSerializer
Expand All @@ -16,11 +16,11 @@ object ByteArrayBase64Serializer : KSerializer<ByteArray> {
PrimitiveSerialDescriptor("ByteArrayBase64Serializer", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: ByteArray) {
encoder.encodeString(value.encodeToString(Base64(strict = true)))
encoder.encodeString(value.encodeToString(Base64Strict))
}

override fun deserialize(decoder: Decoder): ByteArray {
return decoder.decodeString().decodeToByteArrayOrNull(Base64(strict = true)) ?: byteArrayOf()
return decoder.decodeString().decodeToByteArrayOrNull(Base64Strict) ?: byteArrayOf()
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package at.asitplus.wallet.lib.jws

import at.asitplus.wallet.lib.data.Base64Url
import at.asitplus.wallet.lib.data.Base64UrlStrict
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.KSerializer
Expand All @@ -16,11 +16,11 @@ object ByteArrayBase64UrlSerializer : KSerializer<ByteArray> {
PrimitiveSerialDescriptor("ByteArrayBase64UrlSerializer", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: ByteArray) {
encoder.encodeString(value.encodeToString(Base64Url))
encoder.encodeString(value.encodeToString(Base64UrlStrict))
}

override fun deserialize(decoder: Decoder): ByteArray {
return decoder.decodeString().decodeToByteArrayOrNull(Base64Url) ?: byteArrayOf()
return decoder.decodeString().decodeToByteArrayOrNull(Base64UrlStrict) ?: byteArrayOf()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package at.asitplus.wallet.lib.jws

import at.asitplus.KmmResult
import at.asitplus.wallet.lib.CryptoPublicKey
import at.asitplus.wallet.lib.data.Base64Strict
import at.asitplus.wallet.lib.data.jsonSerializer
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -136,9 +136,9 @@ data class JsonWebKey(
}

override fun toString(): String {
return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeToString(Base64(strict = true))}, y=${
y?.encodeToString(Base64(strict = true))
})"
return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId," +
" x=${x?.encodeToString(Base64Strict)}," +
" y=${y?.encodeToString(Base64Strict)})"
}

fun toCryptoPublicKey(): CryptoPublicKey? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package at.asitplus.wallet.lib.jws

import at.asitplus.wallet.lib.data.Base64Url
import at.asitplus.wallet.lib.data.Base64Strict
import at.asitplus.wallet.lib.data.Base64UrlStrict
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString

Expand All @@ -23,11 +23,11 @@ data class JweEncrypted(
get() = JweHeader.deserialize(headerAsParsed.decodeToString())

fun serialize(): String {
return headerAsParsed.encodeToString(Base64Url) +
".${encryptedKey?.encodeToString(Base64Url) ?: ""}" +
".${iv.encodeToString(Base64Url)}" +
".${ciphertext.encodeToString(Base64Url)}" +
".${authTag.encodeToString(Base64Url)}"
return headerAsParsed.encodeToString(Base64UrlStrict) +
".${encryptedKey?.encodeToString(Base64UrlStrict) ?: ""}" +
".${iv.encodeToString(Base64UrlStrict)}" +
".${ciphertext.encodeToString(Base64UrlStrict)}" +
".${authTag.encodeToString(Base64UrlStrict)}"
}

override fun equals(other: Any?): Boolean {
Expand Down Expand Up @@ -62,14 +62,14 @@ data class JweEncrypted(
fun parse(it: String): JweEncrypted? {
val stringList = it.replace("[^A-Za-z0-9-_.]".toRegex(), "").split(".")
if (stringList.size != 5) return null.also { Napier.w("Could not parse JWE: $it") }
val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64(strict = true))
val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64Strict)
?: return null.also { Napier.w("Could not parse JWE: $it") }
val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64(strict = true))
val iv = stringList[2].decodeToByteArrayOrNull(Base64(strict = true))
val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64Strict)
val iv = stringList[2].decodeToByteArrayOrNull(Base64Strict)
?: return null.also { Napier.w("Could not parse JWE: $it") }
val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64(strict = true))
val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64Strict)
?: return null.also { Napier.w("Could not parse JWE: $it") }
val authTag = stringList[4].decodeToByteArrayOrNull(Base64(strict = true))
val authTag = stringList[4].decodeToByteArrayOrNull(Base64Strict)
?: return null.also { Napier.w("Could not parse JWE: $it") }
return JweEncrypted(headerAsParsed, encryptedKey, iv, ciphertext, authTag)
}
Expand Down
Loading

0 comments on commit 11f14a8

Please sign in to comment.