1.3.0 release #2

Merged
Infendro merged 7 commits from develop into main 2026-02-16 18:21:00 +00:00
7 changed files with 50 additions and 108 deletions
Showing only changes of commit d2b901a6ff - Show all commits

View File

@@ -6,15 +6,6 @@ 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,24 @@ repositories {
} }
dependencies { dependencies {
implementation("com.infendro:otp:1.2.2") implementation("com.infendro:otp:1.3.0")
} }
``` ```
## 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 now = System.now()
val otp = totp.generate(<secret>, now)
// generate an OTP totp.verify(<secret>, now, otp)
val otp = totp.generate(secret, now())
// 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.0"
plugins { plugins {
alias(libs.plugins.infendro) alias(libs.plugins.infendro)
@@ -13,9 +16,13 @@ plugins {
kotlin { kotlin {
jvm() jvm()
linuxX64() linuxX64()
js { linuxArm64()
nodejs() mingwX64()
} macosX64()
macosArm64()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
repositories { repositories {
infendro() infendro()
@@ -24,15 +31,13 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation(libs.prng)
implementation(libs.mac) implementation(libs.mac)
implementation(libs.hash) implementation(libs.hash)
implementation(libs.bytearray) implementation(libs.bytes)
} }
} }
compilerOptions { compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes")
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
} }
} }

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" mac = "1.3.0"
mac = "1.2.0" hash = "1.5.1"
hash = "1.3.2" bytes = "1.3.3"
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" }
mac = { module = "com.infendro:mac", version.ref = "mac" } 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" } bytes = { module = "com.infendro:bytes", version.ref = "bytes" }

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 verify(secret: ByteArray, counter: Long, otp: String): Boolean {
secret: UByteArray,
counter: Long,
otp: String,
): Boolean {
return generate(secret, counter) == otp return generate(secret, counter) == otp
} }
fun generate( fun generate(secret: ByteArray, counter: Long): String {
secret: UByteArray,
counter: Long,
): String {
require(counter >= 0) require(counter >= 0)
return otp(secret, counter)
val hash = generateHash(secret, counter.toUByteArray())
return otp(hash)
} }
private fun generateHash( private fun otp(secret: ByteArray, counter: Long): String {
secret: UByteArray, val hash = hmac.hash(secret, counter.toByteArray())
value: UByteArray, val offset = (hash.last() and 0xF).toInt()
): UByteArray { val otp = (hash.toInt(offset) and 0x7FFFFFFF) % 10.pow(length)
return hmac.hash(secret, value) return "$otp".padStart(length, '0')
}
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 verify(secret: ByteArray, instant: Instant, otp: String): Boolean {
secret: UByteArray,
instant: Instant,
otp: String,
): Boolean {
return generate(secret, instant) == otp return generate(secret, instant) == otp
} }
fun generate( fun generate(secret: ByteArray, instant: Instant): String {
secret: UByteArray,
instant: Instant,
): String {
return hotp.generate(secret, counter(instant)) return hotp.generate(secret, counter(instant))
} }
private fun counter( private fun counter(instant: Instant): Long {
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()