Compare commits

...

13 Commits

Author SHA1 Message Date
849a0d022f Merge pull request 'infendro-patch-1' (#4) from infendro-patch-1 into develop
Reviewed-on: #4
2026-04-16 16:09:01 +00:00
99ef6f2085 Update README.md 2026-04-16 16:08:13 +00:00
a2051dbd85 Merge pull request '1.3.1 Release' (#3) from develop into main
All checks were successful
/ publish (push) Successful in 3m6s
Reviewed-on: Infendro/otp-kt#3
2026-03-12 10:34:21 +00:00
767665180a Bump version to 1.3.1
All checks were successful
/ test (pull_request) Successful in 41s
2026-03-12 11:26:23 +01:00
8b0f24c6b4 Upgrade dependencies 2026-03-12 11:25:47 +01:00
baf897e9e3 Merge pull request '1.3.0 release' (#2) from develop into main
All checks were successful
/ publish (push) Successful in 2m38s
Reviewed-on: Infendro/otp-kt#2
2026-02-16 18:21:00 +00:00
e43fc4de86 Upgrade dependencies
All checks were successful
/ test (pull_request) Successful in 1m14s
2026-02-16 19:17:19 +01:00
733d633753 refactor README
All checks were successful
/ test (pull_request) Successful in 33s
2026-01-30 13:31:04 +01:00
f5f073168d minor refactor
All checks were successful
/ test (pull_request) Successful in 33s
2026-01-30 13:23:32 +01:00
59f7d925a3 split ci
All checks were successful
/ test (pull_request) Successful in 1m39s
2026-01-29 18:15:06 +01:00
d2b901a6ff upgrade
Some checks failed
/ test (push) Has been cancelled
/ publish (push) Has been cancelled
too tired to write a detailed commit message or split into self-explanatory commits
2026-01-29 18:12:16 +01:00
f85e22311d improve ci
All checks were successful
/ publish (push) Has been skipped
/ test (push) Successful in 3m57s
2026-01-13 18:06:00 +01:00
37bbe35482 update README
small rewording, link wikipedia article
2025-12-14 14:43:30 +01:00
10 changed files with 138 additions and 160 deletions

View File

@@ -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/

View File

@@ -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 }}

View File

@@ -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

View File

@@ -1,20 +1,11 @@
# otp-kt # 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 - HOTP
- TOTP - TOTP
## Targets
| Target | Status |
|------------------|---------------|
| JVM | Supported |
| JavaScript | Supported |
| Native (Linux) | Supported |
| Native (Windows) | Not Supported |
## Installation ## Installation
Add the following to your `build.gradle.kts`. Add the following to your `build.gradle.kts`.
@@ -25,36 +16,25 @@ repositories {
} }
dependencies { dependencies {
implementation("com.infendro:otp:1.2.2") implementation("com.infendro:otp:1.3.1")
} }
``` ```
## Usage ## Usage
```kotlin ```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() { fun main() {
// create a SecretGenerator and TOTP instance // create TOTP instance
val generator = SecretGenerator(CSPRNG.HMAC(SHA1))
val totp = TOTP( val totp = TOTP(
function = SHA1, function = SHA1(),
length = 6, length = 6,
period = 30.seconds, period = 30.seconds,
) )
// generate a secret using SHA1 // generate and verify OTP
val secret = generator.generate() val secret = "...".encodeToByteArray()
val time = System.now()
// generate an OTP val otp = totp.generate(secret, time)
val otp = totp.generate(secret, now()) totp.verify(secret, time, otp)
// verify an OTP
totp.verify(secret, now(), otp)
} }
``` ```

View File

