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/.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 diff --git a/README.md b/README.md index 4c8828a..d46eb45 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,10 @@ # kdf-kt -`kdf-kt` is a Kotlin Multiplatform library that provides common key derivation functions. +`kdf-kt` is a Kotlin Multiplatform library that provides common [Key Derivation Functions](https://en.wikipedia.org/wiki/Key_derivation_function). -Supported functions: +Supported algorithms: - PBKDF2 -## Targets - -| Target | Status | -|------------------|---------------| -| JVM | Supported | -| JavaScript | Supported | -| Native (Linux) | Supported | -| Native (Windows) | Not Supported | - ## Installation Add the following to your `build.gradle.kts`. @@ -24,24 +15,21 @@ repositories { } dependencies { - implementation("com.infendro:kdf:1.1.0") + implementation("com.infendro:kdf:1.2.0") } ``` ## Usage ```kotlin -import com.infendro.hash.`SHA-256` -import com.infendro.kdf.PBKDF2 - fun main() { - // create a PBKDF2 instance using HMAC SHA256 + // create a PBKDF2 instance using HMAC SHA-256 // this example uses 32 bytes of length and 100_000 iterations val kdf = PBKDF2(32, 100_000, `SHA-256`) // securely hash value with some salt - val value = "password".encodeToByteArray().asUByteArray() - val salt = "salt".encodeToByteArray().asUByteArray() + val value = "password".encodeToByteArray() + val salt = "salt".encodeToByteArray() val hash = kdf.hash(value, salt) } ``` diff --git a/build.gradle.kts b/build.gradle.kts index ae03251..2c3eb7b 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.1.0" +version = "1.2.0" plugins { alias(libs.plugins.infendro) @@ -12,10 +15,14 @@ plugins { kotlin { jvm() - js { - nodejs() - } linuxX64() + linuxArm64() + mingwX64() + macosX64() + macosArm64() + js { nodejs() } + wasmJs { nodejs() } + wasmWasi { nodejs() } repositories { infendro() @@ -24,18 +31,14 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.mac) + implementation(libs.bytes) implementation(libs.hash) - implementation(libs.bytearray) + implementation(libs.mac) } commonTest.dependencies { implementation(libs.test) } } - - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes") - } } publishing.repositories { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06180ff..3dd3ae9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -infendro = "1.0.0" -kotlin = "2.2.21" -mac = "1.2.0" -hash = "1.3.2" -bytearray = "1.2.4" +infendro = "1.1.0" +kotlin = "2.3.0" +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" } +bytes = { module = "com.infendro:bytes", version.ref = "bytes" } hash = { module = "com.infendro:hash", version.ref = "hash" } -bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" } +mac = { module = "com.infendro:mac", version.ref = "mac" } test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/src/commonMain/kotlin/com/infendro/kdf/KDF.kt b/src/commonMain/kotlin/com/infendro/kdf/KDF.kt index 50c31fe..96870b8 100644 --- a/src/commonMain/kotlin/com/infendro/kdf/KDF.kt +++ b/src/commonMain/kotlin/com/infendro/kdf/KDF.kt @@ -5,8 +5,11 @@ interface KDF { val bits: Int get() = bytes * 8 - fun hash( - value: UByteArray, - salt: UByteArray, - ): UByteArray + fun hashInto(value: ByteArray, salt: ByteArray, destination: ByteArray, destinationOffset: Int = 0) + + fun hash(value: ByteArray, salt: ByteArray): ByteArray { + val result = ByteArray(bytes) + hashInto(value, salt, result) + return result + } } diff --git a/src/commonMain/kotlin/com/infendro/kdf/PBKDF2.kt b/src/commonMain/kotlin/com/infendro/kdf/PBKDF2.kt index c09a68a..a44b6a6 100644 --- a/src/commonMain/kotlin/com/infendro/kdf/PBKDF2.kt +++ b/src/commonMain/kotlin/com/infendro/kdf/PBKDF2.kt @@ -1,10 +1,10 @@ package com.infendro.kdf -import com.infendro.bytearray.toUByteArray -import com.infendro.bytearray.xor +import com.infendro.bytes.bytearray.xorInto +import com.infendro.bytes.int.copyInto import com.infendro.hash.HashFunction +import com.infendro.kdf.util.ceilDiv import com.infendro.mac.HMAC -import kotlin.math.min class PBKDF2( override val bytes: Int, @@ -16,34 +16,23 @@ class PBKDF2( require(iterations > 0) } - private val mac = HMAC(function) + private val hmac = HMAC(function) - override fun hash( - value: UByteArray, - salt: UByteArray, - ): UByteArray { - return buildList { - var i = 1 - while (size < bytes) { - addAll( - f(value, salt, i) - .sliceArray(0.. acc xor bytes } + } } } diff --git a/src/commonMain/kotlin/com/infendro/kdf/util/Math.kt b/src/commonMain/kotlin/com/infendro/kdf/util/Math.kt new file mode 100644 index 0000000..3af28df --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/kdf/util/Math.kt @@ -0,0 +1,4 @@ +package com.infendro.kdf.util + +internal fun Int.ceilDiv(n: Int): Int = + (this + n - 1) / n diff --git a/src/commonTest/kotlin/com/infendro/kdf/PBKDF2.kt b/src/commonTest/kotlin/com/infendro/kdf/PBKDF2.kt index 7059848..ea32483 100644 --- a/src/commonTest/kotlin/com/infendro/kdf/PBKDF2.kt +++ b/src/commonTest/kotlin/com/infendro/kdf/PBKDF2.kt @@ -1,13 +1,12 @@ package com.infendro.kdf -import com.infendro.bytearray.encodeToUByteArray import com.infendro.hash.sha2.`SHA-256` import kotlin.test.Test import kotlin.test.assertContentEquals class `PBKDF2 Test` { @Test - fun `32 bytes, 1 iteration`() { + fun `32 bytes 1 iteration`() { val function = PBKDF2(32, 1, `SHA-256`) val expected = ubyteArrayOf( @@ -15,16 +14,16 @@ class `PBKDF2 Test` { 0x43U, 0xe7U, 0x22U, 0x52U, 0x56U, 0xc4U, 0xf8U, 0x37U, 0xa8U, 0x65U, 0x48U, 0xc9U, 0x2cU, 0xccU, 0x35U, 0x48U, 0x08U, 0x05U, 0x98U, 0x7cU, 0xb7U, 0x0bU, 0xe1U, 0x7bU, - ) + ).asByteArray() val actual = function.hash( - "password".encodeToUByteArray(), - "salt".encodeToUByteArray(), + "password".encodeToByteArray(), + "salt".encodeToByteArray(), ) assertContentEquals(expected, actual) } @Test - fun `32 bytes, 2 iterations`() { + fun `32 bytes 2 iterations`() { val function = PBKDF2(32, 2, `SHA-256`) val expected = ubyteArrayOf( @@ -32,16 +31,16 @@ class `PBKDF2 Test` { 0x2dU, 0x0aU, 0xdfU, 0xf9U, 0x28U, 0xf0U, 0x6dU, 0xd0U, 0x2aU, 0x30U, 0x3fU, 0x8eU, 0xf3U, 0xc2U, 0x51U, 0xdfU, 0xd6U, 0xe2U, 0xd8U, 0x5aU, 0x95U, 0x47U, 0x4cU, 0x43U, - ) + ).asByteArray() val actual = function.hash( - "password".encodeToUByteArray(), - "salt".encodeToUByteArray(), + "password".encodeToByteArray(), + "salt".encodeToByteArray(), ) assertContentEquals(expected, actual) } @Test - fun `32 bytes, 4096 iterations`() { + fun `32 bytes 4096 iterations`() { val function = PBKDF2(32, 4096, `SHA-256`) val expected = ubyteArrayOf( @@ -49,16 +48,16 @@ class `PBKDF2 Test` { 0xaaU, 0x53U, 0x0dU, 0xb6U, 0x84U, 0x5cU, 0x4cU, 0x8dU, 0x96U, 0x28U, 0x93U, 0xa0U, 0x01U, 0xceU, 0x4eU, 0x11U, 0xa4U, 0x96U, 0x38U, 0x73U, 0xaaU, 0x98U, 0x13U, 0x4aU, - ) + ).asByteArray() val actual = function.hash( - "password".encodeToUByteArray(), - "salt".encodeToUByteArray(), + "password".encodeToByteArray(), + "salt".encodeToByteArray(), ) assertContentEquals(expected, actual) } @Test - fun `40 bytes, 4096 iterations`() { + fun `40 bytes 4096 iterations`() { val function = PBKDF2(40, 4096, `SHA-256`) val expected = ubyteArrayOf( @@ -67,16 +66,16 @@ class `PBKDF2 Test` { 0x96U, 0x28U, 0x93U, 0xa0U, 0x01U, 0xceU, 0x4eU, 0x11U, 0xa4U, 0x96U, 0x38U, 0x73U, 0xaaU, 0x98U, 0x13U, 0x4aU, 0xf7U, 0xadU, 0x98U, 0xc1U, 0xb4U, 0x58U, 0xceU, 0x3fU, - ) + ).asByteArray() val actual = function.hash( - "password".encodeToUByteArray(), - "salt".encodeToUByteArray(), + "password".encodeToByteArray(), + "salt".encodeToByteArray(), ) assertContentEquals(expected, actual) } @Test - fun `64 bytes, 1 iteration`() { + fun `64 bytes 1 iteration`() { val function = PBKDF2(64, 1, `SHA-256`) val expected = ubyteArrayOf( @@ -88,16 +87,16 @@ class `PBKDF2 Test` { 0x99U, 0x16U, 0x64U, 0xb3U, 0x9dU, 0x77U, 0xefU, 0x31U, 0x7cU, 0x71U, 0xb8U, 0x45U, 0xb1U, 0xe3U, 0x0bU, 0xd5U, 0x09U, 0x11U, 0x20U, 0x41U, 0xd3U, 0xa1U, 0x97U, 0x83U, - ) + ).asByteArray() val actual = function.hash( - "passwd".encodeToUByteArray(), - "salt".encodeToUByteArray(), + "passwd".encodeToByteArray(), + "salt".encodeToByteArray(), ) assertContentEquals(expected, actual) } @Test - fun `64 bytes, 80,000 iterations`() { + fun `64 bytes 80000 iterations`() { val function = PBKDF2(64, 80_000, `SHA-256`) val expected = ubyteArrayOf( @@ -109,10 +108,10 @@ class `PBKDF2 Test` { 0x9aU, 0xdbU, 0x84U, 0x1bU, 0x51U, 0xc9U, 0xb3U, 0x17U, 0x6aU, 0x27U, 0x2bU, 0xdeU, 0xbbU, 0xa1U, 0xd0U, 0x78U, 0x47U, 0x8fU, 0x62U, 0xb3U, 0x97U, 0xf3U, 0x3cU, 0x8dU, - ) + ).asByteArray() val actual = function.hash( - "Password".encodeToByteArray().toUByteArray(), - "NaCl".encodeToByteArray().toUByteArray() + "Password".encodeToByteArray(), + "NaCl".encodeToByteArray(), ) assertContentEquals(expected, actual) }