diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7344dc90..83a230a78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ composeBom = "2025.12.01" composeRuntimeAnnotation = "1.10.0" coreKtx = "1.17.0" datastore = "1.2.0" +devWhyolegCryptography = "0.5.0" espressoCore = "3.7.0" firebaseBom = "34.7.0" fragmentKtx = "1.8.9" @@ -119,6 +120,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" } 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 27c80e4e8..8ee244f2a 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -253,6 +253,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) } } diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt new file mode 100644 index 000000000..255c1bbb3 --- /dev/null +++ b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt @@ -0,0 +1,81 @@ +/** + * 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 nip06KeyDerivationCommon + +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip06KeyDerivation.Bip32SeedDerivation +import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics +import com.vitorpamplona.quartz.nip06KeyDerivation.KeyPath +import kotlin.test.Test +import kotlin.test.assertEquals + +class Bip32SeedDerivationCommonTest { + val seedDerivation = Bip32SeedDerivation() + + val masterBitcoin = + seedDerivation.generate( + Bip39Mnemonics.toSeed("gun please vital unable phone catalog explain raise erosion zoo truly exist", ""), + ) + + val nostrMnemonic0 = + seedDerivation.generate( + Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", ""), + ) + + val nostrMnemonic1 = + seedDerivation.generate( + Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", ""), + ) + + @Test + fun restoreBIP44Wallet() { + val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/44'/1'/0'")) + assertEquals("50b3e7905c642309c8a8b73df5a49757a10f2bebb5804571b9db9004cce8a190", privateKey.toHexKey()) + } + + @Test + fun restoreBIP49Wallet() { + val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'")) + assertEquals("154c02c0b66899291a19012207642ba096a2d3ebf51baf153c9495976feb1b30", privateKey.toHexKey()) + } + + @Test + fun restoreBIP84Wallet() { + val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'")) + assertEquals("53e8c09a0e3ddcd8d68821c1e99e823966e99df91fb253e1f453a443ba543cb2", privateKey.toHexKey()) + } + + @Test + fun testGenerateMasterKeyForNostrMnemonics0() { + assertEquals( + "dbbcc0e112894d1430d5bc348d1bd72e8ac339952702be1fe572de80fe1b7fcb", + nostrMnemonic0.secretkeybytes.toHexKey(), + ) + } + + @Test + fun testGenerateMasterKeyForNostrMnemonics1() { + assertEquals( + "d58d40d5724435552fa442350b75e0ff95a19d990e908e3a516bcc88f780108f", + nostrMnemonic1.secretkeybytes.toHexKey(), + ) + } +} diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt new file mode 100644 index 000000000..97a818ac3 --- /dev/null +++ b/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt @@ -0,0 +1,68 @@ +/** + * 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 nip06KeyDerivationCommon + +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +class Nip06CommonTest { + val nip06 = Nip06() + + // private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a + // nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp + private val menemonic0 = "leader monkey parrot ring guide accident before fence cannon height naive bean" + + // private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add + // nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel + private val menemonic1 = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade" + + // private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add + // nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel + private val snortTest = "clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless" + + @Test + fun fromSeedNip06TestVector0() { + val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic0).toHexKey() + assertEquals("7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a", privateKeyHex) + + val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic0, 21).toHexKey() + assertEquals("576390ec69951fcfbf159f2aac0965bb2e6d7a07da2334992af3225c57eaefca", privateKeyHex21) + } + + @Test + fun fromSeedNip06TestVector1() { + val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic1).toHexKey() + assertEquals("c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add", privateKeyHex) + + val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic1, 42).toHexKey() + assertEquals("ad993054383da74e955f8b86346365b5ffd6575992e1de3738dda9f94407052b", privateKeyHex21) + } + + @Test + @Ignore() + fun fromSeedNip06FromSnort() { + val privateKeyNsec = nip06.privateKeyFromMnemonic(snortTest).toHexKey() + assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec) + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt index 0f5b326a4..b8cfaeb16 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt @@ -20,37 +20,60 @@ */ package com.vitorpamplona.quartz.utils.mac +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.algorithms.HMAC +import dev.whyoleg.cryptography.algorithms.SHA256 +import dev.whyoleg.cryptography.algorithms.SHA512 +import dev.whyoleg.cryptography.providers.apple.Apple + actual class MacInstance actual constructor( algorithm: String, key: ByteArray, ) { + private val cryptoProvider = CryptographyProvider.Apple + + private var internalMacInstance: HMAC.Key = + cryptoProvider + .get(HMAC) + .keyDecoder(digestForAlgorithm(algorithm)) + .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) + + private val hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction() + actual fun init( key: ByteArray, algorithm: String, ) { - TODO("Not yet implemented") + internalMacInstance = + cryptoProvider + .get(HMAC) + .keyDecoder(digestForAlgorithm(algorithm)) + .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) } - actual fun getMacLength(): Int { - TODO("Not yet implemented") - } + actual fun getMacLength(): Int = hmacSignFunction.signIntoByteArray(internalMacInstance.encodeToByteArrayBlocking(HMAC.Key.Format.RAW)) actual fun update(array: ByteArray) { - TODO("Not yet implemented") + hmacSignFunction.update(array) } actual fun update(byte: Byte) { - TODO("Not yet implemented") + hmacSignFunction.update(byteArrayOf(byte)) } - actual fun doFinal(): ByteArray { - TODO("Not yet implemented") - } + actual fun doFinal(): ByteArray = hmacSignFunction.signToByteArray() actual fun doFinal( output: ByteArray, offset: Int, ) { - TODO("Not yet implemented") + hmacSignFunction.signIntoByteArray(output, offset) } + + private fun digestForAlgorithm(algorithm: String) = + when (algorithm) { + "HmacSHA256" -> SHA256 + "HmacSHA512" -> SHA512 + else -> error("Algorithm is not yet supported.") + } }