diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/presist/data/DriverFactory.kt b/composeApp/src/commonMain/kotlin/com/clipevery/dao/DriverFactory.kt similarity index 90% rename from composeApp/src/commonMain/kotlin/com/clipevery/presist/data/DriverFactory.kt rename to composeApp/src/commonMain/kotlin/com/clipevery/dao/DriverFactory.kt index 3334ede57..275604f13 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/presist/data/DriverFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/dao/DriverFactory.kt @@ -1,4 +1,4 @@ -package com.clipevery.presist.data +package com.clipevery.dao import app.cash.sqldelight.db.SqlDriver import com.clipevery.Database diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/dao/store/IdentityKeyStoreFactory.kt b/composeApp/src/commonMain/kotlin/com/clipevery/dao/store/IdentityKeyStoreFactory.kt new file mode 100644 index 000000000..5861bc2cc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/dao/store/IdentityKeyStoreFactory.kt @@ -0,0 +1,8 @@ +package com.clipevery.dao.store + +import org.signal.libsignal.protocol.state.IdentityKeyStore + +interface IdentityKeyStoreFactory { + + fun createIdentityKeyStore(): IdentityKeyStore +} diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/exception/ClipException.kt b/composeApp/src/commonMain/kotlin/com/clipevery/exception/ClipException.kt new file mode 100644 index 000000000..c37293fb4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/exception/ClipException.kt @@ -0,0 +1,26 @@ +package com.clipevery.exception + +class ClipException: RuntimeException { + + private val errorCode: ErrorCode + + constructor(errorCode: ErrorCode) : super() { + this.errorCode = errorCode + } + + constructor(errorCode: ErrorCode, message: String) : super(message) { + this.errorCode = errorCode + } + + constructor(errorCode: ErrorCode, message: String, cause: Throwable) : super(message, cause) { + this.errorCode = errorCode + } + + constructor(errorCode: ErrorCode, cause: Throwable) : super(cause) { + this.errorCode = errorCode + } + + fun getErrorCode(): ErrorCode { + return errorCode + } +} diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/exception/ErrorCode.kt b/composeApp/src/commonMain/kotlin/com/clipevery/exception/ErrorCode.kt new file mode 100644 index 000000000..09a921f98 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/exception/ErrorCode.kt @@ -0,0 +1,56 @@ +package com.clipevery.exception + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.Objects + +class ErrorCode @JsonCreator constructor( + @JsonProperty("code") code: Int, + @JsonProperty("name") name: String, + @JsonProperty("type") type: ErrorType) { + @get:JsonProperty + val code: Int + + @get:JsonProperty + val name: String + private val type: ErrorType + + init { + require(code >= 0) { "code is negative" } + this.code = code + this.name = name + this.type = type + } + + @JsonProperty + fun getType(): ErrorType { + return type + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + val errorCode = other as ErrorCode + return code == errorCode.code && name == errorCode.name && type === errorCode.type + } + + override fun hashCode(): Int { + return Objects.hash(code, name, type) + } +} + + +enum class ErrorType { + USER_ERROR, + INTERNAL_ERROR, + EXTERNAL +} + + +interface ErrorCodeSupplier { + fun toErrorCode(): ErrorCode +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/exception/StandardErrorCode.kt b/composeApp/src/commonMain/kotlin/com/clipevery/exception/StandardErrorCode.kt new file mode 100644 index 000000000..451c59a4a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/exception/StandardErrorCode.kt @@ -0,0 +1,20 @@ +package com.clipevery.exception + +enum class StandardErrorCode(code: Int, errorType: ErrorType): ErrorCodeSupplier { + UNKNOWN_ERROR(0, ErrorType.INTERNAL_ERROR), + BOOTSTRAP_ERROR(1, ErrorType.INTERNAL_ERROR), + + SYNC_TIMEOUT(1000, ErrorType.USER_ERROR), + SYNC_INVALID(1001, ErrorType.USER_ERROR); + + private val errorCode: ErrorCode + + init { + errorCode = ErrorCode(code, name, errorType) + } + + override fun toErrorCode(): ErrorCode { + return errorCode + } + +} diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/model/AppInfo.kt b/composeApp/src/commonMain/kotlin/com/clipevery/model/AppInfo.kt index c360198de..a4019f97e 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/model/AppInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/model/AppInfo.kt @@ -1,7 +1,10 @@ package com.clipevery.model +import kotlinx.serialization.Serializable + const val AppName: String = "Clipevery" +@Serializable data class AppInfo( val appInstanceId: String, val appVersion: String, diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/model/RequestEndpointInfo.kt b/composeApp/src/commonMain/kotlin/com/clipevery/model/RequestEndpointInfo.kt index 9ea1436d8..df9a2f219 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/model/RequestEndpointInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/model/RequestEndpointInfo.kt @@ -9,19 +9,19 @@ import java.io.DataOutputStream data class RequestEndpointInfo(val deviceInfo: DeviceInfo, val port: Int) { - fun getBase64Encode(salt: Int): String { + fun getBase64Encode(token: Int): String { val byteStream = ByteArrayOutputStream() val dataStream = DataOutputStream(byteStream) encodeDeviceInfo(dataStream) dataStream.writeInt(port) val byteArray = byteStream.toByteArray() val size = byteArray.size - val offset = salt % size + val offset = token % size val byteArrayRotate = byteArray.rotate(offset) val saltByteStream = ByteArrayOutputStream() val saltDataStream = DataOutputStream(saltByteStream) saltDataStream.write(byteArrayRotate) - saltDataStream.writeInt(salt) + saltDataStream.writeInt(token) return base64Encode(saltByteStream.toByteArray()) } diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/RequestSyncInfo.kt b/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/RequestSyncInfo.kt index ab256f30f..73ffed4a9 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/RequestSyncInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/RequestSyncInfo.kt @@ -1,26 +1,34 @@ package com.clipevery.model.sync +import com.clipevery.model.AppInfo import com.clipevery.model.RequestEndpointInfo import kotlinx.serialization.Serializable @Serializable -data class RequestSyncInfo(val requestEndpointInfo: RequestEndpointInfo, - val preKeyBundle: ByteArray) { +data class RequestSyncInfo(val appInfo: AppInfo, + val requestEndpointInfo: RequestEndpointInfo, + val preKeyBundle: ByteArray, + val token: Int) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as RequestSyncInfo + if (appInfo != other.appInfo) return false if (requestEndpointInfo != other.requestEndpointInfo) return false if (!preKeyBundle.contentEquals(other.preKeyBundle)) return false + if (token != other.token) return false return true } override fun hashCode(): Int { - var result = requestEndpointInfo.hashCode() + var result = appInfo.hashCode() + result = 31 * result + requestEndpointInfo.hashCode() result = 31 * result + preKeyBundle.contentHashCode() + result = 31 * result + token return result } + } diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/ResponseSyncInfo.kt b/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/ResponseSyncInfo.kt new file mode 100644 index 000000000..e91918056 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/model/sync/ResponseSyncInfo.kt @@ -0,0 +1,8 @@ +package com.clipevery.model.sync + +import com.clipevery.model.AppInfo +import com.clipevery.model.RequestEndpointInfo + +data class ResponseSyncInfo(val appInfo: AppInfo, + val requestEndpointInfo: RequestEndpointInfo +) diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/net/SyncValidator.kt b/composeApp/src/commonMain/kotlin/com/clipevery/net/SyncValidator.kt new file mode 100644 index 000000000..5db2a59eb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/net/SyncValidator.kt @@ -0,0 +1,20 @@ +package com.clipevery.net + +import com.clipevery.exception.ClipException +import com.clipevery.model.SyncInfo +import com.clipevery.model.sync.RequestSyncInfo +import org.signal.libsignal.protocol.state.PreKeyBundle + +interface SyncValidator { + + fun createToken(): Int + + fun getCurrentToken(): Int + + fun getRefreshTime(): Long + + @Throws(ClipException::class) + suspend fun validate(requestSyncInfo: RequestSyncInfo): SyncInfoWithPreKeyBundle +} + +data class SyncInfoWithPreKeyBundle(val syncInfo: SyncInfo, val preKeyBundle: PreKeyBundle) diff --git a/composeApp/src/commonMain/sqldelight/com/clipevery/data/IdentityKey.sq b/composeApp/src/commonMain/sqldelight/com/clipevery/data/IdentityKey.sq index 5f8296bbf..1e3417028 100644 --- a/composeApp/src/commonMain/sqldelight/com/clipevery/data/IdentityKey.sq +++ b/composeApp/src/commonMain/sqldelight/com/clipevery/data/IdentityKey.sq @@ -1,15 +1,11 @@ CREATE TABLE identityKey ( app_instance_id TEXT NOT NULL PRIMARY KEY, - registrationId INTEGER NOT NULL, serialized BLOB NOT NULL ); -select: -SELECT * FROM identityKey WHERE app_instance_id = ?; - -tryInit: -INSERT OR FAIL INTO identityKey (app_instance_id, registrationId, serialized) VALUES (?, ?, ?); +selectIndentity: +SELECT serialized FROM identityKey WHERE app_instance_id = ?; update: -UPDATE identityKey SET serialized = ? WHERE app_instance_id = ? AND registrationId = ?; +UPDATE identityKey SET serialized = ? WHERE app_instance_id = ?; diff --git a/composeApp/src/commonMain/sqldelight/com/clipevery/data/PreKey.sq b/composeApp/src/commonMain/sqldelight/com/clipevery/data/PreKey.sq index f71273bb7..f5b6dab29 100644 --- a/composeApp/src/commonMain/sqldelight/com/clipevery/data/PreKey.sq +++ b/composeApp/src/commonMain/sqldelight/com/clipevery/data/PreKey.sq @@ -1,5 +1,5 @@ CREATE TABLE preKey ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY , serialized BLOB NOT NULL ); diff --git a/composeApp/src/commonMain/sqldelight/com/clipevery/data/Session.sq b/composeApp/src/commonMain/sqldelight/com/clipevery/data/Session.sq new file mode 100644 index 000000000..b23226e81 --- /dev/null +++ b/composeApp/src/commonMain/sqldelight/com/clipevery/data/Session.sq @@ -0,0 +1,22 @@ +CREATE TABLE session ( + app_instance_id TEXT NOT NULL PRIMARY KEY, + sessionRecord BLOB NOT NULL +); + +selectSessionRecord: +SELECT sessionRecord FROM session WHERE app_instance_id = ?; + +selectSessionRecords: +SELECT sessionRecord FROM session WHERE app_instance_id IN ?; + +selectSubDevice: +SELECT app_instance_id FROM session WHERE app_instance_id = ?; + +updateSessionRecord: +UPDATE session SET sessionRecord = ? WHERE app_instance_id = ?; + +count: +SELECT COUNT(1) FROM session WHERE app_instance_id = ?; + +delete: +DELETE FROM session WHERE app_instance_id = ?; diff --git a/composeApp/src/commonMain/sqldelight/com/clipevery/data/SignedPreKey.sq b/composeApp/src/commonMain/sqldelight/com/clipevery/data/SignedPreKey.sq index c60b20eab..2a7dcfe36 100644 --- a/composeApp/src/commonMain/sqldelight/com/clipevery/data/SignedPreKey.sq +++ b/composeApp/src/commonMain/sqldelight/com/clipevery/data/SignedPreKey.sq @@ -1,5 +1,5 @@ CREATE TABLE signedPreKey ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY , serialized BLOB NOT NULL ); diff --git a/composeApp/src/commonMain/sqldelight/com/clipevery/data/Sync.sq b/composeApp/src/commonMain/sqldelight/com/clipevery/data/Sync.sq index 805e0b989..77098b8b0 100644 --- a/composeApp/src/commonMain/sqldelight/com/clipevery/data/Sync.sq +++ b/composeApp/src/commonMain/sqldelight/com/clipevery/data/Sync.sq @@ -2,52 +2,25 @@ CREATE TABLE syncInfo ( app_instance_id TEXT NOT NULL PRIMARY KEY, app_version TEXT NOT NULL, app_user_name TEXT NOT NULL, - device_id TEXT NOT NULL, + device_id TEXT NOT NULL, device_name TEXT NOT NULL, - device_state TEXT NOT NULL, + sync_state TEXT NOT NULL, host_address TEXT NOT NULL, port INTEGER NOT NULL, platform_name TEXT NOT NULL, platform_arch TEXT NOT NULL, platform_bit_mode INTEGER NOT NULL, - platform_version TEXT NOT NULL, - public_key BLOB, - sessionRecord BLOB + platform_version TEXT NOT NULL ); -CREATE INDEX device_state_index ON syncInfo (device_state); - -selectAll: -SELECT * -FROM syncInfo; - -selectSessionRecord: -SELECT sessionRecord FROM syncInfo WHERE app_instance_id = ?; - -selectSessionRecords: -SELECT sessionRecord FROM syncInfo WHERE app_instance_id IN ?; - -selectPublicKey: -SELECT public_key FROM syncInfo WHERE app_instance_id = ?; - -selectSubDevice: -SELECT app_instance_id FROM syncInfo WHERE app_instance_id = ?; +CREATE INDEX sync_state_index ON syncInfo (sync_state); insert: INSERT INTO syncInfo(app_instance_id, app_version, app_user_name, -device_id, device_name, device_state, host_address, port, platform_name, +device_id, device_name, sync_state, host_address, port, platform_name, platform_arch, platform_bit_mode, platform_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); -updatePublicKey: -UPDATE syncInfo SET public_key = ? WHERE app_instance_id = ?; - -updateSessionRecord: -UPDATE syncInfo SET sessionRecord = ? WHERE app_instance_id = ?; - count: SELECT COUNT(1) FROM syncInfo WHERE app_instance_id = ?; - -delete: -DELETE FROM syncInfo WHERE app_instance_id = ?; diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/controller/SyncController.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/controller/SyncController.kt new file mode 100644 index 000000000..0b8de185f --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/controller/SyncController.kt @@ -0,0 +1,107 @@ +package com.clipevery.controller + +import com.clipevery.dao.SyncDao +import com.clipevery.device.DeviceInfoFactory +import com.clipevery.encrypt.decodePreKeyBundle +import com.clipevery.exception.ClipException +import com.clipevery.exception.StandardErrorCode +import com.clipevery.model.AppInfo +import com.clipevery.model.EndpointInfo +import com.clipevery.model.RequestEndpointInfo +import com.clipevery.model.SyncInfo +import com.clipevery.model.SyncState +import com.clipevery.model.sync.RequestSyncInfo +import com.clipevery.model.sync.ResponseSyncInfo +import com.clipevery.net.ClipServer +import com.clipevery.net.SyncInfoWithPreKeyBundle +import com.clipevery.net.SyncValidator +import com.clipevery.utils.telnet +import kotlinx.coroutines.runBlocking +import org.signal.libsignal.protocol.InvalidKeyException +import org.signal.libsignal.protocol.SessionBuilder +import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.libsignal.protocol.state.IdentityKeyStore +import org.signal.libsignal.protocol.state.PreKeyBundle +import org.signal.libsignal.protocol.state.PreKeyStore +import org.signal.libsignal.protocol.state.SessionStore +import org.signal.libsignal.protocol.state.SignedPreKeyStore +import java.io.IOException + +class SyncController(private val appInfo: AppInfo, + private val syncDao: SyncDao, + private val sessionStore: SessionStore, + private val preKeyStore: PreKeyStore, + private val signedPreKeyStore: SignedPreKeyStore, + private val identityKeyStore: IdentityKeyStore, + private val clipServer: Lazy, + private val deviceInfoFactory: DeviceInfoFactory): SyncValidator { + + fun receiveEndpointSyncInfo(requestSyncInfo: RequestSyncInfo): ResponseSyncInfo { + val (syncInfo, preKeyBundle) = runBlocking { validate(requestSyncInfo) } + val signalProtocolAddress = SignalProtocolAddress(syncInfo.appInfo.appInstanceId, 1) + val sessionBuilder = SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore, identityKeyStore, signalProtocolAddress) + syncDao.database.transaction { + syncDao.saveSyncEndpoint(syncInfo) + sessionBuilder.process(preKeyBundle) + } + return ResponseSyncInfo(appInfo, RequestEndpointInfo(deviceInfoFactory.createDeviceInfo(), clipServer.value.port())) + } + + private var token: Int? = null + private var generateTime: Long = 0 + private val refreshTime: Long = 10000 + + override fun createToken(): Int { + token = (0..999999).random() + generateTime = System.currentTimeMillis() + return token!! + } + + override fun getCurrentToken(): Int { + return token ?: createToken() + } + + override fun getRefreshTime(): Long { + return refreshTime + } + + override suspend fun validate(requestSyncInfo: RequestSyncInfo): SyncInfoWithPreKeyBundle { + val currentTimeMillis = System.currentTimeMillis() + if (currentTimeMillis - generateTime > refreshTime) { + throw ClipException(StandardErrorCode.SYNC_TIMEOUT.toErrorCode(), "token expired") + } else if (requestSyncInfo.token != this.token) { + throw ClipException(StandardErrorCode.SYNC_INVALID.toErrorCode(), "token invalid") + } + val hostInfoList = requestSyncInfo.requestEndpointInfo.deviceInfo.hostInfoList + if (hostInfoList.isEmpty()) { + throw ClipException(StandardErrorCode.SYNC_INVALID.toErrorCode(), "cant find host info") + } + val host: String = telnet( + requestSyncInfo.requestEndpointInfo.deviceInfo.hostInfoList.map { hostInfo -> hostInfo.hostAddress }, + requestSyncInfo.requestEndpointInfo.port, 1000 + ) ?: throw ClipException(StandardErrorCode.SYNC_INVALID.toErrorCode(), "cant find telnet success host") + val hostInfo = hostInfoList.find { hostInfo -> hostInfo.hostAddress == host }!! + + + val preKeyBundleBytes = requestSyncInfo.preKeyBundle + val preKeyBundle: PreKeyBundle = try { + decodePreKeyBundle(preKeyBundleBytes) + } catch (e: IOException) { + throw ClipException(StandardErrorCode.SYNC_INVALID.toErrorCode(), e) + } catch (e: InvalidKeyException) { + throw ClipException(StandardErrorCode.SYNC_INVALID.toErrorCode(), e) + } + + val appInfo = requestSyncInfo.appInfo + + val endpointInfo = EndpointInfo( + requestSyncInfo.requestEndpointInfo.deviceInfo.deviceId, + requestSyncInfo.requestEndpointInfo.deviceInfo.deviceName, + requestSyncInfo.requestEndpointInfo.deviceInfo.platform, + hostInfo, requestSyncInfo.requestEndpointInfo.port + ) + + return SyncInfoWithPreKeyBundle(SyncInfo(appInfo, endpointInfo, SyncState.ONLINE), preKeyBundle) + } +} + diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/DriverFactory.desktop.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/DriverFactory.desktop.kt similarity index 81% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/DriverFactory.desktop.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/dao/DriverFactory.desktop.kt index 26d3f35d4..bd00955bb 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/DriverFactory.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/DriverFactory.desktop.kt @@ -1,4 +1,4 @@ -package com.clipevery.presist.data +package com.clipevery.dao import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver @@ -13,7 +13,9 @@ actual class DriverFactory { actual fun createDriver(): SqlDriver { val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:${dbFilePath.toAbsolutePath()}") - Database.Schema.create(driver) + if (!dbFilePath.toFile().exists()) { + Database.Schema.create(driver) + } return driver } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/dao/SyncDao.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/SyncDao.kt new file mode 100644 index 000000000..383c52286 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/SyncDao.kt @@ -0,0 +1,37 @@ +package com.clipevery.dao + +import com.clipevery.Database +import com.clipevery.model.SyncInfo + +class SyncDao(val database: Database) { + + fun saveSyncEndpoint(syncInfo: SyncInfo) { + val appInstanceId = syncInfo.appInfo.appInstanceId + val appVersion = syncInfo.appInfo.appVersion + val userName = syncInfo.appInfo.userName + val deviceId = syncInfo.endpointInfo.deviceId + val deviceName = syncInfo.endpointInfo.deviceName + val syncState = syncInfo.state + val hostAddress = syncInfo.endpointInfo.hostInfo.hostAddress + val port = syncInfo.endpointInfo.port + val platformName = syncInfo.endpointInfo.platform.name + val platformArch = syncInfo.endpointInfo.platform.arch + val platformBitMode = syncInfo.endpointInfo.platform.bitMode + val platformVersion = syncInfo.endpointInfo.platform.version + + database.syncQueries.insert( + appInstanceId, + appVersion, + userName, + deviceId, + deviceName, + syncState.name, + hostAddress, + port.toLong(), + platformName, + platformArch, + platformBitMode.toLong(), + platformVersion + ) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/IdentityKeyStore.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/IdentityKeyStore.kt new file mode 100644 index 000000000..a4d93d124 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/IdentityKeyStore.kt @@ -0,0 +1,197 @@ +package com.clipevery.dao.store + +import com.clipevery.Database +import com.clipevery.config.FileType +import com.clipevery.encrypt.decryptData +import com.clipevery.encrypt.encryptData +import com.clipevery.encrypt.generateAESKey +import com.clipevery.encrypt.secretKeyToString +import com.clipevery.encrypt.stringToSecretKey +import com.clipevery.macos.MacosKeychainHelper +import com.clipevery.model.AppInfo +import com.clipevery.path.getPathProvider +import com.clipevery.platform.currentPlatform +import com.clipevery.presist.DesktopOneFilePersist +import com.clipevery.windows.WindowDapiHelper +import io.github.oshai.kotlinlogging.KotlinLogging +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.IdentityKeyPair +import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.libsignal.protocol.state.IdentityKeyStore +import org.signal.libsignal.protocol.util.KeyHelper +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream + +class DesktopIdentityKeyStore(private val database: Database, + private val appInstanceId: String, + private val identityKeyPair: IdentityKeyPair, + private val registrationId: Int): IdentityKeyStore { + + override fun getIdentityKeyPair(): IdentityKeyPair { + return identityKeyPair + } + + override fun getLocalRegistrationId(): Int { + return registrationId + } + + override fun saveIdentity(address: SignalProtocolAddress, identityKey: IdentityKey): Boolean { + return try { + database.identityKeyQueries.update(identityKey.serialize(), appInstanceId) + true + } catch (e: Exception) { + false + } + } + + override fun isTrustedIdentity( + address: SignalProtocolAddress, + identityKey: IdentityKey, + direction: IdentityKeyStore.Direction + ): Boolean { + val identity: IdentityKey? = getIdentity(address) + return identity?.let { it == identityKey } ?: true + } + + override fun getIdentity(address: SignalProtocolAddress): IdentityKey? { + return database.identityKeyQueries.selectIndentity(address.name) + .executeAsOneOrNull()?.let { + IdentityKey(it) + } + } + +} + +val logger = KotlinLogging.logger {} + +fun getIdentityKeyStoreFactory(appInfo: AppInfo, database: Database): IdentityKeyStoreFactory { + val currentPlatform = currentPlatform() + return if (currentPlatform.isMacos()) { + MacosIdentityKeyStoreFactory(appInfo, database) + } else if (currentPlatform.isWindows()) { + WindowsIdentityKeyStoreFactory(appInfo, database) + } else { + throw IllegalStateException("Unknown platform: ${currentPlatform.name}") + } +} + +private data class IdentityKeyPairAndRegistrationId(val identityKeyPair: IdentityKeyPair, val registrationId: Int) + +private fun readIdentityKeyAndRegistrationId(data: ByteArray): IdentityKeyPairAndRegistrationId { + val byteArrayInputStream = ByteArrayInputStream(data) + val inputStream = DataInputStream(byteArrayInputStream) + val byteSize = inputStream.readInt() + val byteArray = ByteArray(byteSize) + inputStream.read(byteArray) + val identityKeyPair = IdentityKeyPair(byteArray) + val registrationId = inputStream.readInt() + return IdentityKeyPairAndRegistrationId(identityKeyPair, registrationId) +} + +private fun writeIdentityKeyAndRegistrationId(identityKeyPair: IdentityKeyPair, registrationId: Int): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + val dataOutputStream = DataOutputStream(byteArrayOutputStream) + val identityKeyPairBytes = identityKeyPair.serialize() + dataOutputStream.writeInt(identityKeyPairBytes.size) + dataOutputStream.write(identityKeyPairBytes) + dataOutputStream.writeInt(registrationId) + return byteArrayOutputStream.toByteArray() +} + +class MacosIdentityKeyStoreFactory(private val appInfo: AppInfo, + private val database: Database): IdentityKeyStoreFactory { + + private val filePersist = DesktopOneFilePersist( + getPathProvider() + .resolve("signal.data", FileType.ENCRYPT)) + + override fun createIdentityKeyStore(): IdentityKeyStore { + val file = filePersist.path.toFile() + if (file.exists()) { + logger.info { "Found ideIdentityKey encrypt file" } + val bytes = file.readBytes() + val password = MacosKeychainHelper.getPassword(appInfo.appInstanceId, appInfo.userName) + + password?.let { + logger.info { "Found password in keychain by ${appInfo.appInstanceId} ${appInfo.userName}" } + try { + val secretKey = stringToSecretKey(it) + val decryptData = decryptData(secretKey, bytes) + val (identityKeyPair, registrationId) = readIdentityKeyAndRegistrationId(decryptData) + return DesktopIdentityKeyStore(database, appInfo.appInstanceId, identityKeyPair, registrationId) + } catch (e: Exception) { + logger.error(e) { "Failed to decrypt signalProtocol" } + } + } + + if (file.delete()) { + logger.info { "Delete ideIdentityKey encrypt file" } + } + + } else { + logger.info { "No found ideIdentityKey encrypt file" } + } + + logger.info { "Creating new ideIdentityKey" } + val identityKeyPair = IdentityKeyPair.generate() + val registrationId = KeyHelper.generateRegistrationId(false) + val data = writeIdentityKeyAndRegistrationId(identityKeyPair, registrationId) + val password = MacosKeychainHelper.getPassword(appInfo.appInstanceId, appInfo.userName) + + val secretKey = password?.let { + logger.info { "Found password in keychain by ${appInfo.appInstanceId} ${appInfo.userName}" } + stringToSecretKey(it) + } ?: run { + logger.info { "Generating new password in keychain by ${appInfo.appInstanceId} ${appInfo.userName}" } + val secretKey = generateAESKey() + MacosKeychainHelper.setPassword(appInfo.appInstanceId, appInfo.userName, secretKeyToString(secretKey)) + secretKey + } + + val encryptData = encryptData(secretKey, data) + filePersist.saveBytes(encryptData) + return DesktopIdentityKeyStore(database, appInfo.appInstanceId, identityKeyPair, registrationId) + } +} + + +class WindowsIdentityKeyStoreFactory(private val appInfo: AppInfo, + private val database: Database) : IdentityKeyStoreFactory { + + private val filePersist = DesktopOneFilePersist( + getPathProvider() + .resolve("signal.data", FileType.ENCRYPT)) + + override fun createIdentityKeyStore(): IdentityKeyStore { + val file = filePersist.path.toFile() + if (file.exists()) { + logger.info { "Found ideIdentityKey encrypt file" } + filePersist.readBytes()?.let { + try { + val decryptData = WindowDapiHelper.decryptData(it) + decryptData?.let { byteArray -> + val (identityKeyPair, registrationId) = readIdentityKeyAndRegistrationId(byteArray) + return DesktopIdentityKeyStore(database, appInfo.appInstanceId, identityKeyPair, registrationId) + } + } catch (e: Exception) { + logger.error(e) { "Failed to decrypt ideIdentityKey" } + } + } + if (file.delete()) { + logger.info { "Delete ideIdentityKey encrypt file" } + } + } else { + logger.info { "No found ideIdentityKey encrypt file" } + } + + logger.info { "Creating new ideIdentityKey" } + val identityKeyPair = IdentityKeyPair.generate() + val registrationId = KeyHelper.generateRegistrationId(false) + val data = writeIdentityKeyAndRegistrationId(identityKeyPair, registrationId) + val encryptData = WindowDapiHelper.encryptData(data) + filePersist.saveBytes(encryptData!!) + return DesktopIdentityKeyStore(database, appInfo.appInstanceId, identityKeyPair, registrationId) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/PreKeyStore.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/PreKeyStore.kt similarity index 96% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/PreKeyStore.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/PreKeyStore.kt index 61e7afe2d..108e25dc5 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/PreKeyStore.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/PreKeyStore.kt @@ -1,4 +1,4 @@ -package com.clipevery.presist.data +package com.clipevery.dao.store import com.clipevery.Database import com.clipevery.data.PreKey diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SessionStore.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SessionStore.kt similarity index 54% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SessionStore.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SessionStore.kt index 3b365ecc9..a0deb3261 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SessionStore.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SessionStore.kt @@ -1,4 +1,4 @@ -package com.clipevery.presist.data +package com.clipevery.dao.store import com.clipevery.Database import org.signal.libsignal.protocol.SignalProtocolAddress @@ -8,11 +8,8 @@ import org.signal.libsignal.protocol.state.SessionStore class DesktopSessionStore(private val database: Database): SessionStore { override fun loadSession(address: SignalProtocolAddress): SessionRecord? { - return database.syncQueries.selectSessionRecord(address.name).executeAsOneOrNull()?.let { selectSessionRecord -> - val sessionRecord: ByteArray? = selectSessionRecord.sessionRecord - return sessionRecord?.let { - return SessionRecord(it) - } + return database.sessionQueries.selectSessionRecord(address.name).executeAsOneOrNull()?.let { + return SessionRecord(it) } } @@ -20,32 +17,32 @@ class DesktopSessionStore(private val database: Database): SessionStore { if (addresses!!.isEmpty()) { return mutableListOf() } - database.syncQueries.selectSessionRecords(addresses.map { it.name }).executeAsList().let { sessionRecords -> - return sessionRecords.mapNotNull { it -> it.sessionRecord?.let { SessionRecord(it) } }.toMutableList() + database.sessionQueries.selectSessionRecords(addresses.map { it.name }).executeAsList().let { sessionRecords -> + return sessionRecords.map { SessionRecord(it) }.toMutableList() } } override fun getSubDeviceSessions(name: String): MutableList { - database.syncQueries.selectSubDevice(name).executeAsOneOrNull()?.let { + database.sessionQueries.selectSubDevice(name).executeAsOneOrNull()?.let { return mutableListOf(1) } ?: return mutableListOf() } override fun storeSession(address: SignalProtocolAddress, record: SessionRecord) { - database.syncQueries.updateSessionRecord(record.serialize(), address.name) + database.sessionQueries.updateSessionRecord(record.serialize(), address.name) } override fun containsSession(address: SignalProtocolAddress): Boolean { - return database.syncQueries.count(address.name).executeAsOneOrNull()?.let { + return database.sessionQueries.count(address.name).executeAsOneOrNull()?.let { return it > 0 } ?: false } override fun deleteSession(address: SignalProtocolAddress) { - database.syncQueries.delete(address.name) + database.sessionQueries.delete(address.name) } override fun deleteAllSessions(name: String) { - database.syncQueries.delete(name) + database.sessionQueries.delete(name) } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SignedPreKeyStore.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SignedPreKeyStore.kt similarity index 97% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SignedPreKeyStore.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SignedPreKeyStore.kt index 354600c10..5ad1568b2 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/SignedPreKeyStore.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/dao/store/SignedPreKeyStore.kt @@ -1,4 +1,4 @@ -package com.clipevery.presist.data +package com.clipevery.dao.store import com.clipevery.Database import com.clipevery.data.SignedPreKey diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/encrypt/PrekeyBundleUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/encrypt/PrekeyBundleUtils.kt new file mode 100644 index 000000000..f90b27a1c --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/encrypt/PrekeyBundleUtils.kt @@ -0,0 +1,40 @@ +package com.clipevery.encrypt + +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.InvalidKeyException +import org.signal.libsignal.protocol.ecc.ECPublicKey +import org.signal.libsignal.protocol.state.PreKeyBundle +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.io.IOException + +@Throws(IOException::class, InvalidKeyException::class) +fun decodePreKeyBundle(encoded: ByteArray): PreKeyBundle { + val byteStream = ByteArrayInputStream(encoded) + val dataStream = DataInputStream(byteStream) + val registrationId = dataStream.readInt() + val deviceId = dataStream.readInt() + val preKeyId = dataStream.readInt() + val preKeyPublicSize = dataStream.readInt() + val preKeyPublicBytes = ByteArray(preKeyPublicSize) + dataStream.read(preKeyPublicBytes) + val preKeyPublic = ECPublicKey(preKeyPublicBytes) + val signedPreKeyId = dataStream.readInt() + + val signedPreKeyPublicSize = dataStream.readInt() + val signedPreKeyPublicBytes = ByteArray(signedPreKeyPublicSize) + dataStream.read(signedPreKeyPublicBytes) + val signedPreKeyPublic = ECPublicKey(signedPreKeyPublicBytes) + + val signedPreKeySignatureSize = dataStream.readInt() + val signedPreKeySignatureBytes = ByteArray(signedPreKeySignatureSize) + dataStream.read(signedPreKeyPublicBytes) + + val identityKeySize = dataStream.readInt() + val identityKeyBytes = ByteArray(identityKeySize) + dataStream.read(identityKeyBytes) + val identityKey = IdentityKey(ECPublicKey(identityKeyBytes)) + + return PreKeyBundle(registrationId, deviceId, preKeyId, preKeyPublic, signedPreKeyId, + signedPreKeyPublic, signedPreKeySignatureBytes, identityKey) +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt index 95463bfd3..9c0c67603 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt @@ -20,6 +20,14 @@ import com.clipevery.clip.getDesktopClipboardService import com.clipevery.config.ConfigManager import com.clipevery.config.DefaultConfigManager import com.clipevery.config.FileType +import com.clipevery.controller.SyncController +import com.clipevery.dao.DriverFactory +import com.clipevery.dao.SyncDao +import com.clipevery.dao.createDatabase +import com.clipevery.dao.store.DesktopPreKeyStore +import com.clipevery.dao.store.DesktopSessionStore +import com.clipevery.dao.store.DesktopSignedPreKeyStore +import com.clipevery.dao.store.getIdentityKeyStoreFactory import com.clipevery.device.DesktopDeviceInfoFactory import com.clipevery.device.DeviceInfoFactory import com.clipevery.i18n.GlobalCopywriter @@ -32,16 +40,11 @@ import com.clipevery.net.ClipClient import com.clipevery.net.ClipServer import com.clipevery.net.DesktopClipClient import com.clipevery.net.DesktopClipServer +import com.clipevery.net.SyncValidator import com.clipevery.path.getPathProvider import com.clipevery.platform.currentPlatform +import com.clipevery.presist.DesktopFilePersist import com.clipevery.presist.FilePersist -import com.clipevery.presist.data.DesktopIdentityKeyStore -import com.clipevery.presist.data.DesktopPreKeyStore -import com.clipevery.presist.data.DesktopSessionStore -import com.clipevery.presist.data.DesktopSignedPreKeyStore -import com.clipevery.presist.data.DriverFactory -import com.clipevery.presist.data.createDatabase -import com.clipevery.presist.file.DesktopFilePersist import com.clipevery.ui.DesktopThemeDetector import com.clipevery.ui.ThemeDetector import com.clipevery.ui.getTrayMouseAdapter @@ -70,21 +73,25 @@ fun initKoinApplication(ioScope: CoroutineScope): KoinApplication { single { DesktopAppInfoFactory(get()).createAppInfo() } single { DesktopFilePersist() } single { DefaultConfigManager(get()).initConfig() } - single { DesktopClipServer().start() } + single { DesktopClipServer(get()).start() } + single> { lazy { get() } } single { DesktopClipClient() } single { DesktopDeviceInfoFactory() } - single { DesktopQRCodeGenerator(get(), get()) } + single { DesktopQRCodeGenerator(get(), get(), get()) } single { GlobalCopywriterImpl(get()) } single { getDesktopClipboardService(get()) } single { DesktopTransferableConsumer() } single { GlobalListener() } single { DriverFactory() } single { DesktopThemeDetector(get()) } - single { DesktopIdentityKeyStore(get(), get()) } - single { DesktopPreKeyStore(get()) } + single { getIdentityKeyStoreFactory(get(), get()).createIdentityKeyStore() } single { DesktopSessionStore(get()) } + single { DesktopPreKeyStore(get()) } single { DesktopSignedPreKeyStore(get()) } single { createDatabase(DriverFactory()) } + single { SyncController(get(), get(), get(), get(), get(), get(), get(), get()) } + single { get() } + single { SyncDao(get()) } } return startKoin { modules(appModule) diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipServer.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipServer.kt index 6eec951df..1b3c970db 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipServer.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/net/DesktopClipServer.kt @@ -1,6 +1,8 @@ package com.clipevery.net +import com.clipevery.controller.SyncController import com.clipevery.model.sync.RequestSyncInfo +import com.clipevery.model.sync.ResponseSyncInfo import com.papsign.ktor.openapigen.OpenAPIGen import com.papsign.ktor.openapigen.route.apiRouting import com.papsign.ktor.openapigen.route.path.normal.post @@ -15,7 +17,7 @@ import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import kotlinx.coroutines.runBlocking -class DesktopClipServer: ClipServer { +class DesktopClipServer(private val syncController: SyncController): ClipServer { private val logger = KotlinLogging.logger {} @@ -36,8 +38,8 @@ class DesktopClipServer: ClipServer { } apiRouting { route("/sync") { - post { _, requestSyncInfo -> - respond(requestSyncInfo) + post { _, requestSyncInfo -> + respond(syncController.receiveEndpointSyncInfo(requestSyncInfo)) } } } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopFilePersist.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopFilePersist.kt similarity index 73% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopFilePersist.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopFilePersist.kt index 5e9ea32da..955946238 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopFilePersist.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopFilePersist.kt @@ -1,9 +1,7 @@ -package com.clipevery.presist.file +package com.clipevery.presist import com.clipevery.path.PathProvider import com.clipevery.path.getPathProvider -import com.clipevery.presist.FilePersist -import com.clipevery.presist.OneFilePersist import java.nio.file.Path class DesktopFilePersist: FilePersist { diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopOneFilePersist.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopOneFilePersist.kt similarity index 93% rename from composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopOneFilePersist.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopOneFilePersist.kt index 6b36d1f0c..1411b0b5d 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/file/DesktopOneFilePersist.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/presist/DesktopOneFilePersist.kt @@ -1,6 +1,5 @@ -package com.clipevery.presist.file +package com.clipevery.presist -import com.clipevery.presist.OneFilePersist import kotlinx.serialization.json.Json import kotlinx.serialization.serializer import java.nio.file.Path diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/IdentityKeyStore.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/IdentityKeyStore.kt deleted file mode 100644 index 14538df79..000000000 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/presist/data/IdentityKeyStore.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.clipevery.presist.data - -import com.clipevery.Database -import com.clipevery.model.AppInfo -import io.github.oshai.kotlinlogging.KotlinLogging -import org.signal.libsignal.protocol.IdentityKey -import org.signal.libsignal.protocol.IdentityKeyPair -import org.signal.libsignal.protocol.IdentityKeyPair.generate -import org.signal.libsignal.protocol.SignalProtocolAddress -import org.signal.libsignal.protocol.state.IdentityKeyStore -import org.signal.libsignal.protocol.util.KeyHelper - -class DesktopIdentityKeyStore(private val database: Database, - private val appInfo: AppInfo): IdentityKeyStore { - - - private val logger = KotlinLogging.logger {} - - private val identityKeyPair: IdentityKeyPair - - private val registrationId: Int - - init { - val identityKeyPair = generate() - val registrationId = KeyHelper.generateRegistrationId(false) - try { - database.identityKeyQueries.tryInit(appInfo.appInstanceId, registrationId.toLong(), identityKeyPair.serialize()) - logger.info { "init identityKey success, appInstanceId = ${appInfo.appInstanceId}" } - } catch (ignore: Throwable) { - logger.info { "identityKey exist, appInstanceId = ${appInfo.appInstanceId}" } - } - val identityKey = database.identityKeyQueries.select(appInfo.appInstanceId).executeAsOneOrNull() - if (identityKey == null) { - logger.error { "Failed to get identityKey, appInstanceId = ${appInfo.appInstanceId}" } - this.identityKeyPair = identityKeyPair - this.registrationId = registrationId - } else { - logger.info { "get identityKey success, appInstanceId = ${appInfo.appInstanceId}" } - this.identityKeyPair = IdentityKeyPair(identityKey.serialized) - this.registrationId = identityKey.registrationId.toInt() - } - } - - override fun getIdentityKeyPair(): IdentityKeyPair { - return identityKeyPair - } - - override fun getLocalRegistrationId(): Int { - return registrationId - } - - override fun saveIdentity(address: SignalProtocolAddress, identityKey: IdentityKey): Boolean { - return try { - database.identityKeyQueries.update(identityKey.serialize(), appInfo.appInstanceId, registrationId.toLong()) - true - } catch (e: Exception) { - false - } - } - - override fun isTrustedIdentity( - address: SignalProtocolAddress, - identityKey: IdentityKey, - direction: IdentityKeyStore.Direction - ): Boolean { - val identity: IdentityKey? = getIdentity(address) - return identity?.let { it == identityKey } ?: true - } - - override fun getIdentity(address: SignalProtocolAddress): IdentityKey? { - return database.syncQueries.selectPublicKey(address.name) - .executeAsOneOrNull()?.let { selectPublicKey -> - return selectPublicKey.public_key?.let { - return IdentityKey(it) - } - } - } - -} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt new file mode 100644 index 000000000..16af71019 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/NetUtils.kt @@ -0,0 +1,43 @@ +package com.clipevery.utils + +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.selects.select +import java.net.InetSocketAddress +import java.net.Socket + +fun telnet(host: String, port: Int, timeout: Int): Boolean { + return try { + Socket().use { socket -> + socket.connect(InetSocketAddress(host, port), timeout) + true + } + } catch (e: Exception) { + false + } +} + +suspend fun telnet(hosts: List, port: Int, timeout: Int): String? { + val deferreds = coroutineScope { + hosts.map { host -> + async { + if (telnet(host, port, timeout)) host else null + } + } + } + + var result: String? = null + while (deferreds.isNotEmpty() && result == null) { + select { + deferreds.forEach { deferred -> + deferred.onAwait { hostResult -> + if (hostResult != null) { + result = hostResult + deferreds.forEach { it.cancel() } + } + } + } + } + } + return result +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/QRCodeUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/QRCodeUtils.kt index c7bb7298e..bee9fd85d 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/QRCodeUtils.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/QRCodeUtils.kt @@ -5,26 +5,26 @@ import androidx.compose.ui.graphics.toComposeImageBitmap import com.clipevery.device.DeviceInfoFactory import com.clipevery.model.RequestEndpointInfo import com.clipevery.net.ClipServer +import com.clipevery.net.SyncValidator import com.google.zxing.BarcodeFormat import com.google.zxing.qrcode.QRCodeWriter import java.awt.Color import java.awt.image.BufferedImage -class DesktopQRCodeGenerator(private val clipServer: ClipServer, +class DesktopQRCodeGenerator(private val syncValidator: SyncValidator, + private val clipServer: ClipServer, private val deviceInfoFactory: DeviceInfoFactory): QRCodeGenerator { - private var salt: Int = 0 - - private fun bindInfo(): String { - salt = (0..999999).random() + private fun endpointInfo(): String { + val token = syncValidator.createToken() val deviceInfo = deviceInfoFactory.createDeviceInfo() val port = clipServer.port() - return RequestEndpointInfo(deviceInfo, port).getBase64Encode(salt) + return RequestEndpointInfo(deviceInfo, port).getBase64Encode(token) } override fun generateQRCode(width: Int, height: Int): ImageBitmap { val writer = QRCodeWriter() - val bitMatrix = writer.encode(bindInfo(), BarcodeFormat.QR_CODE, width, height) + val bitMatrix = writer.encode(endpointInfo(), BarcodeFormat.QR_CODE, width, height) val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) for (x in 0 until width) { for (y in 0 until height) { @@ -34,7 +34,7 @@ class DesktopQRCodeGenerator(private val clipServer: ClipServer, return image.toComposeImageBitmap() } - fun getSalt(): Int { - return salt + fun getRefreshTime(): Long { + return syncValidator.getRefreshTime() } } diff --git a/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/SessionBuilderTest.kt b/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/SessionBuilderTest.kt new file mode 100644 index 000000000..55da5bba2 --- /dev/null +++ b/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/SessionBuilderTest.kt @@ -0,0 +1,220 @@ +package com.clipevery.dao.store + +import org.junit.Assert +import org.junit.Test +import org.signal.libsignal.protocol.DuplicateMessageException +import org.signal.libsignal.protocol.InvalidKeyException +import org.signal.libsignal.protocol.InvalidKeyIdException +import org.signal.libsignal.protocol.InvalidMessageException +import org.signal.libsignal.protocol.InvalidVersionException +import org.signal.libsignal.protocol.LegacyMessageException +import org.signal.libsignal.protocol.NoSessionException +import org.signal.libsignal.protocol.SessionBuilder +import org.signal.libsignal.protocol.SessionCipher +import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.libsignal.protocol.UntrustedIdentityException +import org.signal.libsignal.protocol.ecc.Curve +import org.signal.libsignal.protocol.message.CiphertextMessage +import org.signal.libsignal.protocol.message.PreKeySignalMessage +import org.signal.libsignal.protocol.message.SignalMessage +import org.signal.libsignal.protocol.state.PreKeyBundle +import org.signal.libsignal.protocol.state.PreKeyRecord +import org.signal.libsignal.protocol.state.SignalProtocolStore +import org.signal.libsignal.protocol.state.SignedPreKeyRecord +import org.signal.libsignal.protocol.util.Medium +import org.signal.libsignal.protocol.util.Pair +import java.util.Random + +class SessionBuilderTest { + + private val ALICE_ADDRESS = SignalProtocolAddress("+14151111111", 1) + private val BOB_ADDRESS = SignalProtocolAddress("+14152222222", 1) + + @Test + @Throws( + InvalidKeyException::class, + InvalidVersionException::class, + InvalidMessageException::class, + InvalidKeyIdException::class, + DuplicateMessageException::class, + LegacyMessageException::class, + UntrustedIdentityException::class, + NoSessionException::class + ) + fun testBasicPreKey() { + var aliceStore: SignalProtocolStore = TestInMemorySignalProtocolStore() + var aliceSessionBuilder = SessionBuilder(aliceStore, aliceStore, aliceStore, aliceStore, BOB_ADDRESS) + val bobStore: SignalProtocolStore = TestInMemorySignalProtocolStore() + val bundleFactory = X3DHBundleFactory() + val bobPreKey: PreKeyBundle = bundleFactory.createBundle(bobStore) + aliceSessionBuilder.process(bobPreKey) + Assert.assertTrue(aliceStore.containsSession(BOB_ADDRESS)) + Assert.assertTrue(aliceStore.loadSession(BOB_ADDRESS).sessionVersion == 3) + val originalMessage = "Good, fast, cheap: pick two" + var aliceSessionCipher = SessionCipher(aliceStore, BOB_ADDRESS) + var outgoingMessage = aliceSessionCipher.encrypt(originalMessage.toByteArray()) + Assert.assertTrue(outgoingMessage.type == CiphertextMessage.PREKEY_TYPE) + val incomingMessage = PreKeySignalMessage(outgoingMessage.serialize()) + val bobSessionCipher = SessionCipher(bobStore, ALICE_ADDRESS) + var plaintext = bobSessionCipher.decrypt(incomingMessage) + Assert.assertTrue(bobStore.containsSession(ALICE_ADDRESS)) + Assert.assertEquals( + bobStore.loadSession(ALICE_ADDRESS).sessionVersion.toLong(), + 3.toLong() + ) + Assert.assertNotNull(bobStore.loadSession(ALICE_ADDRESS).aliceBaseKey) + Assert.assertTrue(originalMessage == String(plaintext!!)) + val bobOutgoingMessage = bobSessionCipher.encrypt(originalMessage.toByteArray()) + Assert.assertTrue(bobOutgoingMessage.type == CiphertextMessage.WHISPER_TYPE) + val alicePlaintext = + aliceSessionCipher.decrypt(SignalMessage(bobOutgoingMessage.serialize())) + Assert.assertTrue(String(alicePlaintext) == originalMessage) + runInteraction(aliceStore, bobStore) + aliceStore = TestInMemorySignalProtocolStore() + aliceSessionBuilder = SessionBuilder(aliceStore, BOB_ADDRESS) + aliceSessionCipher = SessionCipher(aliceStore, BOB_ADDRESS) + val anotherBundle: PreKeyBundle = bundleFactory.createBundle(bobStore) + aliceSessionBuilder.process(anotherBundle) + outgoingMessage = aliceSessionCipher.encrypt(originalMessage.toByteArray()) + try { + plaintext = bobSessionCipher.decrypt(PreKeySignalMessage(outgoingMessage.serialize())) + Assert.fail("shouldn't be trusted!") + } catch (uie: UntrustedIdentityException) { + bobStore.saveIdentity( + ALICE_ADDRESS, + PreKeySignalMessage(outgoingMessage.serialize()).identityKey + ) + } + plaintext = bobSessionCipher.decrypt(PreKeySignalMessage(outgoingMessage.serialize())) + Assert.assertTrue(String(plaintext) == originalMessage) + val random = Random() + val badIdentityBundle = PreKeyBundle( + bobStore.localRegistrationId, + 1, + random.nextInt(Medium.MAX_VALUE), + Curve.generateKeyPair().publicKey, + random.nextInt(Medium.MAX_VALUE), + bobPreKey.signedPreKey, + bobPreKey.signedPreKeySignature, + aliceStore.identityKeyPair.publicKey + ) + try { + aliceSessionBuilder.process(badIdentityBundle) + Assert.fail("shoulnd't be trusted!") + } catch (uie: UntrustedIdentityException) { + // good + } + } + + @Throws( + DuplicateMessageException::class, + LegacyMessageException::class, + InvalidMessageException::class, + InvalidVersionException::class, + InvalidKeyException::class, + NoSessionException::class, + UntrustedIdentityException::class + ) + private fun runInteraction(aliceStore: SignalProtocolStore, bobStore: SignalProtocolStore) { + val aliceSessionCipher = SessionCipher(aliceStore, BOB_ADDRESS) + val bobSessionCipher = SessionCipher(bobStore, ALICE_ADDRESS) + val originalMessage = "smert ze smert" + val aliceMessage = aliceSessionCipher.encrypt(originalMessage.toByteArray()) + Assert.assertEquals(aliceMessage.type.toLong(), CiphertextMessage.WHISPER_TYPE.toLong()) + var plaintext = bobSessionCipher.decrypt(SignalMessage(aliceMessage.serialize())) + Assert.assertTrue(String(plaintext!!) == originalMessage) + val bobMessage = bobSessionCipher.encrypt(originalMessage.toByteArray()) + Assert.assertEquals(bobMessage.type.toLong(), CiphertextMessage.WHISPER_TYPE.toLong()) + plaintext = aliceSessionCipher.decrypt(SignalMessage(bobMessage.serialize())) + Assert.assertTrue(String(plaintext) == originalMessage) + for (i in 0..9) { + val loopingMessage = ("What do we mean by saying that existence precedes essence? " + + "We mean that man first of all exists, encounters himself, " + + "surges up in the world--and defines himself aftward. " + + i) + val aliceLoopingMessage = aliceSessionCipher.encrypt(loopingMessage.toByteArray()) + val loopingPlaintext = + bobSessionCipher.decrypt(SignalMessage(aliceLoopingMessage.serialize())) + Assert.assertTrue(String(loopingPlaintext) == loopingMessage) + } + for (i in 0..9) { + val loopingMessage = ("What do we mean by saying that existence precedes essence? " + + "We mean that man first of all exists, encounters himself, " + + "surges up in the world--and defines himself aftward. " + + i) + val bobLoopingMessage = bobSessionCipher.encrypt(loopingMessage.toByteArray()) + val loopingPlaintext = + aliceSessionCipher.decrypt(SignalMessage(bobLoopingMessage.serialize())) + Assert.assertTrue(String(loopingPlaintext) == loopingMessage) + } + val aliceOutOfOrderMessages: MutableSet> = HashSet() + for (i in 0..9) { + val loopingMessage = ("What do we mean by saying that existence precedes essence? " + + "We mean that man first of all exists, encounters himself, " + + "surges up in the world--and defines himself aftward. " + + i) + val aliceLoopingMessage = aliceSessionCipher.encrypt(loopingMessage.toByteArray()) + aliceOutOfOrderMessages.add(Pair(loopingMessage, aliceLoopingMessage)) + } + for (i in 0..9) { + val loopingMessage = ("What do we mean by saying that existence precedes essence? " + + "We mean that man first of all exists, encounters himself, " + + "surges up in the world--and defines himself aftward. " + + i) + val aliceLoopingMessage = aliceSessionCipher.encrypt(loopingMessage.toByteArray()) + val loopingPlaintext = + bobSessionCipher.decrypt(SignalMessage(aliceLoopingMessage.serialize())) + Assert.assertTrue(String(loopingPlaintext) == loopingMessage) + } + for (i in 0..9) { + val loopingMessage = "You can only desire based on what you know: $i" + val bobLoopingMessage = bobSessionCipher.encrypt(loopingMessage.toByteArray()) + val loopingPlaintext = + aliceSessionCipher.decrypt(SignalMessage(bobLoopingMessage.serialize())) + Assert.assertTrue(String(loopingPlaintext) == loopingMessage) + } + for (aliceOutOfOrderMessage in aliceOutOfOrderMessages) { + val outOfOrderPlaintext = + bobSessionCipher.decrypt(SignalMessage(aliceOutOfOrderMessage.second().serialize())) + Assert.assertTrue(String(outOfOrderPlaintext) == aliceOutOfOrderMessage.first()) + } + } +} + +interface BundleFactory { + @Throws(InvalidKeyException::class) + fun createBundle(store: SignalProtocolStore): PreKeyBundle +} + + +class X3DHBundleFactory : BundleFactory { + @Throws(InvalidKeyException::class) + override fun createBundle(store: SignalProtocolStore): PreKeyBundle { + val preKeyPair = Curve.generateKeyPair() + val signedPreKeyPair = Curve.generateKeyPair() + val signedPreKeySignature = Curve.calculateSignature( + store.identityKeyPair.privateKey, + signedPreKeyPair.publicKey.serialize() + ) + val random = Random() + val preKeyId = random.nextInt(Medium.MAX_VALUE) + val signedPreKeyId = random.nextInt(Medium.MAX_VALUE) + store.storePreKey(preKeyId, PreKeyRecord(preKeyId, preKeyPair)) + store.storeSignedPreKey( + signedPreKeyId, + SignedPreKeyRecord( + signedPreKeyId, System.currentTimeMillis(), signedPreKeyPair, signedPreKeySignature + ) + ) + return PreKeyBundle( + store.localRegistrationId, + 1, + preKeyId, + preKeyPair.publicKey, + signedPreKeyId, + signedPreKeyPair.publicKey, + signedPreKeySignature, + store.identityKeyPair.publicKey + ) + } +} diff --git a/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/TestInMemorySignalProtocolStore.kt b/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/TestInMemorySignalProtocolStore.kt new file mode 100644 index 000000000..abf4552b0 --- /dev/null +++ b/composeApp/src/desktopTest/kotlin/com/clipevery/dao/store/TestInMemorySignalProtocolStore.kt @@ -0,0 +1,23 @@ +package com.clipevery.dao.store + +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.IdentityKeyPair +import org.signal.libsignal.protocol.ecc.Curve +import org.signal.libsignal.protocol.state.impl.InMemorySignalProtocolStore +import org.signal.libsignal.protocol.util.KeyHelper + + +class TestInMemorySignalProtocolStore( + identityKeyPair: IdentityKeyPair = generateIdentityKeyPair(), + registrationId: Int = generateRegistrationId()) : InMemorySignalProtocolStore(identityKeyPair, registrationId) {} + +private fun generateIdentityKeyPair(): IdentityKeyPair { + val identityKeyPairKeys = Curve.generateKeyPair() + return IdentityKeyPair( + IdentityKey(identityKeyPairKeys.publicKey), identityKeyPairKeys.privateKey + ) +} + +private fun generateRegistrationId(): Int { + return KeyHelper.generateRegistrationId(false) +} \ No newline at end of file