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
- 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(<secret>, now)
totp.verify(<secret>, now, otp)
}
```

View File

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

View File

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

View File

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

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

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