├── .github └── workflows │ ├── publish-release.yml │ └── publish-snapshot.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── Version.kt │ ├── kmp-library-convention.gradle.kts │ ├── print-sdk-version-convention.gradle.kts │ └── publication-convention.gradle.kts ├── build.gradle.kts ├── client ├── build.gradle.kts ├── ktor │ ├── build.gradle.kts │ └── src │ │ ├── commonMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── client │ │ │ └── ktor │ │ │ ├── DataCenter.kt │ │ │ └── websocket │ │ │ └── KtorWebsocketTransport.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── client │ │ │ └── ktor │ │ │ └── DataCenter.ios.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── client │ │ │ └── ktor │ │ │ └── DataCenter.js.kt │ │ └── jvmMain │ │ └── kotlin │ │ └── ktproto │ │ └── client │ │ └── ktor │ │ ├── ClientMain.kt │ │ ├── DataCenter.jvm.kt │ │ └── socket │ │ └── KtorSocketTransport.kt └── src │ └── commonMain │ └── kotlin │ └── ktproto │ └── client │ ├── MTProtoClient.kt │ ├── authorization │ ├── CreateAuthorizationKey.kt │ ├── ExchangeKeys.kt │ └── InitDH.kt │ ├── plain │ └── PlainMTProtoClient.kt │ ├── requests │ ├── DHParamsRequest.kt │ ├── PQRequest.kt │ └── TLPQInnerDataDC.kt │ ├── rsa │ ├── RsaPublicKey.kt │ └── TLRsaPublicKey.kt │ └── serialization │ ├── MTProtoClient.kt │ ├── MTProtoRequest.kt │ └── MTProtoRequestDescriptor.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── libs ├── crypto │ ├── build.gradle.kts │ └── src │ │ ├── appleMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── crypto │ │ │ ├── aes │ │ │ └── Aes.apple.kt │ │ │ └── sha │ │ │ ├── Sha1.apple.kt │ │ │ └── Sha256.apple.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── crypto │ │ │ ├── aes │ │ │ ├── AESIge.kt │ │ │ ├── Aes.kt │ │ │ ├── AesBlock.kt │ │ │ ├── AesIV.kt │ │ │ └── AesKey.kt │ │ │ ├── asn1 │ │ │ ├── Asn1Object.kt │ │ │ └── Asn1Parser.kt │ │ │ ├── factorization │ │ │ └── PollardRhoBrent.kt │ │ │ ├── rsa │ │ │ └── RsaPublicKey.kt │ │ │ └── sha │ │ │ ├── Sha1.kt │ │ │ └── Sha256.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── crypto │ │ │ └── sha │ │ │ ├── TestAes.kt │ │ │ ├── TestBigInt.kt │ │ │ └── TestSha1.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── ktproto │ │ │ └── crypto │ │ │ ├── aes │ │ │ └── Aes.js.kt │ │ │ └── sha │ │ │ ├── Crypto.kt │ │ │ ├── Sha1.js.kt │ │ │ └── Sha256.js.kt │ │ └── jvmMain │ │ └── kotlin │ │ └── ktproto │ │ └── crypto │ │ ├── aes │ │ ├── Aes256.jvm.kt │ │ └── AesKey.jvm.kt │ │ ├── bigint │ │ └── CommonBigIntMain.kt │ │ ├── rsa │ │ └── RsaPublicKey.jvm.kt │ │ └── sha │ │ ├── Sha1.jvm.kt │ │ └── Sha256.jvm.kt ├── io │ ├── build.gradle.kts │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── ktproto │ │ └── io │ │ ├── annotation │ │ └── OngoingConnection.kt │ │ ├── input │ │ ├── ByteArrayInput.kt │ │ └── Input.kt │ │ ├── memory │ │ ├── Flatten.kt │ │ ├── MemoryArena.kt │ │ ├── Read.kt │ │ └── Write.kt │ │ └── output │ │ ├── ByteArrayOutput.kt │ │ └── Output.kt └── stdlib-extensions │ ├── build.gradle.kts │ └── src │ ├── commonMain │ └── kotlin │ │ └── ktproto │ │ └── stdlib │ │ ├── bigint │ │ └── BigInt.kt │ │ ├── bit │ │ ├── Bit.kt │ │ └── BitArray.kt │ │ ├── bytes │ │ ├── ByteArrayPad.kt │ │ ├── GreaterThanBigEndian.kt │ │ ├── Int.kt │ │ ├── Long.kt │ │ ├── Pad.kt │ │ ├── ToBinaryString.kt │ │ └── Xor.kt │ │ ├── int │ │ └── NearestMultiple.kt │ │ ├── random │ │ └── NextInt128.kt │ │ └── scope │ │ └── WeakCoroutineScope.kt │ └── jsMain │ └── kotlin │ └── ktproto │ └── stdlib │ └── platform │ └── JsPlatform.kt ├── session ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── ktproto │ └── session │ ├── AuthKeyId.kt │ ├── MTProtoSafeSession.kt │ ├── MTProtoSession.kt │ ├── MessageId.kt │ ├── MessageIdProvider.kt │ ├── encrypted │ ├── EncodeMessage.kt │ ├── EncryptedData.kt │ ├── MTProtoEncryptedSession.kt │ ├── MTProtoEnvelope.kt │ ├── Salt.kt │ ├── SeqNo.kt │ └── SessionId.kt │ └── plain │ ├── MTProtoPlainEnvelope.kt │ └── MTProtoPlainSession.kt ├── settings.gradle.kts ├── transport ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── ktproto │ └── transport │ ├── MTProtoIntermediate.kt │ ├── MTProtoTransport.kt │ ├── ThrowTransportExceptions.kt │ ├── Transport.kt │ └── exception │ ├── IOException.kt │ └── TransportException.kt └── types ├── build.gradle.kts └── src ├── commonMain └── kotlin │ └── ktproto │ ├── exception │ └── MTProtoException.kt │ └── time │ └── Clock.kt ├── iosMain └── kotlin │ └── ktproto │ └── time │ └── Clock.ios.kt ├── jsMain └── kotlin │ └── ktproto │ └── time │ └── Clock.js.kt └── jvmMain └── kotlin └── ktproto └── time └── Clock.jvm.kt /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Library Release Deploy 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | workflow_dispatch: 7 | 8 | env: 9 | GITHUB_USERNAME: ${{ github.actor }} 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | jobs: 13 | 14 | deploy-multiplatform: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | packages: write 19 | outputs: 20 | release_version: ${{ steps.output_version.outputs.release_version }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Setup Java 24 | uses: actions/setup-java@v3 25 | with: 26 | distribution: temurin 27 | java-version: 11 28 | - name: Gradle Cache Setup 29 | uses: gradle/gradle-build-action@v2 30 | - name: Gradle Sync 31 | run: ./gradlew 32 | - name: Add Version to Env 33 | run: | 34 | release_version=$(./gradlew printVersion -q) 35 | echo "release_version=$release_version" >> $GITHUB_ENV 36 | - name: Publish ${{ env.release_version }} 37 | run: ./gradlew publishKotlinMultiplatformPublicationToGitHubRepository 38 | - name: Add Sdk Version to Output 39 | id: output_version 40 | run: echo "release_version=${{ env.release_version }}" >> $GITHUB_OUTPUT 41 | 42 | deploy-jvm: 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | packages: write 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Setup Java 50 | uses: actions/setup-java@v3 51 | with: 52 | distribution: temurin 53 | java-version: 11 54 | - name: Gradle Cache Setup 55 | uses: gradle/gradle-build-action@v2 56 | - name: Gradle Sync 57 | run: ./gradlew 58 | - name: Add Version to Env 59 | run: | 60 | release_version=$(./gradlew printVersion -q) 61 | echo "release_version=$release_version" >> $GITHUB_ENV 62 | - name: Publish ${{ env.release_version }} 63 | run: ./gradlew publishJvmPublicationToGitHubRepository 64 | 65 | deploy-js: 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | packages: write 70 | steps: 71 | - uses: actions/checkout@v3 72 | - name: Setup Java 73 | uses: actions/setup-java@v3 74 | with: 75 | distribution: temurin 76 | java-version: 11 77 | - name: Gradle Cache Setup 78 | uses: gradle/gradle-build-action@v2 79 | - name: Gradle Sync 80 | run: ./gradlew 81 | - name: Add Version to Env 82 | run: | 83 | release_version=$(./gradlew printVersion -q) 84 | echo "release_version=$release_version" >> $GITHUB_ENV 85 | - name: Publish ${{ env.release_version }} 86 | run: ./gradlew publishJsPublicationToGitHubRepository 87 | 88 | deploy-konan: 89 | runs-on: macos-latest 90 | permissions: 91 | contents: write 92 | packages: write 93 | steps: 94 | - uses: actions/checkout@v3 95 | - name: Setup Java 96 | uses: actions/setup-java@v3 97 | with: 98 | distribution: temurin 99 | java-version: 11 100 | - name: Gradle Cache Setup 101 | uses: gradle/gradle-build-action@v2 102 | - name: Konan Cache Setup 103 | uses: actions/cache@v3 104 | with: 105 | path: ~/.konan 106 | key: konan-cache 107 | - name: Gradle Sync 108 | run: ./gradlew 109 | - name: Add Version to Env 110 | run: | 111 | release_version=$(./gradlew printVersion -q) 112 | echo "release_version=$release_version" >> $GITHUB_ENV 113 | - name: Publish ${{ env.release_version }} 114 | run: | 115 | ./gradlew publishIosX64PublicationToGitHubRepository \ 116 | publishIosSimulatorArm64PublicationToGitHubRepository \ 117 | publishIosArm64PublicationToGitHubRepository 118 | 119 | create-release: 120 | runs-on: ubuntu-latest 121 | permissions: 122 | contents: write 123 | packages: write 124 | needs: 125 | - deploy-multiplatform 126 | - deploy-jvm 127 | - deploy-js 128 | - deploy-konan 129 | steps: 130 | - name: Create Release 131 | uses: actions/create-release@v1 132 | with: 133 | tag_name: ${{ needs.deploy-multiplatform.outputs.release_version }} 134 | release_name: Release ${{ needs.deploy-multiplatform.outputs.release_version }} 135 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Library Snapshot Deploy 2 | 3 | on: 4 | push: 5 | branches-ignore: [ "master" ] 6 | workflow_dispatch: 7 | 8 | env: 9 | GITHUB_USERNAME: ${{ github.actor }} 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 12 | ORG_GRADLE_PROJECT_snapshot: true 13 | ORG_GRADLE_PROJECT_commit: ${{ github.sha }} 14 | ORG_GRADLE_PROJECT_attempt: ${{ github.run_attempt }} 15 | 16 | jobs: 17 | 18 | deploy-multiplatform: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | packages: write 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Gradle Cache Setup 26 | uses: gradle/gradle-build-action@v2 27 | with: 28 | cache-read-only: ${{ github.ref != 'refs/heads/dev' }} 29 | - name: Gradle Sync 30 | run: ./gradlew 31 | - name: Add Sdk Version to Env 32 | run: | 33 | snapshot_version=$(./gradlew printVersion -q) 34 | echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV 35 | - name: Publish ${{ env.snapshot_version }} 36 | run: ./gradlew publishKotlinMultiplatformPublicationToGitHubRepository 37 | 38 | deploy-jvm: 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: read 42 | packages: write 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Gradle Cache Setup 46 | uses: gradle/gradle-build-action@v2 47 | with: 48 | cache-read-only: ${{ github.ref != 'refs/heads/dev' }} 49 | - name: Gradle Sync 50 | run: ./gradlew 51 | - name: Add Sdk Version to Env 52 | run: | 53 | snapshot_version=$(./gradlew printVersion -q) 54 | echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV 55 | - name: Publish ${{ env.snapshot_version }} 56 | run: ./gradlew publishJvmPublicationToGitHubRepository 57 | 58 | deploy-js: 59 | runs-on: ubuntu-latest 60 | permissions: 61 | contents: read 62 | packages: write 63 | steps: 64 | - uses: actions/checkout@v3 65 | - name: Gradle Cache Setup 66 | uses: gradle/gradle-build-action@v2 67 | with: 68 | cache-read-only: ${{ github.ref != 'refs/heads/dev' }} 69 | - name: Gradle Sync 70 | run: ./gradlew 71 | - name: Add Sdk Version to Env 72 | run: | 73 | snapshot_version=$(./gradlew printVersion -q) 74 | echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV 75 | - name: Publish ${{ env.snapshot_version }} 76 | run: ./gradlew publishJsPublicationToGitHubRepository 77 | 78 | deploy-konan: 79 | runs-on: macos-latest 80 | permissions: 81 | contents: read 82 | packages: write 83 | steps: 84 | - uses: actions/checkout@v3 85 | - name: Gradle Cache Setup 86 | uses: gradle/gradle-build-action@v2 87 | with: 88 | cache-read-only: ${{ github.ref != 'refs/heads/dev' }} 89 | - name: Konan Cache Setup 90 | uses: actions/cache@v3 91 | with: 92 | path: ~/.konan 93 | key: konan-cache 94 | - name: Gradle Sync 95 | run: ./gradlew 96 | - name: Add Sdk Version to Env 97 | run: | 98 | snapshot_version=$(./gradlew printVersion -q) 99 | echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV 100 | - name: Publish ${{ env.snapshot_version }} 101 | run: | 102 | ./gradlew publishIosX64PublicationToGitHubRepository \ 103 | publishIosSimulatorArm64PublicationToGitHubRepository \ 104 | publishIosArm64PublicationToGitHubRepository -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | .gradle 4 | local.properties -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Alexander Sokolinsky 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ktproto - Kotlin Library for MTProto Protocol 2 | 3 | ⚠️ ALL OF THIS IS WORK IN PROGRESS ⚠️ 4 | 5 | `ktproto` is a Kotlin library designed to simplify working with Telegram's MTProto protocol. This library provides the tools you need to establish connections, perform authentication, and interact with the Telegram API using the MTProto protocol. 6 | 7 | ## Features 8 | 9 | - Establish connections to Telegram's servers. 10 | - Interact with the Telegram API using MTProto protocol. 11 | - Built-in integration with TL (Maintained in a separate repo: https://github.com/kotlin-telegram/ktproto) 12 | 13 | ## Usage 14 | 15 | ```kotlin 16 | @OngoingConnection 17 | private suspend fun main(): Unit = weakCoroutineScope { 18 | val transport = openKtorSocketTransport( 19 | hostname = "149.154.167.51", 20 | port = 443 21 | ) 22 | val client = plainMTProtoClient( 23 | transport = transport, 24 | scope = this 25 | ) 26 | repeat(10) { 27 | createAuthorizationKey(client, keys) 28 | } 29 | // Sending encrypted requests are not supported ATM 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | `kotlin-dsl` 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | google() 9 | gradlePluginPortal() 10 | } 11 | 12 | dependencies { 13 | api(libs.kotlinPlugin) 14 | api(libs.kotlinxSerializationPlugin) 15 | } 16 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | } 6 | 7 | versionCatalogs { 8 | create("libs") { 9 | from(files("../gradle/libs.versions.toml")) 10 | } 11 | } 12 | } 13 | 14 | include(":library-deploy") 15 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/Version.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | 3 | fun Project.versionFromProperties(acceptor: (String) -> Unit) { 4 | afterEvaluate { 5 | acceptor(versionFromProperties()) 6 | } 7 | } 8 | 9 | fun Project.versionFromProperties(): String { 10 | val snapshot = project.findProperty("snapshot")?.toString()?.toBooleanStrict() 11 | if (snapshot == null || !snapshot) return project.version.toString() 12 | 13 | val commit = project.property("commit").toString() 14 | val attempt = project.property("attempt").toString().toInt() 15 | 16 | val version = buildString { 17 | append(project.version) 18 | append("-build") 19 | append(commit.take(n = 7)) 20 | if (attempt > 1) { 21 | append(attempt) 22 | } 23 | } 24 | 25 | return version 26 | } 27 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/kmp-library-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class) 2 | 3 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 4 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 5 | 6 | plugins { 7 | kotlin("multiplatform") 8 | kotlin("plugin.serialization") 9 | id("publication-convention") 10 | } 11 | 12 | kotlin { 13 | jvm { 14 | jvmToolchain(8) 15 | } 16 | js { 17 | browser() 18 | nodejs() 19 | } 20 | iosArm64() 21 | iosX64() 22 | iosSimulatorArm64() 23 | 24 | explicitApi() 25 | 26 | targets.all { 27 | compilations.all { 28 | compilerOptions.configure { 29 | freeCompilerArgs.add("-Xexpect-actual-classes") 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/print-sdk-version-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE") 2 | 3 | import org.gradle.kotlin.dsl.creating 4 | 5 | tasks { 6 | val printVersion by creating { 7 | group = "CI" 8 | 9 | doFirst { 10 | println(versionFromProperties()) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/publication-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.maven-publish") 3 | } 4 | 5 | group = "me.y9san9.ktproto" 6 | 7 | publishing { 8 | repositories { 9 | maven { 10 | name = "GitHub" 11 | url = uri("https://maven.pkg.github.com/kotlin-telegram/ktproto") 12 | credentials { 13 | username = System.getenv("GITHUB_USERNAME") 14 | password = System.getenv("GITHUB_TOKEN") 15 | } 16 | } 17 | } 18 | 19 | publications.withType { 20 | versionFromProperties { version -> 21 | this.version = version 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | id("print-sdk-version-convention") 5 | } 6 | 7 | version = libs.versions.ktprotoVersion.get() 8 | 9 | dependencies { 10 | commonMainImplementation(libs.kotlinxSerialization) 11 | } 12 | -------------------------------------------------------------------------------- /client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainApi(projects.session) 10 | commonMainApi(libs.koTL.serialization) 11 | 12 | commonMainImplementation(projects.libs.crypto) 13 | commonMainImplementation(libs.kotlinxSerialization) 14 | commonMainImplementation(libs.kotlinxCoroutines) 15 | commonMainImplementation(projects.libs.stdlibExtensions) 16 | commonMainImplementation(projects.libs.io) 17 | commonMainImplementation(projects.types) 18 | } 19 | -------------------------------------------------------------------------------- /client/ktor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainApi(projects.client) 10 | commonMainImplementation(projects.libs.io) 11 | commonMainImplementation(projects.libs.stdlibExtensions) 12 | commonMainImplementation(projects.types) 13 | commonMainImplementation(libs.koTL.serialization) 14 | commonMainApi(libs.ktor.client) 15 | commonMainApi(libs.kotlinxSerialization) 16 | jvmMainImplementation(libs.ktor.client.cio) 17 | jvmMainImplementation(libs.ktor.client.logging) 18 | } 19 | -------------------------------------------------------------------------------- /client/ktor/src/commonMain/kotlin/ktproto/client/ktor/DataCenter.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor 2 | 3 | // todo: move file to kotel, since it's telegram, not MTProto 4 | // ktproto may also be used for TON (lib name kton) 5 | 6 | internal expect fun isJS(): Boolean 7 | 8 | public interface DataCenter { 9 | public val name: String 10 | public val isTest: Boolean 11 | } 12 | 13 | public fun DataCenter.websocketUrl( 14 | isSecure: Boolean = true, 15 | includeCORS: Boolean = isJS() 16 | ): String = url( 17 | isWebSocket = true, 18 | isSecure, includeCORS 19 | ) 20 | 21 | public fun DataCenter.url( 22 | isWebSocket: Boolean, 23 | isSecure: Boolean = true, 24 | includeCORS: Boolean = isJS(), 25 | ): String = buildString { 26 | append("http") 27 | if (isSecure) append('s') 28 | append("://$name.web.telegram.org") 29 | if (isSecure) append(":443") else append(":80") 30 | append("/api") 31 | // if (includeCORS) 32 | append('w') 33 | if (isWebSocket) append('s') 34 | if (isTest) append("_test") 35 | } 36 | -------------------------------------------------------------------------------- /client/ktor/src/commonMain/kotlin/ktproto/client/ktor/websocket/KtorWebsocketTransport.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor.websocket 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.websocket.* 5 | import io.ktor.client.request.* 6 | import io.ktor.http.* 7 | import io.ktor.websocket.* 8 | import kotlinx.coroutines.* 9 | import ktproto.io.annotation.OngoingConnection 10 | import ktproto.io.input.Input 11 | import ktproto.io.memory.* 12 | import ktproto.io.output.Output 13 | import ktproto.transport.Transport 14 | import kotlin.math.max 15 | 16 | @OngoingConnection 17 | public suspend fun connectKtorWebsocketTransport( 18 | httpClient: HttpClient, 19 | urlString: String, 20 | scope: CoroutineScope, 21 | request: HttpRequestBuilder.() -> Unit = {} 22 | ): Transport { 23 | val deferred = CompletableDeferred() 24 | 25 | scope.launch { 26 | ktorWebsocketTransport(httpClient, urlString, request) { transport -> 27 | deferred.complete(transport) 28 | awaitCancellation() 29 | } 30 | } 31 | 32 | return deferred.await() 33 | } 34 | 35 | @OptIn(OngoingConnection::class) 36 | public suspend inline fun ktorWebsocketTransport( 37 | httpClient: HttpClient, 38 | urlString: String, 39 | crossinline request: HttpRequestBuilder.() -> Unit = {}, 40 | crossinline block: suspend (KtorWebsocketTransport) -> Unit 41 | ) { 42 | httpClient.config { 43 | install(WebSockets) 44 | }.webSocket( 45 | urlString = urlString, 46 | request = { 47 | url { 48 | protocol = if (it.protocol.isSecure()) URLProtocol.WSS else URLProtocol.WS 49 | } 50 | header(HttpHeaders.SecWebSocketProtocol, "binary") 51 | request() 52 | } 53 | ) { 54 | val transport = KtorWebsocketTransport(webSocket = this) 55 | block(transport) 56 | } 57 | } 58 | 59 | @OngoingConnection 60 | public class KtorWebsocketTransport( 61 | private val webSocket: DefaultClientWebSocketSession 62 | ) : Transport { 63 | override val input: Input = Input { destination -> readToMemory(destination) } 64 | override val output: Output = Output { source -> writeFromMemory(source) } 65 | 66 | private var remaining: MemoryArena = MemoryArena.allocate(n = 0) 67 | 68 | private suspend fun readToMemory(destination: MemoryArena) { 69 | val cachedMemory = remaining 70 | 71 | val dataMemory = if (cachedMemory.size >= destination.size) { 72 | cachedMemory 73 | } else { 74 | val socketBytes = webSocket.incoming.receive().data 75 | MemoryArena 76 | .allocate(n = cachedMemory.size + socketBytes.size) 77 | .write(cachedMemory) 78 | .write(socketBytes) 79 | } 80 | 81 | val maxSize = max(destination.size, dataMemory.size) 82 | this.remaining = dataMemory.drop(maxSize) 83 | 84 | if (dataMemory.size < destination.size) { 85 | val remaining = destination.write(dataMemory) 86 | return readToMemory(remaining) 87 | } 88 | 89 | destination.write(dataMemory.take(destination.size)) 90 | } 91 | 92 | private suspend fun writeFromMemory(source: MemoryArena) { 93 | if (source.size <= webSocket.maxFrameSize) { 94 | return webSocket.outgoing.send( 95 | element = Frame.Binary( 96 | fin = true, 97 | data = source.toByteArray() 98 | ) 99 | ) 100 | } 101 | 102 | val maxFrame = source.take(webSocket.maxFrameSize.toInt()) 103 | webSocket.outgoing.send( 104 | element = Frame.Binary( 105 | fin = false, 106 | data = maxFrame.toByteArray() 107 | ) 108 | ) 109 | webSocket.flush() 110 | return writeFromMemory(source.drop(webSocket.maxFrameSize.toInt())) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /client/ktor/src/iosMain/kotlin/ktproto/client/ktor/DataCenter.ios.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor 2 | 3 | internal actual fun isJS(): Boolean = false 4 | -------------------------------------------------------------------------------- /client/ktor/src/jsMain/kotlin/ktproto/client/ktor/DataCenter.js.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor 2 | 3 | internal actual fun isJS(): Boolean = true 4 | -------------------------------------------------------------------------------- /client/ktor/src/jvmMain/kotlin/ktproto/client/ktor/ClientMain.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.logging.* 5 | import ktproto.client.authorization.createAuthorizationKey 6 | import ktproto.client.ktor.socket.ktorSocketTransport 7 | import ktproto.client.plain.plainMTProtoClient 8 | import ktproto.client.rsa.RsaPublicKey 9 | import ktproto.io.annotation.OngoingConnection 10 | import ktproto.stdlib.scope.weakCoroutineScope 11 | 12 | private object DC : DataCenter { 13 | override val name: String = "pluto" 14 | override val isTest: Boolean = false 15 | } 16 | 17 | private val httpClient = HttpClient { 18 | Logging { 19 | level = LogLevel.ALL 20 | logger = object : Logger { 21 | override fun log(message: String) = println(message) 22 | } 23 | } 24 | } 25 | 26 | // Fingerprint: d09d1d85de64fd85 27 | private val productionKey = RsaPublicKey(""" 28 | -----BEGIN RSA PUBLIC KEY----- 29 | MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g 30 | 5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO 31 | 62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/ 32 | +aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9 33 | t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs 34 | 5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB 35 | -----END RSA PUBLIC KEY----- 36 | """.trimIndent()) 37 | 38 | // Fingerprint: b25898df208d2603 39 | private val testKey = RsaPublicKey(""" 40 | -----BEGIN RSA PUBLIC KEY----- 41 | MIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR 42 | yy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv 43 | plUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/ 44 | j25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1 45 | aHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO 46 | j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB 47 | -----END RSA PUBLIC KEY----- 48 | """.trimIndent()) 49 | 50 | private val keys = listOf(productionKey, testKey) 51 | 52 | @OngoingConnection 53 | private suspend fun main(): Unit = weakCoroutineScope { 54 | val client = plainMTProtoClient( 55 | scope = this, 56 | transport = ktorSocketTransport( 57 | hostname = "149.154.167.50", 58 | port = 443 59 | ) 60 | ) 61 | println("Hostname: 149.154.167.51") 62 | println("Port: 443") 63 | repeat(10) { 64 | createAuthorizationKey(client, 2, keys) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/ktor/src/jvmMain/kotlin/ktproto/client/ktor/DataCenter.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor 2 | 3 | internal actual fun isJS(): Boolean = false 4 | -------------------------------------------------------------------------------- /client/ktor/src/jvmMain/kotlin/ktproto/client/ktor/socket/KtorSocketTransport.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.ktor.socket 2 | 3 | import io.ktor.network.selector.* 4 | import io.ktor.network.sockets.* 5 | import io.ktor.utils.io.* 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.job 9 | import ktproto.io.annotation.OngoingConnection 10 | import ktproto.io.input.Input 11 | import ktproto.io.memory.* 12 | import ktproto.io.output.Output 13 | import ktproto.transport.Transport 14 | import ktproto.transport.exception.IOException 15 | import ktproto.transport.exception.throwIO 16 | 17 | @OptIn(OngoingConnection::class) 18 | public suspend fun ktorSocketTransport( 19 | hostname: String, 20 | port: Int 21 | ): Transport.Connector = Transport.Connector { scope -> 22 | runCatching { 23 | val manager = SelectorManager(dispatcher = Dispatchers.IO + scope.coroutineContext.job) 24 | val socket = aSocket(manager).tcp().connect(hostname, port) 25 | KtorSocketTransport(socket) 26 | }.getOrElse { cause -> 27 | cause.throwIO() 28 | } 29 | } 30 | 31 | @OngoingConnection 32 | public class KtorSocketTransport( 33 | socket: Socket 34 | ) : Transport { 35 | private val readChannel = socket.openReadChannel() 36 | private val writeChannel = socket.openWriteChannel(autoFlush = true) 37 | 38 | override val input: Input = Input { destination -> 39 | runCatching { readToMemory(destination) } 40 | .getOrElse { cause -> cause.throwIO() } 41 | } 42 | override val output: Output = Output { source -> 43 | runCatching { writeFromMemory(source) } 44 | .getOrElse { cause -> cause.throwIO() } 45 | } 46 | 47 | private suspend fun readToMemory(destination: MemoryArena) { 48 | readChannel.readFully(destination.data, destination.start, destination.size) 49 | } 50 | private suspend fun writeFromMemory(source: MemoryArena) { 51 | writeChannel.writeFully(source.data, source.start, source.size) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/MTProtoClient.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client 2 | 3 | import kotl.core.descriptor.TLExpressionDescriptor 4 | import kotl.core.element.TLExpression 5 | import kotl.core.element.TLFunction 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | public interface MTProtoClient { 9 | public val updates: Flow 10 | 11 | public suspend fun execute( 12 | function: TLFunction, 13 | responseDescriptor: TLExpressionDescriptor 14 | ): TLExpression 15 | } 16 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/authorization/CreateAuthorizationKey.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.authorization 2 | 3 | import ktproto.client.MTProtoClient 4 | import ktproto.client.rsa.RsaPublicKey 5 | 6 | // fixme: move this function to kotel since 7 | // it contains telegram-specific data (Data Center ID) 8 | public suspend fun createAuthorizationKey( 9 | client: MTProtoClient, 10 | dc: Int, 11 | keys: List 12 | ) { 13 | val (pq, p, q, publicKey, nonce, serverNonce) = initDH(client, keys) 14 | exchangeKeys(client, pq, p, q, publicKey, nonce, serverNonce, dc) 15 | } 16 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/authorization/ExchangeKeys.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.authorization 2 | 3 | import kotl.serialization.TL 4 | import kotl.serialization.bytes.Bytes 5 | import kotl.serialization.int.Int128 6 | import kotlinx.serialization.encodeToByteArray 7 | import ktproto.client.MTProtoClient 8 | import ktproto.client.requests.TLPQInnerData 9 | import ktproto.client.requests.TLPQInnerDataDC 10 | import ktproto.client.requests.getDHParams 11 | import ktproto.client.rsa.RsaPublicKey 12 | import ktproto.client.rsa.fingerprint 13 | import ktproto.crypto.aes.AesIV 14 | import ktproto.crypto.aes.AesKey 15 | import ktproto.crypto.aes.encryptAesIge 16 | import ktproto.crypto.sha.sha256 17 | import ktproto.stdlib.bigint.toBigInt 18 | import ktproto.stdlib.bytes.padStart 19 | import ktproto.stdlib.bytes.xor 20 | import ktproto.stdlib.random.nextInt128 21 | import kotlin.random.Random 22 | 23 | @OptIn(ExperimentalStdlibApi::class) 24 | internal suspend fun exchangeKeys( 25 | client: MTProtoClient, 26 | pq: Bytes, 27 | p: Bytes, 28 | q: Bytes, 29 | serverPublicKey: RsaPublicKey, 30 | nonce: Int128, 31 | serverNonce: Int128, 32 | dc: Int 33 | ) { 34 | println("Server Key Fingerprint: ${serverPublicKey.fingerprint().toHexString()}") 35 | // 4) 36 | val newNonce = Random.nextInt128() 37 | 38 | val request = TLPQInnerDataDC( 39 | pq = pq, 40 | p = p, 41 | q = q, 42 | nonce = nonce, 43 | serverNonce = serverNonce, 44 | newNonce = newNonce, 45 | dc = dc 46 | ) 47 | 48 | println("p_q_inner_data_dc: $request") 49 | 50 | val data = TL.encodeToByteArray(request) 51 | println(data.toHexString()) 52 | val encryptedData = rsaPad(data, serverPublicKey) 53 | 54 | val params = client.getDHParams( 55 | nonce = nonce, 56 | serverNonce = serverNonce, 57 | p = p, 58 | q = q, 59 | publicKeyFingerprint = serverPublicKey.fingerprint(), 60 | encryptedData = Bytes(encryptedData) 61 | ) 62 | println(params) 63 | } 64 | 65 | private suspend fun rsaPad(data: ByteArray, publicKey: RsaPublicKey): ByteArray { 66 | // 4.1) 67 | require(data.size <= 144) { "One has to check that data is not longer than 144 bytes. https://core.telegram.org/mtproto/auth_key" } 68 | 69 | // data_with_padding := data + random_padding_bytes; 70 | // -- where random_padding_bytes are chosen so that the 71 | // resulting length of data_with_padding is precisely 192 bytes, 72 | // and data is the TL-serialized data to be encrypted as before. 73 | val paddingSize = 192 - data.size 74 | val dataWithPadding = data + Random.nextBytes(paddingSize) 75 | 76 | // data_pad_reversed := BYTE_REVERSE(data_with_padding); 77 | // -- is obtained from data_with_padding by reversing the byte order. 78 | val dataPadReversed = dataWithPadding.reversedArray() 79 | 80 | while (true) { 81 | // a random 32-byte temp_key is generated. 82 | val tempKey = Random 83 | .nextBytes(AesKey.Bits256.SIZE_BYTES) 84 | .let(AesKey::Bits256) 85 | 86 | // data_with_hash := data_pad_reversed + SHA256(temp_key + data_with_padding); 87 | // -- after this assignment, data_with_hash is exactly 224 bytes long. 88 | val dataWithHash = dataPadReversed + (tempKey.bytes + dataWithPadding).sha256() 89 | // aes_encrypted := AES256_IGE(data_with_hash, temp_key, 0); 90 | // -- AES256-IGE encryption with zero IV. 91 | val aesEncrypted = dataWithHash.encryptAesIge(tempKey, AesIV.Zero) 92 | // temp_key_xor := temp_key XOR SHA256(aes_encrypted); 93 | // -- adjusted key, 32 bytes 94 | val tempKeyXor = tempKey.bytes xor aesEncrypted.sha256() 95 | // key_aes_encrypted := temp_key_xor + aes_encrypted; 96 | // -- exactly 256 bytes (2048 bits) long 97 | val keyAesEncrypted = tempKeyXor + aesEncrypted 98 | 99 | val publicKeyModulusInt = publicKey.modulus.toBigInt(signed = false) 100 | val publicKeyExponentInt = publicKey.publicExponent.toBigInt(signed = false) 101 | val keyAesEncryptedInt = keyAesEncrypted.toBigInt(signed = false) 102 | 103 | // The value of key_aes_encrypted is compared with the RSA-modulus 104 | // of server_pubkey as a big-endian 2048-bit (256-byte) unsigned 105 | // integer. If key_aes_encrypted turns out to be greater 106 | // than or equal to the RSA modulus, the previous steps 107 | // starting from the generation of new random temp_key are 108 | // repeated. Otherwise the final step is performed: 109 | if (keyAesEncryptedInt >= publicKeyModulusInt) continue 110 | 111 | // encrypted_data := RSA(key_aes_encrypted, server_pubkey); 112 | // -- 256-byte big-endian integer is elevated to the requisite power 113 | // from the RSA public key modulo the RSA modulus 114 | val encryptedData = keyAesEncryptedInt.modPow( 115 | exponent = publicKeyExponentInt, 116 | modulus = publicKeyModulusInt 117 | ).toByteArray() 118 | 119 | // and the result is 120 | // stored as a big-endian integer consisting of exactly 256 bytes 121 | // (with leading zero bytes if required). 122 | return if (encryptedData.size < 256) { 123 | encryptedData.padStart(desiredLength = 256) 124 | } else { 125 | encryptedData.drop(n = encryptedData.size - 256).toByteArray() 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/authorization/InitDH.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.authorization 2 | 3 | import kotl.serialization.bytes.Bytes 4 | import kotl.serialization.int.Int128 5 | import ktproto.client.MTProtoClient 6 | import ktproto.client.requests.getPQ 7 | import ktproto.client.rsa.RsaPublicKey 8 | import ktproto.client.rsa.fingerprint 9 | import ktproto.crypto.factorization.PollardRhoBrent 10 | import ktproto.stdlib.bigint.toBigInt 11 | import ktproto.stdlib.bytes.decodeLong 12 | import ktproto.stdlib.random.nextInt128 13 | import kotlin.random.Random 14 | 15 | internal data class InitDHResult( 16 | val pq: Bytes, 17 | val p: Bytes, 18 | val q: Bytes, 19 | val publicKey: RsaPublicKey, 20 | val nonce: Int128, 21 | val serverNonce: Int128 22 | ) 23 | 24 | internal suspend fun initDH( 25 | client: MTProtoClient, 26 | keys: List 27 | ): InitDHResult { 28 | // 1) 29 | val nonce = Random.nextInt128() 30 | val response = client.getPQ(nonce) 31 | 32 | require(response.nonce.data.contentEquals(nonce.data)) { 33 | "Server responded with invalid nonce (actual: $nonce, expected: ${response.nonce})" 34 | } 35 | require(response.pq.payload.size <= 8) { 36 | "Resulted payload size is more than is supported by ktproto. This should never happen, but if it did, then that is not your fault, just report it to https://github.com/ktproto/issues" 37 | } 38 | 39 | // 2) 40 | val publicKey = keys.firstOrNull { key -> 41 | key.fingerprint() in response.serverPublicKeyFingerprints 42 | } ?: error("Couldn't match any server_public_key_fingerprints. Response: ${response.serverPublicKeyFingerprints}, Expected: ${keys.map { key -> key.fingerprint() }}") 43 | 44 | // 3) 45 | val pq = response.pq.payload 46 | .apply(ByteArray::reverse) 47 | .decodeLong() 48 | .toULong() 49 | 50 | val (p, q) = PollardRhoBrent.factorize(pq) 51 | val pBytes = Bytes(p.toBigInt().toByteArray()) 52 | val qBytes = Bytes(q.toBigInt().toByteArray()) 53 | return InitDHResult(response.pq, pBytes, qBytes, publicKey,nonce, response.serverNonce) 54 | } 55 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/plain/PlainMTProtoClient.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.plain 2 | 3 | import kotl.core.decoder.decodeFromByteArray 4 | import kotl.core.descriptor.TLExpressionDescriptor 5 | import kotl.core.element.TLExpression 6 | import kotl.core.element.TLFunction 7 | import kotl.core.encoder.encodeToByteArray 8 | import ktproto.time.Clock 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.awaitCancellation 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | import ktproto.client.MTProtoClient 14 | import ktproto.io.annotation.OngoingConnection 15 | import ktproto.session.MTProtoSession 16 | import ktproto.session.MTProtoSafeSession 17 | import ktproto.session.messageIdProvider 18 | import ktproto.session.plain.mtprotoPlainSession 19 | import ktproto.transport.MTProtoTransport 20 | import ktproto.transport.Transport 21 | import ktproto.transport.mtprotoIntermediate 22 | 23 | @OptIn(OngoingConnection::class) 24 | public suspend fun plainMTProtoClient( 25 | scope: CoroutineScope, 26 | clock: Clock = Clock.System, 27 | transport: Transport.Connector, 28 | ): MTProtoClient { 29 | val mtprotoTransport = mtprotoIntermediate(transport) 30 | return plainMTProtoClient(scope, clock, mtprotoTransport) 31 | } 32 | 33 | @OptIn(OngoingConnection::class) 34 | public suspend fun plainMTProtoClient( 35 | scope: CoroutineScope, 36 | clock: Clock = Clock.System, 37 | transport: MTProtoTransport.Connector 38 | ): MTProtoClient { 39 | val client = PlainMTProtoClient(transport, scope, clock) 40 | client.connect() 41 | return client 42 | } 43 | 44 | @OptIn(OngoingConnection::class) 45 | private class PlainMTProtoClient( 46 | transport: MTProtoTransport.Connector, 47 | scope: CoroutineScope, 48 | clock: Clock = Clock.System 49 | ) : MTProtoClient { 50 | private val session: MTProtoSafeSession 51 | 52 | init { 53 | val connector = MTProtoSession.Connector { connectorScope -> 54 | mtprotoPlainSession( 55 | scope = connectorScope, 56 | transport = transport.connect(connectorScope), 57 | messageIdProvider = messageIdProvider(clock) 58 | ) 59 | } 60 | session = MTProtoSafeSession(connector, scope) 61 | } 62 | 63 | suspend fun connect() = session.connect() 64 | 65 | override val updates: Flow = flow { awaitCancellation() } 66 | 67 | override suspend fun execute( 68 | function: TLFunction, 69 | responseDescriptor: TLExpressionDescriptor 70 | ): TLExpression { 71 | val bytes = function.encodeToByteArray() 72 | val message = MTProtoSession.Message(bytes) 73 | val response = session.sendRequest(message) { it } 74 | return responseDescriptor.decodeFromByteArray(response.bytes) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/requests/DHParamsRequest.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.requests 2 | 3 | import kotl.serialization.annotation.Crc32 4 | import kotl.serialization.annotation.TLRpc 5 | import kotl.serialization.bytes.Bytes 6 | import kotl.serialization.int.Int128 7 | import kotlinx.serialization.Serializable 8 | import ktproto.client.MTProtoClient 9 | import ktproto.client.serialization.MTProtoRequest 10 | import ktproto.client.serialization.execute 11 | 12 | // req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params 13 | @Serializable 14 | @TLRpc(crc32 = 0xd712e4be_u) 15 | public data class TLDHParamsRequest( 16 | val nonce: Int128, 17 | val serverNonce: Int128, 18 | val p: Bytes, 19 | val q: Bytes, 20 | val publicKeyFingerprint: Long, 21 | val encryptedData: Bytes 22 | ) : MTProtoRequest 23 | 24 | // server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params 25 | @Serializable 26 | @Crc32(value = 0xd0e8075c_u) 27 | public data class TLDHParamsResponse( 28 | val nonce: Int128, 29 | val serverNonce: Int128, 30 | val encryptedAnswer: Bytes 31 | ) 32 | 33 | public suspend fun MTProtoClient.getDHParams( 34 | nonce: Int128, 35 | serverNonce: Int128, 36 | p: Bytes, 37 | q: Bytes, 38 | publicKeyFingerprint: Long, 39 | encryptedData: Bytes 40 | ): TLDHParamsResponse = execute( 41 | request = TLDHParamsRequest( 42 | nonce = nonce, 43 | serverNonce = serverNonce, 44 | p = p, 45 | q = q, 46 | publicKeyFingerprint = publicKeyFingerprint, 47 | encryptedData = encryptedData 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/requests/PQRequest.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.requests 2 | 3 | import kotl.serialization.annotation.Crc32 4 | import kotl.serialization.annotation.TLRpc 5 | import kotl.serialization.bytes.Bytes 6 | import kotl.serialization.int.Int128 7 | import kotlinx.serialization.Serializable 8 | import ktproto.client.MTProtoClient 9 | import ktproto.client.serialization.MTProtoRequest 10 | import ktproto.client.serialization.execute 11 | 12 | // req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; 13 | @Serializable 14 | @TLRpc(crc32 = 0xbe7e8ef1_u) 15 | public data class TLGetPQRequest( 16 | public val nonce: Int128 17 | ) : MTProtoRequest 18 | 19 | // resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector long = ResPQ; 20 | @Serializable 21 | @Crc32(value = 0x05162463_u) 22 | public data class TPGetQResponse( 23 | public val nonce: Int128, 24 | public val serverNonce: Int128, 25 | public val pq: Bytes, 26 | public val serverPublicKeyFingerprints: List 27 | ) 28 | 29 | public suspend fun MTProtoClient.getPQ( 30 | nonce: Int128 31 | ): TPGetQResponse = execute(TLGetPQRequest(nonce)) 32 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/requests/TLPQInnerDataDC.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.requests 2 | 3 | import kotl.serialization.annotation.Crc32 4 | import kotl.serialization.bytes.Bytes 5 | import kotl.serialization.int.Int128 6 | import kotlinx.serialization.Serializable 7 | 8 | // p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; 9 | @Serializable 10 | @Crc32(value = 0xa9f55f95_u) 11 | public data class TLPQInnerDataDC( 12 | val pq: Bytes, 13 | val p: Bytes, 14 | val q: Bytes, 15 | val nonce: Int128, 16 | val serverNonce: Int128, 17 | val newNonce: Int128, 18 | val dc: Int 19 | ) 20 | 21 | // p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; 22 | @Deprecated( 23 | message = "This constructor was deprecated by Telegram", 24 | replaceWith = ReplaceWith( 25 | expression = "TLPQInnerDataDC(pq = pq, p = p, q = q, nonce = nonce, serverNonce = serverNonce, newNonce = newNonce, dc = )", 26 | imports = ["ktproto.client.requests.TLPQInnerDataDC"] 27 | ) 28 | ) 29 | @Serializable 30 | @Crc32(value = 0x83c95aec_u) 31 | public data class TLPQInnerData( 32 | val pq: Bytes, 33 | val p: Bytes, 34 | val q: Bytes, 35 | val nonce: Int128, 36 | val serverNonce: Int128, 37 | val newNonce: Int128 38 | ) 39 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/rsa/RsaPublicKey.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.rsa 2 | 3 | import kotl.serialization.TL 4 | import kotl.serialization.bytes.Bytes 5 | import kotlinx.serialization.encodeToByteArray 6 | import ktproto.crypto.sha.sha1 7 | import ktproto.io.memory.MemoryArena 8 | import ktproto.io.memory.drop 9 | import ktproto.io.memory.scanLong 10 | import kotl.serialization.bare.bare 11 | import ktproto.crypto.rsa.RsaPublicKey as InternalRsaPublicKey 12 | 13 | public data class RsaPublicKey( 14 | public val modulus: ByteArray, 15 | public val publicExponent: ByteArray 16 | ) { 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (other !is RsaPublicKey) return false 20 | 21 | if (!modulus.contentEquals(other.modulus)) return false 22 | if (!publicExponent.contentEquals(other.publicExponent)) return false 23 | 24 | return true 25 | } 26 | 27 | override fun hashCode(): Int { 28 | var result = modulus.contentHashCode() 29 | result = 31 * result + publicExponent.contentHashCode() 30 | return result 31 | } 32 | } 33 | 34 | public fun RsaPublicKey(string: String): RsaPublicKey { 35 | val (n, e) = InternalRsaPublicKey(string) 36 | return RsaPublicKey(n, e) 37 | } 38 | 39 | public fun RsaPublicKey(bytes: ByteArray): RsaPublicKey { 40 | val (n, e) = InternalRsaPublicKey(bytes) 41 | return RsaPublicKey(n, e) 42 | } 43 | 44 | public suspend fun RsaPublicKey.fingerprint(): Long { 45 | val n = Bytes(modulus) 46 | val e = Bytes(publicExponent) 47 | val key = TLRsaPublicKey(n, e).bare 48 | val bytes = TL.encodeToByteArray(key) 49 | val sha1 = MemoryArena.of(bytes.sha1()) 50 | return sha1.drop(n = 12).scanLong() 51 | } 52 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/rsa/TLRsaPublicKey.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.rsa 2 | 3 | import kotl.serialization.bytes.Bytes 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public class TLRsaPublicKey( 8 | public val n: Bytes, 9 | public val e: Bytes 10 | ) 11 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/serialization/MTProtoClient.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.serialization 2 | 3 | import kotl.core.element.TLFunction 4 | import kotl.serialization.TL 5 | import kotl.serialization.extensions.asTLDescriptor 6 | import kotlinx.serialization.serializer 7 | import ktproto.client.MTProtoClient 8 | import kotlin.reflect.KType 9 | import kotlin.reflect.typeOf 10 | 11 | public suspend inline fun , reified R> MTProtoClient.execute(request: T): R { 12 | return execute(request, typeOf(), typeOf()) 13 | } 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | public suspend fun MTProtoClient.execute( 17 | request: T, 18 | requestType: KType, 19 | responseType: KType 20 | ): R { 21 | val function = TL.encodeToTLElement(serializer(requestType), request) 22 | if (function !is TLFunction) error("Can only execute TLFunction, but got $function") 23 | val responseSerializer = serializer(responseType) 24 | val responseDescriptor = responseSerializer.descriptor.asTLDescriptor() 25 | val expression = execute(function, responseDescriptor) 26 | return TL.decodeFromTLElement(responseSerializer, expression) as R 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/serialization/MTProtoRequest.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.serialization 2 | 3 | public interface MTProtoRequest 4 | -------------------------------------------------------------------------------- /client/src/commonMain/kotlin/ktproto/client/serialization/MTProtoRequestDescriptor.kt: -------------------------------------------------------------------------------- 1 | package ktproto.client.serialization 2 | 3 | import kotlin.reflect.KType 4 | 5 | public data class MTProtoRequestDescriptor( 6 | public val functionType: KType, 7 | public val returnType: KType 8 | ) 9 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.js.compiler=ir 3 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "1.9.20-RC2" 4 | kotlVersion = "0.0.15" 5 | ktprotoVersion = "0.0.2" 6 | kotlinxSerialization = "1.5.0" 7 | kotlinxCoroutines = "1.7.3" 8 | ktor = "2.3.4" 9 | 10 | [libraries] 11 | 12 | koTL = { module = "me.y9san9.kotl:core", version.ref = "kotlVersion" } 13 | koTL-serialization = { module = "me.y9san9.kotl:serialization", version.ref = "kotlVersion" } 14 | ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 15 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } 16 | ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } 17 | kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 18 | kotlinxCoroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } 19 | kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" } 20 | 21 | # Gradle plugins 22 | kotlinxSerializationPlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } 23 | kotlinPlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-telegram/ktproto/8076de829abc32c059461e3f9cb42613eda93d41/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /libs/crypto/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainImplementation(libs.kotlinxCoroutines) 10 | commonMainImplementation(projects.libs.stdlibExtensions) 11 | commonMainImplementation(projects.libs.io) 12 | commonTestImplementation(libs.kotlinxCoroutinesTest) 13 | commonTestImplementation(kotlin("test")) 14 | } 15 | -------------------------------------------------------------------------------- /libs/crypto/src/appleMain/kotlin/ktproto/crypto/aes/Aes.apple.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import kotlinx.cinterop.* 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import platform.CoreCrypto.* 7 | 8 | @OptIn(ExperimentalForeignApi::class) 9 | public actual suspend fun AesBlock.encrypted(key: AesKey): AesBlock = withContext(Dispatchers.Default) { 10 | bytes.usePinned { inputBytes -> 11 | key.bytes.usePinned { keyBytes -> 12 | memScoped { 13 | val inputSize = bytes.size 14 | 15 | val dataOut = allocArray(inputSize) 16 | val dataOutMoved = alloc() 17 | 18 | val status = CCCrypt( 19 | op = kCCEncrypt, 20 | alg = kCCAlgorithmAES, 21 | options = kCCOptionECBMode, 22 | key = keyBytes.addressOf(index = 0), 23 | keyLength = key.bytes.size.convert(), 24 | iv = null, 25 | dataIn = inputBytes.addressOf(index = 0), 26 | dataInLength = inputSize.convert(), 27 | dataOut = dataOut, 28 | dataOutAvailable = inputSize.convert(), 29 | dataOutMoved = dataOutMoved.ptr 30 | ) 31 | 32 | require(dataOutMoved.value.toInt() == inputSize) { "Was moved: ${dataOutMoved.value}, expected: ${bytes.size}" } 33 | require(status == kCCSuccess) { "Data was not encrypted" } 34 | 35 | val bytes = dataOut.readBytes(inputSize) 36 | AesBlock(bytes) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/crypto/src/appleMain/kotlin/ktproto/crypto/sha/Sha1.apple.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.addressOf 5 | import kotlinx.cinterop.convert 6 | import kotlinx.cinterop.usePinned 7 | import platform.CoreCrypto.CC_SHA1 8 | import platform.CoreCrypto.CC_SHA1_DIGEST_LENGTH 9 | 10 | @OptIn(ExperimentalForeignApi::class) 11 | public actual suspend fun ByteArray.sha1(): ByteArray { 12 | val digest = UByteArray(CC_SHA1_DIGEST_LENGTH) 13 | this.usePinned { input -> 14 | digest.usePinned { digest -> 15 | CC_SHA1( 16 | input.addressOf(index = 0), 17 | this.size.convert(), 18 | digest.addressOf(index = 0) 19 | ) 20 | } 21 | } 22 | return digest.toByteArray() 23 | } 24 | -------------------------------------------------------------------------------- /libs/crypto/src/appleMain/kotlin/ktproto/crypto/sha/Sha256.apple.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.addressOf 5 | import kotlinx.cinterop.convert 6 | import kotlinx.cinterop.usePinned 7 | import platform.CoreCrypto.CC_SHA256 8 | import platform.CoreCrypto.CC_SHA256_DIGEST_LENGTH 9 | 10 | @OptIn(ExperimentalForeignApi::class) 11 | public actual suspend fun ByteArray.sha256(): ByteArray { 12 | val digest = UByteArray(CC_SHA256_DIGEST_LENGTH) 13 | this.usePinned { input -> 14 | digest.usePinned { digest -> 15 | CC_SHA256( 16 | input.addressOf(index = 0), 17 | this.size.convert(), 18 | digest.addressOf(index = 0) 19 | ) 20 | } 21 | } 22 | return digest.toByteArray() 23 | } 24 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/aes/AESIge.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import ktproto.stdlib.bytes.xor 4 | import kotlin.experimental.xor 5 | 6 | public suspend fun ByteArray.encryptAesIge( 7 | key: AesKey, 8 | iv: AesIV 9 | ): ByteArray { 10 | require(this.size % AesBlock.SIZE_BYTES == 0) { 11 | "Input data is not correctly padded" 12 | } 13 | 14 | val n = this.size / AesBlock.SIZE_BYTES 15 | val encryptedBytes = ByteArray(this.size) 16 | 17 | var previousBlock = iv.bytes.copyOfRange(0, 16) 18 | var previousCipher = iv.bytes.copyOfRange(16, 32) 19 | 20 | repeat(n) { i -> 21 | val plaintextBlock = this.copyOfRange(i * 16, (i + 1) * 16) 22 | val xorResult = plaintextBlock xor previousCipher 23 | val plainBlock = AesBlock(xorResult) 24 | val encryptedBlock = plainBlock.encrypted(key) 25 | val ciphertextBlock = encryptedBlock.bytes xor previousBlock 26 | ciphertextBlock.copyInto(encryptedBytes, i * 16) 27 | previousBlock = plaintextBlock 28 | previousCipher = ciphertextBlock 29 | } 30 | 31 | return encryptedBytes 32 | } 33 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/aes/Aes.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | public expect suspend fun AesBlock.encrypted(key: AesKey): AesBlock 4 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/aes/AesBlock.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | @JvmInline 6 | public value class AesBlock( 7 | public val bytes: ByteArray 8 | ) { 9 | init { 10 | require(bytes.size == SIZE_BYTES) { "size: ${bytes.size}, required: $SIZE_BYTES" } 11 | } 12 | 13 | public companion object { 14 | public const val SIZE_BYTES: Int = 16 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/aes/AesIV.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | @JvmInline 6 | public value class AesIV(public val bytes: ByteArray) { 7 | init { 8 | require(bytes.size == SIZE_BYTES) 9 | } 10 | 11 | public companion object { 12 | public const val SIZE_BYTES: Int = 32 13 | public val Zero: AesIV = AesIV(ByteArray(SIZE_BYTES)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/aes/AesKey.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | public sealed interface AesKey { 6 | public val bytes: ByteArray 7 | 8 | @JvmInline 9 | public value class Bits256(public override val bytes: ByteArray) : AesKey { 10 | init { 11 | require(bytes.size == SIZE_BYTES) 12 | } 13 | 14 | public companion object { 15 | public const val SIZE_BYTES: Int = 32 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/asn1/Asn1Object.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.asn1 2 | 3 | public sealed interface Asn1Object { 4 | public val bytes: ByteArray? 5 | public val children: List? 6 | 7 | public data class Container( 8 | override val children: List 9 | ) : Asn1Object { 10 | override val bytes: Nothing? = null 11 | } 12 | 13 | public data class Value( 14 | override val bytes: ByteArray? 15 | ) : Asn1Object { 16 | override val children: Nothing? = null 17 | 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (other !is Value) return false 21 | 22 | if (bytes != null) { 23 | if (other.bytes == null) return false 24 | if (!bytes.contentEquals(other.bytes)) return false 25 | } else if (other.bytes != null) return false 26 | 27 | return true 28 | } 29 | 30 | override fun hashCode(): Int { 31 | return bytes?.contentHashCode() ?: 0 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/asn1/Asn1Parser.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.asn1 2 | 3 | import ktproto.io.memory.* 4 | import ktproto.stdlib.bytes.decodeInt 5 | import ktproto.stdlib.bytes.padEnd 6 | 7 | public fun parseAsn1Der(data: ByteArray): Asn1Object { 8 | val (_, result) = readAsn1Der(MemoryArena.of(data)) 9 | return result 10 | } 11 | 12 | private const val SEQUENCE: UByte = 0x30u 13 | 14 | private fun readAsn1Der(memory: MemoryArena): Pair { 15 | val tag: Byte 16 | val dropTag = memory.readByte { tag = it } 17 | return when (tag.toUByte()) { 18 | SEQUENCE -> readAsn1Container(dropTag) 19 | else -> readAsn1Value(dropTag) 20 | } 21 | } 22 | 23 | private fun readAsn1Container( 24 | memory: MemoryArena 25 | ): Pair { 26 | val (dropLength, length) = readAsn1DerLength(memory) 27 | var mutable = dropLength.take(length) 28 | val children = buildList { 29 | while (mutable.size > 0) { 30 | val (remaining, element) = readAsn1Der(mutable) 31 | mutable = remaining 32 | add(element) 33 | } 34 | } 35 | return dropLength.drop(length) to Asn1Object.Container(children) 36 | } 37 | 38 | private fun readAsn1Value( 39 | memory: MemoryArena 40 | ): Pair { 41 | val (remaining, length) = readAsn1DerLength(memory) 42 | val bytes = remaining.take(length) 43 | val result = Asn1Object.Value(bytes.toByteArray()) 44 | return remaining.drop(length) to result 45 | } 46 | 47 | private fun readAsn1DerLength(memory: MemoryArena): Pair { 48 | val lengthFirstByte: Byte 49 | val dropFirst = memory.readByte { byte -> 50 | lengthFirstByte = byte 51 | } 52 | 53 | // Short-Form 54 | if (lengthFirstByte >= 0) { 55 | return dropFirst to lengthFirstByte.toInt() 56 | } 57 | 58 | // Remove sign-bit 59 | val lengthOfTheLength = lengthFirstByte.toInt() and 0b01111111 60 | 61 | val length: Int 62 | 63 | val remaining = dropFirst.readBytes(lengthOfTheLength) { bytes -> 64 | val intBytes = bytes 65 | .apply { reverse() } 66 | .padEnd(4) 67 | length = intBytes.decodeInt() 68 | } 69 | 70 | return remaining to length 71 | } 72 | -------------------------------------------------------------------------------- /libs/crypto/src/commonMain/kotlin/ktproto/crypto/factorization/PollardRhoBrent.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.factorization 2 | 3 | import kotlin.math.min 4 | import kotlin.random.Random 5 | import kotlin.random.nextULong 6 | 7 | public object PollardRhoBrent { 8 | // Code was rewritten to Kotlin from CPP: 9 | // https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L103 10 | 11 | public fun factorize(pq: ULong): Pair { 12 | if (pq and 1uL == 0uL) { 13 | return 2uL to pq / 2uL 14 | } 15 | 16 | var g = 0uL 17 | 18 | var i = 0 19 | var iter = 0 20 | 21 | while (i < 3 || iter < 1000) { 22 | val q = Random.nextULong(from = 17uL, until = 32u) % (pq - 1uL) 23 | var x = Random.nextULong() % (pq - 1uL) 24 | var y = x 25 | val lim = 1 shl (min(5, i) + 18) 26 | for (j in 1.. 1uL && g < pq) { 37 | break 38 | } 39 | i++ 40 | } 41 | 42 | val p: ULong 43 | val q: ULong 44 | 45 | if (g == 0uL) return 1uL to pq 46 | 47 | val other = pq / g 48 | 49 | if (other < g) { 50 | p = other 51 | q = g 52 | } else { 53 | p = g 54 | q = other 55 | } 56 | 57 | return p to q 58 | } 59 | } 60 | 61 | private fun pqAddMul( 62 | c: ULong, 63 | a: ULong, 64 | b: ULong, 65 | pq: ULong 66 | ): ULong { 67 | var cVar = c 68 | var aVar = a 69 | var bVar = b 70 | 71 | while (bVar != 0uL) { 72 | if ((bVar and 1uL) == 1uL) { 73 | cVar += aVar 74 | if (cVar >= pq) { 75 | cVar -= pq 76 | } 77 | } 78 | aVar += aVar 79 | if (aVar >= pq) { 80 | aVar -= pq 81 | } 82 | bVar = bVar shr 1 83 | } 84 | 85 | return cVar 86 | } 87 | 88 | private tailrec fun gcd(a: ULong, b: ULong): ULong { 89 | if (b == 0uL) return a 90 | return gcd(b, a % b) 91 | } 92 | 93 | private inline fun repeat(n: ULong, block: (ULong) -> Unit) { 94 | for (i in 0uL.. 20 | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey 21 | @JsName("importKey") 22 | fun importKeyInterop( 23 | format: String, 24 | key: dynamic, 25 | algorithm: dynamic, 26 | extractable: Boolean, 27 | usages: Array 28 | ): Promise 29 | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt 30 | @JsName("encrypt") 31 | fun encryptInterop(algorithm: dynamic, key: CryptoKey, data: ByteArray): Promise 32 | } 33 | 34 | internal suspend fun Subtle.digest(algorithm: dynamic, data: ByteArray): ByteArray { 35 | val buffer = digestInterop(algorithm, data).await() 36 | return Uint8Array(buffer).unsafeCast() 37 | } 38 | 39 | internal suspend fun Subtle.encrypt(algorithm: dynamic, key: CryptoKey, data: ByteArray): ByteArray { 40 | val buffer = encryptInterop(algorithm, key, data).await() 41 | return Uint8Array(buffer).unsafeCast() 42 | } 43 | 44 | internal suspend fun Subtle.importKey( 45 | format: String, 46 | key: dynamic, 47 | algorithm: dynamic, 48 | extractable: Boolean, 49 | usages: List 50 | ): CryptoKey = importKeyInterop(format, key, algorithm, extractable, usages.toTypedArray()).await() 51 | 52 | internal val JsPlatform.crypto: Crypto get() = when (this) { 53 | JsPlatform.Browser -> window.asDynamic().crypto 54 | JsPlatform.Node -> eval("require")("crypto") 55 | }.unsafeCast() ?: throw UnsupportedOperationException("Web Crypto API not available") 56 | -------------------------------------------------------------------------------- /libs/crypto/src/jsMain/kotlin/ktproto/crypto/sha/Sha1.js.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import ktproto.stdlib.platform.jsRuntime 4 | 5 | public actual suspend fun ByteArray.sha1(): ByteArray = 6 | jsRuntime.crypto.subtle.digest("SHA-1", this) 7 | -------------------------------------------------------------------------------- /libs/crypto/src/jsMain/kotlin/ktproto/crypto/sha/Sha256.js.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import ktproto.stdlib.platform.jsRuntime 4 | 5 | public actual suspend fun ByteArray.sha256(): ByteArray = 6 | jsRuntime.crypto.subtle.digest("SHA-256", this) 7 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/aes/Aes256.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import javax.crypto.Cipher 6 | 7 | public actual suspend fun AesBlock.encrypted(key: AesKey): AesBlock = 8 | withContext(Dispatchers.IO) { 9 | val cipher = Cipher.getInstance("AES/ECB/NoPadding") 10 | cipher.init(Cipher.ENCRYPT_MODE, key.keySpec) 11 | val bytes = cipher.doFinal(bytes) 12 | AesBlock(bytes) 13 | } 14 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/aes/AesKey.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.aes 2 | 3 | import javax.crypto.spec.SecretKeySpec 4 | 5 | public val AesKey.keySpec: SecretKeySpec get() = SecretKeySpec(bytes, "AES") 6 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/bigint/CommonBigIntMain.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.bigint 2 | 3 | import ktproto.stdlib.bigint.toBigInt 4 | import ktproto.stdlib.bytes.toBinaryString 5 | import java.math.BigInteger 6 | import kotlin.random.Random 7 | import kotlin.random.nextUBytes 8 | 9 | @OptIn(ExperimentalUnsignedTypes::class) 10 | public fun main() { 11 | repeat(10_000_000) { 12 | val small = Random.nextLong() 13 | val int = small.toBigInt() 14 | val jint = BigInteger.valueOf(small) 15 | require(int.toUByteArray() contentEquals jint.toByteArray().toUByteArray()) { "$small: expected: ${int.toUByteArray().toBinaryString()}, was ${jint.toByteArray().toBinaryString()}" } 16 | } 17 | repeat(10_000_000) { 18 | val bytes = Random.nextUBytes((1..8).random()) 19 | val int = bytes.toBigInt() 20 | val jint = BigInteger(bytes.toByteArray()) 21 | val jintString = buildString { 22 | if (jint < BigInteger.ZERO) append('-') 23 | append("0x") 24 | append(jint.abs().toString(16)) 25 | } 26 | require(int.toString() == jintString) { "${bytes.toBinaryString()}: expected: $jintString, was: $int" } 27 | } 28 | // repeat(10_000) { 29 | // val long = Long.MIN_VALUE 30 | // val bigInt = BigInteger.valueOf(long) 31 | // println("Java: ${bigInt.toString(16)}") 32 | // val bytes = bigInt.toByteArray() 33 | // print("Bytes: ") 34 | // println(bytes.joinToString(" ") { byte -> byte.toUByte().toString(radix = 2).padStart(8, '0') }) 35 | //// val kBigInt = bytes.toBigInt() 36 | //// println("Kotlin: $kBigInt") 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/rsa/RsaPublicKey.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.rsa 2 | 3 | import java.math.BigInteger 4 | import java.security.spec.RSAPublicKeySpec 5 | 6 | public val RsaPublicKey.keySpec: RSAPublicKeySpec get() = 7 | RSAPublicKeySpec(BigInteger(modulus), BigInteger(publicExponent)) 8 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/sha/Sha1.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | 6 | public actual suspend fun ByteArray.sha1(): ByteArray { 7 | val input = this 8 | return withContext(Dispatchers.Default) { 9 | val messageDigest = java.security.MessageDigest.getInstance("SHA-1") 10 | val hashBytes = messageDigest.digest(input) 11 | hashBytes 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/crypto/src/jvmMain/kotlin/ktproto/crypto/sha/Sha256.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.crypto.sha 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import java.security.MessageDigest 6 | 7 | public actual suspend fun ByteArray.sha256(): ByteArray { 8 | val input = this 9 | return withContext(Dispatchers.Default) { 10 | val messageDigest = MessageDigest.getInstance("SHA-256") 11 | val hashBytes = messageDigest.digest(input) 12 | hashBytes 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libs/io/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainImplementation(projects.libs.stdlibExtensions) 10 | commonMainImplementation(libs.kotlinxCoroutines) 11 | } 12 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/annotation/OngoingConnection.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.annotation 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.WARNING, 5 | message = "This declaration has ongoing IO state, consider to handle all IO exceptions and do not forget to close connection" 6 | ) 7 | public annotation class OngoingConnection 8 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/input/ByteArrayInput.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.input 2 | 3 | import ktproto.io.annotation.OngoingConnection 4 | import ktproto.io.memory.* 5 | 6 | @OptIn(OngoingConnection::class) 7 | public class ByteArrayInput(private var source: MemoryArena) : Input { 8 | 9 | override suspend fun read(destination: MemoryArena) { 10 | if (destination.size > source.size) 11 | throw IndexOutOfBoundsException() 12 | destination.write(source.take(destination.size)) 13 | source = source.drop(destination.size) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/input/Input.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.input 2 | 3 | import ktproto.io.annotation.OngoingConnection 4 | import ktproto.io.memory.MemoryArena 5 | 6 | @OngoingConnection 7 | public fun interface Input { 8 | public suspend fun read(destination: MemoryArena) 9 | } 10 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/memory/Flatten.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.memory 2 | 3 | public fun List.flatten(): MemoryArena { 4 | val size = sumOf { it.size } 5 | val newArena = MemoryArena.allocate(size) 6 | fold(newArena) { arena, current -> arena.write(current) } 7 | return newArena 8 | } 9 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/memory/MemoryArena.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.memory 2 | 3 | import ktproto.stdlib.bytes.encodeToByteArray 4 | 5 | public class MemoryArena( 6 | public val data: ByteArray, 7 | public val range: IntRange 8 | ) { 9 | public constructor( 10 | data: ByteArray, 11 | start: Int, 12 | end: Int 13 | ) : this(data, start..end) 14 | 15 | init { 16 | require( 17 | value = size >= 0 && 18 | (range.isEmpty() || 19 | range.first in data.indices && 20 | range.last in data.indices) 21 | ) { "Invalid bounds (memoryRange: $range, data.indices: ${data.indices}, ${range.first in data.indices} ${range.last in data.indices})" } 22 | } 23 | 24 | public companion object { 25 | public fun allocate(n: Int): MemoryArena = MemoryArena(ByteArray(n), 0.. String 70 | ) { 71 | require(size >= n) { message() } 72 | } 73 | 74 | public inline fun MemoryArena.ensureCapacity( 75 | n: Int, 76 | f: (Int) -> Int = { it.takeHighestOneBit() shl 1 } 77 | ): MemoryArena { 78 | if (size >= n) return this 79 | val new = MemoryArena.allocate(f(n)) 80 | new.write(source = this) 81 | return new 82 | } 83 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/memory/Read.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalContracts::class) 2 | 3 | package ktproto.io.memory 4 | 5 | import ktproto.stdlib.bytes.decodeInt 6 | import ktproto.stdlib.bytes.decodeLong 7 | import kotlin.contracts.ExperimentalContracts 8 | import kotlin.contracts.InvocationKind 9 | import kotlin.contracts.contract 10 | 11 | public fun MemoryArena.scanBytes(n: Int): ByteArray { 12 | val result: ByteArray 13 | readBytes(n) { result = it } 14 | return result 15 | } 16 | 17 | public inline fun MemoryArena.readMemory( 18 | n: Int, block: (MemoryArena) -> Unit 19 | ): MemoryArena { 20 | contract { 21 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 22 | } 23 | 24 | checkCapacity(n) { "Cannot read $n bytes, max is $size" } 25 | 26 | val read = take(n) 27 | block(read) 28 | 29 | return MemoryArena( 30 | data = data, 31 | start = start + n, 32 | end = end 33 | ) 34 | } 35 | 36 | public inline fun MemoryArena.readBytes( 37 | n: Int, block: (ByteArray) -> Unit 38 | ): MemoryArena { 39 | contract { 40 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 41 | } 42 | return readMemory(n) { memory -> block(memory.toByteArray()) } 43 | } 44 | 45 | public inline fun MemoryArena.readByte( 46 | block: (Byte) -> Unit 47 | ): MemoryArena { 48 | contract { 49 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 50 | } 51 | return readBytes(n = 1) { bytes -> block(bytes[0]) } 52 | } 53 | 54 | public fun MemoryArena.scanInt(): Int { 55 | val result: Int 56 | readInt { result = it } 57 | return result 58 | } 59 | 60 | @OptIn(ExperimentalContracts::class) 61 | public inline fun MemoryArena.readInt(block: (Int) -> Unit): MemoryArena { 62 | contract { 63 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 64 | } 65 | return readBytes(Int.SIZE_BYTES) { block(it.decodeInt()) } 66 | } 67 | 68 | public fun MemoryArena.scanLong(): Long { 69 | val result: Long 70 | readLong { result = it } 71 | return result 72 | } 73 | 74 | @OptIn(ExperimentalContracts::class) 75 | public inline fun MemoryArena.readLong(block: (Long) -> Unit): MemoryArena { 76 | contract { 77 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 78 | } 79 | return readBytes(Long.SIZE_BYTES) { block(it.decodeLong()) } 80 | } 81 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/memory/Write.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.memory 2 | 3 | public fun MemoryArena.write(source: MemoryArena): MemoryArena { 4 | checkCapacity(source.size) { "Insufficient size in the current area (this.size: ${this.size}, source.size: ${source.size})" } 5 | 6 | source.data.copyInto( 7 | destination = data, 8 | destinationOffset = start, 9 | startIndex = source.start, 10 | endIndex = source.end + 1 11 | ) 12 | 13 | return MemoryArena( 14 | data = data, 15 | start = start + source.size, 16 | end = end 17 | ) 18 | } 19 | 20 | public fun MemoryArena.write(array: ByteArray): MemoryArena = 21 | write(MemoryArena.of(array)) 22 | 23 | public fun MemoryArena.write(int: Int): MemoryArena = 24 | write(MemoryArena.of(int)) 25 | 26 | public fun MemoryArena.write(int: UInt): MemoryArena = 27 | write(MemoryArena.of(int)) 28 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/output/ByteArrayOutput.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.output 2 | 3 | import ktproto.io.annotation.OngoingConnection 4 | import ktproto.io.memory.* 5 | 6 | @OptIn(OngoingConnection::class) 7 | public class ByteArrayOutput : Output { 8 | private var memory = MemoryArena.allocate(n = 32) 9 | 10 | private fun ensureCapacity(n: Int) { 11 | if (memory.size >= n) return 12 | val allocated = MemoryArena.allocate(n = n.takeHighestOneBit() shl 1) 13 | allocated.write(memory) 14 | memory = allocated 15 | } 16 | 17 | override suspend fun write(source: MemoryArena) { 18 | ensureCapacity(source.size) 19 | memory.write(source) 20 | } 21 | 22 | public fun toByteArray(): ByteArray = memory.toByteArray() 23 | } 24 | -------------------------------------------------------------------------------- /libs/io/src/commonMain/kotlin/ktproto/io/output/Output.kt: -------------------------------------------------------------------------------- 1 | package ktproto.io.output 2 | 3 | import ktproto.io.annotation.OngoingConnection 4 | import ktproto.io.memory.MemoryArena 5 | 6 | @OngoingConnection 7 | public fun interface Output { 8 | public suspend fun write(source: MemoryArena) 9 | } 10 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | kotlin { 9 | js { 10 | nodejs() 11 | binaries.executable() 12 | } 13 | } 14 | 15 | dependencies { 16 | commonMainImplementation(libs.kotlinxCoroutines) 17 | commonMainImplementation(libs.koTL.serialization) 18 | commonTestImplementation(kotlin("test")) 19 | } 20 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bigint/BigInt.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalUnsignedTypes::class) 2 | 3 | package ktproto.stdlib.bigint 4 | 5 | import ktproto.stdlib.bigint.BigInt.Companion.BASE 6 | import ktproto.stdlib.bigint.BigInt.Companion.BASE_SIZE 7 | import ktproto.stdlib.bigint.BigInt.Companion.ZERO 8 | import kotlin.math.log2 9 | import kotlin.math.max 10 | import kotlin.math.min 11 | import kotlin.math.sign 12 | 13 | /** 14 | * Kotlin Multiplatform implementation of Big Integer numbers (KBigInteger). 15 | * 16 | * @source https://github.com/SciProgCentre/kmath/blob/5129f29084ed810871a93d0d816205e0f638fa71/kmath-core/src/commonMain/kotlin/space/kscience/kmath/operations/CommonBigInt.kt#L4 17 | * @author Robert Drynkin 18 | * @author Peter Klimai 19 | */ 20 | public class BigInt internal constructor( 21 | private val sign: Byte, 22 | private val underlying: UIntArray, 23 | ) : Comparable { 24 | override fun compareTo(other: BigInt): Int = when { 25 | (sign == 0.toByte()) and (other.sign == 0.toByte()) -> 0 26 | sign < other.sign -> -1 27 | sign > other.sign -> 1 28 | else -> sign * compareMagnitudes(underlying, other.underlying) 29 | } 30 | 31 | override fun equals(other: Any?): Boolean = other is BigInt && compareTo(other) == 0 32 | 33 | override fun hashCode(): Int = underlying.hashCode() + sign 34 | 35 | public fun abs(): BigInt = if (sign == 0.toByte()) this else BigInt(1, underlying) 36 | 37 | public operator fun unaryMinus(): BigInt = 38 | if (this.sign == 0.toByte()) this else BigInt((-sign).toByte(), underlying) 39 | 40 | public operator fun plus(b: BigInt): BigInt = when { 41 | b.sign == 0.toByte() -> this 42 | sign == 0.toByte() -> b 43 | this == -b -> ZERO 44 | sign == b.sign -> BigInt(sign, addMagnitudes(underlying, b.underlying)) 45 | 46 | else -> { 47 | val comp = compareMagnitudes(underlying, b.underlying) 48 | 49 | if (comp == 1) 50 | BigInt(sign, subtractMagnitudes(underlying, b.underlying)) 51 | else 52 | BigInt((-sign).toByte(), subtractMagnitudes(b.underlying, underlying)) 53 | } 54 | } 55 | 56 | public operator fun inc(): BigInt = plus(ONE) 57 | 58 | public operator fun minus(b: BigInt): BigInt = this + (-b) 59 | 60 | public operator fun dec(): BigInt = minus(ONE) 61 | 62 | public operator fun times(b: BigInt): BigInt = when { 63 | this.sign == 0.toByte() -> ZERO 64 | b.sign == 0.toByte() -> ZERO 65 | b.underlying.size == 1 -> this * b.underlying[0] * b.sign.toInt() 66 | this.underlying.size == 1 -> b * this.underlying[0] * this.sign.toInt() 67 | else -> BigInt((sign * b.sign).toByte(), multiplyMagnitudes(underlying, b.underlying)) 68 | } 69 | 70 | public operator fun times(other: UInt): BigInt = when { 71 | sign == 0.toByte() -> ZERO 72 | other == 0U -> ZERO 73 | other == 1U -> this 74 | else -> BigInt(sign, multiplyMagnitudeByUInt(underlying, other)) 75 | } 76 | 77 | public fun pow(exponent: BigInt): BigInt = when { 78 | this == ZERO && exponent == ZERO -> throw ArithmeticException("Cannot pow 0 ^ 0") 79 | exponent == ZERO -> ONE 80 | this == ZERO -> ZERO 81 | this == ONE -> ONE 82 | exponent < ZERO -> ONE / pow(-exponent) 83 | else -> this * pow(exponent = exponent - ONE) 84 | } 85 | 86 | public operator fun times(other: Int): BigInt = when { 87 | other > 0 -> this * kotlin.math.abs(other).toUInt() 88 | other != Int.MIN_VALUE -> -this * kotlin.math.abs(other).toUInt() 89 | else -> times(other.toBigInt()) 90 | } 91 | 92 | public operator fun div(other: UInt): BigInt = 93 | BigInt(sign, divideMagnitudeByUInt(underlying, other)) 94 | 95 | public operator fun div(other: Int): BigInt = BigInt( 96 | (sign * other.sign).toByte(), 97 | divideMagnitudeByUInt(underlying, kotlin.math.abs(other).toUInt()) 98 | ) 99 | 100 | private fun division(other: BigInt): Pair { 101 | // Long division algorithm: 102 | // https://en.wikipedia.org/wiki/Division_algorithm#Integer_division_(unsigned)_with_remainder 103 | // TODO: Implement more effective algorithm 104 | var q = ZERO 105 | var r = ZERO 106 | 107 | val bitSize = 108 | (BASE_SIZE * (this.underlying.size - 1) + log2(this.underlying.lastOrNull()?.toFloat() ?: (0f + 1))).toInt() 109 | 110 | for (i in bitSize downTo 0) { 111 | r = r shl 1 112 | r = r or ((abs(this) shr i) and ONE) 113 | 114 | if (r >= abs(other)) { 115 | r -= abs(other) 116 | q += (ONE shl i) 117 | } 118 | } 119 | 120 | return Pair(BigInt((sign * other.sign).toByte(), q.underlying), r) 121 | } 122 | 123 | public operator fun div(other: BigInt): BigInt = division(other).first 124 | 125 | public infix fun shl(i: Int): BigInt { 126 | if (this == ZERO) return ZERO 127 | if (i == 0) return this 128 | val fullShifts = i / BASE_SIZE + 1 129 | val relShift = i % BASE_SIZE 130 | val shiftLeft = { x: UInt -> if (relShift >= 32) 0U else x shl relShift } 131 | val shiftRight = { x: UInt -> if (BASE_SIZE - relShift >= 32) 0U else x shr (BASE_SIZE - relShift) } 132 | val newMagnitude = UIntArray(underlying.size + fullShifts) 133 | 134 | for (j in underlying.indices) { 135 | newMagnitude[j + fullShifts - 1] = shiftLeft(this.underlying[j]) 136 | 137 | if (j != 0) 138 | newMagnitude[j + fullShifts - 1] = newMagnitude[j + fullShifts - 1] or shiftRight(this.underlying[j - 1]) 139 | } 140 | 141 | newMagnitude[underlying.size + fullShifts - 1] = shiftRight(underlying.last()) 142 | return BigInt(sign, stripLeadingZeros(newMagnitude)) 143 | } 144 | 145 | public infix fun shr(i: Int): BigInt { 146 | if (this == ZERO) return ZERO 147 | if (i == 0) return this 148 | val fullShifts = i / BASE_SIZE 149 | val relShift = i % BASE_SIZE 150 | val shiftRight = { x: UInt -> if (relShift >= 32) 0U else x shr relShift } 151 | val shiftLeft = { x: UInt -> if (BASE_SIZE - relShift >= 32) 0U else x shl (BASE_SIZE - relShift) } 152 | if (this.underlying.size - fullShifts <= 0) return ZERO 153 | val newMagnitude = UIntArray(underlying.size - fullShifts) 154 | 155 | for (j in fullShifts.. ONE 196 | exponent % 2 == 1 -> (this * modPow(exponent - ONE, modulus)) % modulus 197 | 198 | else -> { 199 | val sqRoot = modPow(exponent / 2, modulus) 200 | (sqRoot * sqRoot) % modulus 201 | } 202 | } 203 | 204 | public fun toUByteArray(): UByteArray { 205 | val absolute = underlying.reversedArray().toUByteArray() 206 | val signed = absolute.twosComplementSign(sign) 207 | return signed.stripLeadingSignBytes(sign) 208 | } 209 | 210 | public fun toByteArray(): ByteArray = toUByteArray().toByteArray() 211 | 212 | override fun toString(): String { 213 | if (this.sign == 0.toByte()) { 214 | return "0x0" 215 | } 216 | var res: String = if (this.sign == (-1).toByte()) "-0x" else "0x" 217 | var numberStarted = false 218 | 219 | for (i in this.underlying.size - 1 downTo 0) { 220 | for (j in BASE_SIZE / 4 - 1 downTo 0) { 221 | val curByte = (this.underlying[i] shr 4 * j) and 0xfU 222 | if (numberStarted or (curByte != 0U)) { 223 | numberStarted = true 224 | res += hexMapping[curByte] 225 | } 226 | } 227 | } 228 | 229 | return res 230 | } 231 | 232 | public companion object { 233 | public const val BASE: ULong = 0xffffffffUL 234 | public const val BASE_SIZE: Int = 32 235 | public val ZERO: BigInt = BigInt(0, uintArrayOf()) 236 | public val ONE: BigInt = BigInt(1, uintArrayOf(1u)) 237 | private const val KARATSUBA_THRESHOLD = 80 238 | 239 | private val hexMapping: HashMap = hashMapOf( 240 | 0U to "0", 1U to "1", 2U to "2", 3U to "3", 241 | 4U to "4", 5U to "5", 6U to "6", 7U to "7", 242 | 8U to "8", 9U to "9", 10U to "a", 11U to "b", 243 | 12U to "c", 13U to "d", 14U to "e", 15U to "f" 244 | ) 245 | 246 | private fun compareMagnitudes(mag1: UIntArray, mag2: UIntArray): Int { 247 | return when { 248 | mag1.size > mag2.size -> 1 249 | mag1.size < mag2.size -> -1 250 | 251 | else -> { 252 | for (i in mag1.size - 1 downTo 0) return when { 253 | mag1[i] > mag2[i] -> 1 254 | mag1[i] < mag2[i] -> -1 255 | else -> continue 256 | } 257 | 258 | 0 259 | } 260 | } 261 | } 262 | 263 | private fun addMagnitudes(mag1: UIntArray, mag2: UIntArray): UIntArray { 264 | val resultLength = max(mag1.size, mag2.size) + 1 265 | val result = UIntArray(resultLength) 266 | var carry = 0uL 267 | 268 | for (i in 0..= mag1.size -> mag2[i].toULong() + carry 271 | i >= mag2.size -> mag1[i].toULong() + carry 272 | else -> mag1[i].toULong() + mag2[i].toULong() + carry 273 | } 274 | 275 | result[i] = (res and BASE).toUInt() 276 | carry = res shr BASE_SIZE 277 | } 278 | 279 | result[resultLength - 1] = carry.toUInt() 280 | return stripLeadingZeros(result) 281 | } 282 | 283 | private fun subtractMagnitudes(mag1: UIntArray, mag2: UIntArray): UIntArray { 284 | val resultLength = mag1.size 285 | val result = UIntArray(resultLength) 286 | var carry = 0L 287 | 288 | for (i in 0.. 320 | naiveMultiplyMagnitudes(mag1, mag2) 321 | // TODO implement Fourier 322 | else -> karatsubaMultiplyMagnitudes(mag1, mag2) 323 | } 324 | 325 | internal fun naiveMultiplyMagnitudes(mag1: UIntArray, mag2: UIntArray): UIntArray { 326 | val resultLength = mag1.size + mag2.size 327 | val result = UIntArray(resultLength) 328 | 329 | for (i in mag1.indices) { 330 | var carry = 0uL 331 | 332 | for (j in mag2.indices) { 333 | val cur: ULong = result[i + j].toULong() + mag1[i].toULong() * mag2[j].toULong() + carry 334 | result[i + j] = (cur and BASE).toUInt() 335 | carry = cur shr BASE_SIZE 336 | } 337 | 338 | result[i + mag2.size] = (carry and BASE).toUInt() 339 | } 340 | 341 | return stripLeadingZeros(result) 342 | } 343 | 344 | internal fun karatsubaMultiplyMagnitudes(mag1: UIntArray, mag2: UIntArray): UIntArray { 345 | //https://en.wikipedia.org/wiki/Karatsuba_algorithm 346 | val halfSize = min(mag1.size, mag2.size) / 2 347 | val x0 = mag1.sliceArray(0 until halfSize).toBigInt(1) 348 | val x1 = mag1.sliceArray(halfSize until mag1.size).toBigInt(1) 349 | val y0 = mag2.sliceArray(0 until halfSize).toBigInt(1) 350 | val y1 = mag2.sliceArray(halfSize until mag2.size).toBigInt(1) 351 | 352 | val z0 = x0 * y0 353 | val z2 = x1 * y1 354 | val z1 = (x0 - x1) * (y1 - y0) + z0 + z2 355 | 356 | return (z2.shl(2 * halfSize * BASE_SIZE) + z1.shl(halfSize * BASE_SIZE) + z0).underlying 357 | } 358 | 359 | private fun divideMagnitudeByUInt(mag: UIntArray, x: UInt): UIntArray { 360 | val resultLength = mag.size 361 | val result = UIntArray(resultLength) 362 | var carry = 0uL 363 | 364 | for (i in mag.size - 1 downTo 0) { 365 | val cur: ULong = mag[i].toULong() + (carry shl BASE_SIZE) 366 | result[i] = (cur / x).toUInt() 367 | carry = cur % x 368 | } 369 | 370 | return stripLeadingZeros(result) 371 | } 372 | } 373 | } 374 | 375 | private fun stripLeadingZeros(mag: UIntArray): UIntArray { 376 | if (mag.isEmpty() || mag.last() != 0U) return mag 377 | var resSize = mag.size - 1 378 | 379 | while (mag[resSize] == 0U) { 380 | if (resSize == 0) break 381 | resSize -= 1 382 | } 383 | 384 | return mag.sliceArray(IntRange(0, resSize)) 385 | } 386 | 387 | /** 388 | * Returns the absolute value of the given value [x]. 389 | */ 390 | public fun abs(x: BigInt): BigInt = x.abs() 391 | 392 | /** 393 | * Convert this [Int] to [BigInt] 394 | */ 395 | public fun Int.toBigInt(): BigInt = 396 | BigInt(sign.toByte(), uintArrayOf(kotlin.math.abs(this).toUInt())) 397 | 398 | /** 399 | * Convert this [Long] to [BigInt] 400 | */ 401 | public fun Long.toBigInt(): BigInt = BigInt( 402 | sign.toByte(), 403 | stripLeadingZeros( 404 | uintArrayOf( 405 | (kotlin.math.abs(this).toULong() and BASE).toUInt(), 406 | ((kotlin.math.abs(this).toULong() shr BASE_SIZE) and BASE).toUInt() 407 | ) 408 | ) 409 | ) 410 | 411 | /** 412 | * Convert UInt to [BigInt] 413 | */ 414 | public fun UInt.toBigInt(): BigInt = 415 | BigInt(1, uintArrayOf(this)) 416 | 417 | /** 418 | * Convert ULong to [BigInt] 419 | */ 420 | public fun ULong.toBigInt(): BigInt = BigInt( 421 | 1, 422 | stripLeadingZeros( 423 | uintArrayOf( 424 | (this and BASE).toUInt(), 425 | ((this shr BASE_SIZE) and BASE).toUInt() 426 | ) 427 | ) 428 | ) 429 | 430 | /** 431 | * Create a [BigInt] with this array of magnitudes with protective copy 432 | */ 433 | public fun UIntArray.toBigInt(sign: Byte): BigInt { 434 | require(sign != 0.toByte() || isEmpty()) 435 | return BigInt(sign, copyOf()) 436 | } 437 | 438 | private fun UIntArray.toUByteArray(): UByteArray { 439 | val bytes = UByteArray(size = size * Int.SIZE_BYTES) 440 | 441 | for ((i, int) in this.withIndex()) { 442 | bytes[i * 4 + 0] = (int shr 24).toUByte() 443 | bytes[i * 4 + 1] = (int shr 16).toUByte() 444 | bytes[i * 4 + 2] = (int shr 8).toUByte() 445 | bytes[i * 4 + 3] = (int shr 0).toUByte() 446 | } 447 | 448 | return bytes 449 | } 450 | 451 | // mutating 452 | private fun UByteArray.twosComplementSign(sign: Byte): UByteArray { 453 | if (isEmpty()) return this 454 | 455 | if (sign < 0) { 456 | for (i in this.indices) { 457 | this[i] = this[i].inv() 458 | } 459 | } 460 | 461 | val signBit = if (sign > 0) 0u else 1u 462 | 463 | // prepend sign-byte if need 464 | // Example: 465 | // 10000000 is an absolute value for 128 466 | // if we want to have positive 128, we need to prepend 00000000, 467 | // so the number will be 00000000 10000000 468 | val result = if (this[0].toUInt() shr 7 == signBit) { 469 | this 470 | } else { 471 | val signByte = if (sign > 0) UByte.MIN_VALUE else UByte.MAX_VALUE 472 | ubyteArrayOf(signByte) + this 473 | } 474 | 475 | if (sign > 0) return result 476 | 477 | for (i in lastIndex downTo 0) { 478 | val byte = this[i] 479 | this[i] = (byte + 1u).toUByte() 480 | 481 | // if we have overflow (0b11111111 + 0b1 = 0b00000000), 482 | // then we need to continue addition until no overflow 483 | // or until the end of the array 484 | val overflow = this[i] < byte 485 | if (!overflow) break 486 | } 487 | 488 | return result 489 | } 490 | 491 | private fun UByteArray.stripLeadingSignBytes(sign: Byte): UByteArray { 492 | if (isEmpty()) return this 493 | 494 | val signBit = if (sign > 0) 0u else 1u 495 | val signByte = if (sign > 0) UByte.MIN_VALUE else UByte.MAX_VALUE 496 | var leadingBytes = 0 497 | 498 | for ((i, byte) in this.withIndex()) { 499 | if (i == this.lastIndex) break 500 | // if the byte is not 0b11111111 or 0b00000000, we cannot strip anymore 501 | if (byte != signByte) break 502 | val nextByte = this[i + 1] 503 | // if the first bit of the next byte is other than 1 (for negative) or 0 (for positive) 504 | // current byte is required to indicate its sign 505 | if (nextByte.toUInt() shr 7 != signBit) break 506 | leadingBytes++ 507 | } 508 | 509 | if (leadingBytes == 0) return this 510 | 511 | return sliceArray(leadingBytes..lastIndex) 512 | } 513 | 514 | // protecting copy 515 | public fun UByteArray.toBigInt(signed: Boolean = true): BigInt { 516 | if (isEmpty()) return ZERO 517 | if (all { byte -> byte == UByte.MIN_VALUE }) return ZERO 518 | 519 | val sign = if (signed) { 520 | (this[0].toUInt() shr 7).toByte() 521 | } else { 522 | 0 523 | } 524 | 525 | val absoluteValue = copyOf() 526 | .twosComplementAbs(sign) 527 | .padToInt() 528 | .toUIntArray() 529 | 530 | val result = BigInt( 531 | sign = if (sign > 0) -1 else 1, 532 | underlying = absoluteValue 533 | ) 534 | if (result == ZERO) return ZERO 535 | return result 536 | } 537 | 538 | public fun ByteArray.toBigInt(signed: Boolean = true): BigInt { 539 | return toUByteArray().toBigInt(signed) 540 | } 541 | 542 | // mutating 543 | private fun UByteArray.twosComplementAbs(sign: Byte): UByteArray { 544 | if (sign == 0.toByte()) return this 545 | 546 | for (i in lastIndex downTo 0) { 547 | val byte = this[i] 548 | this[i] = (byte - 1u).toUByte() 549 | 550 | // if we have overflow (0b00000000 - 0b1 = 0b11111111), 551 | // then we need to continue subtraction until no overflow 552 | // or until the end of the array 553 | val underflow = this[i] > byte 554 | if (!underflow) break 555 | } 556 | 557 | for (i in this.indices) { 558 | this[i] = this[i].inv() 559 | } 560 | 561 | return this 562 | } 563 | 564 | private fun UByteArray.padToInt(): UByteArray { 565 | if (this.size % 4 == 0) return this 566 | return UByteArray(4 - this.size % 4) + this 567 | } 568 | 569 | private fun UByteArray.toUIntArray(): UIntArray { 570 | val result = UIntArray(size = size / Int.SIZE_BYTES) 571 | 572 | for (i in result.indices) { 573 | result[i] = result[i] or (this[i * 4 + 0].toUInt() shl 24) 574 | result[i] = result[i] or (this[i * 4 + 1].toUInt() shl 16) 575 | result[i] = result[i] or (this[i * 4 + 2].toUInt() shl 8) 576 | result[i] = result[i] or (this[i * 4 + 3].toUInt() shl 0) 577 | } 578 | 579 | result.reverse() 580 | return result 581 | } 582 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bit/Bit.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bit 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | @JvmInline 6 | public value class Bit(public val enabled: Boolean) { 7 | public fun toInt(): Int = if (enabled) 1 else 0 8 | 9 | override fun toString(): String = "Bit[${toInt()}]" 10 | 11 | public companion object { 12 | public val Enabled: Bit = true.bit 13 | public val Disabled: Bit = false.bit 14 | } 15 | } 16 | 17 | public val Boolean.bit: Bit get() = Bit(enabled = this) 18 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bit/BitArray.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bit 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | @OptIn(ExperimentalUnsignedTypes::class) 6 | @JvmInline 7 | public value class BitArray( 8 | // Padded payload, where padding is determined by bit-switch from 1 to 0 9 | // 0b00101001001100111 10 | // And the respective padding is 0111 11 | // 12 | // When payload is already padded, 13 | // you should append one padding-only byte with the 14 | // following value: [data]01111111 15 | private val payload: UByteArray 16 | ) : Iterable { 17 | public val size: Int get() = payload.size * 8 - paddingSize 18 | 19 | public val paddingSize: Int get() { 20 | var index = 1 21 | while (getBitUnsafe(n = (payload.size * 8) - index).enabled) index++ 22 | return index 23 | } 24 | 25 | public val lastIndex: Int get() = size - 1 26 | 27 | public operator fun set(n: Int, bit: Bit) { 28 | checkIndex(n) 29 | if (bit.enabled) setBit(n) else resetBit(n) 30 | } 31 | 32 | public fun setBit(n: Int) { 33 | checkIndex(n) 34 | val bit = 0b10000000u shr (n % 8) 35 | payload[n / 8] = payload[n / 8] or bit.toUByte() 36 | } 37 | 38 | public fun resetBit(n: Int) { 39 | checkIndex(n) 40 | val bit = (0b10000000u shr (n % 8)).inv() 41 | payload[n / 8] = payload[n / 8] and bit.toUByte() 42 | } 43 | 44 | public fun getBit(n: Int): Bit { 45 | checkIndex(n) 46 | return getBitUnsafe(n) 47 | } 48 | 49 | // Allows to get padding bits 50 | private fun getBitUnsafe(n: Int): Bit { 51 | val bit = payload[n / 8].toUInt() shl (n % 8) shr 7 and 1u 52 | return Bit(enabled = bit == 1u) 53 | } 54 | 55 | public override fun iterator(): Iterator = iterator block@{ 56 | repeat(size) { i -> 57 | yield(getBit(i)) 58 | } 59 | } 60 | 61 | public fun toUByteArray(): UByteArray = payload.copyOf() 62 | 63 | private fun checkIndex(n: Int) { 64 | require(n <= lastIndex) { throw IndexOutOfBoundsException("$n") } 65 | } 66 | 67 | override fun toString(): String = "0b" + payload 68 | .joinToString(separator = "") { byte -> 69 | byte.toString(radix = 2).padStart(length = 8, padChar = '0') 70 | } 71 | 72 | public companion object { 73 | public const val PADDING_SIZE_AUTO: Int = -1 74 | 75 | public fun allocateBits(n: Int): BitArray { 76 | val payload = UByteArray( 77 | size = n / 8 + 1 78 | ) 79 | val padding = when (val rem = 8 - n % 8) { 80 | 0 -> 8 81 | else -> rem 82 | } 83 | val paddingByte = 0b11111111u shr (8 - padding + 1) 84 | payload[n / 8] = paddingByte.toUByte() 85 | return BitArray(payload) 86 | } 87 | 88 | 89 | public fun of( 90 | bytes: UByteArray, 91 | padded: Boolean = false 92 | ): BitArray { 93 | val paddedBytes = if (padded) { 94 | bytes 95 | } else { 96 | bytes + 0b01111111u 97 | } 98 | val bits = BitArray(paddedBytes) 99 | return bits 100 | } 101 | } 102 | } 103 | 104 | public fun BitArray.setBits(offset: Int, bits: BitArray) { 105 | for ((i, bit) in bits.withIndex()) { 106 | this[offset + i] = bit 107 | } 108 | } 109 | 110 | public fun BitArray.setByte(offset: Int, byte: UByte) { 111 | this[offset + 0] = (byte.toUInt() shr 7 and 0b1u == 1u).bit 112 | this[offset + 1] = (byte.toUInt() shr 6 and 0b1u == 1u).bit 113 | this[offset + 2] = (byte.toUInt() shr 5 and 0b1u == 1u).bit 114 | this[offset + 3] = (byte.toUInt() shr 4 and 0b1u == 1u).bit 115 | this[offset + 4] = (byte.toUInt() shr 3 and 0b1u == 1u).bit 116 | this[offset + 5] = (byte.toUInt() shr 2 and 0b1u == 1u).bit 117 | this[offset + 6] = (byte.toUInt() shr 1 and 0b1u == 1u).bit 118 | this[offset + 7] = (byte.toUInt() shr 0 and 0b1u == 1u).bit 119 | } 120 | 121 | @OptIn(ExperimentalUnsignedTypes::class) 122 | public fun main() { 123 | val bits = BitArray.allocateBits(10) 124 | bits[1] = Bit.Enabled 125 | val bytes = bits.toUByteArray() 126 | println(bytes.contentToString()) 127 | val restored = BitArray.of(bytes, padded = true) 128 | println(restored.size) 129 | for ((i, bit) in restored.withIndex()) { 130 | print("$i: $bit, ") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/ByteArrayPad.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | @JvmInline 6 | public value class ByteArrayPad( 7 | public val upstream: ByteArray 8 | ) { 9 | public operator fun get(index: Int): Byte = 10 | if (index in upstream.indices) { 11 | upstream[index] 12 | } else { 13 | 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/GreaterThanBigEndian.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | public infix fun ByteArray.greaterThanBigEndian(other: ByteArray): Boolean { 4 | if (size > other.size) return true 5 | if (size < other.size) return false 6 | 7 | val firstIterator = iterator() 8 | val secondIterator = other.iterator() 9 | 10 | while (firstIterator.hasNext()) { 11 | val first = firstIterator.nextByte() 12 | val second = secondIterator.nextByte() 13 | if (first > second) return true 14 | if (first < second) return false 15 | } 16 | 17 | return false 18 | } 19 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/Int.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | import kotlin.random.Random 4 | 5 | public fun Int.encodeToByteArray(): ByteArray = encodeToByteArray(ByteArray(Int.SIZE_BYTES)) 6 | 7 | public fun Int.encodeToByteArray(to: ByteArray, offset: Int = 0): ByteArray { 8 | to[offset + 0] = (this shr 0).toByte() 9 | to[offset + 1] = (this shr 8).toByte() 10 | to[offset + 2] = (this shr 16).toByte() 11 | to[offset + 3] = (this shr 24).toByte() 12 | return to 13 | } 14 | 15 | public fun ByteArray.decodeInt(offset: Int = 0): Int = 16 | (this[offset + 0].toInt() and 0xff shl 0) or 17 | (this[offset + 1].toInt() and 0xff shl 8) or 18 | (this[offset + 2].toInt() and 0xff shl 16) or 19 | (this[offset + 3].toInt() and 0xff shl 24) 20 | 21 | private fun main() { 22 | repeat(1_000_000_000) { 23 | val int = Random.nextInt() 24 | require(int.encodeToByteArray().decodeInt(offset = 0) == int) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/Long.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | import kotlin.random.Random 4 | 5 | public fun Long.encodeToByteArray(): ByteArray = byteArrayOf( 6 | (this shr 0).toByte(), 7 | (this shr 8).toByte(), 8 | (this shr 16).toByte(), 9 | (this shr 24).toByte(), 10 | (this shr 32).toByte(), 11 | (this shr 40).toByte(), 12 | (this shr 48).toByte(), 13 | (this shr 56).toByte() 14 | ) 15 | 16 | public fun ByteArray.decodeLong(offset: Int = 0): Long = 17 | (this[offset + 0].toLong() and 0xff shl 0) or 18 | (this[offset + 1].toLong() and 0xff shl 8) or 19 | (this[offset + 2].toLong() and 0xff shl 16) or 20 | (this[offset + 3].toLong() and 0xff shl 24) or 21 | (this[offset + 4].toLong() and 0xff shl 32) or 22 | (this[offset + 5].toLong() and 0xff shl 40) or 23 | (this[offset + 6].toLong() and 0xff shl 48) or 24 | (this[offset + 7].toLong() and 0xff shl 56) 25 | 26 | private fun main() { 27 | repeat(1_000_000_000) { 28 | val long = Random.nextLong() 29 | require(long.encodeToByteArray().decodeLong(offset = 0) == long) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/Pad.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | public fun ByteArray.padStart( 4 | desiredLength: Int, 5 | padByte: Byte = 0 6 | ): ByteArray { 7 | val padSize = desiredLength - this.size 8 | return ByteArray(padSize) { padByte } + this 9 | } 10 | 11 | public fun ByteArray.padEnd( 12 | desiredLength: Int, 13 | padByte: Byte = 0 14 | ): ByteArray { 15 | val padSize = desiredLength - this.size 16 | return this + ByteArray(padSize) { padByte } 17 | } 18 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/ToBinaryString.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalUnsignedTypes::class) 2 | 3 | package ktproto.stdlib.bytes 4 | 5 | public fun UByteArray.toBinaryString(): String = 6 | joinToString(" ") { byte -> byte.toString(radix = 2).padStart(8, '0') } 7 | 8 | public fun ByteArray.toBinaryString(): String = 9 | toUByteArray().toBinaryString() 10 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/bytes/Xor.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.bytes 2 | 3 | import kotlin.experimental.xor 4 | 5 | public infix fun ByteArray.xor(other: ByteArray): ByteArray { 6 | require(this.size == other.size) 7 | return ByteArray(this.size) { i -> this[i] xor other[i] } 8 | } 9 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/int/NearestMultiple.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.int 2 | 3 | public fun Int.nearestMultipleOf(n: Int): Int { 4 | return if (this <= 0) 0 else ((this - 1) / n + 1) * n 5 | } 6 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/random/NextInt128.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.random 2 | 3 | import kotl.serialization.int.Int128 4 | import kotlin.random.Random 5 | 6 | public fun Random.nextInt128(): Int128 = Int128( 7 | data = intArrayOf( 8 | nextInt(), 9 | nextInt(), 10 | nextInt(), 11 | nextInt() 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/commonMain/kotlin/ktproto/stdlib/scope/WeakCoroutineScope.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.scope 2 | 3 | import kotlinx.coroutines.* 4 | 5 | /** 6 | * Cancels the scope after [block] execution 7 | */ 8 | public suspend inline fun weakCoroutineScope( 9 | crossinline block: suspend CoroutineScope.() -> T 10 | ): T = coroutineScope { 11 | val scope = this + Job() 12 | try { 13 | block(scope) 14 | } finally { 15 | scope.cancel() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/stdlib-extensions/src/jsMain/kotlin/ktproto/stdlib/platform/JsPlatform.kt: -------------------------------------------------------------------------------- 1 | package ktproto.stdlib.platform 2 | 3 | public sealed interface JsPlatform { 4 | public data object Node : JsPlatform 5 | public data object Browser : JsPlatform 6 | } 7 | 8 | public val jsRuntime: JsPlatform by lazy { 9 | if (js("typeof window") === "undefined") { 10 | JsPlatform.Node 11 | } else { 12 | JsPlatform.Browser 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /session/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainApi(projects.transport) 10 | commonMainImplementation(libs.kotlinxCoroutines) 11 | commonMainImplementation(projects.types) 12 | commonMainImplementation(projects.libs.stdlibExtensions) 13 | commonMainImplementation(projects.libs.io) 14 | } 15 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/AuthKeyId.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | public value class AuthKeyId(public val bits64: MemoryArena) { 8 | public companion object { 9 | public const val SIZE_BYTES: Int = 8 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/MTProtoSafeSession.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.first 6 | import kotlinx.coroutines.flow.mapNotNull 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.sync.withLock 9 | import ktproto.io.annotation.OngoingConnection 10 | import ktproto.transport.exception.throwIO 11 | 12 | 13 | /** 14 | * TODO: fix error-reporting, now it does not update stacktrace 15 | * 16 | * This class automatically handles and relaunches session when closed 17 | */ 18 | @OptIn(OngoingConnection::class) 19 | public class MTProtoSafeSession( 20 | private val connector: MTProtoSession.Connector, 21 | private val scope: CoroutineScope 22 | ) { 23 | private val requestsScope = scope + SupervisorJob() 24 | private val mutex = Mutex() 25 | 26 | private var session: MTProtoSession? = null 27 | private val messages = MutableSharedFlow>() 28 | private var lastScope = scope + Job() 29 | 30 | // todo: connection not before trying to make request 31 | // but on a separate background coroutine that is constantly 32 | // monitoring job state 33 | // todo v2: or maybe not 34 | @OptIn(DelicateCoroutinesApi::class) 35 | private suspend fun ensureSessionActive(): MTProtoSession = mutex.withLock { 36 | var session = session 37 | 38 | if ( 39 | session == null || 40 | session.incoming.isClosedForReceive && 41 | session.outgoing.isClosedForSend 42 | ) { 43 | lastScope.cancel() 44 | lastScope = scope + Job() 45 | session = connector.connect(lastScope) 46 | collectMessages(session) 47 | } 48 | 49 | this.session = session 50 | session 51 | } 52 | 53 | private suspend fun collectMessages(session: MTProtoSession) { 54 | scope.launch { 55 | try { 56 | for (message in session.incoming) { 57 | messages.emit(Result.success(message)) 58 | } 59 | } catch (throwable: Throwable) { 60 | messages.emit(Result.failure(throwable)) 61 | } 62 | } 63 | } 64 | 65 | public suspend fun connect() { 66 | ensureSessionActive() 67 | } 68 | 69 | /** 70 | * When [transform] returns not-null value, that value is returned 71 | */ 72 | public suspend fun sendRequest( 73 | request: MTProtoSession.Message, 74 | transform: (MTProtoSession.Message) -> T? 75 | ): T { 76 | val session = ensureSessionActive() 77 | val response = requestsScope.async(start = CoroutineStart.UNDISPATCHED) { 78 | messages 79 | .mapNotNull { result -> 80 | result.map { message -> 81 | transform(message) ?: return@mapNotNull null 82 | } 83 | } 84 | .first() 85 | } 86 | session.outgoing.send(request) 87 | val result = response.await().getOrElse { throwable -> throwable.throwIO() } 88 | return result 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/MTProtoSession.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.channels.ReceiveChannel 5 | import kotlinx.coroutines.channels.SendChannel 6 | import ktproto.io.annotation.OngoingConnection 7 | import kotlin.jvm.JvmInline 8 | 9 | @OngoingConnection 10 | public interface MTProtoSession { 11 | public val incoming: ReceiveChannel 12 | public val outgoing: SendChannel 13 | 14 | @JvmInline 15 | public value class Message( 16 | public val bytes: ByteArray 17 | ) 18 | 19 | public fun interface Connector { 20 | public suspend fun connect(scope: CoroutineScope): MTProtoSession 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/MessageId.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | public value class MessageId(public val bits64: MemoryArena) { 8 | public companion object { 9 | public const val SIZE_BYTES: Int = 8 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/MessageIdProvider.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session 2 | 3 | import ktproto.time.Clock 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import ktproto.io.memory.MemoryArena 7 | import kotlin.math.absoluteValue 8 | import kotlin.random.Random 9 | 10 | /** 11 | * A message is rejected over 300 seconds after it is created or 30 seconds before it is created (this is needed to protect from replay attacks). In this situation, it must be re-sent with a different identifier (or placed in a container with a higher identifier). 12 | */ 13 | public fun interface MessageIdProvider { 14 | public fun nextMessageId(): MessageId 15 | } 16 | 17 | public fun messageIdProvider(clock: Clock): MessageIdProvider = 18 | DefaultMessageIdProvider(clock) 19 | 20 | private class DefaultMessageIdProvider(private val clock: Clock) : MessageIdProvider { 21 | private var lastMessageId: Long = 0 22 | 23 | override fun nextMessageId(): MessageId { 24 | val millis = clock.currentTimeMillis() 25 | val leftShift = Int.SIZE_BITS - MILLIS_BITS 26 | val noise = (Random.nextInt() shr MILLIS_BITS).absoluteValue.toLong() 27 | val messageId = millis shl leftShift or noise 28 | 29 | if (messageId <= lastMessageId) { 30 | return nextMessageId() 31 | } 32 | 33 | lastMessageId = messageId 34 | 35 | return MessageId(MemoryArena.of(lastMessageId)) 36 | } 37 | 38 | companion object { 39 | const val MILLIS_BITS = 10 40 | } 41 | } 42 | 43 | public fun main() { 44 | 45 | } 46 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/EncodeMessage.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.annotation.OngoingConnection 4 | import ktproto.session.AuthKeyId 5 | import ktproto.session.MTProtoSession 6 | import ktproto.transport.MTProtoTransport 7 | 8 | @OptIn(OngoingConnection::class) 9 | internal fun MTProtoSession.Message.encode( 10 | salt: Salt, 11 | authKeyId: AuthKeyId, 12 | sessionId: SessionId 13 | ): MTProtoTransport.Message { 14 | TODO() 15 | } 16 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/EncryptedData.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import ktproto.io.memory.flatten 5 | import ktproto.session.MessageId 6 | import kotlin.jvm.JvmInline 7 | 8 | public data class EncryptedData( 9 | public val salt: Salt, 10 | public val sessionId: SessionId, 11 | public val messageId: MessageId, 12 | public val seqNo: SeqNo, 13 | public val dataLength: DataLength, 14 | public val data: MemoryArena, 15 | public val padding: MemoryArena 16 | ) { 17 | public fun toByteArray(): ByteArray = listOf( 18 | salt.bits64, sessionId.bits64, 19 | messageId.bits64, seqNo.bits32, 20 | dataLength.bits32, data, padding 21 | ).flatten().data 22 | 23 | @JvmInline 24 | public value class DataLength(public val bits32: MemoryArena) { 25 | public companion object { 26 | public const val SIZE_BYTES: Int = 4 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/MTProtoEncryptedSession.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.channels.ReceiveChannel 6 | import kotlinx.coroutines.channels.SendChannel 7 | import kotlinx.coroutines.flow.launchIn 8 | import kotlinx.coroutines.flow.map 9 | import kotlinx.coroutines.flow.onEach 10 | import kotlinx.coroutines.flow.receiveAsFlow 11 | import ktproto.io.annotation.OngoingConnection 12 | import ktproto.session.MTProtoSession 13 | import ktproto.transport.MTProtoTransport 14 | 15 | @OngoingConnection 16 | public class MTProtoEncryptedSession( 17 | private val transport: MTProtoTransport, 18 | private val scope: CoroutineScope 19 | ) : MTProtoSession { 20 | private val _incoming = Channel() 21 | override val incoming: ReceiveChannel = _incoming 22 | 23 | private val _outgoing = Channel() 24 | override val outgoing: SendChannel = _outgoing 25 | 26 | init { 27 | init() 28 | } 29 | 30 | private fun init() { 31 | transport.incoming.receiveAsFlow() 32 | .map { message -> message.decode() } 33 | .onEach { message -> _incoming.send(message) } 34 | .launchIn(scope) 35 | 36 | _outgoing.receiveAsFlow() 37 | .map { message -> 38 | // message.encode() 39 | TODO() 40 | } 41 | .onEach { message -> transport.outgoing.send(message) } 42 | .launchIn(scope) 43 | } 44 | } 45 | 46 | @OptIn(OngoingConnection::class) 47 | private fun MTProtoTransport.Message.decode(): MTProtoSession.Message { 48 | TODO() 49 | } 50 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/MTProtoEnvelope.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.memory.* 4 | import ktproto.session.AuthKeyId 5 | import kotlin.jvm.JvmInline 6 | 7 | public data class MTProtoEnvelope( 8 | public val authKeyId: AuthKeyId, 9 | public val messageKey: MessageKey, 10 | public val encryptedData: MemoryArena 11 | ) { 12 | public fun toByteArray(): ByteArray = listOf( 13 | authKeyId.bits64, 14 | messageKey.bits128, 15 | encryptedData 16 | ).flatten().data 17 | 18 | public companion object { 19 | public fun of(memory: MemoryArena): MTProtoEnvelope = 20 | MTProtoEnvelope( 21 | authKeyId = AuthKeyId(memory.take(AuthKeyId.SIZE_BYTES)), 22 | messageKey = MessageKey(memory.drop(AuthKeyId.SIZE_BYTES).take(MessageKey.SIZE_BYTES)), 23 | encryptedData = memory.drop(n = AuthKeyId.SIZE_BYTES + MessageKey.SIZE_BYTES) 24 | ) 25 | } 26 | 27 | @JvmInline 28 | public value class MessageKey(public val bits128: MemoryArena) { 29 | public companion object { 30 | public const val SIZE_BYTES: Int = 16 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/Salt.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | public value class Salt(public val bits64: MemoryArena) { 8 | public companion object { 9 | public const val SIZE_BYTES: Int = 8 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/SeqNo.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | public value class SeqNo(public val bits32: MemoryArena) { 8 | public companion object { 9 | public const val SIZE_BYTES: Int = 4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/encrypted/SessionId.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.encrypted 2 | 3 | import ktproto.io.memory.MemoryArena 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | public value class SessionId(public val bits64: MemoryArena) { 8 | public companion object { 9 | public const val SIZE_BYTES: Int = 8 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/plain/MTProtoPlainEnvelope.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.plain 2 | 3 | import ktproto.io.memory.* 4 | import ktproto.session.AuthKeyId 5 | import ktproto.session.MessageId 6 | import kotlin.jvm.JvmInline 7 | import ktproto.session.AuthKeyId as AuthKeyIdType 8 | 9 | public class MTProtoPlainEnvelope( 10 | public val authKeyId: AuthKeyIdType = AuthKeyId, 11 | public val messageId: MessageId, 12 | public val dataLength: DataLength, 13 | public val data: MemoryArena 14 | ) { 15 | public fun toByteArray(): ByteArray = listOf( 16 | authKeyId.bits64, messageId.bits64, 17 | dataLength.bits32, data 18 | ).flatten().data 19 | 20 | 21 | @JvmInline 22 | public value class DataLength(public val bits32: MemoryArena) { 23 | public companion object { 24 | public const val SIZE_BYTES: Int = Int.SIZE_BYTES 25 | } 26 | } 27 | 28 | public companion object { 29 | public val AuthKeyId: AuthKeyIdType = AuthKeyIdType( 30 | bits64 = MemoryArena.allocate(AuthKeyIdType.SIZE_BYTES) 31 | ) 32 | 33 | public fun of(memory: MemoryArena): MTProtoPlainEnvelope { 34 | val authKeyId: AuthKeyId 35 | val messageId: MessageId 36 | val dataLength: DataLength 37 | 38 | val data = memory 39 | .readMemory(AuthKeyIdType.SIZE_BYTES) { read -> 40 | authKeyId = AuthKeyIdType(read) 41 | }.readMemory(MessageId.SIZE_BYTES) { read -> 42 | messageId = MessageId(read) 43 | }.readMemory(DataLength.SIZE_BYTES) { read -> 44 | dataLength = DataLength(read) 45 | } 46 | 47 | return MTProtoPlainEnvelope(authKeyId, messageId, dataLength, data) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /session/src/commonMain/kotlin/ktproto/session/plain/MTProtoPlainSession.kt: -------------------------------------------------------------------------------- 1 | package ktproto.session.plain 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.channels.ReceiveChannel 6 | import kotlinx.coroutines.channels.SendChannel 7 | import kotlinx.coroutines.flow.* 8 | import ktproto.io.annotation.OngoingConnection 9 | import ktproto.io.memory.* 10 | import ktproto.session.MTProtoSession 11 | import ktproto.session.MessageIdProvider 12 | import ktproto.session.plain.MTProtoPlainEnvelope.DataLength 13 | import ktproto.stdlib.bytes.toBinaryString 14 | import ktproto.transport.MTProtoTransport 15 | 16 | @OngoingConnection 17 | public fun mtprotoPlainSession( 18 | transport: MTProtoTransport, 19 | messageIdProvider: MessageIdProvider, 20 | scope: CoroutineScope 21 | ): MTProtoSession { 22 | val session = MTProtoPlainSession(transport, messageIdProvider) 23 | session.launchIn(scope) 24 | return session 25 | } 26 | 27 | @OngoingConnection 28 | private class MTProtoPlainSession( 29 | private val transport: MTProtoTransport, 30 | private val messageIdProvider: MessageIdProvider 31 | ) : MTProtoSession { 32 | private val _incoming = Channel() 33 | override val incoming: ReceiveChannel = _incoming 34 | 35 | private val _outgoing = Channel() 36 | override val outgoing: SendChannel = _outgoing 37 | 38 | fun launchIn(scope: CoroutineScope) { 39 | transport.incoming.receiveAsFlow() 40 | .onCompletion { cause -> close(cause) } 41 | .map { message -> message.decode() } 42 | .onEach(_incoming::send) 43 | .launchIn(scope) 44 | 45 | _outgoing.receiveAsFlow() 46 | .onCompletion { cause -> close(cause) } 47 | .map { message -> message.encode(messageIdProvider) } 48 | .onEach(transport.outgoing::send) 49 | .launchIn(scope) 50 | } 51 | 52 | private fun close(cause: Throwable?) { 53 | _incoming.close(cause) 54 | _outgoing.close(cause) 55 | } 56 | } 57 | 58 | @OptIn(OngoingConnection::class) 59 | private fun MTProtoSession.Message.encode( 60 | messageId: MessageIdProvider 61 | ): MTProtoTransport.Message { 62 | val memory = MemoryArena.allocate(n = DataLength.SIZE_BYTES + bytes.size) 63 | memory.write(bytes.size).write(bytes) 64 | val dataLengthMemory = memory.take(DataLength.SIZE_BYTES) 65 | val envelope = MTProtoPlainEnvelope( 66 | messageId = messageId.nextMessageId(), 67 | dataLength = DataLength(dataLengthMemory), 68 | data = memory.drop(DataLength.SIZE_BYTES) 69 | ) 70 | return MTProtoTransport.Message(envelope.toByteArray()) 71 | } 72 | 73 | @OptIn(OngoingConnection::class) 74 | private fun MTProtoTransport.Message.decode(): MTProtoSession.Message { 75 | val memory = MemoryArena.of(bytes) 76 | val envelope = MTProtoPlainEnvelope.of(memory) 77 | return MTProtoSession.Message( 78 | bytes = envelope.data.toByteArray() 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | google() 7 | gradlePluginPortal() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositories { 13 | mavenCentral() 14 | google() 15 | maven { 16 | name = "koTL GitHub" 17 | url = uri("https://maven.pkg.github.com/kotlin-telegram/koTL") 18 | credentials { 19 | username = System.getenv("GITHUB_USERNAME") 20 | password = System.getenv("GITHUB_TOKEN") 21 | } 22 | } 23 | } 24 | } 25 | 26 | includeBuild("build-logic") 27 | 28 | include( 29 | ":types", 30 | ":transport", 31 | ":session", 32 | ":client", 33 | ":client:ktor", 34 | ":libs:stdlib-extensions", 35 | ":libs:io", 36 | ":libs:crypto" 37 | ) 38 | 39 | rootProject.name = "ktproto" 40 | -------------------------------------------------------------------------------- /transport/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainImplementation(libs.kotlinxCoroutines) 10 | commonMainImplementation(projects.libs.stdlibExtensions) 11 | commonMainImplementation(projects.libs.io) 12 | commonMainImplementation(projects.types) 13 | } 14 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/MTProtoIntermediate.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.flow.* 6 | import ktproto.io.annotation.OngoingConnection 7 | import ktproto.io.input.Input 8 | import ktproto.io.memory.* 9 | import ktproto.io.output.Output 10 | import kotlin.jvm.JvmInline 11 | 12 | @OptIn(OngoingConnection::class) 13 | public fun mtprotoIntermediate( 14 | transport: Transport.Connector 15 | ): MTProtoTransport.Connector = MTProtoTransport.Connector { scope -> 16 | val result = MTProtoIntermediate( 17 | transport = transport.connect(scope) 18 | ) 19 | result.launchIn(scope) 20 | result 21 | } 22 | 23 | 24 | @OngoingConnection 25 | private class MTProtoIntermediate( 26 | private val transport: Transport 27 | ) : MTProtoTransport { 28 | override val incoming = Channel() 29 | override val outgoing = Channel() 30 | 31 | suspend fun launchIn(scope: CoroutineScope) { 32 | transport.output.write(INIT) 33 | 34 | transport.input 35 | .asMessagesFlow() 36 | .onCompletion { cause -> close(cause) } 37 | .onEach { message -> incoming.send(message) } 38 | .launchIn(scope) 39 | 40 | outgoing.consumeAsFlow() 41 | .attachOutput(transport.output) 42 | .onCompletion { cause -> close(cause) } 43 | .launchIn(scope) 44 | } 45 | 46 | private fun close(cause: Throwable?) { 47 | incoming.close(cause) 48 | outgoing.close(cause) 49 | } 50 | 51 | private companion object { 52 | private val INIT = MemoryArena.of(int = 0xeeeeeeee_u) 53 | } 54 | } 55 | 56 | @OptIn(OngoingConnection::class) 57 | private fun Input.asMessagesFlow() = flow { 58 | var memory = MemoryArena.allocate(n = 32) 59 | 60 | while (true) { 61 | read(memory.take(Int.SIZE_BYTES)) 62 | val length = memory.scanInt() 63 | memory = memory.ensureCapacity(length) 64 | val memoryView = memory.take(length) 65 | read(memoryView) 66 | val bytes = memoryView.toByteArray() 67 | bytes.throwTransportExceptions() 68 | emit(MTProtoTransport.Message(bytes)) 69 | } 70 | } 71 | 72 | @OptIn(OngoingConnection::class) 73 | private fun Flow.attachOutput(output: Output) = flow { 74 | var memory = MemoryArena.allocate(n = 32) 75 | 76 | this@attachOutput.collect { message -> 77 | val messageLength = Int.SIZE_BYTES + message.bytes.size 78 | memory = memory.ensureCapacity(messageLength) 79 | 80 | memory 81 | .write(message.bytes.size) 82 | .write(message.bytes) 83 | 84 | output.write(memory.take(messageLength)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/MTProtoTransport.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.channels.ReceiveChannel 5 | import kotlinx.coroutines.channels.SendChannel 6 | import ktproto.io.annotation.OngoingConnection 7 | import kotlin.jvm.JvmInline 8 | 9 | @OngoingConnection 10 | public interface MTProtoTransport { 11 | public val incoming: ReceiveChannel 12 | public val outgoing: SendChannel 13 | 14 | @JvmInline 15 | public value class Message(public val bytes: ByteArray) 16 | 17 | public fun interface Connector { 18 | public suspend fun connect(scope: CoroutineScope): MTProtoTransport 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/ThrowTransportExceptions.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport 2 | 3 | import ktproto.stdlib.bytes.decodeInt 4 | import ktproto.transport.exception.TransportException 5 | import kotlin.math.absoluteValue 6 | 7 | internal fun ByteArray.throwTransportExceptions() { 8 | if (size != 4) return 9 | val int = decodeInt() 10 | if (int < 0) { 11 | throw TransportException(code = int.absoluteValue) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/Transport.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import ktproto.io.annotation.OngoingConnection 5 | import ktproto.io.input.Input 6 | import ktproto.io.output.Output 7 | 8 | /** 9 | * A real-world transport protocol such as TCP, UDP, Http 10 | */ 11 | @OngoingConnection 12 | public interface Transport { 13 | public val input: Input 14 | public val output: Output 15 | 16 | public fun interface Connector { 17 | public suspend fun connect(scope: CoroutineScope): Transport 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/exception/IOException.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport.exception 2 | 3 | import kotlinx.coroutines.CancellationException 4 | import ktproto.exception.MTProtoException 5 | 6 | public open class IOException( 7 | message: String? = null, 8 | cause: Throwable? = null 9 | ) : MTProtoException(message, cause) 10 | 11 | public fun Throwable.throwIO(): Nothing { 12 | if (this is CancellationException) throw this 13 | if (this is TransportException) throw TransportException(code, this) 14 | throw IOException(message, cause = this) 15 | } 16 | -------------------------------------------------------------------------------- /transport/src/commonMain/kotlin/ktproto/transport/exception/TransportException.kt: -------------------------------------------------------------------------------- 1 | package ktproto.transport.exception 2 | 3 | public class TransportException( 4 | public val code: Int, 5 | cause: Throwable? = null 6 | ) : IOException( 7 | message = "Transport was closed with code $code", 8 | cause = cause 9 | ) 10 | -------------------------------------------------------------------------------- /types/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | id("publication-convention") 4 | } 5 | 6 | version = libs.versions.ktprotoVersion.get() 7 | 8 | dependencies { 9 | commonMainImplementation(projects.libs.stdlibExtensions) 10 | } 11 | -------------------------------------------------------------------------------- /types/src/commonMain/kotlin/ktproto/exception/MTProtoException.kt: -------------------------------------------------------------------------------- 1 | package ktproto.exception 2 | 3 | public open class MTProtoException( 4 | message: String? = null, 5 | cause: Throwable? = null 6 | ) : RuntimeException(message, cause) 7 | -------------------------------------------------------------------------------- /types/src/commonMain/kotlin/ktproto/time/Clock.kt: -------------------------------------------------------------------------------- 1 | package ktproto.time 2 | 3 | public expect interface Clock { 4 | public fun currentTimeMillis(): Long 5 | 6 | public object System : Clock 7 | } 8 | -------------------------------------------------------------------------------- /types/src/iosMain/kotlin/ktproto/time/Clock.ios.kt: -------------------------------------------------------------------------------- 1 | package ktproto.time 2 | 3 | import platform.Foundation.NSDate 4 | import platform.Foundation.timeIntervalSince1970 5 | 6 | public actual interface Clock { 7 | public actual fun currentTimeMillis(): Long 8 | 9 | public actual object System : Clock { 10 | override fun currentTimeMillis(): Long = 11 | (NSDate().timeIntervalSince1970 * 1_000).toLong() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/src/jsMain/kotlin/ktproto/time/Clock.js.kt: -------------------------------------------------------------------------------- 1 | package ktproto.time 2 | 3 | import kotlin.js.Date 4 | 5 | public actual interface Clock { 6 | public actual fun currentTimeMillis(): Long 7 | 8 | public actual object System : Clock { 9 | override fun currentTimeMillis(): Long = Date.now().toLong() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /types/src/jvmMain/kotlin/ktproto/time/Clock.jvm.kt: -------------------------------------------------------------------------------- 1 | package ktproto.time 2 | 3 | public actual interface Clock { 4 | public actual fun currentTimeMillis(): Long 5 | 6 | public actual object System : Clock { 7 | override fun currentTimeMillis(): Long = 8 | java.lang.System.currentTimeMillis() 9 | } 10 | } 11 | --------------------------------------------------------------------------------