From 501ff715e5553e4e414715fd0e02d1c9544f2efe Mon Sep 17 00:00:00 2001 From: Behzod Halil Date: Wed, 31 Jan 2024 17:58:55 +0900 Subject: [PATCH] feat: Add crypto module (#183) --- anycrypto/cipher/build.gradle.kts | 48 -- crypto/cipher/build.gradle.kts | 65 +++ .../kotlin/io/spherelabs/crypto/cipher/AES.kt | 217 +++++++ .../io/spherelabs/crypto/cipher/Argon2.kt | 542 ++++++++++++++++++ .../io/spherelabs/crypto/cipher/ArrayCopy.kt | 109 ++++ .../io/spherelabs/crypto/cipher/Blake2b.kt | 384 +++++++++++++ .../io/spherelabs/crypto/cipher/ChaCha.kt | 92 +++ .../io/spherelabs/crypto/cipher/Chacha7537.kt | 73 +++ .../io/spherelabs/crypto/cipher/Cipher.kt | 22 + .../io/spherelabs/crypto/cipher/CipherMode.kt | 222 +++++++ .../spherelabs/crypto/cipher/CipherPadding.kt | 86 +++ .../io/spherelabs/crypto/cipher/Salsa20.kt | 451 +++++++++++++++ .../cipher/src/commonTest/kotlin/AESTest.kt | 47 ++ .../src/commonTest/kotlin/Argon2Test.kt | 43 ++ crypto/digest/build.gradle.kts | 62 ++ .../spherelabs/crypto/hash/Digest.android.kt | 8 + .../io/spherelabs/crypto/hash/Algorithm.kt | 6 + .../io/spherelabs/crypto/hash/Digest.kt | 20 + .../io/spherelabs/crypto/hash/Sha256.kt | 303 ++++++++++ .../io/spherelabs/crypto/hash/Sha512.kt | 340 +++++++++++ .../kotlin/io/spherelabs/crypto/hash/Utils.kt | 10 + .../src/commonTest/kotlin/Sha256Test.kt | 43 ++ .../src/commonTest/kotlin/Sha512Test.kt | 42 ++ .../io/spherelabs/crypto/hash/Digest.ios.kt | 8 + crypto/rsa/build.gradle.kts | 61 ++ .../io/spherelabs/crypto/rsa/Platform.kt | 7 + .../io/spherelabs/crypto/rsa/Greeting.kt | 9 + .../io/spherelabs/crypto/rsa/Platform.kt | 7 + .../io/spherelabs/crypto/rsa/Platform.kt | 9 + .../secure-random/build.gradle.kts | 0 .../securerandom/SecureRandom.android.kt | 0 .../anycrypto/securerandom/SecureRandom.kt | 0 .../src/commonTest/kotlin/SecureRandomTest.kt | 0 .../securerandom/SecureRandom.ios.kt | 0 crypto/uuid/build.gradle.kts | 61 ++ .../io/spherelabs/crypto/uuid/uuid.android.kt | 29 + .../kotlin/io/spherelabs/crypto/uuid/uuid.kt | 19 + crypto/uuid/src/commonTest/kotlin/UuidTest.kt | 28 + .../io/spherelabs/crypto/uuid/uuid.ios.kt | 23 + settings.gradle.kts | 11 +- tinypass/build.gradle.kts | 65 +++ .../tinypass/database/signature/Signature.kt | 36 ++ 42 files changed, 3557 insertions(+), 51 deletions(-) delete mode 100644 anycrypto/cipher/build.gradle.kts create mode 100644 crypto/cipher/build.gradle.kts create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/AES.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Argon2.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ArrayCopy.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Blake2b.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ChaCha.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Chacha7537.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Cipher.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherMode.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherPadding.kt create mode 100644 crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Salsa20.kt create mode 100644 crypto/cipher/src/commonTest/kotlin/AESTest.kt create mode 100644 crypto/cipher/src/commonTest/kotlin/Argon2Test.kt create mode 100644 crypto/digest/build.gradle.kts create mode 100644 crypto/digest/src/androidMain/kotlin/io/spherelabs/crypto/hash/Digest.android.kt create mode 100644 crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Algorithm.kt create mode 100644 crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Digest.kt create mode 100644 crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha256.kt create mode 100644 crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha512.kt create mode 100644 crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Utils.kt create mode 100644 crypto/digest/src/commonTest/kotlin/Sha256Test.kt create mode 100644 crypto/digest/src/commonTest/kotlin/Sha512Test.kt create mode 100644 crypto/digest/src/iosMain/kotlin/io/spherelabs/crypto/hash/Digest.ios.kt create mode 100644 crypto/rsa/build.gradle.kts create mode 100644 crypto/rsa/src/androidMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt create mode 100644 crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Greeting.kt create mode 100644 crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt create mode 100644 crypto/rsa/src/iosMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt rename {anycrypto => crypto}/secure-random/build.gradle.kts (100%) rename {anycrypto => crypto}/secure-random/src/androidMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.android.kt (100%) rename {anycrypto => crypto}/secure-random/src/commonMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.kt (100%) rename {anycrypto => crypto}/secure-random/src/commonTest/kotlin/SecureRandomTest.kt (100%) rename {anycrypto => crypto}/secure-random/src/iosMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.ios.kt (100%) create mode 100644 crypto/uuid/build.gradle.kts create mode 100644 crypto/uuid/src/androidMain/kotlin/io/spherelabs/crypto/uuid/uuid.android.kt create mode 100644 crypto/uuid/src/commonMain/kotlin/io/spherelabs/crypto/uuid/uuid.kt create mode 100644 crypto/uuid/src/commonTest/kotlin/UuidTest.kt create mode 100644 crypto/uuid/src/iosMain/kotlin/io/spherelabs/crypto/uuid/uuid.ios.kt create mode 100644 tinypass/build.gradle.kts create mode 100644 tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/signature/Signature.kt diff --git a/anycrypto/cipher/build.gradle.kts b/anycrypto/cipher/build.gradle.kts deleted file mode 100644 index b30bdcc7..00000000 --- a/anycrypto/cipher/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -plugins { - kotlin("multiplatform") - id("com.android.library") -} - -@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) -kotlin { - targetHierarchy.default() - - android { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "cipher" - } - } - - sourceSets { - val commonMain by getting { - dependencies { - //put your multiplatform dependencies here - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } - } -} - -android { - namespace = "io.spherelabs.cipher" - compileSdk = 33 - defaultConfig { - minSdk = 24 - } -} \ No newline at end of file diff --git a/crypto/cipher/build.gradle.kts b/crypto/cipher/build.gradle.kts new file mode 100644 index 00000000..5a2ca4f7 --- /dev/null +++ b/crypto/cipher/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "cipher" + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.crypto.secureRandom) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "io.spherelabs.crypto.cipher" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/AES.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/AES.kt new file mode 100644 index 00000000..19993987 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/AES.kt @@ -0,0 +1,217 @@ +package io.spherelabs.crypto.cipher + +/** + * Based on CryptoJS v3.1.2 + * code.google.com/p/crypto-js + * (c) 2009-2013 by Jeff Mott. All rights reserved. + * code.google.com/p/crypto-js/wiki/License + * + * https://github.com/korlibs/korge/blob/main/korlibs-crypto/src/korlibs/crypto/AES.kt + */ +class AES(val keyWords: IntArray) : Cipher { + override val blockSize: Int get() = BLOCK_SIZE + + private val keySize = keyWords.size + private val numRounds = keySize + 6 + private val ksRows = (numRounds + 1) * 4 + private val keySchedule = IntArray(ksRows).apply { + for (ksRow in indices) { + this[ksRow] = when { + ksRow < keySize -> keyWords[ksRow] + else -> { + var t = this[ksRow - 1] + if (0 == (ksRow % keySize)) { + t = (t shl 8) or (t ushr 24) + t = (SBOX[t.ext8(24)] shl 24) or (SBOX[t.ext8(16)] shl 16) or (SBOX[t.ext8(8)] shl 8) or SBOX[t and 0xff] + t = t xor (RCON[(ksRow / keySize) or 0] shl 24) + } else if (keySize > 6 && ksRow % keySize == 4) { + t = (SBOX[t.ext8(24)] shl 24) or (SBOX[t.ext8(16)] shl 16) or (SBOX[t.ext8(8)] shl 8) or SBOX[t and 0xff] + } + this[ksRow - keySize] xor t + } + } + } + } + private val invKeySchedule = IntArray(ksRows).apply { + for (invKsRow in indices) { + val ksRow = ksRows - invKsRow + val t = if ((invKsRow % 4) != 0) keySchedule[ksRow] else keySchedule[ksRow - 4] + this[invKsRow] = if (invKsRow < 4 || ksRow <= 4) t else INV_SUB_MIX_0[SBOX[t.ext8(24)]] xor INV_SUB_MIX_1[SBOX[t.ext8(16)]] xor INV_SUB_MIX_2[SBOX[t.ext8(8)]] xor INV_SUB_MIX_3[SBOX[t and 0xff]] + } + } + + constructor(key: ByteArray) : this(key.toIntArray()) + + override fun encrypt(data: ByteArray, offset: Int, len: Int) { + for (n in 0 until len step BLOCK_SIZE) encryptBlock(data, offset + n) + } + + override fun decrypt(data: ByteArray, offset: Int, len: Int) { + for (n in 0 until len step BLOCK_SIZE) decryptBlock(data, offset + n) + } + + fun encryptBlock(M: ByteArray, offset: Int) { + this.doCryptBlock(M, offset, this.keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) + } + + fun decryptBlock(M: ByteArray, offset: Int) { + this.doCryptBlock( + M, offset, + this.invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX, + swap13 = true + ) + } + + private fun doCryptBlock( + M: IntArray, offset: Int, keySchedule: IntArray, + SUB_MIX_0: IntArray, SUB_MIX_1: IntArray, SUB_MIX_2: IntArray, SUB_MIX_3: IntArray, SBOX: IntArray, + swap13: Boolean = false + ) { + doCryptBlockInternal(M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX, swap13, + get = { array, o, i -> array[o + i] }, + set = { array, o, i, value -> array[o + i] = value }, + ) + } + + private fun doCryptBlock( + M: ByteArray, offset: Int, keySchedule: IntArray, + SUB_MIX_0: IntArray, SUB_MIX_1: IntArray, SUB_MIX_2: IntArray, SUB_MIX_3: IntArray, SBOX: IntArray, + swap13: Boolean = false + ) { + doCryptBlockInternal(M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX, swap13, + get = { array, o, i -> array.getInt(o + i * 4) }, + set = { array, o, i, value -> array.setInt(o + i * 4, value) }, + ) + } + + private inline fun doCryptBlockInternal( + M: T, offset: Int, keySchedule: IntArray, + SUB_MIX_0: IntArray, SUB_MIX_1: IntArray, SUB_MIX_2: IntArray, SUB_MIX_3: IntArray, SBOX: IntArray, + swap13: Boolean = false, + get: (M: T, offset: Int, index: Int) -> Int, + set: (M: T, offset: Int, index: Int, value: Int) -> Unit, + ) { + val O1 = if (!swap13) 1 else 3 + val O3 = if (!swap13) 3 else 1 + var s0 = get(M, offset, 0) xor keySchedule[0] + var s1 = get(M, offset, O1) xor keySchedule[1] + var s2 = get(M, offset, 2) xor keySchedule[2] + var s3 = get(M, offset, O3) xor keySchedule[3] + var ksRow = 4 + + for (round in 1 until numRounds) { + val t0 = SUB_MIX_0[s0.ext8(24)] xor SUB_MIX_1[s1.ext8(16)] xor SUB_MIX_2[s2.ext8(8)] xor SUB_MIX_3[s3.ext8(0)] xor keySchedule[ksRow++] + val t1 = SUB_MIX_0[s1.ext8(24)] xor SUB_MIX_1[s2.ext8(16)] xor SUB_MIX_2[s3.ext8(8)] xor SUB_MIX_3[s0.ext8(0)] xor keySchedule[ksRow++] + val t2 = SUB_MIX_0[s2.ext8(24)] xor SUB_MIX_1[s3.ext8(16)] xor SUB_MIX_2[s0.ext8(8)] xor SUB_MIX_3[s1.ext8(0)] xor keySchedule[ksRow++] + val t3 = SUB_MIX_0[s3.ext8(24)] xor SUB_MIX_1[s0.ext8(16)] xor SUB_MIX_2[s1.ext8(8)] xor SUB_MIX_3[s2.ext8(0)] xor keySchedule[ksRow++] + s0 = t0; s1 = t1; s2 = t2; s3 = t3 + } + + val t0 = ((SBOX[s0.ext8(24)] shl 24) or (SBOX[s1.ext8(16)] shl 16) or (SBOX[s2.ext8(8)] shl 8) or SBOX[s3.ext8(0)]) xor keySchedule[ksRow++] + val t1 = ((SBOX[s1.ext8(24)] shl 24) or (SBOX[s2.ext8(16)] shl 16) or (SBOX[s3.ext8(8)] shl 8) or SBOX[s0.ext8(0)]) xor keySchedule[ksRow++] + val t2 = ((SBOX[s2.ext8(24)] shl 24) or (SBOX[s3.ext8(16)] shl 16) or (SBOX[s0.ext8(8)] shl 8) or SBOX[s1.ext8(0)]) xor keySchedule[ksRow++] + val t3 = ((SBOX[s3.ext8(24)] shl 24) or (SBOX[s0.ext8(16)] shl 16) or (SBOX[s1.ext8(8)] shl 8) or SBOX[s2.ext8(0)]) xor keySchedule[ksRow++] + + set(M, offset, 0, t0) + set(M, offset, O1, t1) + set(M, offset, 2, t2) + set(M, offset, O3, t3) + } + + companion object { + private val SBOX = IntArray(256) + private val INV_SBOX = IntArray(256) + private val SUB_MIX_0 = IntArray(256) + private val SUB_MIX_1 = IntArray(256) + private val SUB_MIX_2 = IntArray(256) + private val SUB_MIX_3 = IntArray(256) + private val INV_SUB_MIX_0 = IntArray(256) + private val INV_SUB_MIX_1 = IntArray(256) + private val INV_SUB_MIX_2 = IntArray(256) + private val INV_SUB_MIX_3 = IntArray(256) + private val RCON = intArrayOf(0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36) + + private const val BLOCK_SIZE = 16 + + init { + val d = IntArray(256) { if (it >= 128) (it shl 1) xor 0x11b else (it shl 1) } + var x = 0 + var xi = 0 + for (i in 0 until 256) { + var sx = xi xor (xi shl 1) xor (xi shl 2) xor (xi shl 3) xor (xi shl 4) + sx = (sx ushr 8) xor (sx and 0xff) xor 0x63 + SBOX[x] = sx + INV_SBOX[sx] = x + val x2 = d[x] + val x4 = d[x2] + val x8 = d[x4] + ((d[sx] * 0x101) xor (sx * 0x1010100)).also { t -> + SUB_MIX_0[x] = (t shl 24) or (t ushr 8) + SUB_MIX_1[x] = (t shl 16) or (t ushr 16) + SUB_MIX_2[x] = (t shl 8) or (t ushr 24) + SUB_MIX_3[x] = (t shl 0) + } + ((x8 * 0x1010101) xor (x4 * 0x10001) xor (x2 * 0x101) xor (x * 0x1010100)).also { t -> + INV_SUB_MIX_0[sx] = (t shl 24) or (t ushr 8) + INV_SUB_MIX_1[sx] = (t shl 16) or (t ushr 16) + INV_SUB_MIX_2[sx] = (t shl 8) or (t ushr 24) + INV_SUB_MIX_3[sx] = (t shl 0) + } + + if (x == 0) { + x = 1; xi = 1 + } else { + x = x2 xor d[d[d[x8 xor x2]]] + xi = xi xor d[d[xi]] + } + } + } + + fun encryptAesEcb(data: ByteArray, key: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.ECB, padding].encrypt(data) + + fun decryptAesEcb(data: ByteArray, key: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.ECB, padding].decrypt(data) + + fun encryptAesCbc(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CBC, padding, iv].encrypt(data) + + fun decryptAesCbc(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CBC, padding, iv].decrypt(data) + + fun encryptAesPcbc(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.PCBC, padding, iv].encrypt(data) + + fun decryptAesPcbc(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.PCBC, padding, iv].decrypt(data) + + fun encryptAesCfb(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CFB, padding, iv].encrypt(data) + + fun decryptAesCfb(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CFB, padding, iv].decrypt(data) + + fun encryptAesOfb(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.OFB, padding, iv].encrypt(data) + + fun decryptAesOfb(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.OFB, padding, iv].decrypt(data) + + fun encryptAesCtr(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CTR, padding, iv].encrypt(data) + + fun decryptAesCtr(data: ByteArray, key: ByteArray, iv: ByteArray, padding: Padding): ByteArray = + AES(key)[CipherMode.CTR, padding, iv].decrypt(data) + } +} + +private fun ByteArray.getu(offset: Int): Int = (this[offset].toInt() and 0xFF) +private inline fun Int.ext8(offset: Int): Int = (this ushr offset) and 0xFF +private fun ByteArray.toIntArray(): IntArray = IntArray(size / 4).also { for (n in it.indices) it[n] = getInt(n * 4) } +private fun ByteArray.getInt(offset: Int): Int = (getu(offset + 0) shl 24) or (getu(offset + 1) shl 16) or (getu(offset + 2) shl 8) or (getu(offset + 3) shl 0) +private fun ByteArray.setInt(offset: Int, value: Int) { + this[offset + 0] = ((value shr 24) and 0xFF).toByte() + this[offset + 1] = ((value shr 16) and 0xFF).toByte() + this[offset + 2] = ((value shr 8) and 0xFF).toByte() + this[offset + 3] = ((value shr 0) and 0xFF).toByte() +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Argon2.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Argon2.kt new file mode 100644 index 00000000..2ab1fe04 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Argon2.kt @@ -0,0 +1,542 @@ +package io.spherelabs.crypto.cipher + + +private const val Argon2BlockSize = 1024 +private const val Argon2QwordsInBlock = Argon2BlockSize / 8 +private const val Argon2AddressesInBlock = 128 +private const val Argon2PreHashDigestLength = 64 +private const val Argon2PreHashSeedLength = 72 +private const val Argon2SyncPoints = 4 + +// Minimum and maximum digest size in bytes +private const val MinOutLen = 4 + +// Minimum and maximum number of passes +private const val M32L = 0xFFFFFFFFL + +private val ZeroBytes = ByteArray(4) + +internal class Argon2( + private val type: Type = Type.Argon2D, + private val version: Version = Version.Ver13, + private val salt: ByteArray, + private val secret: ByteArray? = null, + private val additional: ByteArray? = null, + private val iterations: Int = 3, + private val parallelism: Int = 1, + private val memory: Int, +) { + private val blocks: Array + private var segmentLength = 0 + private var laneLength = 0 + + enum class Type(val id: Int) { + Argon2D(0x00), + Argon2I(0x01), + Argon2Id(0x02) + } + + enum class Version(val id: Int) { + Ver10(0x10), + Ver13(0x13); + + companion object { + fun from(id: UInt) = when (id.toInt()) { + Ver13.id -> Ver13 + else -> Ver10 + } + } + } + + init { + /** + * 2. Align memory size + * Minimum memoryBlocks = 8L blocks, where L is the number of lanes + */ + var memoryBlocks = memory + if (memoryBlocks < 2 * Argon2SyncPoints * parallelism) { + memoryBlocks = 2 * Argon2SyncPoints * parallelism + } + segmentLength = memoryBlocks / (parallelism * Argon2SyncPoints) + laneLength = segmentLength * Argon2SyncPoints + + // Ensure that all segments have equal length + memoryBlocks = segmentLength * (parallelism * Argon2SyncPoints) + + blocks = Array(memoryBlocks) { Block() } + } + + fun encrypt( + password: ByteArray, + out: ByteArray, + outOff: Int = 0, + length: Int = out.size, + ): Int { + check(length >= MinOutLen) { "Output length less than $MinOutLen" } + val tmpBlockBytes = ByteArray(Argon2BlockSize) + initialize(tmpBlockBytes, password, length) + fillMemoryBlocks() + digest(tmpBlockBytes, out, outOff, length) + reset() + return length + } + + private fun reset() = blocks.forEach(Block::clear) + + private fun fillMemoryBlocks() { + val filler = FillBlock() + val position = Position() + for (pass in 0 until iterations) { + position.pass = pass + for (slice in 0 until Argon2SyncPoints) { + position.slice = slice + for (lane in 0 until parallelism) { + position.lane = lane + fillSegment(filler, position) + } + } + } + } + + private fun fillSegment(filler: FillBlock, position: Position) { + var addressBlock: Block? = null + var inputBlock: Block? = null + val dataIndependentAddressing = isDataIndependentAddressing(position) + val startingIndex = getStartingIndex(position) + var currentOffset = + position.lane * laneLength + position.slice * segmentLength + startingIndex + var prevOffset = getPrevOffset(currentOffset) + + if (dataIndependentAddressing) { + addressBlock = filler.addressBlock.clear() + inputBlock = filler.inputBlock.clear() + initAddressBlocks(filler, position, inputBlock, addressBlock) + } + val withXor = isWithXor(position) + + for (index in startingIndex until segmentLength) { + val pseudoRandom = getPseudoRandom( + filler, + index, + addressBlock, + inputBlock, + prevOffset, + dataIndependentAddressing, + ) + val refLane = getRefLane(position, pseudoRandom) + val refColumn = getRefColumn(position, index, pseudoRandom, refLane == position.lane) + + // 2 Creating a new block + val prevBlock = blocks[prevOffset] + val refBlock = blocks[laneLength * refLane + refColumn] + val currentBlock = blocks[currentOffset] + if (withXor) { + filler.fillBlockWithXor(prevBlock, refBlock, currentBlock) + } else { + filler.fillBlock(prevBlock, refBlock, currentBlock) + } + prevOffset = currentOffset + currentOffset++ + } + } + + private fun isDataIndependentAddressing(position: Position): Boolean { + return type == Type.Argon2I || (type == Type.Argon2Id && position.pass == 0 && position.slice < Argon2SyncPoints / 2) + } + + private fun initAddressBlocks( + filler: FillBlock, + position: Position, + inputBlock: Block, + addressBlock: Block, + ) { + inputBlock.v[0] = intToLong(position.pass) + inputBlock.v[1] = intToLong(position.lane) + inputBlock.v[2] = intToLong(position.slice) + inputBlock.v[3] = intToLong(blocks.size) + inputBlock.v[4] = intToLong(iterations) + inputBlock.v[5] = intToLong(type.id) + + if (position.pass == 0 && position.slice == 0) { + // Don't forget to generate the first block of addresses: + nextAddresses(filler, inputBlock, addressBlock) + } + } + + private fun isWithXor(position: Position): Boolean { + return !(position.pass == 0 || version == Version.Ver10) + } + + private fun getPrevOffset(currentOffset: Int): Int { + return if (currentOffset % laneLength == 0) { + // Last block in this lane + currentOffset + laneLength - 1 + } else { + // Previous block + currentOffset - 1 + } + } + + private fun nextAddresses(filler: FillBlock, inputBlock: Block, addressBlock: Block?) { + inputBlock.v[6]++ + filler.fillBlock(inputBlock, addressBlock) + filler.fillBlock(addressBlock, addressBlock) + } + + /** + * 1.2 Computing the index of the reference block + * 1.2.1 Taking pseudo-random value from the previous block + */ + private fun getPseudoRandom( + filler: FillBlock, + index: Int, + addressBlock: Block?, + inputBlock: Block?, + prevOffset: Int, + dataIndependentAddressing: Boolean, + ): Long { + return if (dataIndependentAddressing) { + val addressIndex = index % Argon2AddressesInBlock + if (addressIndex == 0) { + nextAddresses(filler, inputBlock!!, addressBlock) + } + addressBlock!!.v[addressIndex] + } else { + blocks[prevOffset].v[0] + } + } + + private fun getRefLane(position: Position, pseudoRandom: Long): Int { + var refLane = ((pseudoRandom ushr 32) % parallelism).toInt() + if (position.pass == 0 && position.slice == 0) { + // Can not reference other lanes yet + refLane = position.lane + } + return refLane + } + + private fun getRefColumn( + position: Position, + index: Int, + pseudoRandom: Long, + sameLane: Boolean, + ): Int { + val referenceAreaSize: Int + val startPosition: Int + + if (position.pass == 0) { + startPosition = 0 + referenceAreaSize = if (sameLane) { + // The same lane => add current segment + position.slice * segmentLength + index - 1 + } else { + // pass == 0 && !sameLane => position.slice > 0 + position.slice * segmentLength + if (index == 0) -1 else 0 + } + } else { + startPosition = (position.slice + 1) * segmentLength % laneLength + referenceAreaSize = if (sameLane) { + laneLength - segmentLength + index - 1 + } else { + laneLength - segmentLength + if (index == 0) -1 else 0 + } + } + var relativePosition = pseudoRandom and 0xFFFFFFFFL + relativePosition = relativePosition * relativePosition ushr 32 + relativePosition = referenceAreaSize - 1 - (referenceAreaSize * relativePosition ushr 32) + + return (startPosition + relativePosition).toInt() % laneLength + } + + private fun digest(tmpBlockBytes: ByteArray, out: ByteArray, outOff: Int, outLen: Int) { + val finalBlock = blocks[laneLength - 1] + + // XOR the last blocks + for (i in 1 until parallelism) { + val lastBlockInLane = i * laneLength + (laneLength - 1) + finalBlock.xorWith(blocks[lastBlockInLane]) + } + finalBlock.toBytes(tmpBlockBytes) + hash(tmpBlockBytes, out, outOff, outLen) + } + + /** + * H' - hash - variable length hash function + */ + private fun hash(input: ByteArray, out: ByteArray, outOff: Int, outLen: Int) { + val outLenBytes = ByteArray(4) + intToLittleEndian(outLen, outLenBytes, 0) + val blake2bLength = 64 + + if (outLen <= blake2bLength) { + val blake = Blake2bDigest(outLen * 8) + blake.update(outLenBytes, 0, outLenBytes.size) + blake.update(input, 0, input.size) + blake.doFinal(out, outOff) + } else { + var digest = Blake2bDigest(blake2bLength * 8) + val outBuffer = ByteArray(blake2bLength) + + // V1 + digest.update(outLenBytes, 0, outLenBytes.size) + digest.update(input, 0, input.size) + digest.doFinal(outBuffer, 0) + val halfLen = blake2bLength / 2 + var outPos = outOff + arraycopy(outBuffer, 0, out, outPos, halfLen) + outPos += halfLen + val r = (outLen + 31) / 32 - 2 + var i = 2 + + while (i <= r) { + // V2 to Vr + digest.update(outBuffer, 0, outBuffer.size) + digest.doFinal(outBuffer, 0) + arraycopy(outBuffer, 0, out, outPos, halfLen) + i++ + outPos += halfLen + } + val lastLength = outLen - 32 * r + + // Vr+1 + digest = Blake2bDigest(lastLength * 8) + digest.update(outBuffer, 0, outBuffer.size) + digest.doFinal(out, outPos) + } + } + + /* + * H0 = H64(p, τ, m, t, v, y, |P|, P, |S|, S, |L|, K, |X|, X) + * -> 64 byte (ARGON2_PREHASH_DIGEST_LENGTH) + */ + private fun initialize(tmpBlockBytes: ByteArray, password: ByteArray, outputLength: Int) { + val blake = Blake2bDigest(Argon2PreHashDigestLength * 8) + val values = intArrayOf(parallelism, outputLength, memory, iterations, version.id, type.id) + + intToLittleEndian(values, tmpBlockBytes, 0) + blake.update(tmpBlockBytes, 0, values.size * 4) + + addByteString(tmpBlockBytes, blake, password) + addByteString(tmpBlockBytes, blake, salt) + addByteString(tmpBlockBytes, blake, secret) + addByteString(tmpBlockBytes, blake, additional) + + val initialHashWithZeros = ByteArray(Argon2PreHashSeedLength) + blake.doFinal(initialHashWithZeros, 0) + fillFirstBlocks(tmpBlockBytes, initialHashWithZeros) + } + + /** + * (H0 || 0 || i) 72 byte -> 1024 byte + * (H0 || 1 || i) 72 byte -> 1024 byte + */ + private fun fillFirstBlocks(tmpBlockBytes: ByteArray, initialHashWithZeros: ByteArray) { + val initialHashWithOnes = ByteArray(Argon2PreHashSeedLength) + arraycopy(initialHashWithZeros, 0, initialHashWithOnes, 0, Argon2PreHashDigestLength) + initialHashWithOnes[Argon2PreHashDigestLength] = 1 + + for (i in 0 until parallelism) { + intToLittleEndian(i, initialHashWithZeros, Argon2PreHashDigestLength + 4) + intToLittleEndian(i, initialHashWithOnes, Argon2PreHashDigestLength + 4) + hash(initialHashWithZeros, tmpBlockBytes, 0, Argon2BlockSize) + blocks[i * laneLength + 0].fromBytes(tmpBlockBytes) + hash(initialHashWithOnes, tmpBlockBytes, 0, Argon2BlockSize) + blocks[i * laneLength + 1].fromBytes(tmpBlockBytes) + } + } + + private fun intToLong(x: Int): Long { + return (x and M32L.toInt()).toLong() + } + + @Suppress("PropertyName") + private class FillBlock { + var R = Block() + var Z = Block() + var addressBlock = Block() + var inputBlock = Block() + + private fun applyBlake() { + /* + * Apply Blake2 on columns of 64-bit words: (0,1,...,15), + * then (16,17,..31)... finally (112,113,...127) + */ + for (i in 0..7) { + val i16 = 16 * i + roundFunction( + Z, + i16, i16 + 1, i16 + 2, + i16 + 3, i16 + 4, i16 + 5, + i16 + 6, i16 + 7, i16 + 8, + i16 + 9, i16 + 10, i16 + 11, + i16 + 12, i16 + 13, i16 + 14, + i16 + 15, + ) + } + + /* Apply Blake2 on rows of 64-bit words: (0,1,16,17,...112,113), then + (2,3,18,19,...,114,115).. finally (14,15,30,31,...,126,127) */ + for (i in 0..7) { + val i2 = 2 * i + roundFunction( + Z, + i2, i2 + 1, i2 + 16, + i2 + 17, i2 + 32, i2 + 33, + i2 + 48, i2 + 49, i2 + 64, + i2 + 65, i2 + 80, i2 + 81, + i2 + 96, i2 + 97, i2 + 112, + i2 + 113, + ) + } + } + + fun fillBlock(Y: Block?, currentBlock: Block?) { + Z.copyBlock(Y) + applyBlake() + currentBlock!!.xor(Y, Z) + } + + fun fillBlock(X: Block?, Y: Block?, currentBlock: Block?) { + R.xor(X, Y) + Z.copyBlock(R) + applyBlake() + currentBlock!!.xor(R, Z) + } + + fun fillBlockWithXor(X: Block?, Y: Block?, currentBlock: Block?) { + R.xor(X, Y) + Z.copyBlock(R) + applyBlake() + currentBlock!!.xorWith(R, Z) + } + } + + private class Block { + private val Size = Argon2QwordsInBlock + + // 128 * 8 Byte QWords + val v: LongArray = LongArray(Size) + + fun fromBytes(input: ByteArray) { + require(input.size >= Argon2BlockSize) { "Input shorter than blocksize" } + littleEndianToLong(input, 0, v) + } + + fun toBytes(output: ByteArray) { + require(output.size >= Argon2BlockSize) { "Output shorter than blocksize" } + longToLittleEndian(v, output, 0) + } + + fun copyBlock(other: Block?) { + arraycopy(other!!.v, 0, v, 0, Size) + } + + fun xor(b1: Block?, b2: Block?) { + val v0 = v + val v1 = b1!!.v + val v2 = b2!!.v + for (i in 0 until Size) { + v0[i] = v1[i] xor v2[i] + } + } + + fun xorWith(b1: Block) { + val v0 = v + val v1 = b1.v + for (i in 0 until Size) { + v0[i] = v0[i] xor v1[i] + } + } + + fun xorWith(b1: Block, b2: Block) { + val v0 = v + val v1 = b1.v + val v2 = b2.v + for (i in 0 until Size) { + v0[i] = v0[i] xor (v1[i] xor v2[i]) + } + } + + fun clear(): Block { + v.fill(0) + return this + } + } + + private class Position { + var pass = 0 + var lane = 0 + var slice = 0 + } + + private fun getStartingIndex(position: Position): Int { + return if (position.pass == 0 && position.slice == 0) { + 2 // we have already generated the first two blocks + } else { + 0 + } + } + + private fun addByteString( + tmpBlockBytes: ByteArray, + digest: Blake2bDigest, + octets: ByteArray?, + ) { + if (octets == null) { + digest.update(ZeroBytes, 0, 4) + return + } + intToLittleEndian(octets.size, tmpBlockBytes, 0) + digest.update(tmpBlockBytes, 0, 4) + digest.update(octets, 0, octets.size) + } + + companion object { + private fun roundFunction( + block: Block, + v0: Int, + v1: Int, + v2: Int, + v3: Int, + v4: Int, + v5: Int, + v6: Int, + v7: Int, + v8: Int, + v9: Int, + v10: Int, + v11: Int, + v12: Int, + v13: Int, + v14: Int, + v15: Int, + ) { + val v = block.v + F(v, v0, v4, v8, v12) + F(v, v1, v5, v9, v13) + F(v, v2, v6, v10, v14) + F(v, v3, v7, v11, v15) + F(v, v0, v5, v10, v15) + F(v, v1, v6, v11, v12) + F(v, v2, v7, v8, v13) + F(v, v3, v4, v9, v14) + } + + private fun F(v: LongArray, a: Int, b: Int, c: Int, d: Int) { + quarterRound(v, a, b, d, 32) + quarterRound(v, c, d, b, 24) + quarterRound(v, a, b, d, 16) + quarterRound(v, c, d, b, 63) + } + + private fun quarterRound(v: LongArray, x: Int, y: Int, z: Int, s: Int) { + var a = v[x] + val b = v[y] + var c = v[z] + a += b + 2 * (a and M32L) * (b and M32L) + c = (c xor a).rotateRight(s) + v[x] = a + v[z] = c + } + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ArrayCopy.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ArrayCopy.kt new file mode 100644 index 00000000..fd538e82 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ArrayCopy.kt @@ -0,0 +1,109 @@ +package io.spherelabs.crypto.cipher + +/** + * https://github.com/korlibs/korge/blob/main/korlibs-crypto/src/korlibs/internal/InternalCryptoArrays.kt + */ +// NEED TO TEST +/** Copies [size] elements of [src] starting at [srcPos] into [dst] at [dstPos] */ +internal fun arraycopy(src: ByteArray, srcPos: Int, dst: ByteArray, dstPos: Int, size: Int) { + src.copyInto(dst, dstPos, srcPos, srcPos + size) +} + + +internal inline infix fun Int.leftRotate(bitCount: Int): Int { + return (this shl bitCount) or (this ushr (32 - bitCount)) +} + +@Suppress("NOTHING_TO_INLINE") // Pending `kotlin.experimental.xor` becoming stable +internal inline infix fun Byte.xor(other: Byte): Byte = (toInt() xor other.toInt()).toByte() + +/** Copies [size] elements of [src] starting at [srcPos] into [dst] at [dstPos] */ +internal fun arraycopy(src: IntArray, srcPos: Int, dst: IntArray, dstPos: Int, size: Int) { + src.copyInto(dst, dstPos, srcPos, srcPos + size) +} + +internal fun arraycopy(src: LongArray, srcPos: Int, dst: LongArray, dstPos: Int, size: Int) { + src.copyInto(dst, dstPos, srcPos, srcPos + size) +} + + +internal fun littleEndianToInt(bs: ByteArray, offset: Int): Int { + var off = offset + var n: Int = bs[off].toInt() and 0xff + n = n or (bs[++off].toInt() and 0xff shl 8) + n = n or (bs[++off].toInt() and 0xff shl 16) + n = n or (bs[++off].toInt() shl 24) + return n +} + +internal fun littleEndianToInt(bs: ByteArray, bOff: Int, ns: IntArray, nOff: Int, count: Int) { + var byteArrayOffset = bOff + for (i in 0 until count) { + ns[nOff + i] = littleEndianToInt(bs, byteArrayOffset) + byteArrayOffset += 4 + } +} + +internal fun littleEndianToInt(bs: ByteArray, off: Int, count: Int): IntArray { + var offset = off + val ns = IntArray(count) + for (i in ns.indices) { + ns[i] = littleEndianToInt(bs, offset) + offset += 4 + } + return ns +} + +internal fun littleEndianToLong(bs: ByteArray, off: Int, ns: LongArray) { + var offset = off + for (i in ns.indices) { + ns[i] = littleEndianToLong(bs, offset) + offset += 8 + } +} + +internal fun littleEndianToLong(bs: ByteArray, off: Int): Long { + val lo = littleEndianToInt(bs, off) + val hi = littleEndianToInt(bs, off + 4) + return (hi.toLong() and 0xffffffffL) shl 32 or (lo.toLong() and 0xffffffffL) +} + +internal fun intToLittleEndian(n: Int) = ByteArray(4).apply { + intToLittleEndian(n, this, 0) +} + +internal fun intToLittleEndian(n: Int, bs: ByteArray, off: Int) { + bs[off] = n.toByte() + bs[off + 1] = (n ushr 8).toByte() + bs[off + 2] = (n ushr 16).toByte() + bs[off + 3] = (n ushr 24).toByte() +} + +internal fun intToLittleEndian(ns: IntArray, bs: ByteArray, off: Int) { + var offset = off + for (i in ns.indices) { + intToLittleEndian(ns[i], bs, offset) + offset += 4 + } +} + +internal fun longToLittleEndian(n: Long) = ByteArray(8).apply { + longToLittleEndian(n, this, 0) +} + +internal fun longToLittleEndian(ns: LongArray, bs: ByteArray, off: Int) { + var offset = off + for (i in ns.indices) { + longToLittleEndian(ns[i], bs, offset) + offset += 8 + } +} + +internal fun longToLittleEndian(n: Long, bs: ByteArray, off: Int) { + intToLittleEndian((n and 0xffffffffL).toInt(), bs, off) + intToLittleEndian((n ushr 32).toInt(), bs, off + 4) +} + +fun Int.rotateLeft(distance: Int): Int { + return this shl distance or (this ushr -distance) +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Blake2b.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Blake2b.kt new file mode 100644 index 00000000..3096894a --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Blake2b.kt @@ -0,0 +1,384 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package io.spherelabs.crypto.cipher + +/** + * Implementation of the cryptographic hash function Blakbe2b. + * + * + * Blake2b offers a built-in keying mechanism to be used directly + * for authentication ("Prefix-MAC") rather than a HMAC construction. + * + * + * Blake2b offers a built-in support for a salt for randomized hashing + * and a personal string for defining a unique hash function for each application. + * + * + * BLAKE2b is optimized for 64-bit platforms and produces digests of any size + * between 1 and 64 bytes. + */ +internal class Blake2bDigest { + private var digestSize = 64 // 1- 64 bytes + private var keyLength = 0 // 0 - 64 bytes for keyed hashing for MAC + private var salt: ByteArray? = null // new byte[16]; + private var personalization: ByteArray? = null // new byte[16]; + + private var key: ByteArray? = null + + /** + * Tree hashing parameters: + * Because this class does not implement the Tree Hashing Mode, + * these parameters can be treated as constants (see init() function) + * + * private int fanout = 1; + * 0-255 private int depth = 1; + * 1-255 private int leafLength= 0; + * private long nodeOffset = 0L; + * private int nodeDepth = 0; + * private int innerHashLength = 0; + * whenever this buffer overflows, it will be processed in the compress() function. + * For performance issues, long messages will not use this buffer. + */ + private val buffer: ByteArray // new byte[BLOCK_LENGTH_BYTES]; + + // Position of last inserted byte: + private var bufferPos = 0 // a value from 0 up to 128 + private val internalState = LongArray(16) // In the Blake2b paper it is + + // called: v + private lateinit var chainValue: LongArray // state vector, in the Blake2b paper it + + // is called: h + private var t0 = 0L // holds last significant bits, counter (counts bytes) + private var t1 = 0L // counter: Length up to 2^128 are supported + private var f0 = 0L // finalization flag, for last block: ~0L + + /** + * Basic sized constructor - size in bits. + * + * @param digestSize size of the digest in bits + */ + constructor(digestSize: Int = 512) { + require(!(digestSize < 8 || digestSize > 512 || digestSize % 8 != 0)) { + "BLAKE2b digest bit length must be a multiple of 8 and not greater than 512" + } + buffer = ByteArray(ByteLength) + keyLength = 0 + this.digestSize = digestSize / 8 + initChainValue() + } + + /** + * Blake2b for authentication ("Prefix-MAC mode"). + * After calling the doFinal() method, the key will + * remain to be used for further computations of + * this instance. + * The key can be overwritten using the clearKey() method. + * + * @param key A key up to 64 bytes or null + */ + constructor(key: ByteArray?) { + buffer = ByteArray(ByteLength) + if (key != null) { + this.key = ByteArray(key.size) + arraycopy(key, 0, this.key!!, 0, key.size) + require(key.size <= 64) { "Keys > 64 are not supported" } + keyLength = key.size + arraycopy(key, 0, buffer, 0, key.size) + bufferPos = ByteLength // zero padding + } + digestSize = 64 + initChainValue() + } + + /** + * Blake2b with key, required digest length (in bytes), salt and personalization. + * After calling the doFinal() method, the key, the salt and the personal string + * will remain and might be used for further computations with this instance. + * The key can be overwritten using the clearKey() method, the salt (pepper) + * can be overwritten using the clearSalt() method. + * + * @param key A key up to 64 bytes or null + * @param digestLength from 1 up to 64 bytes + * @param salt 16 bytes or null + * @param personalization 16 bytes or null + */ + constructor(key: ByteArray?, digestLength: Int, salt: ByteArray?, personalization: ByteArray?) { + buffer = ByteArray(ByteLength) + require(digestLength in (1..64)) { "Invalid digest length (required: 1-64)." } + digestSize = digestLength + if (salt != null) { + require(salt.size == 16) { "Salt length must be exactly 16 bytes." } + this.salt = salt.copyOf() + } + if (personalization != null) { + require(personalization.size == 16) { "Personalization length must be exactly 16 bytes." } + this.personalization = personalization.copyOf() + } + if (key != null) { + this.key = key.copyOf() + require(key.size <= 64) { "Keys > 64 are not supported." } + keyLength = key.size + arraycopy(key, 0, buffer, 0, key.size) + bufferPos = ByteLength // zero padding + } + initChainValue() + } + + private fun initChainValue() { + chainValue = LongArray(8) + chainValue[0] = ( + Blake2bIV[0] + xor (digestSize or (keyLength shl 8) or 0x1010000).toLong() + ) + // 0x1010000 = ((fanout << 16) | (depth << 24) | (leafLength << 32)); + // with fanout = 1; depth = 0; leafLength = 0; + chainValue[1] = Blake2bIV[1] // ^ nodeOffset; with nodeOffset = 0; + chainValue[2] = Blake2bIV[2] // ^ ( nodeDepth | (innerHashLength << 8) ); + // with nodeDepth = 0; innerHashLength = 0; + chainValue[3] = Blake2bIV[3] + chainValue[4] = Blake2bIV[4] + chainValue[5] = Blake2bIV[5] + + if (salt != null) { + chainValue[4] = chainValue[4] xor littleEndianToLong(salt!!, 0) + chainValue[5] = chainValue[5] xor littleEndianToLong(salt!!, 8) + } + chainValue[6] = Blake2bIV[6] + chainValue[7] = Blake2bIV[7] + + if (personalization != null) { + chainValue[6] = chainValue[6] xor littleEndianToLong(personalization!!, 0) + chainValue[7] = chainValue[7] xor littleEndianToLong(personalization!!, 8) + } + } + + private fun initializeInternalState() { + arraycopy(chainValue, 0, internalState, 0, chainValue.size) + arraycopy(Blake2bIV, 0, internalState, chainValue.size, 4) + internalState[12] = t0 xor Blake2bIV[4] + internalState[13] = t1 xor Blake2bIV[5] + internalState[14] = f0 xor Blake2bIV[6] + internalState[15] = Blake2bIV[7] // ^ f1 with f1 = 0 + } + + /** + * Update the message digest with a single byte. + * + * @param b the input byte to be entered. + */ + fun update(b: Byte) { + val remainingLength = ByteLength - bufferPos + if (remainingLength == 0) { // full buffer + t0 += ByteLength.toLong() + if (t0 == 0L) { // if message > 2^64 + t1++ + } + compress(buffer, 0) + buffer.fill(0) + buffer[0] = b + bufferPos = 1 + } else { + buffer[bufferPos] = b + bufferPos++ + return + } + } + + /** + * Update the message digest with a block of bytes. + * + * @param message the byte array containing the data. + * @param offset the offset into the byte array where the data starts. + * @param len the length of the data. + */ + fun update(message: ByteArray?, offset: Int, len: Int) { + if (message == null || len == 0) { + return + } + var remainingLength = 0 // left bytes of buffer + if (bufferPos != 0) { // commenced, incomplete buffer + + // complete the buffer: + remainingLength = ByteLength - bufferPos + if (remainingLength < len) { // full buffer + at least 1 byte + arraycopy( + message, + offset, + buffer, + bufferPos, + remainingLength, + ) + t0 += ByteLength.toLong() + if (t0 == 0L) { // if message > 2^64 + t1++ + } + compress(buffer, 0) + bufferPos = 0 + buffer.fill(0) + } else { + arraycopy(message, offset, buffer, bufferPos, len) + bufferPos += len + return + } + } + + // Process blocks except last block (also if last block is full) + val blockWiseLastPos = offset + len - ByteLength + var messagePos = offset + remainingLength + + while (messagePos < blockWiseLastPos) { + // Block wise 128 bytes without buffer: + t0 += ByteLength.toLong() + if (t0 == 0L) { + t1++ + } + compress(message, messagePos) + messagePos += ByteLength + } + + // Fill the buffer with left bytes, this might be a full block + arraycopy(message, messagePos, buffer, 0, offset + len - messagePos) + bufferPos += offset + len - messagePos + } + + /** + * Close the digest, producing the final digest value. + * The doFinal call leaves the digest reset. + * Key, salt and personal string remain. + * + * @param out the array the digest is to be copied into. + * @param outOffset the offset into the out array the digest is to start at. + */ + fun doFinal(out: ByteArray, outOffset: Int): Int { + f0 = -0x1L + t0 += bufferPos.toLong() + if (bufferPos > 0 && t0 == 0L) { + t1++ + } + compress(buffer, 0) + buffer.fill(0) + internalState.fill(0) + var i = 0 + while (i < chainValue.size && i * 8 < digestSize) { + val bytes = longToLittleEndian(chainValue[i]) + if (i * 8 < digestSize - 8) { + arraycopy(bytes, 0, out, outOffset + i * 8, 8) + } else { + arraycopy(bytes, 0, out, outOffset + i * 8, digestSize - i * 8) + } + i++ + } + chainValue.fill(0) + reset() + return digestSize + } + + /** + * Reset the digest back to it's initial state. + * The key, the salt and the personal string will + * remain for further computations. + */ + fun reset() { + bufferPos = 0 + f0 = 0L + t0 = 0L + t1 = 0L + buffer.fill(0) + key?.let { + arraycopy(it, 0, buffer, 0, it.size) + bufferPos = ByteLength // Zero padding + } + initChainValue() + } + + private fun compress(message: ByteArray, messagePos: Int) { + initializeInternalState() + val m = LongArray(16) + for (j in 0..15) { + m[j] = littleEndianToLong(message, messagePos + j * 8) + } + for (round in 0 until Rounds) { + // G apply to columns of internalState:m[blake2b_sigma[round][2 * blockPos]] /+1 + G(m[Blake2bSigma[round][0].toInt()], m[Blake2bSigma[round][1].toInt()], 0, 4, 8, 12) + G(m[Blake2bSigma[round][2].toInt()], m[Blake2bSigma[round][3].toInt()], 1, 5, 9, 13) + G(m[Blake2bSigma[round][4].toInt()], m[Blake2bSigma[round][5].toInt()], 2, 6, 10, 14) + G(m[Blake2bSigma[round][6].toInt()], m[Blake2bSigma[round][7].toInt()], 3, 7, 11, 15) + + // G apply to diagonals of internalState: + G(m[Blake2bSigma[round][8].toInt()], m[Blake2bSigma[round][9].toInt()], 0, 5, 10, 15) + G(m[Blake2bSigma[round][10].toInt()], m[Blake2bSigma[round][11].toInt()], 1, 6, 11, 12) + G(m[Blake2bSigma[round][12].toInt()], m[Blake2bSigma[round][13].toInt()], 2, 7, 8, 13) + G(m[Blake2bSigma[round][14].toInt()], m[Blake2bSigma[round][15].toInt()], 3, 4, 9, 14) + } + + // Update chain values: + for (offset in chainValue.indices) { + chainValue[offset] = + chainValue[offset] xor internalState[offset] xor internalState[offset + 8] + } + } + + private fun G(m1: Long, m2: Long, posA: Int, posB: Int, posC: Int, posD: Int) { + internalState[posA] = internalState[posA] + internalState[posB] + m1 + internalState[posD] = + internalState[posD] xor internalState[posA].rotateRight(32) + internalState[posC] = internalState[posC] + internalState[posD] + internalState[posB] = internalState[posB] xor internalState[posC].rotateRight(24) // replaces 25 of BLAKE + internalState[posA] = internalState[posA] + internalState[posB] + m2 + internalState[posD] = internalState[posD] xor internalState[posA].rotateRight(16) + internalState[posC] = internalState[posC] + internalState[posD] + internalState[posB] = internalState[posB] xor internalState[posC].rotateRight(63) + } + + /** + * Overwrite the key if it is no longer used + */ + fun clearKey() { + if (key != null) { + key?.fill(0) + buffer.fill(0) + } + } + + /** + * Overwrite the salt (pepper) if it is secret and no longer used + */ + fun clearSalt() = salt?.fill(0) + + companion object { + private val Blake2bIV = longArrayOf( + 0x6a09e667f3bcc908L, + -0x4498517a7b3558c5L, + 0x3c6ef372fe94f82bL, + -0x5ab00ac5a0e2c90fL, + 0x510e527fade682d1L, + -0x64fa9773d4c193e1L, + 0x1f83d9abfb41bd6bL, + 0x5be0cd19137e2179L, + ) + + private val Blake2bSigma = arrayOf( + byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + byteArrayOf(14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3), + byteArrayOf(11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4), + byteArrayOf(7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8), + byteArrayOf(9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13), + byteArrayOf(2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9), + byteArrayOf(12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11), + byteArrayOf(13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10), + byteArrayOf(6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5), + byteArrayOf(10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0), + byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + byteArrayOf(14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3), + ) + + private const val Rounds = 12 + + /** + * Size in bytes of the internal buffer the digest applies + * it's compression function to. + */ + const val ByteLength = 128 + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ChaCha.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ChaCha.kt new file mode 100644 index 00000000..e4f66949 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/ChaCha.kt @@ -0,0 +1,92 @@ +package io.spherelabs.crypto.cipher + + +/** + * Implementation of Daniel J. Bernstein's ChaCha stream cipher. + */ + +private const val DefaultRounds = 20 + +internal class ChaCha(rounds: Int = DefaultRounds) : Salsa20(rounds) { + override val algorithmName = "ChaCha" + + override fun advanceCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + if (hi > 0) { + engineState[13] += hi + } + val oldState = engineState[12] + engineState[12] += lo + if (oldState != 0 && engineState[12] < oldState) { + engineState[13]++ + } + } + + override fun advanceCounter() { + if (++engineState[12] == 0) { + ++engineState[13] + } + } + + override fun retreatCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + if (hi != 0) { + if (engineState[13] and 0xffffffffL.toInt() >= hi and 0xffffffffL.toInt()) { + engineState[13] -= hi + } else { + throw IllegalStateException("Attempt to reduce counter past zero.") + } + } + if (engineState[12] and 0xffffffffL.toInt() >= lo and 0xffffffffL.toInt()) { + engineState[12] -= lo + } else { + if (engineState[13] != 0) { + --engineState[13] + engineState[12] -= lo + } else { + throw IllegalStateException("Attempt to reduce counter past zero.") + } + } + } + + override fun retreatCounter() { + check(engineState[12] != 0 || engineState[13] != 0) { + "Attempt to reduce counter past zero." + } + if (--engineState[12] == -1) { + --engineState[13] + } + } + + override fun getCounter(): Long { + return engineState[13].toLong() shl 32 or ((engineState[12] and 0xffffffffL.toInt()).toLong()) + } + + override fun resetCounter() { + engineState[13] = 0 + engineState[12] = engineState[13] + } + + override fun setKey(key: ByteArray?, iv: ByteArray) { + if (key != null) { + require(key.size == 16 || key.size == 32) { + "$algorithmName requires 128 bit or 256 bit key" + } + packTauOrSigma(key.size, engineState) + + // Key + littleEndianToInt(key, 0, engineState, 4, 4) + littleEndianToInt(key, key.size - 16, engineState, 8, 4) + } + + // IV + littleEndianToInt(iv, 0, engineState, 14, 2) + } + + override fun generateKeyStream(output: ByteArray) { + chachaCore(rounds, engineState, x) + intToLittleEndian(x, output, 0) + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Chacha7537.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Chacha7537.kt new file mode 100644 index 00000000..f24d62da --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Chacha7537.kt @@ -0,0 +1,73 @@ +package io.spherelabs.crypto.cipher + + +/** + * Implementation of Daniel J. Bernstein's ChaCha stream cipher. + */ + +internal class ChaCha7539 : Salsa20() { + override val algorithmName = "ChaCha7539" + + override fun advanceCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + check(hi <= 0) { + "Attempt to increase counter past 2^32." + } + + val oldState = engineState[12] + engineState[12] += lo + check(!(oldState != 0 && engineState[12] < oldState)) { + "Attempt to increase counter past 2^32." + } + } + + override fun advanceCounter() { + check(++engineState[12] != 0) { "Attempt to increase counter past 2^32." } + } + + override fun retreatCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + check(hi == 0) { "Attempt to reduce counter past zero." } + + if (engineState[12] and 0xffffffffL.toInt() >= lo and 0xffffffffL.toInt()) { + engineState[12] -= lo + } else { + throw IllegalStateException("Attempt to reduce counter past zero.") + } + } + + override fun retreatCounter() { + check(engineState[12] != 0) { "Attempt to reduce counter past zero." } + --engineState[12] + } + + override fun getCounter(): Long { + return (engineState[12] and 0xffffffffL.toInt()).toLong() + } + + override fun resetCounter() { + engineState[12] = 0 + } + + override fun setKey(key: ByteArray?, iv: ByteArray) { + if (key != null) { + require(key.size == 32) { + "$algorithmName requires 256 bit key" + } + packTauOrSigma(key.size, engineState) + + // Key + littleEndianToInt(key, 0, engineState, 4, 8) + } + + // IV + littleEndianToInt(iv, 0, engineState, 13, 3) + } + + override fun generateKeyStream(output: ByteArray) { + chachaCore(rounds, engineState, x) + intToLittleEndian(x, output, 0) + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Cipher.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Cipher.kt new file mode 100644 index 00000000..6d69435a --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Cipher.kt @@ -0,0 +1,22 @@ +package io.spherelabs.crypto.cipher + +/** + * https://github.com/korlibs/korge/blob/main/korlibs-crypto/src/korlibs/crypto/Cipher.kt + */ +interface Cipher { + val blockSize: Int + fun encrypt(data: ByteArray, offset: Int = 0, len: Int = data.size - offset) + fun decrypt(data: ByteArray, offset: Int = 0, len: Int = data.size - offset) +} + +class CipherWithModeAndPadding(val cipher: Cipher, val mode: CipherMode, val padding: CipherPadding, val iv: ByteArray? = null) { + fun encrypt(data: ByteArray, offset: Int = 0, len: Int = data.size - offset): ByteArray { + return mode.encryptSafe(data.copyOfRange(offset, offset + len), cipher, padding, iv) + } + + fun decrypt(data: ByteArray, offset: Int = 0, len: Int = data.size - offset): ByteArray = + mode.decryptSafe(data.copyOfRange(offset, offset + len), cipher, padding, iv) +} + +fun Cipher.with(mode: CipherMode, padding: CipherPadding, iv: ByteArray? = null): CipherWithModeAndPadding = CipherWithModeAndPadding(this, mode, padding, iv) +operator fun Cipher.get(mode: CipherMode, padding: CipherPadding, iv: ByteArray? = null): CipherWithModeAndPadding = with(mode, padding, iv) diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherMode.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherMode.kt new file mode 100644 index 00000000..dde8a3f4 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherMode.kt @@ -0,0 +1,222 @@ +package io.spherelabs.crypto.cipher + +import kotlin.experimental.xor + +/** + * https://github.com/korlibs/korge/blob/main/korlibs-crypto/src/korlibs/crypto/CipherMode.kt + */ +interface CipherMode { + companion object { + val ECB: CipherMode get() = CipherModeECB + val CBC: CipherMode get() = CipherModeCBC + val PCBC: CipherMode get() = CipherModePCBC + val CFB: CipherMode get() = CipherModeCFB + val OFB: CipherMode get() = CipherModeOFB + val CTR: CipherMode get() = CipherModeCTR + } + + val name: String + fun encrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray + fun decrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray +} + +private fun Int.nextMultipleOf(multiple: Int) = if (this % multiple == 0) this else (((this / multiple) + 1) * multiple) + +fun CipherMode.encryptSafe(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + if (padding == CipherPadding.NoPadding) { + return encrypt(data, cipher, CipherPadding.ZeroPadding, iv).copyOf(data.size) + } + return encrypt(data, cipher, padding, iv) +} +fun CipherMode.decryptSafe(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + if (padding == CipherPadding.NoPadding) { + return decrypt(data.copyOf(data.size.nextMultipleOf(cipher.blockSize)), cipher, CipherPadding.ZeroPadding, iv).copyOf(data.size) + } + return decrypt(data, cipher, padding, iv) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private object CipherModeECB : CipherModeBase("ECB") { + override fun encrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + val pData = padding.add(data, cipher.blockSize) + cipher.encrypt(pData, 0, pData.size) + return pData + } + + override fun decrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + cipher.decrypt(data, 0, data.size) + return padding.remove(data) + } +} + +private object CipherModeCBC : CipherModeIV("CBC") { + override fun coreEncrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + for (n in pData.indices step cipher.blockSize) { + arrayxor(pData, n, ivb) + cipher.encrypt(pData, n, cipher.blockSize) + arraycopy(pData, n, ivb, 0, cipher.blockSize) + } + } + + override fun coreDecrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val tempBytes = ByteArray(blockSize) + + for (n in pData.indices step blockSize) { + arraycopy(pData, n, tempBytes, 0, blockSize) + cipher.decrypt(pData, n, blockSize) + arrayxor(pData, n, ivb) + arraycopy(tempBytes, 0, ivb, 0, blockSize) + } + } +} + +private object CipherModePCBC : CipherModeIV("PCBC") { + override fun coreEncrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val plaintext = ByteArray(blockSize) + + for (n in pData.indices step blockSize) { + arraycopy(pData, n, plaintext, 0, blockSize) + arrayxor(pData, n, ivb) + cipher.encrypt(pData, n, cipher.blockSize) + arraycopy(pData, n, ivb, 0, blockSize) + arrayxor(ivb, 0, plaintext) + } + } + + override fun coreDecrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val cipherText = ByteArray(cipher.blockSize) + + for (n in pData.indices step cipher.blockSize) { + arraycopy(pData, n, cipherText, 0, blockSize) + cipher.decrypt(pData, n, cipher.blockSize) + arrayxor(pData, n, ivb) + arraycopy(pData, n, ivb, 0, blockSize) + arrayxor(ivb, 0, cipherText) + } + } +} + +private object CipherModeCFB : CipherModeIV("CFB") { + override fun coreEncrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val cipherText = ByteArray(blockSize) + + cipher.encrypt(ivb) + arraycopy(ivb, 0, cipherText, 0, blockSize) + for (n in pData.indices step blockSize) { + arrayxor(cipherText, 0, blockSize, pData, n) + arraycopy(cipherText, 0, pData, n, blockSize) + + if (n + blockSize < pData.size) { + cipher.encrypt(cipherText) + } + } + } + + override fun coreDecrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val plainText = ByteArray(blockSize) + val cipherText = ByteArray(blockSize) + + cipher.encrypt(ivb) + arraycopy(ivb, 0, cipherText, 0, blockSize) + for (n in pData.indices step blockSize) { + arraycopy(cipherText, 0, plainText, 0, blockSize) + arrayxor(plainText, 0, blockSize, pData, n) + + arraycopy(pData, n, cipherText, 0, blockSize) + arraycopy(plainText, 0, pData, n, blockSize) + if (n + blockSize < pData.size) { + cipher.encrypt(cipherText) + } + } + } +} + +private object CipherModeOFB : CipherModeIVDE("OFB") { + override fun coreCrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val cipherText = ByteArray(blockSize) + cipher.encrypt(ivb) + for (n in pData.indices step blockSize) { + arraycopy(pData, n, cipherText, 0, blockSize) + arrayxor(cipherText, 0, ivb) + arraycopy(cipherText, 0, pData, n, blockSize) + if (n + blockSize < pData.size) { + cipher.encrypt(ivb) + } + } + } +} + +// https://github.com/Jens-G/haxe-crypto/blob/dcf6d994773abba80b0720b2f5e9d5b26de0dbe3/src/com/hurlant/crypto/symmetric/mode/CTRMode.hx +private object CipherModeCTR : CipherModeIVDE("CTR") { + override fun coreCrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) { + val blockSize = cipher.blockSize + val temp = ByteArray(ivb.size) + for (n in pData.indices step blockSize) { + arraycopy(ivb, 0, temp, 0, temp.size) + cipher.encrypt(temp, 0, blockSize) + arrayxor(pData, n, temp) + for (j in blockSize - 1 downTo 0) { + ivb[j]++ + if (ivb[j].toInt() != 0) break + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private abstract class CipherModeBase(override val name: String) : CipherMode { + override fun toString(): String = name +} + +private abstract class CipherModeIV(name: String) : CipherModeBase(name) { + final override fun encrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + val ivb = getIV(iv, cipher.blockSize) + val pData = padding.add(data, cipher.blockSize) + coreEncrypt(pData, cipher, ivb) + return pData + } + + final override fun decrypt(data: ByteArray, cipher: Cipher, padding: Padding, iv: ByteArray?): ByteArray { + val ivb = getIV(iv, cipher.blockSize) + val pData = data.copyOf() + coreDecrypt(pData, cipher, ivb) + return padding.remove(pData) + } + + protected abstract fun coreEncrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) + protected abstract fun coreDecrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) +} + +private abstract class CipherModeIVDE(name: String) : CipherModeIV(name) { + final override fun coreEncrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) = coreCrypt(pData, cipher, ivb) + final override fun coreDecrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) = coreCrypt(pData, cipher, ivb) + + protected abstract fun coreCrypt(pData: ByteArray, cipher: Cipher, ivb: ByteArray) +} + +private fun arrayxor(data: ByteArray, offset: Int, xor: ByteArray) { + for (n in xor.indices) data[offset + n] = data[offset + n] xor xor[n] +} + +private fun arrayxor(data: ByteArray, offset: Int, size: Int, xor: ByteArray, xoroffset: Int) { + for (n in 0 until size) data[offset + n] = data[offset + n] xor xor[xoroffset + n] +} + +private fun getIV(srcIV: ByteArray?, blockSize: Int): ByteArray { + if (srcIV == null) TODO("IV not provided") + if (srcIV.size < blockSize) throw IllegalArgumentException("Wrong IV length: must be $blockSize bytes long") + return srcIV.copyOf(blockSize) + //return ByteArray(blockSize).also { dstIV -> arraycopy(srcIV, 0, dstIV, 0, kotlin.math.min(srcIV.size, dstIV.size)) } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherPadding.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherPadding.kt new file mode 100644 index 00000000..ed91ab97 --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/CipherPadding.kt @@ -0,0 +1,86 @@ +package io.spherelabs.crypto.cipher + +import io.spherelabs.anycrypto.securerandom.buildSecureRandom + +typealias Padding = CipherPadding + +sealed class CipherPadding { + companion object { + val NoPadding: CipherPadding get() = CipherPaddingNo + val PKCS7Padding: CipherPadding get() = CipherPaddingPKCS7 + val ANSIX923Padding: CipherPadding get() = CipherPaddingANSIX923 + val ISO10126Padding: CipherPadding get() = CipherPaddingISO10126 + val ZeroPadding: CipherPadding get() = CipherPaddingZero + + fun padding(data: ByteArray, blockSize: Int, padding: Padding): ByteArray = + padding.add(data, blockSize) + + fun removePadding(data: ByteArray, padding: Padding): ByteArray = padding.remove(data) + } + + fun add(data: ByteArray, blockSize: Int): ByteArray { + val paddingSize = paddingSize(data.size, blockSize) + val result = ByteArray(data.size + paddingSize) + arraycopy(data, 0, result, 0, data.size) + addInternal(result, data.size, paddingSize) + return result + } + + fun remove(data: ByteArray): ByteArray { + val result = data.copyOf() + val size = removeInternal(data) + return result.copyOf(size) + } + + protected open fun paddingSize(dataSize: Int, blockSize: Int): Int = + blockSize - dataSize % blockSize + + protected open fun addInternal(result: ByteArray, dataSize: Int, paddingSize: Int): Unit = Unit + protected open fun removeInternal(data: ByteArray): Int = + data.size - (data[data.size - 1].toInt() and 0xFF) +} + +private object CipherPaddingNo : CipherPadding() { + override fun paddingSize(dataSize: Int, blockSize: Int): Int { + if (dataSize % blockSize != 0) { + throw IllegalArgumentException("Data ($dataSize) is not multiple of ${blockSize}, and padding was set to $NoPadding") + } + return 0 + } + + override fun addInternal(result: ByteArray, dataSize: Int, paddingSize: Int) = Unit + override fun removeInternal(data: ByteArray): Int = data.size +} + +private object CipherPaddingPKCS7 : CipherPadding() { + override fun addInternal(result: ByteArray, dataSize: Int, paddingSize: Int) { + for (i in dataSize until result.size) result[i] = paddingSize.toByte() + } +} + +private object CipherPaddingANSIX923 : CipherPadding() { + override fun addInternal(result: ByteArray, dataSize: Int, paddingSize: Int) { + result[result.size - 1] = paddingSize.toByte() + } +} + +private object CipherPaddingISO10126 : CipherPadding() { + override fun addInternal(result: ByteArray, dataSize: Int, paddingSize: Int) { + val randomBytes = buildSecureRandom().nextBytes(paddingSize) + randomBytes[paddingSize - 1] = paddingSize.toByte() + arraycopy(randomBytes, 0, result, dataSize, randomBytes.size) + } +} + +private object CipherPaddingZero : CipherPadding() { + override fun removeInternal(data: ByteArray): Int { + var paddingSize = 0 + for (i in data.size - 1 downTo 0) { + if (data[i].toInt() != 0) { + break + } + ++paddingSize + } + return data.size - paddingSize + } +} diff --git a/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Salsa20.kt b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Salsa20.kt new file mode 100644 index 00000000..bb8be51e --- /dev/null +++ b/crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Salsa20.kt @@ -0,0 +1,451 @@ +package io.spherelabs.crypto.cipher + +/** + * Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005 + */ + +private const val DefaultRounds = 20 +private const val StateSize = 16 // 16, 32 bit ints = 64 bytes + +private val TauSigma = littleEndianToInt( + bs = ("expand 16-byte k" + "expand 32-byte k").encodeToByteArray(), + off = 0, + count = 8, +) + +internal open class Salsa20(protected val rounds: Int = DefaultRounds) { + open val algorithmName = "Salsa20" + + /* + * Internal counter + */ + private var cW0 = 0 + private var cW1 = 0 + private var cW2 = 0 + + private var index = 0 + private var initialised = false + + private val keyStream = ByteArray(StateSize * 4) // expanded state, 64 bytes + + protected var engineState = IntArray(StateSize) // state + + protected var x = IntArray(StateSize) // internal buffer + + init { + require(rounds > 0 && rounds and 1 == 0) { + "'rounds' must be a positive, even number" + } + } + + fun init(key: ByteArray?, iv: ByteArray) { + if (key == null) { + check(initialised) { + "$algorithmName KeyParameter can not be null for first initialisation" + } + setKey(null, iv) + } else { + setKey(key, iv) + } + reset() + initialised = true + } + + fun getBytes(numberOfBytes: Int): ByteArray { + val output = ByteArray(numberOfBytes) + + for (i in 0 until numberOfBytes) { + output[i] = keyStream[index] + index = (index + 1) % 64 + if (index == 0) { + advanceCounter() + generateKeyStream(keyStream) + } + } + return output + } + + fun processBytes(input: ByteArray): ByteArray { + val output = ByteArray(input.size) + processBytes(input, 0, input.size, output, 0) + return output + } + + fun processBytes( + input: ByteArray, + inputOffset: Int, + length: Int, + output: ByteArray, + roundsOffset: Int, + ): Int { + check(initialised) { "$algorithmName not initialised" } + + if (inputOffset + length > input.size) { + throw Exception("Input buffer too short") + } + if (roundsOffset + length > output.size) { + throw Exception("Output buffer too short") + } + if (limitExceeded(length)) { + throw Exception("2^70 byte limit per IV would be exceeded; Change IV") + } + for (i in 0 until length) { + output[i + roundsOffset] = (keyStream[index] xor input[i + inputOffset]) + index = (index + 1) % 64 + if (index == 0) { + advanceCounter() + generateKeyStream(keyStream) + } + } + return length + } + + fun skip(numberOfBytes: Long): Long { + if (numberOfBytes >= 0) { + var remaining = numberOfBytes + if (remaining >= 64) { + val count = remaining / 64 + advanceCounter(count) + remaining -= count * 64 + } + val oldIndex = index + index = (index + remaining.toInt()) % 64 + if (index < oldIndex) { + advanceCounter() + } + } else { + var remaining = -numberOfBytes + if (remaining >= 64) { + val count = remaining / 64 + retreatCounter(count) + remaining -= count * 64 + } + for (i in 0 until remaining) { + if (index == 0) { + retreatCounter() + } + index = (index - 1) % 64 + } + } + generateKeyStream(keyStream) + return numberOfBytes + } + + fun seekTo(position: Long): Long { + reset() + return skip(position) + } + + fun getPosition(): Long = getCounter() * 64 + index + + fun reset() { + index = 0 + resetLimitCounter() + resetCounter() + generateKeyStream(keyStream) + } + + protected fun packTauOrSigma(keyLength: Int, state: IntArray) { + val tsOff = (keyLength - 16) / 4 + state[0] = TauSigma[tsOff] + state[1] = TauSigma[tsOff + 1] + state[2] = TauSigma[tsOff + 2] + state[3] = TauSigma[tsOff + 3] + } + + protected open fun advanceCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + if (hi > 0) { + engineState[9] += hi + } + val oldState = engineState[8] + engineState[8] += lo + if (oldState != 0 && engineState[8] < oldState) { + engineState[9]++ + } + } + + protected open fun advanceCounter() { + if (++engineState[8] == 0) { + ++engineState[9] + } + } + + protected open fun retreatCounter(diff: Long) { + val hi = (diff ushr 32).toInt() + val lo = diff.toInt() + if (hi != 0) { + if (engineState[9] and 0xffffffffL.toInt() >= hi and 0xffffffffL.toInt()) { + engineState[9] -= hi + } else { + throw IllegalStateException("attempt to reduce counter past zero.") + } + } + if (engineState[8] and 0xffffffffL.toInt() >= lo and 0xffffffffL.toInt()) { + engineState[8] -= lo + } else { + if (engineState[9] != 0) { + --engineState[9] + engineState[8] -= lo + } else { + throw IllegalStateException("Attempt to reduce counter past zero.") + } + } + } + + protected open fun retreatCounter() { + check(engineState[8] != 0 || engineState[9] != 0) { + "Attempt to reduce counter past zero." + } + if (--engineState[8] == -1) { + --engineState[9] + } + } + + protected open fun getCounter(): Long { + return engineState[9].toLong() shl 32 or ((engineState[8] and 0xffffffffL.toInt()).toLong()) + } + + protected open fun resetCounter() { + engineState[9] = 0 + engineState[8] = engineState[9] + } + + protected open fun setKey(key: ByteArray?, iv: ByteArray) { + if (key != null) { + require(key.size == 16 || key.size == 32) { + "$algorithmName requires 128 bit or 256 bit key" + } + val tauSigmaOffset = (key.size - 16) / 4 + engineState[0] = TauSigma[tauSigmaOffset] + engineState[5] = TauSigma[tauSigmaOffset + 1] + engineState[10] = TauSigma[tauSigmaOffset + 2] + engineState[15] = TauSigma[tauSigmaOffset + 3] + + // Key + littleEndianToInt(key, 0, engineState, 1, 4) + littleEndianToInt(key, key.size - 16, engineState, 11, 4) + } + + // IV + littleEndianToInt(iv, 0, engineState, 6, 2) + } + + protected open fun generateKeyStream(output: ByteArray) { + salsaCore(rounds, engineState, x) + intToLittleEndian(x, output, 0) + } + + private fun resetLimitCounter() { + cW0 = 0 + cW1 = 0 + cW2 = 0 + } + + private fun limitExceeded(): Boolean { + if (++cW0 == 0) { + if (++cW1 == 0) { + return ++cW2 and 0x20 != 0 // 2^(32 + 32 + 6) + } + } + return false + } + + /* + * This relies on the fact len will always be positive. + */ + private fun limitExceeded(length: Int): Boolean { + cW0 += length + if (cW0 in 0 until length) { + if (++cW1 == 0) { + return ++cW2 and 0x20 != 0 // 2^(32 + 32 + 6) + } + } + return false + } + + private fun salsaCore(rounds: Int, input: IntArray, x: IntArray) { + require(input.size == 16) + require(x.size == 16) + require(rounds % 2 == 0) { "Number of rounds must be even" } + + var i = rounds + var x00 = input[0] + var x01 = input[1] + var x02 = input[2] + var x03 = input[3] + var x04 = input[4] + var x05 = input[5] + var x06 = input[6] + var x07 = input[7] + var x08 = input[8] + var x09 = input[9] + var x10 = input[10] + var x11 = input[11] + var x12 = input[12] + var x13 = input[13] + var x14 = input[14] + var x15 = input[15] + + while (i > 0) { + x04 = x04 xor x00 + x12 leftRotate (7) + x08 = x08 xor x04 + x00 leftRotate 9 + x12 = x12 xor x08 + x04 leftRotate 13 + x00 = x00 xor x12 + x08 leftRotate 18 + x09 = x09 xor x05 + x01 leftRotate 7 + x13 = x13 xor x09 + x05 leftRotate 9 + x01 = x01 xor x13 + x09 leftRotate 13 + x05 = x05 xor x01 + x13 leftRotate 18 + x14 = x14 xor x10 + x06 leftRotate 7 + x02 = x02 xor x14 + x10 leftRotate 9 + x06 = x06 xor x02 + x14 leftRotate 13 + x10 = x10 xor x06 + x02 leftRotate 18 + x03 = x03 xor x15 + x11 leftRotate 7 + x07 = x07 xor x03 + x15 leftRotate 9 + x11 = x11 xor x07 + x03 leftRotate 13 + x15 = x15 xor x11 + x07 leftRotate 18 + x01 = x01 xor x00 + x03 leftRotate 7 + x02 = x02 xor x01 + x00 leftRotate 9 + x03 = x03 xor x02 + x01 leftRotate 13 + x00 = x00 xor x03 + x02 leftRotate 18 + x06 = x06 xor x05 + x04 leftRotate 7 + x07 = x07 xor x06 + x05 leftRotate 9 + x04 = x04 xor x07 + x06 leftRotate 13 + x05 = x05 xor x04 + x07 leftRotate 18 + x11 = x11 xor x10 + x09 leftRotate 7 + x08 = x08 xor x11 + x10 leftRotate 9 + x09 = x09 xor x08 + x11 leftRotate 13 + x10 = x10 xor x09 + x08 leftRotate 18 + x12 = x12 xor x15 + x14 leftRotate 7 + x13 = x13 xor x12 + x15 leftRotate 9 + x14 = x14 xor x13 + x12 leftRotate 13 + x15 = x15 xor x14 + x13 leftRotate 18 + i -= 2 + } + x[0] = x00 + input[0] + x[1] = x01 + input[1] + x[2] = x02 + input[2] + x[3] = x03 + input[3] + x[4] = x04 + input[4] + x[5] = x05 + input[5] + x[6] = x06 + input[6] + x[7] = x07 + input[7] + x[8] = x08 + input[8] + x[9] = x09 + input[9] + x[10] = x10 + input[10] + x[11] = x11 + input[11] + x[12] = x12 + input[12] + x[13] = x13 + input[13] + x[14] = x14 + input[14] + x[15] = x15 + input[15] + } +} + +internal fun chachaCore(rounds: Int, input: IntArray, x: IntArray) { + require(input.size == 16) + require(x.size == 16) + require(rounds % 2 == 0) { "Number of rounds must be even" } + + var i = rounds + var x00 = input[0] + var x01 = input[1] + var x02 = input[2] + var x03 = input[3] + var x04 = input[4] + var x05 = input[5] + var x06 = input[6] + var x07 = input[7] + var x08 = input[8] + var x09 = input[9] + var x10 = input[10] + var x11 = input[11] + var x12 = input[12] + var x13 = input[13] + var x14 = input[14] + var x15 = input[15] + + while (i > 0) { + x00 += x04 + x12 = Integer.rotateLeft(x12 xor x00, 16) + x08 += x12 + x04 = Integer.rotateLeft(x04 xor x08, 12) + x00 += x04 + x12 = Integer.rotateLeft(x12 xor x00, 8) + x08 += x12 + x04 = Integer.rotateLeft(x04 xor x08, 7) + x01 += x05 + x13 = Integer.rotateLeft(x13 xor x01, 16) + x09 += x13 + x05 = Integer.rotateLeft(x05 xor x09, 12) + x01 += x05 + x13 = Integer.rotateLeft(x13 xor x01, 8) + x09 += x13 + x05 = Integer.rotateLeft(x05 xor x09, 7) + x02 += x06 + x14 = Integer.rotateLeft(x14 xor x02, 16) + x10 += x14 + x06 = Integer.rotateLeft(x06 xor x10, 12) + x02 += x06 + x14 = Integer.rotateLeft(x14 xor x02, 8) + x10 += x14 + x06 = Integer.rotateLeft(x06 xor x10, 7) + x03 += x07 + x15 = Integer.rotateLeft(x15 xor x03, 16) + x11 += x15 + x07 = Integer.rotateLeft(x07 xor x11, 12) + x03 += x07 + x15 = Integer.rotateLeft(x15 xor x03, 8) + x11 += x15 + x07 = Integer.rotateLeft(x07 xor x11, 7) + x00 += x05 + x15 = Integer.rotateLeft(x15 xor x00, 16) + x10 += x15 + x05 = Integer.rotateLeft(x05 xor x10, 12) + x00 += x05 + x15 = Integer.rotateLeft(x15 xor x00, 8) + x10 += x15 + x05 = Integer.rotateLeft(x05 xor x10, 7) + x01 += x06 + x12 = Integer.rotateLeft(x12 xor x01, 16) + x11 += x12 + x06 = Integer.rotateLeft(x06 xor x11, 12) + x01 += x06 + x12 = Integer.rotateLeft(x12 xor x01, 8) + x11 += x12 + x06 = Integer.rotateLeft(x06 xor x11, 7) + x02 += x07 + x13 = Integer.rotateLeft(x13 xor x02, 16) + x08 += x13 + x07 = Integer.rotateLeft(x07 xor x08, 12) + x02 += x07 + x13 = Integer.rotateLeft(x13 xor x02, 8) + x08 += x13 + x07 = Integer.rotateLeft(x07 xor x08, 7) + x03 += x04 + x14 = Integer.rotateLeft(x14 xor x03, 16) + x09 += x14 + x04 = Integer.rotateLeft(x04 xor x09, 12) + x03 += x04 + x14 = Integer.rotateLeft(x14 xor x03, 8) + x09 += x14 + x04 = Integer.rotateLeft(x04 xor x09, 7) + i -= 2 + } + x[0] = x00 + input[0] + x[1] = x01 + input[1] + x[2] = x02 + input[2] + x[3] = x03 + input[3] + x[4] = x04 + input[4] + x[5] = x05 + input[5] + x[6] = x06 + input[6] + x[7] = x07 + input[7] + x[8] = x08 + input[8] + x[9] = x09 + input[9] + x[10] = x10 + input[10] + x[11] = x11 + input[11] + x[12] = x12 + input[12] + x[13] = x13 + input[13] + x[14] = x14 + input[14] + x[15] = x15 + input[15] +} diff --git a/crypto/cipher/src/commonTest/kotlin/AESTest.kt b/crypto/cipher/src/commonTest/kotlin/AESTest.kt new file mode 100644 index 00000000..38467ee5 --- /dev/null +++ b/crypto/cipher/src/commonTest/kotlin/AESTest.kt @@ -0,0 +1,47 @@ +import io.spherelabs.anycrypto.securerandom.SecureRandom +import io.spherelabs.anycrypto.securerandom.buildSecureRandom +import io.spherelabs.crypto.cipher.AES +import io.spherelabs.crypto.cipher.Padding +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class AESTest { + + private lateinit var random: SecureRandom + + @BeforeTest + fun setup() { + random = buildSecureRandom() + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN the cipher key and iv WHEN no padding THEN checks encrypting and decrypting the plain text`() { + val padding = Padding.NoPadding + val cipherKey = random.nextBytes(32) + val iv = random.nextBytes(16) + + val plainText = random.nextBytes(16) + + val result = AES.encryptAesCbc(plainText, cipherKey, iv, padding) + val decryptedResult = AES.decryptAesCbc(result, cipherKey, iv, padding) + + assertEquals(plainText.toHexString(), decryptedResult.toHexString()) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN the cipher key and iv WHEN padding is PKCS7 THEN checks encrypting and decrypting the plain text`() { + val padding = Padding.PKCS7Padding + val cipherKey = random.nextBytes(32) + val iv = random.nextBytes(24) + + val plainText = random.nextBytes(24) + + val result = AES.encryptAesCbc(plainText, cipherKey, iv, padding) + val decryptedResult = AES.decryptAesCbc(result, cipherKey, iv, padding) + + assertEquals(plainText.toHexString(), decryptedResult.toHexString()) + } +} diff --git a/crypto/cipher/src/commonTest/kotlin/Argon2Test.kt b/crypto/cipher/src/commonTest/kotlin/Argon2Test.kt new file mode 100644 index 00000000..05ef9af3 --- /dev/null +++ b/crypto/cipher/src/commonTest/kotlin/Argon2Test.kt @@ -0,0 +1,43 @@ +import io.spherelabs.anycrypto.securerandom.SecureRandom +import io.spherelabs.anycrypto.securerandom.buildSecureRandom +import io.spherelabs.crypto.cipher.Argon2 +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class Argon2Test { + + private lateinit var random: SecureRandom + + @BeforeTest + fun setup() { + random = buildSecureRandom() + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN the argon2 WHEN generate hash value THEN equals same value`() { + val randomOutput = random.nextBytes(32) + val result = "011ed3fc11e41d40bceb3d48e3443417a57a9f7bbfce2c9360ff56956edc9fe4" + + val salt = random.nextBytes(16) + + val plainText = random.nextBytes(32) + val secret = random.nextBytes(16) + val additional = random.nextBytes(16) + + Argon2( + type = Argon2.Type.Argon2Id, + version = Argon2.Version.Ver13, + salt = salt, + secret = secret, + additional = additional, + iterations = 3, + parallelism = 4, + memory = 32, + ).encrypt(plainText, randomOutput) + + println(randomOutput.toHexString()) + assertEquals(result, randomOutput.toHexString()) + } +} diff --git a/crypto/digest/build.gradle.kts b/crypto/digest/build.gradle.kts new file mode 100644 index 00000000..a8276969 --- /dev/null +++ b/crypto/digest/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "hash" + } + } + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(projects.crypto.secureRandom) + } + } + val androidMain by getting + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "io.spherelabs.crypto.hash" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} diff --git a/crypto/digest/src/androidMain/kotlin/io/spherelabs/crypto/hash/Digest.android.kt b/crypto/digest/src/androidMain/kotlin/io/spherelabs/crypto/hash/Digest.android.kt new file mode 100644 index 00000000..3743b3a0 --- /dev/null +++ b/crypto/digest/src/androidMain/kotlin/io/spherelabs/crypto/hash/Digest.android.kt @@ -0,0 +1,8 @@ +package io.spherelabs.crypto.hash + +actual fun digest(type: Algorithm): Digest { + return when (type) { + Algorithm.Sha256 -> Sha256() + Algorithm.Sha512 -> Sha512() + } +} diff --git a/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Algorithm.kt b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Algorithm.kt new file mode 100644 index 00000000..69efd934 --- /dev/null +++ b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Algorithm.kt @@ -0,0 +1,6 @@ +package io.spherelabs.crypto.hash + +enum class Algorithm { + Sha256, + Sha512 +} diff --git a/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Digest.kt b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Digest.kt new file mode 100644 index 00000000..cb4cdbf6 --- /dev/null +++ b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Digest.kt @@ -0,0 +1,20 @@ +package io.spherelabs.crypto.hash + +/** + * The [Digest] is used for generating a fixed-size hash value or digest from variable-sized input data. + * + * It provides applications the functionality of a message digest algorithm, such as SHA-1 or SHA-256. + * + * Message digests are secure one-way hash functions that take arbitrary-sized data and output a fixed-length hash value. + * + * https://docs.oracle.com/javase/8/docs/api/java/security/MessageDigest.html + * + * + * + */ +interface Digest { + fun digest(buffer: ByteArray, offset: Int = 0, length: Int = buffer.size) + fun digest(): ByteArray +} + +expect fun digest(type: Algorithm): Digest diff --git a/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha256.kt b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha256.kt new file mode 100644 index 00000000..dc55c1e4 --- /dev/null +++ b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha256.kt @@ -0,0 +1,303 @@ +package io.spherelabs.crypto.hash + + +/* +* https://en.wikipedia.org/wiki/SHA-2 +* https://nordvpn.com/blog/sha-256/ +* https://github.com/square/okio/blob/master/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt +*/ + + + +class Sha256 : Digest { + + private var messageLength = 0L + private val unprocessed = ByteArray(64) + private var unprocessedLimit = 0 + private val words = IntArray(64) + + // Initialize hash values: + //(first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19): + private var h0 = 1779033703 + private var h1 = -1150833019 + private var h2 = 1013904242 + private var h3 = -1521486534 + private var h4 = 1359893119 + private var h5 = -1694144372 + private var h6 = 528734635 + private var h7 = 1541459225 + + override fun digest( + buffer: ByteArray, + offset: Int, + length: Int, + ) { + messageLength += length + var pos = offset + val limit = pos + length + val unprocessed = this.unprocessed + val unprocessedLimit = this.unprocessedLimit + + if (unprocessedLimit > 0) { + if (unprocessedLimit + length < 64) { + // Not enough bytes for a chunk. + buffer.copyInto(unprocessed, unprocessedLimit, pos, limit) + this.unprocessedLimit = unprocessedLimit + length + return + } + + + val consumeByteCount = 64 - unprocessedLimit + buffer.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount) + compress(unprocessed, 0) + this.unprocessedLimit = 0 + pos += consumeByteCount + } + + while (pos < limit) { + val nextPos = pos + 64 + + if (nextPos > limit) { + // Not enough bytes for a chunk. + buffer.copyInto(unprocessed, 0, pos, limit) + this.unprocessedLimit = limit - pos + return + } + + // Process a chunk. + compress(buffer, pos) + pos = nextPos + } + } + + private fun compress(input: ByteArray, pos: Int) { + val words = this.words + + var localPos = pos + for (w in 0 until 16) { + words[w] = (((input[localPos++] and 0xff) shl 24) or + ((input[localPos++] and 0xff) shl 16) or + ((input[localPos++] and 0xff) shl 8) or + ((input[localPos++] and 0xff))) + } + + for (w in 16 until 64) { + val w15 = words[w - 15] + val s0 = + ((w15 ushr 7) or (w15 shl 25)) xor ((w15 ushr 18) or (w15 shl 14)) xor (w15 ushr 3) + val w2 = words[w - 2] + val s1 = + ((w2 ushr 17) or (w2 shl 15)) xor ((w2 ushr 19) or (w2 shl 13)) xor (w2 ushr 10) + val w16 = words[w - 16] + val w7 = words[w - 7] + words[w] = w16 + s0 + w7 + s1 + } + + hash(words) + } + + private fun hash( + words: IntArray, + ) { + val localK = K + var a = h0 + var b = h1 + var c = h2 + var d = h3 + var e = h4 + var f = h5 + var g = h6 + var h = h7 + + for (i in 0 until 64) { + val s0 = ((a ushr 2) or (a shl 30)) xor + ((a ushr 13) or (a shl 19)) xor + ((a ushr 22) or (a shl 10)) + val s1 = ((e ushr 6) or (e shl 26)) xor + ((e ushr 11) or (e shl 21)) xor + ((e ushr 25) or (e shl 7)) + + val ch = (e and f) xor + (e.inv() and g) + val maj = (a and b) xor + (a and c) xor + (b and c) + + val t1 = h + s1 + ch + localK[i] + words[i] + val t2 = s0 + maj + + h = g + g = f + f = e + e = d + t1 + d = c + c = b + b = a + a = t1 + t2 + } + + h0 += a + h1 += b + h2 += c + h3 += d + h4 += e + h5 += f + h6 += g + h7 += h + } + + override fun digest(): ByteArray { + val unprocessed = this.unprocessed + var unprocessedLimit = this.unprocessedLimit + val messageLengthBits = messageLength * 8 + + unprocessed[unprocessedLimit++] = 0x80.toByte() + if (unprocessedLimit > 56) { + unprocessed.fill(0, unprocessedLimit, 64) + compress(unprocessed, 0) + unprocessed.fill(0, 0, unprocessedLimit) + } else { + unprocessed.fill(0, unprocessedLimit, 56) + } + unprocessed[56] = (messageLengthBits ushr 56).toByte() + unprocessed[57] = (messageLengthBits ushr 48).toByte() + unprocessed[58] = (messageLengthBits ushr 40).toByte() + unprocessed[59] = (messageLengthBits ushr 32).toByte() + unprocessed[60] = (messageLengthBits ushr 24).toByte() + unprocessed[61] = (messageLengthBits ushr 16).toByte() + unprocessed[62] = (messageLengthBits ushr 8).toByte() + unprocessed[63] = (messageLengthBits).toByte() + compress(unprocessed, 0) + + val a = h0 + val b = h1 + val c = h2 + val d = h3 + val e = h4 + val f = h5 + val g = h6 + val h = h7 + + reset() + + return byteArrayOf( + (a shr 24).toByte(), + (a shr 16).toByte(), + (a shr 8).toByte(), + (a).toByte(), + (b shr 24).toByte(), + (b shr 16).toByte(), + (b shr 8).toByte(), + (b).toByte(), + (c shr 24).toByte(), + (c shr 16).toByte(), + (c shr 8).toByte(), + (c).toByte(), + (d shr 24).toByte(), + (d shr 16).toByte(), + (d shr 8).toByte(), + (d).toByte(), + (e shr 24).toByte(), + (e shr 16).toByte(), + (e shr 8).toByte(), + (e).toByte(), + (f shr 24).toByte(), + (f shr 16).toByte(), + (f shr 8).toByte(), + (f).toByte(), + (g shr 24).toByte(), + (g shr 16).toByte(), + (g shr 8).toByte(), + (g).toByte(), + (h shr 24).toByte(), + (h shr 16).toByte(), + (h shr 8).toByte(), + (h).toByte(), + ) + } + + private fun reset() { + messageLength = 0L + unprocessed.fill(0) + unprocessedLimit = 0 + words.fill(0) + + h0 = 1779033703 + h1 = -1150833019 + h2 = 1013904242 + h3 = -1521486534 + h4 = 1359893119 + h5 = -1694144372 + h6 = 528734635 + h7 = 1541459225 + } + + companion object { + private val K = intArrayOf( + 1116352408, + 1899447441, + -1245643825, + -373957723, + 961987163, + 1508970993, + -1841331548, + -1424204075, + -670586216, + 310598401, + 607225278, + 1426881987, + 1925078388, + -2132889090, + -1680079193, + -1046744716, + -459576895, + -272742522, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + -1740746414, + -1473132947, + -1341970488, + -1084653625, + -958395405, + -710438585, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + -2117940946, + -1838011259, + -1564481375, + -1474664885, + -1035236496, + -949202525, + -778901479, + -694614492, + -200395387, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + -2067236844, + -1933114872, + -1866530822, + -1538233109, + -1090935817, + -965641998, + ) + } +} diff --git a/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha512.kt b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha512.kt new file mode 100644 index 00000000..47fe0757 --- /dev/null +++ b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Sha512.kt @@ -0,0 +1,340 @@ +package io.spherelabs.crypto.hash + +/** + * https://en.wikipedia.org/wiki/SHA-2 + */ + +class Sha512 : Digest { + private var messageLength = 0L + private val unprocessed = ByteArray(128) + private var unprocessedLimit = 0 + private val words = LongArray(80) + + // Initialize hash values: + //(first 64 bits of the fractional parts of the square roots of the first 8 primes 2..19): + private var h0 = 7640891576956012808L + private var h1 = -4942790177534073029L + private var h2 = 4354685564936845355L + private var h3 = -6534734903238641935L + private var h4 = 5840696475078001361L + private var h5 = -7276294671716946913L + private var h6 = 2270897969802886507L + private var h7 = 6620516959819538809L + + override fun digest( + buffer: ByteArray, + offset: Int, + length: Int, + ) { + messageLength += length + var pos = offset + val limit = pos + length + val unprocessed = this.unprocessed + val unprocessedLimit = this.unprocessedLimit + + if (unprocessedLimit > 0) { + if (unprocessedLimit + length < 128) { + // Not enough bytes for a chunk. + buffer.copyInto(unprocessed, unprocessedLimit, pos, limit) + this.unprocessedLimit = unprocessedLimit + length + return + } + + // Process a chunk combining leftover bytes and the input. + val consumeByteCount = 128 - unprocessedLimit + buffer.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount) + compress(unprocessed, 0) + this.unprocessedLimit = 0 + pos += consumeByteCount + } + + while (pos < limit) { + val nextPos = pos + 128 + + if (nextPos > limit) { + // Not enough bytes for a chunk. + buffer.copyInto(unprocessed, 0, pos, limit) + this.unprocessedLimit = limit - pos + return + } + + // Process a chunk. + compress(buffer, pos) + pos = nextPos + } + } + + private fun compress(input: ByteArray, pos: Int) { + val words = this.words + + var localPos = pos + for (w in 0 until 16) { + words[w] = ((input[localPos++].toLong() and 0xff) shl 56) or + ((input[localPos++].toLong() and 0xff) shl 48) or + ((input[localPos++].toLong() and 0xff) shl 40) or + ((input[localPos++].toLong() and 0xff) shl 32) or + ((input[localPos++].toLong() and 0xff) shl 24) or + ((input[localPos++].toLong() and 0xff) shl 16) or + ((input[localPos++].toLong() and 0xff) shl 8) or + ((input[localPos++].toLong() and 0xff)) + } + + for (i in 16 until 80) { + val w15 = words[i - 15] + val s0 = (w15 rightRotate 1) xor (w15 rightRotate 8) xor (w15 ushr 7) + val w2 = words[i - 2] + val s1 = (w2 rightRotate 19) xor (w2 rightRotate 61) xor (w2 ushr 6) + val w16 = words[i - 16] + val w7 = words[i - 7] + words[i] = w16 + s0 + w7 + s1 + } + + hash(words) + } + + private fun hash(words: LongArray) { + val localK = k + var a = h0 + var b = h1 + var c = h2 + var d = h3 + var e = h4 + var f = h5 + var g = h6 + var h = h7 + + for (i in 0 until 80) { + val s0 = (a rightRotate 28) xor (a rightRotate 34) xor (a rightRotate 39) + val s1 = (e rightRotate 14) xor (e rightRotate 18) xor (e rightRotate 41) + + val ch = (e and f) xor (e.inv() and g) + val maj = (a and b) xor (a and c) xor (b and c) + + val t1 = h + s1 + ch + localK[i] + words[i] + val t2 = s0 + maj + + h = g + g = f + f = e + e = d + t1 + d = c + c = b + b = a + a = t1 + t2 + } + + h0 += a + h1 += b + h2 += c + h3 += d + h4 += e + h5 += f + h6 += g + h7 += h + } + + /* ktlint-disable */ + override fun digest(): ByteArray { + val unprocessed = this.unprocessed + var unprocessedLimit = this.unprocessedLimit + val messageLengthBits = messageLength * 8 + + unprocessed[unprocessedLimit++] = 0x80.toByte() + if (unprocessedLimit > 112) { + unprocessed.fill(0, unprocessedLimit, 128) + compress(unprocessed, 0) + unprocessed.fill(0, 0, unprocessedLimit) + } else { + unprocessed.fill(0, unprocessedLimit, 120) + } + unprocessed[120] = (messageLengthBits ushr 56).toByte() + unprocessed[121] = (messageLengthBits ushr 48).toByte() + unprocessed[122] = (messageLengthBits ushr 40).toByte() + unprocessed[123] = (messageLengthBits ushr 32).toByte() + unprocessed[124] = (messageLengthBits ushr 24).toByte() + unprocessed[125] = (messageLengthBits ushr 16).toByte() + unprocessed[126] = (messageLengthBits ushr 8).toByte() + unprocessed[127] = (messageLengthBits).toByte() + compress(unprocessed, 0) + + val a = h0 + val b = h1 + val c = h2 + val d = h3 + val e = h4 + val f = h5 + val g = h6 + val h = h7 + + reset() + + return byteArrayOf( + (a shr 56).toByte(), + (a shr 48).toByte(), + (a shr 40).toByte(), + (a shr 32).toByte(), + (a shr 24).toByte(), + (a shr 16).toByte(), + (a shr 8).toByte(), + (a).toByte(), + (b shr 56).toByte(), + (b shr 48).toByte(), + (b shr 40).toByte(), + (b shr 32).toByte(), + (b shr 24).toByte(), + (b shr 16).toByte(), + (b shr 8).toByte(), + (b).toByte(), + (c shr 56).toByte(), + (c shr 48).toByte(), + (c shr 40).toByte(), + (c shr 32).toByte(), + (c shr 24).toByte(), + (c shr 16).toByte(), + (c shr 8).toByte(), + (c).toByte(), + (d shr 56).toByte(), + (d shr 48).toByte(), + (d shr 40).toByte(), + (d shr 32).toByte(), + (d shr 24).toByte(), + (d shr 16).toByte(), + (d shr 8).toByte(), + (d).toByte(), + (e shr 56).toByte(), + (e shr 48).toByte(), + (e shr 40).toByte(), + (e shr 32).toByte(), + (e shr 24).toByte(), + (e shr 16).toByte(), + (e shr 8).toByte(), + (e).toByte(), + (f shr 56).toByte(), + (f shr 48).toByte(), + (f shr 40).toByte(), + (f shr 32).toByte(), + (f shr 24).toByte(), + (f shr 16).toByte(), + (f shr 8).toByte(), + (f).toByte(), + (g shr 56).toByte(), + (g shr 48).toByte(), + (g shr 40).toByte(), + (g shr 32).toByte(), + (g shr 24).toByte(), + (g shr 16).toByte(), + (g shr 8).toByte(), + (g).toByte(), + (h shr 56).toByte(), + (h shr 48).toByte(), + (h shr 40).toByte(), + (h shr 32).toByte(), + (h shr 24).toByte(), + (h shr 16).toByte(), + (h shr 8).toByte(), + (h).toByte(), + ) + } + /* ktlint-enable */ + + private fun reset() { + messageLength = 0L + unprocessed.fill(0) + unprocessedLimit = 0 + words.fill(0) + + h0 = 7640891576956012808L + h1 = -4942790177534073029L + h2 = 4354685564936845355L + h3 = -6534734903238641935L + h4 = 5840696475078001361L + h5 = -7276294671716946913L + h6 = 2270897969802886507L + h7 = 6620516959819538809L + } + + companion object { + private val k = longArrayOf( + 4794697086780616226L, + 8158064640168781261L, + -5349999486874862801L, + -1606136188198331460L, + 4131703408338449720L, + 6480981068601479193L, + -7908458776815382629L, + -6116909921290321640L, + -2880145864133508542L, + 1334009975649890238L, + 2608012711638119052L, + 6128411473006802146L, + 8268148722764581231L, + -9160688886553864527L, + -7215885187991268811L, + -4495734319001033068L, + -1973867731355612462L, + -1171420211273849373L, + 1135362057144423861L, + 2597628984639134821L, + 3308224258029322869L, + 5365058923640841347L, + 6679025012923562964L, + 8573033837759648693L, + -7476448914759557205L, + -6327057829258317296L, + -5763719355590565569L, + -4658551843659510044L, + -4116276920077217854L, + -3051310485924567259L, + 489312712824947311L, + 1452737877330783856L, + 2861767655752347644L, + 3322285676063803686L, + 5560940570517711597L, + 5996557281743188959L, + 7280758554555802590L, + 8532644243296465576L, + -9096487096722542874L, + -7894198246740708037L, + -6719396339535248540L, + -6333637450476146687L, + -4446306890439682159L, + -4076793802049405392L, + -3345356375505022440L, + -2983346525034927856L, + -860691631967231958L, + 1182934255886127544L, + 1847814050463011016L, + 2177327727835720531L, + 2830643537854262169L, + 3796741975233480872L, + 4115178125766777443L, + 5681478168544905931L, + 6601373596472566643L, + 7507060721942968483L, + 8399075790359081724L, + 8693463985226723168L, + -8878714635349349518L, + -8302665154208450068L, + -8016688836872298968L, + -6606660893046293015L, + -4685533653050689259L, + -4147400797238176981L, + -3880063495543823972L, + -3348786107499101689L, + -1523767162380948706L, + -757361751448694408L, + 500013540394364858L, + 748580250866718886L, + 1242879168328830382L, + 1977374033974150939L, + 2944078676154940804L, + 3659926193048069267L, + 4368137639120453308L, + 4836135668995329356L, + 5532061633213252278L, + 6448918945643986474L, + 6902733635092675308L, + 7801388544844847127L, + ) + } +} diff --git a/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Utils.kt b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Utils.kt new file mode 100644 index 00000000..8ff945c5 --- /dev/null +++ b/crypto/digest/src/commonMain/kotlin/io/spherelabs/crypto/hash/Utils.kt @@ -0,0 +1,10 @@ +package io.spherelabs.crypto.hash + + +@Suppress("NOTHING_TO_INLINE") // Syntactic sugar. +internal inline infix fun Byte.and(other: Int): Int = toInt() and other + + +internal inline infix fun Long.rightRotate(bitCount: Int): Long { + return (this ushr bitCount) or (this shl (64 - bitCount)) +} diff --git a/crypto/digest/src/commonTest/kotlin/Sha256Test.kt b/crypto/digest/src/commonTest/kotlin/Sha256Test.kt new file mode 100644 index 00000000..6f57493a --- /dev/null +++ b/crypto/digest/src/commonTest/kotlin/Sha256Test.kt @@ -0,0 +1,43 @@ +import io.spherelabs.anycrypto.securerandom.SecureRandom +import io.spherelabs.anycrypto.securerandom.buildSecureRandom +import io.spherelabs.crypto.hash.Algorithm +import io.spherelabs.crypto.hash.Digest +import io.spherelabs.crypto.hash.digest +import kotlin.test.* + +class Sha256Test { + + private lateinit var sha256: Digest + private lateinit var random: SecureRandom + + @BeforeTest + fun setup() { + sha256 = digest(Algorithm.Sha256) + random = buildSecureRandom() + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN sha256 WHEN the buffer is anypass THEN it returns hash`() { + val buffer = random.nextBytes(10) + + sha256.digest(buffer) + + val expected = sha256.digest() + + assertNotNull(expected.toHexString()) + assertTrue(expected.toHexString().length == 64) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN sha512 WHEN the buffer is anypass THEN the length equals 64`() { + val buffer = random.nextBytes(10) + + sha256.digest(buffer) + + val expected = sha256.digest() + + assertTrue(expected.toHexString().length == 64) + } +} diff --git a/crypto/digest/src/commonTest/kotlin/Sha512Test.kt b/crypto/digest/src/commonTest/kotlin/Sha512Test.kt new file mode 100644 index 00000000..ced337f4 --- /dev/null +++ b/crypto/digest/src/commonTest/kotlin/Sha512Test.kt @@ -0,0 +1,42 @@ +import io.spherelabs.anycrypto.securerandom.SecureRandom +import io.spherelabs.anycrypto.securerandom.buildSecureRandom +import io.spherelabs.crypto.hash.Algorithm +import io.spherelabs.crypto.hash.Digest +import io.spherelabs.crypto.hash.digest +import kotlin.test.* + +class Sha512Test { + + private lateinit var sha512: Digest + private lateinit var random: SecureRandom + + @BeforeTest + fun setup() { + sha512 = digest(Algorithm.Sha512) + random = buildSecureRandom() + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN sha512 WHEN the buffer is anypass THEN it returns hash`() { + val buffer = random.nextBytes(10) + + sha512.digest(buffer) + + val expected = sha512.digest() + + assertNotNull(expected.toHexString()) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `GIVEN sha512 WHEN the buffer is anypass THEN the length equals 128`() { + val buffer = random.nextBytes(10) + + sha512.digest(buffer) + + val expected = sha512.digest() + + assertTrue(expected.toHexString().length == 128) + } +} diff --git a/crypto/digest/src/iosMain/kotlin/io/spherelabs/crypto/hash/Digest.ios.kt b/crypto/digest/src/iosMain/kotlin/io/spherelabs/crypto/hash/Digest.ios.kt new file mode 100644 index 00000000..3743b3a0 --- /dev/null +++ b/crypto/digest/src/iosMain/kotlin/io/spherelabs/crypto/hash/Digest.ios.kt @@ -0,0 +1,8 @@ +package io.spherelabs.crypto.hash + +actual fun digest(type: Algorithm): Digest { + return when (type) { + Algorithm.Sha256 -> Sha256() + Algorithm.Sha512 -> Sha512() + } +} diff --git a/crypto/rsa/build.gradle.kts b/crypto/rsa/build.gradle.kts new file mode 100644 index 00000000..679dd726 --- /dev/null +++ b/crypto/rsa/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "rsa" + } + } + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "io.spherelabs.crypto.rsa" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} \ No newline at end of file diff --git a/crypto/rsa/src/androidMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt b/crypto/rsa/src/androidMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt new file mode 100644 index 00000000..89eb60b1 --- /dev/null +++ b/crypto/rsa/src/androidMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt @@ -0,0 +1,7 @@ +package io.spherelabs.crypto.rsa + +class AndroidPlatform : Platform { + override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Greeting.kt b/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Greeting.kt new file mode 100644 index 00000000..e7c3038d --- /dev/null +++ b/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Greeting.kt @@ -0,0 +1,9 @@ +package io.spherelabs.crypto.rsa + +class Greeting { + private val platform: Platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} \ No newline at end of file diff --git a/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt b/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt new file mode 100644 index 00000000..73816854 --- /dev/null +++ b/crypto/rsa/src/commonMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt @@ -0,0 +1,7 @@ +package io.spherelabs.crypto.rsa + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform \ No newline at end of file diff --git a/crypto/rsa/src/iosMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt b/crypto/rsa/src/iosMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt new file mode 100644 index 00000000..73d57f97 --- /dev/null +++ b/crypto/rsa/src/iosMain/kotlin/io/spherelabs/crypto/rsa/Platform.kt @@ -0,0 +1,9 @@ +package io.spherelabs.crypto.rsa + +import platform.UIKit.UIDevice + +class IOSPlatform: Platform { + override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} + +actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/anycrypto/secure-random/build.gradle.kts b/crypto/secure-random/build.gradle.kts similarity index 100% rename from anycrypto/secure-random/build.gradle.kts rename to crypto/secure-random/build.gradle.kts diff --git a/anycrypto/secure-random/src/androidMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.android.kt b/crypto/secure-random/src/androidMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.android.kt similarity index 100% rename from anycrypto/secure-random/src/androidMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.android.kt rename to crypto/secure-random/src/androidMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.android.kt diff --git a/anycrypto/secure-random/src/commonMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.kt b/crypto/secure-random/src/commonMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.kt similarity index 100% rename from anycrypto/secure-random/src/commonMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.kt rename to crypto/secure-random/src/commonMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.kt diff --git a/anycrypto/secure-random/src/commonTest/kotlin/SecureRandomTest.kt b/crypto/secure-random/src/commonTest/kotlin/SecureRandomTest.kt similarity index 100% rename from anycrypto/secure-random/src/commonTest/kotlin/SecureRandomTest.kt rename to crypto/secure-random/src/commonTest/kotlin/SecureRandomTest.kt diff --git a/anycrypto/secure-random/src/iosMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.ios.kt b/crypto/secure-random/src/iosMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.ios.kt similarity index 100% rename from anycrypto/secure-random/src/iosMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.ios.kt rename to crypto/secure-random/src/iosMain/kotlin/io/spherelabs/anycrypto/securerandom/SecureRandom.ios.kt diff --git a/crypto/uuid/build.gradle.kts b/crypto/uuid/build.gradle.kts new file mode 100644 index 00000000..39111023 --- /dev/null +++ b/crypto/uuid/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "uuid" + } + } + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "io.spherelabs.crypto.uuid" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} \ No newline at end of file diff --git a/crypto/uuid/src/androidMain/kotlin/io/spherelabs/crypto/uuid/uuid.android.kt b/crypto/uuid/src/androidMain/kotlin/io/spherelabs/crypto/uuid/uuid.android.kt new file mode 100644 index 00000000..3de3e17b --- /dev/null +++ b/crypto/uuid/src/androidMain/kotlin/io/spherelabs/crypto/uuid/uuid.android.kt @@ -0,0 +1,29 @@ +package io.spherelabs.crypto.uuid + +import java.util.UUID + +internal class AndroidUuid( + private val uuid: UUID, +) : Uuid { + + override val mostSignificantBits: Long = uuid.mostSignificantBits + override val leastSignificantBits: Long = uuid.leastSignificantBits + + override fun toString(): String { + return uuid.toString() + } + + override fun isZero(): Boolean { + return leastSignificantBits == 0L && mostSignificantBits == 0L + } + + override fun of(mostSigBits: Long, leastSigBits: Long): Uuid { + return UUID(mostSigBits, leastSigBits).toAndroidUuid() + } +} + +internal fun UUID.toAndroidUuid(): Uuid { + return AndroidUuid(this) +} + +actual fun uuid4(): Uuid = AndroidUuid(UUID.randomUUID()) diff --git a/crypto/uuid/src/commonMain/kotlin/io/spherelabs/crypto/uuid/uuid.kt b/crypto/uuid/src/commonMain/kotlin/io/spherelabs/crypto/uuid/uuid.kt new file mode 100644 index 00000000..18bf014e --- /dev/null +++ b/crypto/uuid/src/commonMain/kotlin/io/spherelabs/crypto/uuid/uuid.kt @@ -0,0 +1,19 @@ +package io.spherelabs.crypto.uuid + + +/** + * UUID stands for Universally Unique Identifier + * UUIDs are 36 character strings containing numbers, letters and dashes. + * UUIDs are designed to be globally unique. + * UUIDs are written in 5 groups of hexadecimal digits separated by hyphens. + * The length of each group is: 8-4-4-4-12. UUIDs are fixed length. + */ +interface Uuid { + public val mostSignificantBits: Long + public val leastSignificantBits: Long + public fun isZero(): Boolean + public fun of(mostSigBits: Long, leastSigBits: Long): Uuid +} + +expect fun uuid4(): Uuid + diff --git a/crypto/uuid/src/commonTest/kotlin/UuidTest.kt b/crypto/uuid/src/commonTest/kotlin/UuidTest.kt new file mode 100644 index 00000000..4e1675ba --- /dev/null +++ b/crypto/uuid/src/commonTest/kotlin/UuidTest.kt @@ -0,0 +1,28 @@ +import io.spherelabs.crypto.uuid.Uuid +import io.spherelabs.crypto.uuid.uuid4 +import kotlin.test.* + +class UuidTest { + + private lateinit var uuid: Uuid + + @BeforeTest + fun setup() { + uuid = uuid4() + } + + @Test + fun `GIVEN create uuid WHEN generate random uuid THEN checks it is not null`() { + val result = uuid.toString() + + assertNotNull(result) + } + + @Test + fun `GIVEN create uuid WHEN generate random uuid THEN consists 32 hexadecimal digits`() { + val result = uuid.toString() + + assertEquals(36, result.length) + assertTrue { !uuid.isZero() } + } +} diff --git a/crypto/uuid/src/iosMain/kotlin/io/spherelabs/crypto/uuid/uuid.ios.kt b/crypto/uuid/src/iosMain/kotlin/io/spherelabs/crypto/uuid/uuid.ios.kt new file mode 100644 index 00000000..237086a1 --- /dev/null +++ b/crypto/uuid/src/iosMain/kotlin/io/spherelabs/crypto/uuid/uuid.ios.kt @@ -0,0 +1,23 @@ +package io.spherelabs.crypto.uuid + +internal class IosUuid : Uuid { + override val mostSignificantBits: Long + get() = TODO("Not yet implemented") + override val leastSignificantBits: Long + get() = TODO("Not yet implemented") + + override fun toString(): String { + TODO("Not yet implemented") + } + + override fun isZero(): Boolean { + TODO("Not yet implemented") + } + + override fun of(mostSigBits: Long, leastSigBits: Long): Uuid { + TODO("Not yet implemented") + } +} + + +actual fun uuid4(): Uuid = IosUuid() diff --git a/settings.gradle.kts b/settings.gradle.kts index f05751ba..bba3ee56 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -121,6 +121,11 @@ include(":features:passwordhistory:passwordhistory-impl") include(":features:passwordhistory:passwordhistory-di") include(":manager:sshkey") -include(":anycrypto") -include(":anycrypto:secure-random") -include(":anycrypto:cipher") + +include(":crypto") +include(":crypto:secure-random") +include(":crypto:uuid") +include(":crypto:digest") +include(":crypto:cipher") +include(":crypto:rsa") +include(":tinypass") diff --git a/tinypass/build.gradle.kts b/tinypass/build.gradle.kts new file mode 100644 index 00000000..26443023 --- /dev/null +++ b/tinypass/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "tinypass" + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("com.squareup.okio:okio:3.7.0") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "io.spherelabs.crypto.tinypass" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} diff --git a/tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/signature/Signature.kt b/tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/signature/Signature.kt new file mode 100644 index 00000000..921f1b9e --- /dev/null +++ b/tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/signature/Signature.kt @@ -0,0 +1,36 @@ +package io.spherelabs.crypto.tinypass.database.signature + +import okio.BufferedSink +import okio.BufferedSource +import okio.ByteString + +/** + * Represents the header of .kdb and .kdbx file formats, consisting of two 4-byte fields for file signatures. + * + * @property first The first 4-byte signature. It will always have a value of 0x9AA2D903. + * @property second The second 4-byte signature. It can have (for now) 3 different value. + */ + +data class Signature( + val first: ByteString, + val second: ByteString, +) { + + fun writeTo(sink: BufferedSink) = writeInternal(sink) + + private fun writeInternal(sink: BufferedSink) = with(sink) { + write(first) + write(second) + } + + companion object { + val FirstSignature = ByteString.of(0x03, 0xd9.toByte(), 0xa2.toByte(), 0x9a.toByte()) + val SecondSignature = ByteString.of(0x67, 0xfb.toByte(), 0x4b, 0xb5.toByte()) + val Default = Signature(FirstSignature, SecondSignature) + + fun readFrom(source: BufferedSource) = Signature( + first = source.readByteString(4), + second = source.readByteString(4), + ) + } +}