Compare commits

..

23 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
8870703608 replace IllegalArgumentException with require 2025-12-14 00:16:09 +01:00
25e6fcdeab update README
bump version
2025-12-13 23:29:47 +01:00
62cbe2de3b bump version to 1.2.2
All checks were successful
/ publish (push) Successful in 3m20s
2025-12-13 23:28:41 +01:00
52f0ab0661 update README 2025-12-13 23:27:49 +01:00
e5bb1f5a44 bump version to 1.2.1
All checks were successful
/ publish (push) Successful in 3m34s
2025-12-13 02:35:09 +01:00
62d5438b3b use UByteArray for SecretGenerator 2025-12-13 02:28:53 +01:00
aa686cd81e Merge remote-tracking branch 'origin/main'
All checks were successful
/ publish (push) Successful in 3m22s
2025-12-13 02:12:49 +01:00
49acfddde7 bump version to 1.2.0
Some checks failed
/ publish (push) Failing after 59s
2025-12-13 01:45:23 +01:00
498a5b5826 update dependencies, use common plugin 2025-12-13 01:45:23 +01:00
e8999ae4e4 update README
fix typo
2025-10-22 09:27:42 +00:00
11 changed files with 158 additions and 201 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,16 +1,10 @@
# Kotlin OTP # otp.kt
This library provides various [One-Time Password](https://en.wikipedia.org/wiki/One-time_password) `otp.kt` is a Kotlin Multiplatform library that provides common [One-Time Password](https://en.wikipedia.org/wiki/One-time_password) algorithms.
implementations in Kotlin Multiplatform.
## Features Supported algorithms:
- HOTP
* [HTOP](https://en.wikipedia.org/wiki/HMAC-based_one-time_password) - TOTP
* [TOTP](https://en.wikipedia.org/wiki/Time-based_one-time_password)
* Multiplatform support
* JVM
* JavaScript
* Native (Linux)
## Installation ## Installation
@@ -22,34 +16,25 @@ repositories {
} }
dependencies { dependencies {
implementation("com.infendro:otp:1.1.1") implementation("com.infendro:otp:1.3.1")
} }
``` ```
## Usage ## Usage
```kotlin ```kotlin
import com.infendro.hash.SHA1
import com.infendro.otp.SecretGenerator
import com.infendro.otp.TOTP
import kotlin.time.Clock.System.now
import kotlin.time.Duration.Companion.seconds
fun main() { fun main() {
// create a TOTP instance using SHA1, a length of 6, and a period of 30 seconds // create TOTP instance
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 = SecretGenerator.generate(SHA1) 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,62 +1,43 @@
group = "com.infendro" @file:OptIn(ExperimentalWasmDsl::class)
version = "1.1.1"
repositories { import com.infendro.plugin.infendro
maven("https://git.infendro.com/api/packages/Infendro/maven") import com.infendro.plugin.token
mavenCentral() import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
}
group = "com.infendro"
version = "1.3.1"
plugins { plugins {
alias(libs.plugins.infendro)
alias(libs.plugins.multiplatform) alias(libs.plugins.multiplatform)
id("maven-publish") id("maven-publish")
} }
kotlin { kotlin {
jvm() jvm()
js {
nodejs()
}
linuxX64() linuxX64()
linuxArm64()
mingwX64()
macosX64()
macosArm64()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
repositories {
infendro()
mavenCentral()
}
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation(libs.random) implementation(libs.bytes)
implementation(libs.mac)
implementation(libs.hash) implementation(libs.hash)
implementation(libs.bytearray) implementation(libs.mac)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
publishing {
repositories {
maven {
url = uri("https://git.infendro.com/api/packages/Infendro/maven")
authentication.create("header", HttpHeaderAuthentication::class)
credentials(HttpHeaderCredentials::class) {
val token = secret("git.token") ?: throw GradleException()
name = "Authorization"
value = "token $token"
}
} }
} }
} }
fun secret( publishing.repositories {
key: String, infendro(token)
): String? {
val property = key
val environment = key
.uppercase()
.replace(".", "__")
val prop = properties[property] as String?
val env = System.getenv(environment)
return prop ?: env
} }

View File

@@ -1,15 +1,15 @@
[versions] [versions]
kotlin = "2.1.20" infendro = "1.1.0"
random = "1.0.0" kotlin = "2.3.0"
mac = "1.1.2" bytes = "1.4.2"
hash = "1.2.2" hash = "1.7.0"
bytearray = "1.1.1" mac = "1.5.0"
[plugins] [plugins]
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]
random = { module = "com.infendro:random", version.ref = "random" } 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 +1,6 @@
rootProject.name = "otp" rootProject.name = "otp"
pluginManagement.repositories {
maven("https://git.infendro.com/api/packages/Infendro/maven")
mavenCentral()
}

View File

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

View File

@@ -1,22 +0,0 @@
package com.infendro.otp
import com.infendro.hash.*
import com.infendro.random.PRNG
object SecretGenerator {
private val random = PRNG.HMAC
fun generate(
algorithm: HashFunction = SHA1,
): ByteArray {
val bytes = when (algorithm) {
MD5 -> 16
SHA1 -> 20
SHA224 -> 28
SHA256 -> 32
SHA384 -> 48
SHA512 -> 64
}
return random.nextBytes(bytes)
}
}

View File

@@ -1,39 +1,27 @@
package com.infendro.otp package com.infendro.otp
import com.infendro.hash.HashFunction import com.infendro.hash.HashFunction
import com.infendro.hash.SHA1 import com.infendro.hash.sha1.SHA1
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds 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,
val period: Duration = 30.seconds, val period: Duration = 30.seconds,
) { ) {
val hotp = HOTP(function, length) private val hotp = HOTP(function, length)
fun verify( fun generate(secret: ByteArray, instant: Instant): String {
secret: ByteArray, return hotp.generate(secret, counter(instant))
instant: Instant, }
otp: String,
): Boolean { fun verify(secret: ByteArray, instant: Instant, otp: String): Boolean {
return generate(secret, instant) == otp return generate(secret, instant) == otp
} }
fun generate( private fun counter(instant: Instant): Long {
secret: ByteArray,
instant: Instant,
): String {
return hotp.generate(
secret,
counter(instant)
)
}
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()