Skip to content

Commit

Permalink
feat!: regular group members were also impacted by unordered messages…
Browse files Browse the repository at this point in the history
…. As a consequence, messages from future epochs are now buffered.

- `decryptMessage` might throw a `BufferedFutureMessage` error which you should catch & ignore
- `commitAccepted` now returns an optional list of decrypted messages
  • Loading branch information
beltram committed Aug 14, 2023
1 parent 2cd91ca commit 415b5c3
Show file tree
Hide file tree
Showing 24 changed files with 8,677 additions and 113 deletions.
Empty file.
6,886 changes: 6,886 additions & 0 deletions crypto-ffi/bindings/android/src/main/kotlin/com/wire/crypto/CoreCrypto.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.wire.crypto.client

import com.wire.crypto.*
import java.io.File

typealias EnrollmentHandle = ByteArray

private class Callbacks : CoreCryptoCallbacks {

override fun authorize(conversationId: ByteArray, clientId: List<UByte>): Boolean = true

override fun userAuthorize(
conversationId: ByteArray,
externalClientId: List<UByte>,
existingClients: List<List<UByte>>
): Boolean = true

override fun clientIsExistingGroupUser(
conversationId: ByteArray,
clientId: List<UByte>,
existingClients: List<List<UByte>>,
parentConversationClients: List<List<UByte>>?
): Boolean = true
}

