Skip to content

Commit

Permalink
✨ Support for encrypted file transfer (#1268)
Browse files Browse the repository at this point in the history
  • Loading branch information
guiyanakuang authored Jun 22, 2024
1 parent bc9553d commit e61a10f
Show file tree
Hide file tree
Showing 22 changed files with 645 additions and 201 deletions.
2 changes: 1 addition & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ compose.desktop {
jvmArgs("-DglobalListener=$globalListener")
jvmArgs("-Dcompose.interop.blending=true")
jvmArgs("-Dio.netty.maxDirectMemory=268435456")
jvmArgs("-DloggerDebugPackages=com.clipevery.routing,com.clipevery.net.clientapi")
jvmArgs("-DloggerDebugPackages=com.clipevery.routing,com.clipevery.net.clientapi,com.clipevery.net.plugin")

// Add download info of jbr on all platforms
val jbrYamlFile = project.projectDir.toPath().resolve("jbr.yaml").toFile()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package com.clipevery.net.clientapi

import com.clipevery.dto.sync.SyncInfo
import com.clipevery.signal.SignalMessageProcessor
import io.ktor.http.*
import org.signal.libsignal.protocol.SessionCipher

interface SyncClientApi {

suspend fun getPreKeyBundle(toUrl: URLBuilder.(URLBuilder) -> Unit): ClientApiResult

suspend fun exchangeSyncInfo(
suspend fun createSession(
syncInfo: SyncInfo,
sessionCipher: SessionCipher,
signalMessageProcessor: SignalMessageProcessor,
toUrl: URLBuilder.(URLBuilder) -> Unit,
): ClientApiResult

suspend fun heartbeat(
syncInfo: SyncInfo,
signalMessageProcessor: SignalMessageProcessor,
toUrl: URLBuilder.(URLBuilder) -> Unit,
): ClientApiResult

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.clipevery.signal

import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.message.CiphertextMessage
import org.signal.libsignal.protocol.message.PreKeySignalMessage
import org.signal.libsignal.protocol.message.SignalMessage

interface SignalMessageProcessor {

val signalProtocolAddress: SignalProtocolAddress

suspend fun encrypt(data: ByteArray): CiphertextMessage

suspend fun decrypt(signalMessage: SignalMessage): ByteArray

suspend fun decrypt(preKeySignalMessage: PreKeySignalMessage): ByteArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.clipevery.signal

interface SignalProcessorCache {

fun getSignalMessageProcessor(appInstanceId: String): SignalMessageProcessor

fun removeSignalMessageProcessor(appInstanceId: String)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.clipevery.sync

import com.clipevery.dao.sync.SyncRuntimeInfo
import org.signal.libsignal.protocol.SessionCipher
import com.clipevery.signal.SignalMessageProcessor

interface SyncHandler {

var recommendedRefreshTime: Long

var syncRuntimeInfo: SyncRuntimeInfo

val sessionCipher: SessionCipher
val signalProcessor: SignalMessageProcessor

suspend fun getConnectHostAddress(): String?

Expand Down
11 changes: 9 additions & 2 deletions composeApp/src/desktopMain/kotlin/com/clipevery/Clipevery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ import com.clipevery.net.clientapi.DesktopSyncClientApi
import com.clipevery.net.clientapi.PullClientApi
import com.clipevery.net.clientapi.SendClipClientApi
import com.clipevery.net.clientapi.SyncClientApi
import com.clipevery.net.plugin.SignalClientDecryptPlugin
import com.clipevery.net.plugin.SignalClientEncryptPlugin
import com.clipevery.path.DesktopPathProvider
import com.clipevery.path.PathProvider
import com.clipevery.platform.currentPlatform
Expand All @@ -109,6 +111,8 @@ import com.clipevery.signal.DesktopPreKeyStore
import com.clipevery.signal.DesktopSessionStore
import com.clipevery.signal.DesktopSignalProtocolStore
import com.clipevery.signal.DesktopSignedPreKeyStore
import com.clipevery.signal.SignalProcessorCache
import com.clipevery.signal.SignalProcessorCacheImpl
import com.clipevery.signal.getClipIdentityKeyStoreFactory
import com.clipevery.sync.DesktopDeviceManager
import com.clipevery.sync.DesktopQRCodeGenerator
Expand Down Expand Up @@ -223,14 +227,14 @@ class Clipevery {
single<ClipTaskDao> { ClipTaskRealm(get<RealmManager>().realm) }

// net component
single<ClipClient> { DesktopClipClient(get<AppInfo>()) }
single<ClipClient> { DesktopClipClient(get<AppInfo>(), get(), get()) }
single<ClipServer> { DesktopClipServer(get<ConfigManager>()) }
single<ClipBonjourService> { DesktopClipBonjourService(get(), get(), get()) }
single<TelnetUtils> { TelnetUtils(get<ClipClient>()) }
single<SyncClientApi> { DesktopSyncClientApi(get(), get()) }
single<SendClipClientApi> { DesktopSendClipClientApi(get(), get()) }
single<PullClientApi> { DesktopPullClientApi(get(), get()) }
single { DesktopSyncManager(get(), get(), get(), get(), get(), get(), lazy { get() }) }
single { DesktopSyncManager(get(), get(), get(), get(), get(), get(), get(), lazy { get() }) }
single<SyncRefresher> { get<DesktopSyncManager>() }
single<SyncManager> { get<DesktopSyncManager>() }
single<DeviceManager> { DesktopDeviceManager(get(), get(), get()) }
Expand All @@ -242,6 +246,9 @@ class Clipevery {
single<PreKeyStore> { DesktopPreKeyStore(get()) }
single<SignedPreKeyStore> { DesktopSignedPreKeyStore(get()) }
single<SignalProtocolStore> { DesktopSignalProtocolStore(get(), get(), get(), get()) }
single<SignalProcessorCache> { SignalProcessorCacheImpl(get()) }
single<SignalClientEncryptPlugin> { SignalClientEncryptPlugin(get()) }
single<SignalClientDecryptPlugin> { SignalClientDecryptPlugin(get()) }

// clip component
single<ClipboardService> { getDesktopClipboardService(get(), get(), get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.clipevery.net

import com.clipevery.app.AppInfo
import com.clipevery.net.plugin.SignalClientEncryption
import com.clipevery.net.plugin.SignalClientDecryptPlugin
import com.clipevery.net.plugin.SignalClientEncryptPlugin
import com.clipevery.utils.DesktopJsonUtils
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
Expand All @@ -15,7 +16,11 @@ import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.util.reflect.*

class DesktopClipClient(private val appInfo: AppInfo) : ClipClient {
class DesktopClipClient(
private val appInfo: AppInfo,
private val signalClientEncryptPlugin: SignalClientEncryptPlugin,
private val signalClientDecryptPlugin: SignalClientDecryptPlugin,
) : ClipClient {

private val clientLogger = KotlinLogging.logger {}

Expand All @@ -35,7 +40,8 @@ class DesktopClipClient(private val appInfo: AppInfo) : ClipClient {
install(ContentNegotiation) {
json(DesktopJsonUtils.JSON, ContentType.Application.Json)
}
install(SignalClientEncryption)
install(signalClientEncryptPlugin)
install(signalClientDecryptPlugin)
}

override suspend fun <T : Any> post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package com.clipevery.net
import com.clipevery.config.ConfigManager
import com.clipevery.exception.StandardErrorCode
import com.clipevery.net.exception.signalExceptionHandler
import com.clipevery.net.plugin.SignalServerDecryption
import com.clipevery.net.plugin.SIGNAL_SERVER_DECRYPT_PLUGIN
import com.clipevery.net.plugin.SIGNAL_SERVER_ENCRYPT_PLUGIN
import com.clipevery.routing.clipRouting
import com.clipevery.routing.pullRouting
import com.clipevery.routing.syncRouting
Expand Down Expand Up @@ -61,8 +62,8 @@ class DesktopClipServer(private val configManager: ConfigManager) : ClipServer {
}
signalExceptionHandler()
}
install(SignalServerDecryption) {
}
install(SIGNAL_SERVER_ENCRYPT_PLUGIN)
install(SIGNAL_SERVER_DECRYPT_PLUGIN)
intercept(ApplicationCallPipeline.Setup) {
logger.info { "Received request: ${call.request.httpMethod.value} ${call.request.uri} ${call.request.contentType()}" }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class DesktopPullClientApi(
messageType = typeInfo<PullFileRequest>(),
targetAppInstanceId = pullFileRequest.appInstanceId,
encrypt = configManager.config.isEncryptSync,
// pull file timeout is 5s
timeout = 5000L,
// pull file timeout is 50s
timeout = 50000L,
urlBuilder = {
toUrl(it)
buildUrl(it, "pull", "file")
Expand All @@ -46,7 +46,7 @@ class DesktopPullClientApi(
): ClientApiResult {
val response =
clipClient.get(
timeout = 5000L,
timeout = 50000L,
urlBuilder = {
toUrl(it)
buildUrl(it, "pull", "icon", source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import com.clipevery.dto.sync.RequestTrust
import com.clipevery.dto.sync.SyncInfo
import com.clipevery.net.ClipClient
import com.clipevery.serializer.PreKeyBundleSerializer
import com.clipevery.signal.SignalMessageProcessor
import com.clipevery.utils.DesktopJsonUtils
import com.clipevery.utils.buildUrl
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.call.*
import io.ktor.http.*
import io.ktor.util.reflect.*
import kotlinx.serialization.encodeToString
import org.signal.libsignal.protocol.SessionCipher
import org.signal.libsignal.protocol.state.SignalProtocolStore

class DesktopSyncClientApi(
Expand All @@ -33,21 +33,41 @@ class DesktopSyncClientApi(
}
}

override suspend fun exchangeSyncInfo(
override suspend fun createSession(
syncInfo: SyncInfo,
sessionCipher: SessionCipher,
signalMessageProcessor: SignalMessageProcessor,
toUrl: URLBuilder.(URLBuilder) -> Unit,
): ClientApiResult {
return request(logger, request = {
val data = DesktopJsonUtils.JSON.encodeToString(syncInfo).toByteArray()
val ciphertextMessage = sessionCipher.encrypt(data)
val ciphertextMessage = signalMessageProcessor.encrypt(data)
val dataContent = DataContent(data = ciphertextMessage.serialize())
clipClient.post(
dataContent,
typeInfo<DataContent>(),
urlBuilder = {
toUrl(it)
buildUrl(it, "sync", "exchangeSyncInfo")
buildUrl(it, "sync", "createSession")
},
)
}, transformData = { true })
}

override suspend fun heartbeat(
syncInfo: SyncInfo,
signalMessageProcessor: SignalMessageProcessor,
toUrl: URLBuilder.(URLBuilder) -> Unit,
): ClientApiResult {
return request(logger, request = {
val data = DesktopJsonUtils.JSON.encodeToString(syncInfo).toByteArray()
val ciphertextMessage = signalMessageProcessor.encrypt(data)
val dataContent = DataContent(data = ciphertextMessage.serialize())
clipClient.post(
dataContent,
typeInfo<DataContent>(),
urlBuilder = {
toUrl(it)
buildUrl(it, "sync", "heartbeat")
},
)
}, transformData = { true })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.clipevery.net.plugin

import com.clipevery.signal.SignalProcessorCache
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import org.signal.libsignal.protocol.message.SignalMessage
import java.io.ByteArrayOutputStream

class SignalClientDecryptPlugin(private val signalProcessorCache: SignalProcessorCache) :
HttpClientPlugin<SignalConfig, SignalClientDecryptPlugin> {

private val logger: KLogger = KotlinLogging.logger {}

override val key = AttributeKey<SignalClientDecryptPlugin>("SignalClientDecryptPlugin")

override fun prepare(block: SignalConfig.() -> Unit): SignalClientDecryptPlugin {
return this
}

@OptIn(InternalAPI::class)
override fun install(
plugin: SignalClientDecryptPlugin,
scope: HttpClient,
) {
scope.receivePipeline.intercept(HttpReceivePipeline.Before) {
val headers = it.call.request.headers
headers["targetAppInstanceId"]?.let { appInstanceId ->
headers["signal"]?.let { signal ->
if (signal == "1") {
logger.debug { "signal client decrypt $appInstanceId" }
val byteReadChannel: ByteReadChannel = it.content

val contentType = it.call.response.contentType()

val processor = signalProcessorCache.getSignalMessageProcessor(appInstanceId)

if (contentType == ContentType.Application.Json) {
val bytes = byteReadChannel.readRemaining().readBytes()
val signalMessage = SignalMessage(bytes)
val decrypt = processor.decrypt(signalMessage)

// Create a new ByteReadChannel to contain the decrypted content
val newChannel = ByteReadChannel(decrypt)
val responseData =
HttpResponseData(
it.status,
it.requestTime,
it.headers,
it.version,
newChannel,
it.coroutineContext,
)
proceedWith(DefaultHttpResponse(it.call, responseData))
} else if (contentType == ContentType.Application.OctetStream) {
val result = ByteArrayOutputStream()
while (!byteReadChannel.isClosedForRead) {
val size = byteReadChannel.readInt()
val byteArray = ByteArray(size)
var bytesRead = 0
while (bytesRead < size) {
val currentRead = byteReadChannel.readAvailable(byteArray, bytesRead, size - bytesRead)
if (currentRead == -1) break
bytesRead += currentRead
}
val signalMessage = SignalMessage(byteArray)
result.write(processor.decrypt(signalMessage))
}
val newChannel = ByteReadChannel(result.toByteArray())
val responseData =
HttpResponseData(
it.status,
it.requestTime,
it.headers,
it.version,
newChannel,
it.coroutineContext,
)
proceedWith(DefaultHttpResponse(it.call, responseData))
}
}
}
}
}
}
}
Loading

0 comments on commit e61a10f

Please sign in to comment.