From 37bbe35482538f1053e70d72a5d3b9d0e7722bb7 Mon Sep 17 00:00:00 2001 From: Infendro Date: Sun, 14 Dec 2025 14:43:30 +0100 Subject: [PATCH 1/7] update README small rewording, link wikipedia article --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a265a51..3c4696d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # otp-kt -`otp-kt` is a Kotlin Multiplatform library that provides common one-time password implementations. +`otp-kt` is a Kotlin Multiplatform library that provides common [One-Time Password](https://en.wikipedia.org/wiki/One-time_password) algorithms. -Supported implementations: +Supported algorithms: - HOTP - TOTP From f85e22311d80db2f4f3c895f2e93309993950971 Mon Sep 17 00:00:00 2001 From: Infendro Date: Tue, 13 Jan 2026 18:06:00 +0100 Subject: [PATCH 2/7] improve ci --- .gitea/workflows/ci.yaml | 92 ++++++++++++++++++++++++++++++++++++++ .gitea/workflows/main.yaml | 36 --------------- build.gradle.kts | 2 +- 3 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 .gitea/workflows/ci.yaml delete mode 100644 .gitea/workflows/main.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..2f46684 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,92 @@ +on: + push: + branches: [ develop, main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: linux + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '24' + distribution: 'temurin' + + - name: Cache Gradle + uses: actions/cache@v4 + with: + key: gradle-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: gradle- + path: | + ~/.gradle/caches/ + ~/.gradle/wrapper/ + + - name: Cache Konan + uses: actions/cache@v4 + with: + key: konan-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: konan- + path: | + ~/.konan/ + + - name: Cache Node + uses: actions/cache@v4 + with: + key: node-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: node- + path: | + ~/.gradle/nodejs + ~/.gradle/yarn + + - name: Test + run: ./gradlew allTests + + publish: + if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + needs: test + runs-on: linux + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '24' + distribution: 'temurin' + + - name: Cache Gradle + uses: actions/cache@v4 + with: + key: gradle-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: gradle- + path: | + ~/.gradle/caches/ + ~/.gradle/wrapper/ + + - name: Cache Konan + uses: actions/cache@v4 + with: + key: konan-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: konan- + path: | + ~/.konan/ + + - name: Cache Node + uses: actions/cache@v4 + with: + key: node-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: node- + path: | + ~/.gradle/nodejs + ~/.gradle/yarn + + - name: Publish + run: ./gradlew publish + env: + INFENDRO__TOKEN: ${{ secrets.TOKEN }} diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml deleted file mode 100644 index 4885477..0000000 --- a/.gitea/workflows/main.yaml +++ /dev/null @@ -1,36 +0,0 @@ -on: - push: - branches: - - main - -env: - INFENDRO__TOKEN: ${{ secrets.TOKEN }} - -jobs: - publish: - runs-on: linux - steps: - - uses: actions/checkout@v4 - - - uses: actions/cache/restore@v4 - with: - key: gradle - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - ~/.konan/ - - - uses: actions/setup-java@v4 - with: - java-version: '24' - distribution: 'temurin' - - - run: ./gradlew publish - - - uses: actions/cache/save@v4 - with: - key: gradle - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - ~/.konan/ diff --git a/build.gradle.kts b/build.gradle.kts index 63bfbd9..f9d9542 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,10 +12,10 @@ plugins { kotlin { jvm() + linuxX64() js { nodejs() } - linuxX64() repositories { infendro() From d2b901a6ff5fc6e8e76f93da5fca3fada463b8b5 Mon Sep 17 00:00:00 2001 From: Infendro Date: Thu, 29 Jan 2026 18:11:17 +0100 Subject: [PATCH 3/7] upgrade too tired to write a detailed commit message or split into self-explanatory commits --- README.md | 33 +++--------- build.gradle.kts | 19 ++++--- gradle/libs.versions.toml | 14 +++-- .../kotlin/com/infendro/otp/HOTP.kt | 51 +++++-------------- .../com/infendro/otp/SecretGenerator.kt | 14 ----- .../kotlin/com/infendro/otp/TOTP.kt | 21 +++----- .../kotlin/com/infendro/otp/util/Math.kt | 6 +++ 7 files changed, 50 insertions(+), 108 deletions(-) delete mode 100644 src/commonMain/kotlin/com/infendro/otp/SecretGenerator.kt create mode 100644 src/commonMain/kotlin/com/infendro/otp/util/Math.kt diff --git a/README.md b/README.md index 3c4696d..43bd150 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,6 @@ Supported algorithms: - HOTP - TOTP -## Targets - -| Target | Status | -|------------------|---------------| -| JVM | Supported | -| JavaScript | Supported | -| Native (Linux) | Supported | -| Native (Windows) | Not Supported | - ## Installation Add the following to your `build.gradle.kts`. @@ -25,36 +16,24 @@ repositories { } dependencies { - implementation("com.infendro:otp:1.2.2") + implementation("com.infendro:otp:1.3.0") } ``` ## Usage ```kotlin -import com.infendro.hash.sha1.SHA1 -import com.infendro.otp.SecretGenerator -import com.infendro.otp.TOTP -import com.infendro.random.csprng.CSPRNG -import kotlin.time.Clock.System.now -import kotlin.time.Duration.Companion.seconds - fun main() { - // create a SecretGenerator and TOTP instance - val generator = SecretGenerator(CSPRNG.HMAC(SHA1)) + // create TOTP instance val totp = TOTP( function = SHA1, length = 6, period = 30.seconds, ) - // generate a secret using SHA1 - val secret = generator.generate() - - // generate an OTP - val otp = totp.generate(secret, now()) - - // verify an OTP - totp.verify(secret, now(), otp) + // generate and verify OTP + val now = System.now() + val otp = totp.generate(, now) + totp.verify(, now, otp) } ``` diff --git a/build.gradle.kts b/build.gradle.kts index f9d9542..011f4d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,11 @@ +@file:OptIn(ExperimentalWasmDsl::class) + import com.infendro.plugin.infendro import com.infendro.plugin.token +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl group = "com.infendro" -version = "1.2.2" +version = "1.3.0" plugins { alias(libs.plugins.infendro) @@ -13,9 +16,13 @@ plugins { kotlin { jvm() linuxX64() - js { - nodejs() - } + linuxArm64() + mingwX64() + macosX64() + macosArm64() + js { nodejs() } + wasmJs { nodejs() } + wasmWasi { nodejs() } repositories { infendro() @@ -24,15 +31,13 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.prng) implementation(libs.mac) implementation(libs.hash) - implementation(libs.bytearray) + implementation(libs.bytes) } } compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes") freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1609a5c..14aa349 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,15 @@ [versions] -infendro = "1.0.0" -kotlin = "2.2.21" -prng = "1.2.0" -mac = "1.2.0" -hash = "1.3.2" -bytearray = "1.2.4" +infendro = "1.1.0" +kotlin = "2.3.0" +mac = "1.3.0" +hash = "1.5.1" +bytes = "1.3.3" [plugins] infendro = { id = "com.infendro.plugin", version.ref = "infendro" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } [libraries] -prng = { module = "com.infendro:prng", version.ref = "prng" } mac = { module = "com.infendro:mac", version.ref = "mac" } hash = { module = "com.infendro:hash", version.ref = "hash" } -bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" } +bytes = { module = "com.infendro:bytes", version.ref = "bytes" } diff --git a/src/commonMain/kotlin/com/infendro/otp/HOTP.kt b/src/commonMain/kotlin/com/infendro/otp/HOTP.kt index f8df4b2..ce240ae 100644 --- a/src/commonMain/kotlin/com/infendro/otp/HOTP.kt +++ b/src/commonMain/kotlin/com/infendro/otp/HOTP.kt @@ -1,55 +1,32 @@ package com.infendro.otp -import com.infendro.bytearray.toInt -import com.infendro.bytearray.toUByteArray +import com.infendro.bytes.bytearray.toInt +import com.infendro.bytes.long.toByteArray import com.infendro.hash.HashFunction import com.infendro.hash.sha1.SHA1 import com.infendro.mac.HMAC -import kotlin.math.pow +import com.infendro.otp.util.pow +import kotlin.experimental.and class HOTP( - function: HashFunction = SHA1, - private val length: Int = 6, + val function: HashFunction = SHA1, + val length: Int = 6, ) { private val hmac = HMAC(function) - fun verify( - secret: UByteArray, - counter: Long, - otp: String, - ): Boolean { + fun verify(secret: ByteArray, counter: Long, otp: String): Boolean { return generate(secret, counter) == otp } - fun generate( - secret: UByteArray, - counter: Long, - ): String { + fun generate(secret: ByteArray, counter: Long): String { require(counter >= 0) - - val hash = generateHash(secret, counter.toUByteArray()) - return otp(hash) + return otp(secret, counter) } - private fun generateHash( - secret: UByteArray, - value: UByteArray, - ): UByteArray { - return hmac.hash(secret, value) - } - - private fun otp( - hash: UByteArray, - ): String { - val offset = (hash.last() and 0xFU).toInt() - - var truncatedHash = hash - .drop(offset).take(4) - .toUByteArray() - .toInt() and 0x7FFFFFFF - truncatedHash %= 10.0.pow(length).toInt() - - return truncatedHash.toString() - .padStart(length, '0') + private fun otp(secret: ByteArray, counter: Long): String { + val hash = hmac.hash(secret, counter.toByteArray()) + val offset = (hash.last() and 0xF).toInt() + val otp = (hash.toInt(offset) and 0x7FFFFFFF) % 10.pow(length) + return "$otp".padStart(length, '0') } } diff --git a/src/commonMain/kotlin/com/infendro/otp/SecretGenerator.kt b/src/commonMain/kotlin/com/infendro/otp/SecretGenerator.kt deleted file mode 100644 index 8aaed62..0000000 --- a/src/commonMain/kotlin/com/infendro/otp/SecretGenerator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.infendro.otp - -import kotlin.random.Random -import kotlin.random.nextUBytes - -class SecretGenerator( - private val random: Random, -) { - fun generate( - bytes: Int = 20, - ): UByteArray { - return random.nextUBytes(bytes) - } -} diff --git a/src/commonMain/kotlin/com/infendro/otp/TOTP.kt b/src/commonMain/kotlin/com/infendro/otp/TOTP.kt index 0a22a1c..2a81438 100644 --- a/src/commonMain/kotlin/com/infendro/otp/TOTP.kt +++ b/src/commonMain/kotlin/com/infendro/otp/TOTP.kt @@ -7,30 +7,21 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant class TOTP( - function: HashFunction = SHA1, - length: Int = 6, - private val period: Duration = 30.seconds, + val function: HashFunction = SHA1, + val length: Int = 6, + val period: Duration = 30.seconds, ) { private val hotp = HOTP(function, length) - fun verify( - secret: UByteArray, - instant: Instant, - otp: String, - ): Boolean { + fun verify(secret: ByteArray, instant: Instant, otp: String): Boolean { return generate(secret, instant) == otp } - fun generate( - secret: UByteArray, - instant: Instant, - ): String { + fun generate(secret: ByteArray, instant: Instant): String { return hotp.generate(secret, counter(instant)) } - private fun counter( - instant: Instant, - ): Long { + private fun counter(instant: Instant): Long { val ms = instant.toEpochMilliseconds() val d = period.inWholeMilliseconds return ms / d diff --git a/src/commonMain/kotlin/com/infendro/otp/util/Math.kt b/src/commonMain/kotlin/com/infendro/otp/util/Math.kt new file mode 100644 index 0000000..4562bca --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/otp/util/Math.kt @@ -0,0 +1,6 @@ +package com.infendro.otp.util + +import kotlin.math.pow + +internal fun Int.pow(x: Int): Int = + this.toDouble().pow(x).toInt() From 59f7d925a350716ae383160fbacd4c2dd46b943b Mon Sep 17 00:00:00 2001 From: Infendro Date: Thu, 29 Jan 2026 18:15:06 +0100 Subject: [PATCH 4/7] split ci --- .gitea/workflows/ci.yaml | 92 ----------------------------------- .gitea/workflows/publish.yaml | 47 ++++++++++++++++++ .gitea/workflows/test.yaml | 28 +++++++++++ 3 files changed, 75 insertions(+), 92 deletions(-) delete mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/publish.yaml create mode 100644 .gitea/workflows/test.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml deleted file mode 100644 index 2f46684..0000000 --- a/.gitea/workflows/ci.yaml +++ /dev/null @@ -1,92 +0,0 @@ -on: - push: - branches: [ develop, main ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: linux - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - java-version: '24' - distribution: 'temurin' - - - name: Cache Gradle - uses: actions/cache@v4 - with: - key: gradle-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: gradle- - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - - - name: Cache Konan - uses: actions/cache@v4 - with: - key: konan-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: konan- - path: | - ~/.konan/ - - - name: Cache Node - uses: actions/cache@v4 - with: - key: node-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: node- - path: | - ~/.gradle/nodejs - ~/.gradle/yarn - - - name: Test - run: ./gradlew allTests - - publish: - if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} - needs: test - runs-on: linux - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - java-version: '24' - distribution: 'temurin' - - - name: Cache Gradle - uses: actions/cache@v4 - with: - key: gradle-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: gradle- - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - - - name: Cache Konan - uses: actions/cache@v4 - with: - key: konan-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: konan- - path: | - ~/.konan/ - - - name: Cache Node - uses: actions/cache@v4 - with: - key: node-${{ hashFiles('**/libs.versions.toml') }} - restore-keys: node- - path: | - ~/.gradle/nodejs - ~/.gradle/yarn - - - name: Publish - run: ./gradlew publish - env: - INFENDRO__TOKEN: ${{ secrets.TOKEN }} diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml new file mode 100644 index 0000000..dad1306 --- /dev/null +++ b/.gitea/workflows/publish.yaml @@ -0,0 +1,47 @@ +on: + push: + branches: [ main ] + +jobs: + publish: + runs-on: linux + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '24' + distribution: 'temurin' + + - name: Cache Gradle + uses: actions/cache@v4 + with: + key: gradle-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: gradle + path: | + ~/.gradle/caches/ + ~/.gradle/wrapper/ + + - name: Cache Konan + uses: actions/cache@v4 + with: + key: konan-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: konan + path: | + ~/.konan/ + + - name: Cache Node + uses: actions/cache@v4 + with: + key: node-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: node + path: | + ~/.gradle/nodejs + ~/.gradle/yarn + + - name: Publish + run: ./gradlew publish + env: + INFENDRO__TOKEN: ${{ secrets.TOKEN }} diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml new file mode 100644 index 0000000..bce9e35 --- /dev/null +++ b/.gitea/workflows/test.yaml @@ -0,0 +1,28 @@ +on: + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: linux + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '24' + distribution: 'temurin' + + - name: Cache Gradle + uses: actions/cache@v4 + with: + key: gradle-${{ hashFiles('**/libs.versions.toml') }} + restore-keys: gradle + path: | + ~/.gradle/caches/ + ~/.gradle/wrapper/ + + - name: Test + run: ./gradlew jvmTest From f5f073168d35e9030e1d38bf38455965d742e7b7 Mon Sep 17 00:00:00 2001 From: Infendro Date: Fri, 30 Jan 2026 13:23:32 +0100 Subject: [PATCH 5/7] minor refactor --- src/commonMain/kotlin/com/infendro/otp/HOTP.kt | 8 ++++---- src/commonMain/kotlin/com/infendro/otp/TOTP.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/com/infendro/otp/HOTP.kt b/src/commonMain/kotlin/com/infendro/otp/HOTP.kt index ce240ae..687e694 100644 --- a/src/commonMain/kotlin/com/infendro/otp/HOTP.kt +++ b/src/commonMain/kotlin/com/infendro/otp/HOTP.kt @@ -14,10 +14,6 @@ class HOTP( ) { private val hmac = HMAC(function) - fun verify(secret: ByteArray, counter: Long, otp: String): Boolean { - return generate(secret, counter) == otp - } - fun generate(secret: ByteArray, counter: Long): String { require(counter >= 0) return otp(secret, counter) @@ -29,4 +25,8 @@ class HOTP( val otp = (hash.toInt(offset) and 0x7FFFFFFF) % 10.pow(length) return "$otp".padStart(length, '0') } + + fun verify(secret: ByteArray, counter: Long, otp: String): Boolean { + return generate(secret, counter) == otp + } } diff --git a/src/commonMain/kotlin/com/infendro/otp/TOTP.kt b/src/commonMain/kotlin/com/infendro/otp/TOTP.kt index 2a81438..1312534 100644 --- a/src/commonMain/kotlin/com/infendro/otp/TOTP.kt +++ b/src/commonMain/kotlin/com/infendro/otp/TOTP.kt @@ -13,10 +13,6 @@ class TOTP( ) { private val hotp = HOTP(function, length) - fun verify(secret: ByteArray, instant: Instant, otp: String): Boolean { - return generate(secret, instant) == otp - } - fun generate(secret: ByteArray, instant: Instant): String { return hotp.generate(secret, counter(instant)) } @@ -26,4 +22,8 @@ class TOTP( val d = period.inWholeMilliseconds return ms / d } + + fun verify(secret: ByteArray, instant: Instant, otp: String): Boolean { + return generate(secret, instant) == otp + } } From 733d6337534d1bb78fbdd887fcc32f626fab9f3f Mon Sep 17 00:00:00 2001 From: Infendro Date: Fri, 30 Jan 2026 13:30:56 +0100 Subject: [PATCH 6/7] refactor README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 43bd150..332cc31 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ fun main() { ) // generate and verify OTP - val now = System.now() - val otp = totp.generate(, now) - totp.verify(, now, otp) + val secret = "...".encodeToByteArray() + val time = System.now() + val otp = totp.generate(secret, time) + totp.verify(secret, time, otp) } ``` From e43fc4de86532e3d3075abbf5ee45b5a58e7dcbf Mon Sep 17 00:00:00 2001 From: Infendro Date: Mon, 16 Feb 2026 19:17:19 +0100 Subject: [PATCH 7/7] Upgrade dependencies --- build.gradle.kts | 8 ++------ gradle/libs.versions.toml | 10 +++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 011f4d8..267fc8f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,15 +31,11 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.mac) - implementation(libs.hash) implementation(libs.bytes) + implementation(libs.hash) + implementation(libs.mac) } } - - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") - } } publishing.repositories { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14aa349..4215199 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] infendro = "1.1.0" kotlin = "2.3.0" -mac = "1.3.0" -hash = "1.5.1" -bytes = "1.3.3" +bytes = "1.4.0" +hash = "1.6.0" +mac = "1.4.0" [plugins] infendro = { id = "com.infendro.plugin", version.ref = "infendro" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } [libraries] -mac = { module = "com.infendro:mac", version.ref = "mac" } -hash = { module = "com.infendro:hash", version.ref = "hash" } bytes = { module = "com.infendro:bytes", version.ref = "bytes" } +hash = { module = "com.infendro:hash", version.ref = "hash" } +mac = { module = "com.infendro:mac", version.ref = "mac" }