@@ -1,8 +1,11 @@
@file:OptIn(ExperimentalWasmDsl::class)
import com.infendro.plugin.infendro import com.infendro.plugin.infendro
import com.infendro.plugin.token import com.infendro.plugin.token
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
group = "com.infendro" group = "com.infendro"
version = "1.2.2" version = "1.3.1"
plugins { plugins {
alias(libs.plugins.infendro) alias(libs.plugins.infendro)
@@ -12,10 +15,14 @@ plugins {
kotlin { kotlin {
jvm() jvm()
js {
nodejs()
}
linuxX64() linuxX64()
linuxArm64()
mingwX64()
macosX64()
macosArm64()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
repositories { repositories {
infendro() infendro()
@@ -24,17 +31,11 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation(libs.prng) implementation(libs.bytes)
implementation(libs.mac)
implementation(libs.hash) implementation(libs.hash)
implementation(libs.bytearray) implementation(libs.mac)
} }
} }
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes")
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
} }
publishing.repositories { publishing.repositories {

View File

@@ -1,17 +1,15 @@
[versions] [versions]
infendro = "1.0.0" infendro = "1.1.0"
kotlin = "2.2.21" kotlin = "2.3.0"
prng = "1.2.0" bytes = "1.4.2"
mac = "1.2.0" hash = "1.7.0"
hash = "1.3.2" mac = "1.5.0"
bytearray = "1.2.4"
[plugins] [plugins]
infendro = { id = "com.infendro.plugin", version.ref = "infendro" } infendro = { id = "com.infendro.plugin", version.ref = "infendro" }
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
[libraries] [libraries]
prng = { module = "com.infendro:prng", version.ref = "prng" } bytes = { module = "com.infendro:bytes", version.ref = "bytes" }
mac = { module = "com.infendro:mac", version.ref = "mac" }
hash = { module = "com.infendro:hash", version.ref = "hash" } hash = { module = "com.infendro:hash", version.ref = "hash" }
bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" } mac = { module = "com.infendro:mac", version.ref = "mac" }

View File

@@ -1,55 +1,32 @@
package com.infendro.otp package com.infendro.otp
import com.infendro.bytearray.toInt import com.infendro.bytes.bytearray.toInt
import com.infendro.bytearray.toUByteArray import com.infendro.bytes.long.toByteArray
import com.infendro.hash.HashFunction import com.infendro.hash.HashFunction
import com.infendro.hash.sha1.SHA1 import com.infendro.hash.sha1.SHA1
import com.infendro.mac.HMAC import com.infendro.mac.HMAC
import kotlin.math.pow import com.infendro.otp.util.pow
import kotlin.experimental.and
class HOTP( class HOTP(
function: HashFunction = SHA1, val function: HashFunction = SHA1(),
private val length: Int = 6, val length: Int = 6,
) { ) {
private val hmac = HMAC(function) private val hmac = HMAC(function)
fun verify( fun generate(secret: ByteArray, counter: Long): String {
secret: UByteArray, require(counter >= 0)
counter: Long, return otp(secret, counter)
otp: String, }
): Boolean {
fun verify(secret: ByteArray, counter: Long, otp: String): Boolean {
return generate(secret, counter) == otp return generate(secret, counter) == otp
} }
fun generate( private fun otp(secret: ByteArray, counter: Long): String {
secret: UByteArray, val hash = hmac.hash(secret, counter.toByteArray())
counter: Long, val offset = (hash.last() and 0xF).toInt()
): String { val otp = (hash.toInt(offset) and 0x7FFFFFFF) % 10.pow(length)
require(counter >= 0) return "$otp".padStart(length, '0')
val hash = generateHash(secret, counter.toUByteArray())
return otp(hash)
}
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')
} }
} }

View File

@@ -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)
}
}

View File

@@ -7,30 +7,21 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant import kotlin.time.Instant
class TOTP( class TOTP(
function: HashFunction = SHA1, val function: HashFunction = SHA1(),
length: Int = 6, val length: Int = 6,
private val period: Duration = 30.seconds, val period: Duration = 30.seconds,
) { ) {
private val hotp = HOTP(function, length) private val hotp = HOTP(function, length)
fun verify( fun generate(secret: ByteArray, instant: Instant): String {
secret: UByteArray,
instant: Instant,
otp: String,
): Boolean {
return generate(secret, instant) == otp
}
fun generate(
secret: UByteArray,
instant: Instant,
): String {
return hotp.generate(secret, counter(instant)) return hotp.generate(secret, counter(instant))
} }
private fun counter( fun verify(secret: ByteArray, instant: Instant, otp: String): Boolean {
instant: Instant, return generate(secret, instant) == otp
): Long { }
private fun counter(instant: Instant): Long {
val ms = instant.toEpochMilliseconds() val ms = instant.toEpochMilliseconds()
val d = period.inWholeMilliseconds val d = period.inWholeMilliseconds
return ms / d return ms / d

View File

@@ -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()