diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f120c3d37..a582ddf6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -119,7 +119,7 @@ coil-gif = { group = "io.coil-kt.coil3", name = "coil-gif", version.ref = "coil" coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" } coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } com-ditchoom-buffer-compression = { module = "com.ditchoom:buffer-compression", version.ref = "comDitchoomBufferCompression" } -dev-whyoleg-cryptography-provider-apple = { module = "dev.whyoleg.cryptography:cryptography-provider-apple", version.ref = "devWhyolegCryptography" } +dev-whyoleg-cryptography-provider-apple-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "devWhyolegCryptography" } drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 051030361..3262cc64e 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -230,7 +230,7 @@ kotlin { implementation(libs.charlietap.cachemap) implementation(libs.net.thauvin.erik.urlencoder.lib) implementation(libs.com.ditchoom.buffer.compression) - implementation(libs.dev.whyoleg.cryptography.provider.apple) + implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt index aed72dad3..eb065059a 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.ios.kt @@ -20,23 +20,47 @@ */ package com.vitorpamplona.quartz.utils.ciphers +import com.vitorpamplona.quartz.utils.Log +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES + actual class AESGCM actual constructor( actual val keyBytes: ByteArray, actual val nonce: ByteArray, ) : NostrCipher { - actual override fun name(): String { - TODO("Not yet implemented") - } + private val provider = CryptographyProvider.Default + private val aesGcm = provider.get(AES.GCM) - actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray { - TODO("Not yet implemented") - } + private val keyDecoder = + aesGcm + .keyDecoder() + .decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) - actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray { - TODO("Not yet implemented") - } + private fun cipher() = keyDecoder.cipher() - actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? { - TODO("Not yet implemented") - } + actual override fun name(): String = "aes-gcm" + + @OptIn(DelicateCryptographyApi::class) + actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray = + with(cipher()) { + encryptWithIvBlocking(nonce, bytesToEncrypt) + } + + @OptIn(DelicateCryptographyApi::class) + actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray = + with(cipher()) { + decryptWithIvBlocking(nonce, bytesToDecrypt) + } + + @OptIn(DelicateCryptographyApi::class) + actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? = + try { + with(cipher()) { + decryptWithIvBlocking(nonce, bytesToDecrypt) + } + } catch (e: Exception) { + Log.w("AESGCM", "Failed to decrypt", e) + null + } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt index 95d552b83..00c7bda27 100644 --- a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt @@ -20,10 +20,15 @@ */ package com.vitorpamplona.quartz +import dev.whyoleg.cryptography.CryptographyProviderApi +import dev.whyoleg.cryptography.providers.base.toByteArray import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.toKString +import kotlinx.io.files.FileNotFoundException +import platform.Foundation.NSData import platform.Foundation.NSString import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.dataWithContentsOfFile import platform.Foundation.stringWithContentsOfFile import platform.posix.getenv @@ -34,4 +39,11 @@ actual class TestResourceLoader { val filePath = "$resourceDir/$file" return NSString.stringWithContentsOfFile(filePath, encoding = NSUTF8StringEncoding, error = null) as String } + + @OptIn(ExperimentalForeignApi::class, CryptographyProviderApi::class) + fun loadFileData(file: String): ByteArray { + val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString() + val filePath = "$resourceDir/$file" + return NSData.dataWithContentsOfFile(filePath)?.toByteArray() ?: throw FileNotFoundException("Resource $file was not found.") + } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt new file mode 100644 index 000000000..e41063c7b --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm + +import com.vitorpamplona.quartz.TestResourceLoader +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.utils.ciphers.AESGCM +import kotlin.test.Test +import kotlin.test.assertEquals + +class AESGCMTest { + val decryptionNonce = "01e77c94bd5aba3e3cbb69594e7ba07c" + val decryptionKey = "c128ecffab90ee7810e3df08e7fb2cc39a8d40f24201f48b2b36e23b34ac50ee" + + val cipher = + AESGCM( + decryptionKey.hexToByteArray(), + decryptionNonce.encodeToByteArray(), + ) + + @Test + fun encryptDecrypt() { + val encrypted = cipher.encrypt("Testing".encodeToByteArray()) + val decrypted = cipher.decrypt(encrypted) + + assertEquals("Testing", decrypted.decodeToString()) + } + + @Test + fun imageTest() { + val image = + TestResourceLoader().loadFileData("ovxxk2vz.jpg") + + val decrypted = cipher.decrypt(image) + + assertEquals(44201, decrypted.size) + } + + @Test + fun videoTest2() { + val myCipher = + AESGCM( + "373d19850ebc8ed5b0fefcca5cd6f27fde9cb6ac54fd32f6b4fad9d68ebe8ee0".hexToByteArray(), + "95e67b6874784a54299b58b8990499bd".hexToByteArray(), + ) + + val encrypted = + TestResourceLoader().loadFileData("trouble_video") + + val decrypted = myCipher.decrypt(encrypted) + + assertEquals(1277122, decrypted.size) + } +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt new file mode 100644 index 000000000..bc2dfa1c7 --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm + +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.collections.immutable.persistentSetOf +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChatroomKeyTest { + @Test + fun testEquals() { + val k1 = ChatroomKey(persistentSetOf("Key1", "Key2")) + val k2 = ChatroomKey(persistentSetOf("Key1", "Key2")) + + assertEquals(k1, k2) + assertEquals(k1.hashCode(), k2.hashCode()) + } +} diff --git a/quartz/src/iosTest/resources/ovxxk2vz.jpg b/quartz/src/iosTest/resources/ovxxk2vz.jpg new file mode 100644 index 000000000..bc96672af Binary files /dev/null and b/quartz/src/iosTest/resources/ovxxk2vz.jpg differ diff --git a/quartz/src/iosTest/resources/trouble_video b/quartz/src/iosTest/resources/trouble_video new file mode 100644 index 000000000..401d5e1df Binary files /dev/null and b/quartz/src/iosTest/resources/trouble_video differ