@Suppress("TooManyFunctions")
@OptIn(ExperimentalUnsignedTypes::class)
class CoreCryptoCentral private constructor(private val cc: CoreCrypto, private val rootDir: String) {
suspend fun proteusClient(): ProteusClient = ProteusClientImpl(cc, rootDir)

suspend fun mlsClient(clientId: ClientId): MLSClient = MLSClient(cc).apply { mlsInit(clientId) }

suspend fun e2eiNewEnrollment(
clientId: String,
displayName: String,
handle: String,
expiryDays: UInt,
ciphersuite: Ciphersuite,
): E2EIClient {
return E2EIClient(cc.e2eiNewEnrollment(clientId, displayName, handle, expiryDays, ciphersuite.lower()))
}

suspend fun e2eiNewActivationEnrollment(
clientId: String,
displayName: String,
handle: String,
expiryDays: UInt,
ciphersuite: Ciphersuite,
): E2EIClient {
return E2EIClient(
cc.e2eiNewActivationEnrollment(
clientId,
displayName,
handle,
expiryDays,
ciphersuite.lower()
)
)
}

suspend fun e2eiNewRotateEnrollment(
clientId: String,
expiryDays: UInt,
ciphersuite: Ciphersuite,
displayName: String? = null,
handle: String? = null,
): E2EIClient {
return E2EIClient(cc.e2eiNewRotateEnrollment(clientId, displayName, handle, expiryDays, ciphersuite.lower()))
}

suspend fun e2eiMlsInitOnly(enrollment: E2EIClient, certificateChain: String): MLSClient {
cc.e2eiMlsInitOnly(enrollment.delegate, certificateChain)
return MLSClient(cc)
}

suspend fun e2eiEnrollmentStash(enrollment: E2EIClient): EnrollmentHandle {
return cc.e2eiEnrollmentStash(enrollment.delegate).toUByteArray().asByteArray()
}

suspend fun e2eiEnrollmentStashPop(handle: EnrollmentHandle): E2EIClient {
return E2EIClient(cc.e2eiEnrollmentStashPop(handle.asUByteArray().asList()))
}

companion object {
private const val KEYSTORE_NAME = "keystore"
fun CiphersuiteName.lower() = (ordinal + 1).toUShort()
val DEFAULT_CREDENTIAL_TYPE = MlsCredentialType.BASIC
val DEFAULT_CIPHERSUITE = CiphersuiteName.MLS_128_DHKEMX25519_AES128GCM_SHA256_ED25519.lower()
val DEFAULT_CIPHERSUITES = listOf(DEFAULT_CIPHERSUITE)

suspend operator fun invoke(rootDir: String, databaseKey: String): CoreCryptoCentral {
val path = "$rootDir/$KEYSTORE_NAME"
File(rootDir).mkdirs()
val cc = coreCryptoDeferredInit(path, databaseKey, DEFAULT_CIPHERSUITES)
cc.setCallbacks(Callbacks())
return CoreCryptoCentral(cc, rootDir)
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package com.wire.crypto.client

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.wire.crypto.client

import com.wire.crypto.WireE2eIdentity
import com.wire.crypto.client.AcmeChallenge.Companion.toAcmeChallenge
import com.wire.crypto.client.AcmeDirectory.Companion.toAcmeDirectory
import com.wire.crypto.client.NewAcmeAuthz.Companion.toNewAcmeAuthz
import com.wire.crypto.client.NewAcmeOrder.Companion.toNewAcmeOrder

typealias JsonRawData = ByteArray
typealias DpopToken = String

data class AcmeDirectory(
val newNonce: String,
val newAccount: String,
val newOrder: String
) {
constructor(delegate: com.wire.crypto.AcmeDirectory) : this(
delegate.newNonce,
delegate.newAccount,
delegate.newOrder
)

companion object {
fun com.wire.crypto.AcmeDirectory.toAcmeDirectory() = AcmeDirectory(this)
}
}

data class NewAcmeOrder(val delegate: JsonRawData, val authorizations: List<String>) {

@OptIn(ExperimentalUnsignedTypes::class)
constructor(delegate: com.wire.crypto.NewAcmeOrder) : this(
delegate.delegate.toUByteArray().asByteArray(),
delegate.authorizations,
)

companion object {
fun com.wire.crypto.NewAcmeOrder.toNewAcmeOrder() = NewAcmeOrder(this)
}
}

data class AcmeChallenge(val delegate: JsonRawData, val url: String) {
@OptIn(ExperimentalUnsignedTypes::class)
constructor(delegate: com.wire.crypto.AcmeChallenge) : this(
delegate.delegate.toUByteArray().asByteArray(), delegate.url
)

companion object {
fun com.wire.crypto.AcmeChallenge.toAcmeChallenge() = AcmeChallenge(this)
}
}

data class NewAcmeAuthz(
val identifier: String,
val wireOidcChallenge: AcmeChallenge?,
val wireDpopChallenge: AcmeChallenge?
) {
constructor(delegate: com.wire.crypto.NewAcmeAuthz) : this(
delegate.identifier,
delegate.wireOidcChallenge?.toAcmeChallenge(),
delegate.wireDpopChallenge?.toAcmeChallenge(),
)

companion object {
fun com.wire.crypto.NewAcmeAuthz.toNewAcmeAuthz() = NewAcmeAuthz(this)
}
}

@Suppress("TooManyFunctions")
@OptIn(ExperimentalUnsignedTypes::class)
class E2EIClient(val delegate: WireE2eIdentity) {

private val defaultDPoPTokenExpiry: UInt = 30U

suspend fun directoryResponse(directory: JsonRawData) =
delegate.directoryResponse(directory.toUByteList()).toAcmeDirectory()

suspend fun newAccountRequest(previousNonce: String) =
delegate.newAccountRequest(previousNonce).toByteArray()

suspend fun accountResponse(account: JsonRawData) =
delegate.newAccountResponse(account.toUByteList())

suspend fun newOrderRequest(previousNonce: String) =
delegate.newOrderRequest(previousNonce).toByteArray()

suspend fun newOrderResponse(order: JsonRawData) =
delegate.newOrderResponse(order.toUByteList()).toNewAcmeOrder()

suspend fun newAuthzRequest(url: String, previousNonce: String) =
delegate.newAuthzRequest(url, previousNonce).toByteArray()

suspend fun authzResponse(authz: JsonRawData) =
delegate.newAuthzResponse(authz.toUByteList()).toNewAcmeAuthz()

suspend fun createDpopToken(backendNonce: String) =
delegate.createDpopToken(expirySecs = defaultDPoPTokenExpiry, backendNonce)

suspend fun newDpopChallengeRequest(accessToken: String, previousNonce: String) =
delegate.newDpopChallengeRequest(accessToken, previousNonce).toByteArray()

suspend fun newOidcChallengeRequest(idToken: String, previousNonce: String) =
delegate.newOidcChallengeRequest(idToken, previousNonce).toByteArray()

suspend fun challengeResponse(challenge: JsonRawData) =
delegate.newChallengeResponse(challenge.toUByteList())

suspend fun checkOrderRequest(orderUrl: String, previousNonce: String) =
delegate.checkOrderRequest(orderUrl, previousNonce).toByteArray()

suspend fun checkOrderResponse(order: JsonRawData) =
delegate.checkOrderResponse(order.toUByteList())

suspend fun finalizeRequest(previousNonce: String) =
delegate.finalizeRequest(previousNonce).toByteArray()

suspend fun finalizeResponse(finalize: JsonRawData) =
delegate.finalizeResponse(finalize.toUByteList())

suspend fun certificateRequest(previousNonce: String) =
delegate.certificateRequest(previousNonce).toByteArray()

companion object {

fun ByteArray.toUByteList(): List<UByte> = map { it.toUByte() }
fun List<UByte>.toByteArray() = toUByteArray().asByteArray()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* 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 http://www.gnu.org/licenses/.
*/

package com.wire.crypto.client

import com.wire.crypto.client.CoreCryptoCentral.Companion.DEFAULT_CIPHERSUITE
import com.wire.crypto.client.CoreCryptoCentral.Companion.DEFAULT_CIPHERSUITES
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

@Suppress("TooManyFunctions")
@OptIn(ExperimentalUnsignedTypes::class)
class MLSClient(private val cc: com.wire.crypto.CoreCrypto) {

companion object {

private val keyRotationDuration: Duration = 30.toDuration(DurationUnit.DAYS)
private val defaultGroupConfiguration =
com.wire.crypto.CustomConfiguration(
java.time.Duration.ofDays(keyRotationDuration.inWholeDays),
com.wire.crypto.MlsWirePolicy.PLAINTEXT
)

}

suspend fun mlsInit(id: ClientId) {
cc.mlsInit(id.lower(), DEFAULT_CIPHERSUITES)
}

suspend fun getPublicKey(ciphersuite: Ciphersuite): SignaturePublicKey {
return cc.clientPublicKey(ciphersuite.lower()).toSignaturePublicKey()
}

suspend fun generateKeyPackages(
ciphersuite: Ciphersuite,
credentialType: CredentialType,
amount: UInt
): List<MLSKeyPackage> {
return cc.clientKeypackages(ciphersuite.lower(), credentialType.lower(), amount)
.map { it.toMLSKeyPackage() }
}

suspend fun validKeyPackageCount(ciphersuite: Ciphersuite, credentialType: CredentialType): ULong {
return cc.clientValidKeypackagesCount(ciphersuite.lower(), credentialType.lower())
}

suspend fun updateKeyingMaterial(id: MLSGroupId) = CommitBundle(cc.updateKeyingMaterial(id.lower()))

suspend fun conversationExists(id: MLSGroupId): Boolean = cc.conversationExists(id.lower())

suspend fun conversationEpoch(id: MLSGroupId): ULong = cc.conversationEpoch(id.lower())

suspend fun joinConversation(
id: MLSGroupId,
epoch: ULong,
ciphersuite: Ciphersuite,
credentialType: CredentialType,
): MlsMessage {
return cc.newExternalAddProposal(id.lower(), epoch, ciphersuite.lower(), credentialType.lower()).toMlsMessage()
}

suspend fun joinByExternalCommit(groupInfo: GroupInfo, credentialType: CredentialType): CommitBundle {
return CommitBundle(
cc.joinByExternalCommit(
groupInfo.lower(),
defaultGroupConfiguration,
credentialType.lower()
)
)
}

suspend fun mergePendingGroupFromExternalCommit(id: MLSGroupId): List<DecryptedMessage>? {
return cc.mergePendingGroupFromExternalCommit(id.lower())?.map { DecryptedMessage(it) }
}

suspend fun clearPendingGroupExternalCommit(id: MLSGroupId) = cc.clearPendingGroupFromExternalCommit(id.lower())

suspend fun createConversation(
id: MLSGroupId,
creatorCredentialType: CredentialType,
externalSenders: List<ExternalSenderKey> = emptyList(),
perDomainTrustAnchors: List<com.wire.crypto.PerDomainTrustAnchor> = emptyList(),
) {
val cfg = com.wire.crypto.ConversationConfiguration(
DEFAULT_CIPHERSUITE,
externalSenders.map { it.lower() },
defaultGroupConfiguration,
perDomainTrustAnchors
)

cc.createConversation(id.lower(), creatorCredentialType.lower(), cfg)
}

suspend fun wipeConversation(id: MLSGroupId) = cc.wipeConversation(id.lower())

suspend fun processWelcomeMessage(welcome: Welcome): MLSGroupId {
return cc.processWelcomeMessage(welcome.lower(), defaultGroupConfiguration).toGroupId()
}

suspend fun encryptMessage(id: MLSGroupId, message: PlaintextMessage): MlsMessage {
return cc.encryptMessage(id.lower(), message.lower()).toMlsMessage()
}

suspend fun updateTrustAnchorsFromConversation(
id: MLSGroupId,
removeDomainNames: List<String>,
addTrustAnchors: List<com.wire.crypto.PerDomainTrustAnchor>,
): CommitBundle {
return CommitBundle(
cc.updateTrustAnchorsFromConversation(id.lower(), removeDomainNames, addTrustAnchors)
)
}

suspend fun decryptMessage(id: MLSGroupId, message: MlsMessage): DecryptedMessage {
return DecryptedMessage(cc.decryptMessage(id.lower(), message.lower()))
}

suspend fun commitAccepted(id: MLSGroupId) = cc.commitAccepted(id.lower())

suspend fun commitPendingProposals(id: MLSGroupId) = cc.commitPendingProposals(id.lower())?.let { CommitBundle(it) }

suspend fun clearPendingCommit(id: MLSGroupId) = cc.clearPendingCommit(id.lower())

suspend fun members(id: MLSGroupId): List<ClientId> = cc.getClientIds(id.lower()).map { it.toClientId() }

suspend fun addMember(id: MLSGroupId, members: Map<ClientId, MLSKeyPackage>): CommitBundle {
val invitees = members.map { (clientId, kp) -> com.wire.crypto.Invitee(clientId.lower(), kp.lower()) }
return CommitBundle(cc.addClientsToConversation(id.lower(), invitees))
}

suspend fun removeMember(id: MLSGroupId, members: List<ClientId>): CommitBundle {
val clientIds = members.map { it.lower() }
return CommitBundle(cc.removeClientsFromConversation(id.lower(), clientIds))
}

suspend fun deriveSecret(id: MLSGroupId, keyLength: UInt): AvsSecret {
return cc.exportSecretKey(id.lower(), keyLength).toAvsSecret()
}

suspend fun e2eiConversationState(id: MLSGroupId): com.wire.crypto.E2eiConversationState {
return cc.e2eiConversationState(id.lower())
}

suspend fun e2eiIsEnabled(ciphersuite: Ciphersuite): Boolean = cc.e2eiIsEnabled(ciphersuite.lower())
}
Loading

0 comments on commit 415b5c3

Please sign in to comment.