diff --git a/Core b/Core index cc66df0..e0b57f6 160000 --- a/Core +++ b/Core @@ -1 +1 @@ -Subproject commit cc66df014eee602c935a48719ba14c328e421810 +Subproject commit e0b57f6c944be5927c79242d1f54198af69e1bec diff --git a/multiplatform-lib/build.gradle.kts b/multiplatform-lib/build.gradle.kts index 179e531..0301948 100644 --- a/multiplatform-lib/build.gradle.kts +++ b/multiplatform-lib/build.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework plugins { alias(libs.plugins.kotlin.multiplatform) alias(core.plugins.android.kmp.library) + alias(core.plugins.kotlin.serialization) alias(libs.plugins.skie) alias(libs.plugins.androidx.room) alias(libs.plugins.ksp) @@ -52,7 +53,9 @@ kotlin { commonMain { dependencies { implementation(core.kotlinx.coroutines.core) + implementation(core.kotlinx.serialization.core) implementation(core.kotlinx.serialization.cbor) + implementation(core.okio) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negociation) implementation(libs.ktor.client.json) diff --git a/multiplatform-lib/src/androidDeviceTest/kotlin/WebAuthnTest.kt b/multiplatform-lib/src/androidDeviceTest/kotlin/WebAuthnTest.kt new file mode 100644 index 0000000..513d6ee --- /dev/null +++ b/multiplatform-lib/src/androidDeviceTest/kotlin/WebAuthnTest.kt @@ -0,0 +1,65 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth + +import com.infomaniak.auth.lib.RegisterPasskeyBuilder +import com.infomaniak.auth.lib.internal.KeyPairManagerImpl +import com.infomaniak.auth.lib.network.models.PasskeysOptions +import com.infomaniak.auth.lib.network.models.PubKeyCredParam +import com.infomaniak.auth.lib.network.models.RelyingParty +import com.infomaniak.auth.lib.network.models.User +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlin.test.Test + +class WebAuthnTest { + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun registerPasskeyGeneration() = runTest { + // This is sent from [GET] /api/authenticator/passkeys/options + val passkeysOptions = PasskeysOptions( + challenge = "U3NkRnF6RlVwUnpKRGhVMw", + relyingParty = RelyingParty( + id = "infomaniak.com", + name = "Infomaniak", + icon = null, + ), + user = User( + id = "MQ", + name = "test@user.com", + displayName = "Test" + ), + pubKeyCredParams = listOf( + PubKeyCredParam( + type = "public-key", + algorithm = -7 // ES256 + ) + ), + excludeCredentials = emptyList(), + ) + + // Just getting the public key to generate RegisterPasskey object + val keyPairManager = KeyPairManagerImpl() + keyPairManager.generateNewKey() + val publicKeyAsByteArray = keyPairManager.retrievePublicKey().firstOrNull()!! + + // Nothing to test on the generated object for now + RegisterPasskeyBuilder(passkeysOptions, publicKeyAsByteArray).build() + } +} diff --git a/multiplatform-lib/src/androidDeviceTest/kotlin/internal/KeyPairManagerTest.kt b/multiplatform-lib/src/androidDeviceTest/kotlin/internal/KeyPairManagerTest.kt index 5c444cd..a954edf 100644 --- a/multiplatform-lib/src/androidDeviceTest/kotlin/internal/KeyPairManagerTest.kt +++ b/multiplatform-lib/src/androidDeviceTest/kotlin/internal/KeyPairManagerTest.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package internal +package com.infomaniak.auth.internal import com.infomaniak.auth.lib.internal.KeyPairManagerImpl import com.infomaniak.auth.lib.internal.Xor diff --git a/multiplatform-lib/src/androidMain/kotlin/PublicKeyUtils.kt b/multiplatform-lib/src/androidMain/kotlin/PublicKeyUtils.kt new file mode 100644 index 0000000..a19ad4f --- /dev/null +++ b/multiplatform-lib/src/androidMain/kotlin/PublicKeyUtils.kt @@ -0,0 +1,61 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.encodeToByteArray +import java.security.KeyFactory +import java.security.PublicKey +import java.security.interfaces.ECPublicKey +import java.security.spec.X509EncodedKeySpec + +actual object PublicKeyUtils { + + @OptIn(ExperimentalSerializationApi::class) + actual fun getPublicKeyCose(publicKeyByteArray: ByteArray): ByteArray { + val publicKey = getPublicKeyFromByteArray(publicKeyByteArray) as ECPublicKey + val w = publicKey.w + val x = w.affineX.toByteArray().padTo32Bytes() + val y = w.affineY.toByteArray().padTo32Bytes() + val coseKey = CoseKey( + kty = 2, // kty: EC2 + alg = -7,// alg: ES256 + crv = -1,// crv: P-2 + x = x, // x coord + y = y // y coord + ) + return Cbor.encodeToByteArray(coseKey) + } + + private fun getPublicKeyFromByteArray(bytes: ByteArray): PublicKey { + val keySpec = X509EncodedKeySpec(bytes) + val keyFactory = KeyFactory.getInstance("EC") + return keyFactory.generatePublic(keySpec) + } + + private fun ByteArray.padTo32Bytes(): ByteArray { + return if (this.size == 32) { + this + } else if (this.size > 32) { + this.copyOfRange(this.size - 32, this.size) + } else { + ByteArray(32 - this.size) { 0x00 } + this + } + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/Account.kt b/multiplatform-lib/src/commonMain/kotlin/Account.kt index 216ca31..06049ba 100644 --- a/multiplatform-lib/src/commonMain/kotlin/Account.kt +++ b/multiplatform-lib/src/commonMain/kotlin/Account.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.infomaniak.auth.lib @ConsistentCopyVisibility diff --git a/multiplatform-lib/src/commonMain/kotlin/AppStatus.kt b/multiplatform-lib/src/commonMain/kotlin/AppStatus.kt index a0d7393..ac93c2f 100644 --- a/multiplatform-lib/src/commonMain/kotlin/AppStatus.kt +++ b/multiplatform-lib/src/commonMain/kotlin/AppStatus.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.infomaniak.auth.lib sealed interface AppStatus { diff --git a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt index 6eea7e1..e9f5a35 100644 --- a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt +++ b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.infomaniak.auth.lib import kotlinx.coroutines.flow.Flow diff --git a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt index 0e7ebb4..ccfeb1a 100644 --- a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt +++ b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt @@ -15,29 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.infomaniak.auth.lib + import com.infomaniak.auth.lib.network.ApiClientProvider import com.infomaniak.auth.lib.network.interfaces.CrashReportInterface import network.repositories.WebAuthnRepository import network.utils.ApiEnvironment -/* - Infomaniak Authenticator - Android - Copyright (C) 2026 Infomaniak Network SA - - This program 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. - - This program 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 this program. If not, see . - */ - class AuthenticatorInjection( private val environment: ApiEnvironment, private val userAgent: String, @@ -45,6 +29,6 @@ class AuthenticatorInjection( private val crashReport: CrashReportInterface, ) { private val apiClientProvider by lazy { ApiClientProvider(userAgent, crashReport) } - + private val webAuthnRepository by lazy { WebAuthnRepository(apiClientProvider, environment) } } diff --git a/multiplatform-lib/src/commonMain/kotlin/CoseKey.kt b/multiplatform-lib/src/commonMain/kotlin/CoseKey.kt new file mode 100644 index 0000000..1627e53 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/CoseKey.kt @@ -0,0 +1,72 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.io.encoding.Base64 + +@Serializable +data class CoseKey( + val kty: Int, + val alg: Int, + val crv: Int, + @Serializable(with = ByteArrayAsByteListBase64Serializer::class) + val x: ByteArray, + @Serializable(with = ByteArrayAsByteListBase64Serializer::class) + val y: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as CoseKey + + if (kty != other.kty) return false + if (alg != other.alg) return false + if (crv != other.crv) return false + if (!x.contentEquals(other.x)) return false + if (!y.contentEquals(other.y)) return false + + return true + } + + override fun hashCode(): Int { + var result = kty + result = 31 * result + alg + result = 31 * result + crv + result = 31 * result + x.contentHashCode() + result = 31 * result + y.contentHashCode() + return result + } +} + +object ByteArrayAsByteListBase64Serializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ByteArrayAsByteListBase64", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ByteArray) { + encoder.encodeString(Base64.UrlSafe.encode(value)) + } + + override fun deserialize(decoder: Decoder) = Base64.UrlSafe.decode(decoder.decodeString()) +} diff --git a/multiplatform-lib/src/commonMain/kotlin/CredentialsForMigration.kt b/multiplatform-lib/src/commonMain/kotlin/CredentialsForMigration.kt index 2912f3a..efd308a 100644 --- a/multiplatform-lib/src/commonMain/kotlin/CredentialsForMigration.kt +++ b/multiplatform-lib/src/commonMain/kotlin/CredentialsForMigration.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.infomaniak.auth.lib data class CredentialsForMigration( diff --git a/multiplatform-lib/src/commonMain/kotlin/NotConnectedAction.kt b/multiplatform-lib/src/commonMain/kotlin/NotConnectedAction.kt index e1cdbeb..2bd3375 100644 --- a/multiplatform-lib/src/commonMain/kotlin/NotConnectedAction.kt +++ b/multiplatform-lib/src/commonMain/kotlin/NotConnectedAction.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.infomaniak.auth.lib sealed interface NotConnectedAction { diff --git a/multiplatform-lib/src/commonMain/kotlin/PublicKeyUtils.kt b/multiplatform-lib/src/commonMain/kotlin/PublicKeyUtils.kt new file mode 100644 index 0000000..5bd18fc --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/PublicKeyUtils.kt @@ -0,0 +1,23 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib + +expect object PublicKeyUtils { + + fun getPublicKeyCose(publicKeyByteArray: ByteArray): ByteArray +} diff --git a/multiplatform-lib/src/commonMain/kotlin/RegisterPassKeyBuilder.kt b/multiplatform-lib/src/commonMain/kotlin/RegisterPassKeyBuilder.kt new file mode 100644 index 0000000..5c9b8fd --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/RegisterPassKeyBuilder.kt @@ -0,0 +1,115 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib + +import com.infomaniak.auth.lib.PublicKeyUtils.getPublicKeyCose +import com.infomaniak.auth.lib.network.models.ClientExtensionResults +import com.infomaniak.auth.lib.network.models.PasskeysOptions +import com.infomaniak.auth.lib.network.models.RegisterPasskey +import com.infomaniak.auth.lib.network.models.RegisterPasskeyResponse +import com.infomaniak.auth.lib.network.models.WebAuthnAttestationObject +import com.infomaniak.auth.lib.network.models.WebAuthnClientData +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.json.Json +import okio.ByteString.Companion.encodeUtf8 +import kotlin.io.encoding.Base64 +import kotlin.random.Random +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +class RegisterPasskeyBuilder( + private val passkeysOptions: PasskeysOptions, + private val publicKey: ByteArray, +) { + + fun build(): RegisterPasskey { + val publicKeyCose = getPublicKeyCose(publicKey) + val randomInt = Random(passkeysOptions.challenge.hashCode().toLong()) + val stringId = "$randomInt${passkeysOptions.user}${passkeysOptions.relyingParty}" + val rawId = stringId.encodeToByteArray() + val id = Base64.encode(rawId) + + // AttestationObject + val attestationObject = generateAttestationObject( + rpId = passkeysOptions.relyingParty.id, + credentialId = rawId, + publicKeyCose = publicKeyCose, + ) + + // AuthenticatorData + // Using rawId for credentialId because it has to be something unique + val authenticatorData = generateAuthData( + rpId = passkeysOptions.relyingParty.id, + credentialId = rawId, + publicKeyCose = publicKeyCose + ) + val clientData = WebAuthnClientData( + type = "webauthn.create", + challenge = passkeysOptions.challenge, + origin = "infomaniak.com", + crossOrigin = false, + ) + + val response = RegisterPasskeyResponse( + attestationObject = Base64.encode(Cbor.encodeToByteArray(attestationObject)), + clientDataJSON = Base64.encode(Json.encodeToString(clientData).encodeToByteArray()), + transports = listOf("internal"), + publicKeyAlgorithm = -7, + publicKey = Base64.UrlSafe.encode(publicKey), + authenticatorData = Base64.encode(authenticatorData), + ) + val type = "public-key" + val clientExtensionResult = ClientExtensionResults + val authenticatorAttachment = "platform" + + return RegisterPasskey( + device = Uuid.random().toHexDashString(), + id = id, + rawId = Base64.encode(rawId), + registerPasskeyResponse = response, + type = type, + clientExtensionResults = clientExtensionResult, + authenticatorAttachment = authenticatorAttachment, + ) + } + + private fun generateAuthData(rpId: String, credentialId: ByteArray, publicKeyCose: ByteArray): ByteArray { + val rpIdHash = rpId.encodeUtf8().sha256().toByteArray() + val flags: Byte = 0x41 + val signCount = byteArrayOf(0x00, 0x00, 0x00, 0x00) + + // attestedCredentialData = AAGUID (16 bytes) + credentialId length + credentialId + publicKeyCose + val aaguid = ByteArray(16) { 0x00 } + val credentialIdLength = byteArrayOf((credentialId.size shr 8).toByte(), (credentialId.size and 0xFF).toByte()) + + return rpIdHash + flags + signCount + aaguid + credentialIdLength + credentialId + publicKeyCose + } + + private fun generateAttestationObject(rpId: String, credentialId: ByteArray, publicKeyCose: ByteArray): ByteArray { + val authData = generateAuthData(rpId, credentialId, publicKeyCose) + + val attestationObject = WebAuthnAttestationObject( + fmt = "none", + authData = authData + ) + + return Cbor.encodeToByteArray(attestationObject) + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt index 1f8fa26..d21147f 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt index 55622c6..8375996 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt index ca06ef0..c106ea9 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt index 4da21af..a4fbb06 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt index cd34ec8..5dfd3d5 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt index 53e1807..7938b82 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt index bcdb5ca..2e5fa68 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt index 8fadffa..67ceded 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt index e7d1e38..18ab67b 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt index 3b05599..b3cf4aa 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable @@ -24,4 +24,7 @@ data class RelyingParty( val id: String, val name: String, val icon: String?, -) +) { + + override fun toString() = "$id:$name" +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt index 77e7b3c..fe71236 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable @@ -24,4 +24,7 @@ data class User( val id: String, val name: String, val displayName: String?, -) +) { + + override fun toString() = "$id:$name:$displayName" +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt index f8d8810..95a5b66 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt index e69d8b3..431afb3 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package network.models +package com.infomaniak.auth.lib.network.models import kotlinx.serialization.Serializable diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnAttestationObject.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnAttestationObject.kt new file mode 100644 index 0000000..fcc0575 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnAttestationObject.kt @@ -0,0 +1,64 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib.network.models + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +data class WebAuthnAttestationObject( + val fmt: String, + @Serializable(with = ByteArrayAsByteListSerializer::class) + val authData: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as WebAuthnAttestationObject + + if (fmt != other.fmt) return false + if (!authData.contentEquals(other.authData)) return false + + return true + } + + override fun hashCode(): Int { + var result = fmt.hashCode() + result = 31 * result + authData.contentHashCode() + return result + } +} + +private object ByteArrayAsByteListSerializer : KSerializer { + private val delegate = ByteArraySerializer() + + override val descriptor: SerialDescriptor = delegate.descriptor + + override fun serialize(encoder: Encoder, value: ByteArray) { + delegate.serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): ByteArray { + return delegate.deserialize(decoder) + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnClientData.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnClientData.kt new file mode 100644 index 0000000..4987c9d --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/WebAuthnClientData.kt @@ -0,0 +1,28 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib.network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class WebAuthnClientData( + val type: String, + val challenge: String, + val origin: String, + val crossOrigin: Boolean +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt b/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt index 0a5977b..c3094f9 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt @@ -18,15 +18,15 @@ package network.repositories import com.infomaniak.auth.lib.network.ApiClientProvider +import com.infomaniak.auth.lib.network.models.AuthResult +import com.infomaniak.auth.lib.network.models.AuthenticationOptions +import com.infomaniak.auth.lib.network.models.ClientExtensionResults +import com.infomaniak.auth.lib.network.models.PasskeysOptions +import com.infomaniak.auth.lib.network.models.RegisterPasskey +import com.infomaniak.auth.lib.network.models.VerifyAuthenticationData +import com.infomaniak.auth.lib.network.models.VerifyResponse import io.ktor.client.HttpClient import kotlinx.serialization.json.Json -import network.models.AuthResult -import network.models.AuthenticationOptions -import network.models.ClientExtensionResults -import network.models.PasskeysOptions -import network.models.RegisterPasskey -import network.models.VerifyAuthenticationData -import network.models.VerifyResponse import network.requests.AuthenticatorRequest import network.utils.ApiEnvironment @@ -42,13 +42,25 @@ class WebAuthnRepository internal constructor(private val authenticatorRequest: internal constructor(environment: ApiEnvironment, json: Json, httpClient: HttpClient) : this(AuthenticatorRequest(environment, json, httpClient)) + // Generate WebAuthn registration options (authentified) + suspend fun getPasskeysOptions(): PasskeysOptions { + //TODO where to get the bearer from ? + return authenticatorRequest.getPasskeysOptions() + } + + // Validate WebAuthn registration and save public key (authentified) + suspend fun registerPasskey(registerPasskey: RegisterPasskey): Result { + //TODO where to get the bearer from ? + return authenticatorRequest.registerPasskey(registerPasskey) + } + // Authentification challenge (not authentified) - suspend fun getAuthenticationOptions(identity: Long): AuthenticationOptions { + suspend fun challenge(identity: Long): AuthenticationOptions { return authenticatorRequest.challenge(identity) } // Authentification verification (not authentified) - suspend fun verifyAuthentication( + suspend fun verify( identity: Long, id: String, rawId: String, @@ -69,16 +81,4 @@ class WebAuthnRepository internal constructor(private val authenticatorRequest: ) ) } - - // Generate WebAuthn registration options (authentified) - suspend fun getRegistrationOptions(): PasskeysOptions { - //TODO where to get the bearer ? - return authenticatorRequest.getPasskeysOptions() - } - - // Validate WebAuthn registration and save public key (authentified) - suspend fun registerCredential(registerPasskey: RegisterPasskey): Result { - //TODO where to get the bearer ? - return authenticatorRequest.registerPasskey(registerPasskey) - } } diff --git a/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt b/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt index dc49f90..337220e 100644 --- a/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt +++ b/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt @@ -17,13 +17,13 @@ */ package network.requests +import com.infomaniak.auth.lib.network.models.AuthResult +import com.infomaniak.auth.lib.network.models.AuthenticationOptions +import com.infomaniak.auth.lib.network.models.PasskeysOptions +import com.infomaniak.auth.lib.network.models.RegisterPasskey +import com.infomaniak.auth.lib.network.models.VerifyAuthenticationData import io.ktor.client.HttpClient import kotlinx.serialization.json.Json -import network.models.AuthResult -import network.models.AuthenticationOptions -import network.models.PasskeysOptions -import network.models.RegisterPasskey -import network.models.VerifyAuthenticationData import network.utils.ApiEnvironment import network.utils.ApiRoutes diff --git a/multiplatform-lib/src/commonMain/kotlin/room/accounts/AccountsDao.kt b/multiplatform-lib/src/commonMain/kotlin/room/accounts/AccountsDao.kt index 56ce6b5..a6c666d 100644 --- a/multiplatform-lib/src/commonMain/kotlin/room/accounts/AccountsDao.kt +++ b/multiplatform-lib/src/commonMain/kotlin/room/accounts/AccountsDao.kt @@ -30,11 +30,11 @@ interface AccountsDao { fun getAsFlow(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) - fun update(account: AccountEntity) + suspend fun update(account: AccountEntity) @Insert - fun insert(account: AccountEntity) + suspend fun insert(account: AccountEntity) @Query("DELETE FROM AccountEntity WHERE id = :id") - fun delete(id: Long) + suspend fun delete(id: Long) } diff --git a/multiplatform-lib/src/iosMain/kotlin/PublicKeyUtils.kt b/multiplatform-lib/src/iosMain/kotlin/PublicKeyUtils.kt new file mode 100644 index 0000000..c60d840 --- /dev/null +++ b/multiplatform-lib/src/iosMain/kotlin/PublicKeyUtils.kt @@ -0,0 +1,41 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.auth.lib + +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.encodeToByteArray + +actual object PublicKeyUtils { + + actual fun getPublicKeyCose(publicKeyByteArray: ByteArray): ByteArray { + require(publicKeyByteArray.size == 65) { "Invalid public key format" } + require(publicKeyByteArray[0] == 0x04.toByte()) { "Expected uncompressed format" } + + val x = publicKeyByteArray.copyOfRange(1, 33) + val y = publicKeyByteArray.copyOfRange(33, 65) + + val coseKey = CoseKey( + kty = 2, // EC2 + alg = -7,// ES256 + crv = 1, // P-256 + x = x, // x coord + y = y // y coord + ) + return Cbor.encodeToByteArray(coseKey) + } +}