diff --git a/base-asymmetric-encryption/build.gradle.kts b/base-asymmetric-encryption/build.gradle.kts index 0caae60ae..f83f95b4b 100644 --- a/base-asymmetric-encryption/build.gradle.kts +++ b/base-asymmetric-encryption/build.gradle.kts @@ -136,9 +136,10 @@ kotlin { dependencies { implementation(project(":utils")) implementation(project(":secure-random")) + implementation(project(":hashing")) implementation("com.ionspin.kotlin:bignum:0.3.7") implementation(project(":base64")) - implementation(project(":hashing")) + implementation("org.kotlincrypto.macs:hmac-sha2:0.3.0") } } val commonTest by getting { diff --git a/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKey.kt b/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKey.kt new file mode 100644 index 000000000..64bf1479f --- /dev/null +++ b/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKey.kt @@ -0,0 +1,134 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.toBigInteger +import io.iohk.atala.prism.apollo.utils.ECConfig +import io.iohk.atala.prism.apollo.utils.ECPrivateKeyDecodingException +import io.iohk.atala.prism.apollo.utils.KMMECSecp256k1PrivateKey +import org.kotlincrypto.macs.hmac.sha2.HmacSHA512 +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Represents and HDKey with its derive methods + */ +@JsExport +class HDKey( + val privateKey: ByteArray? = null, + val publicKey: ByteArray? = null, + val chainCode: ByteArray? = null, + val depth: Int = 0, + val childIndex: BigInteger = BigInteger(0), +) { + + @JsName("InitFromSeed") + constructor(seed: ByteArray, depth: Int, childIndex: BigInteger) : this( + privateKey = sha512(key = "Bitcoin seed".encodeToByteArray(), input = seed).sliceArray(IntRange(0, 31)), + chainCode = sha512("Bitcoin seed".encodeToByteArray(), seed).sliceArray(32 until seed.size), + depth = depth, + childIndex = childIndex + ) { + require(seed.size == 64) { + "Seed expected byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}" + } + } + + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + fun derive(path: String): HDKey { + if (!path.matches(Regex("^[mM].*"))) { + throw Error("Path must start with \"m\" or \"M\"") + } + if (Regex("^[mM]'?$").matches(path)) { + return this + } + val parts = path.replace(Regex("^[mM]'?/"), "").split("/") + var child = this + for (c in parts) { + val m = Regex("^(\\d+)('?)$").find(c)?.groupValues + if (m == null || m.size != 3) { + throw Error("Invalid child index: $c") + } + val idx = m[1].toBigInteger() + if (idx >= HARDENED_OFFSET) { + throw Error("Invalid index") + } + val finalIdx = if (m[2] == "'") idx + HARDENED_OFFSET else idx + child = child.deriveChild(finalIdx) + } + return child + } + + /** + * Method to derive an HDKey child by index + * + * @param index value used to derive a key + */ + fun deriveChild(index: BigInteger): HDKey { + if (chainCode == null) { + throw Exception("No chainCode set") + } + + val data = if (index >= HARDENED_OFFSET) { + val priv = privateKey ?: throw Error("Could not derive hardened child key") + byteArrayOf(0) + priv + index.toByteArray() + } else { + throw Exception("Not supported") + } + + val I = sha512(chainCode, data) + val childTweak = I.sliceArray(IntRange(0, 31)) + val newChainCode = I.sliceArray(32 until I.size) + + if (!isValidPrivateKey(childTweak)) { + throw ECPrivateKeyDecodingException("Expected encoded byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}, but got ${data.size}") + } + + val opt = HDKeyOptions( + versions = Pair(BITCOIN_VERSIONS_PRIVATE, BITCOIN_VERSIONS_PUBLIC), + chainCode = newChainCode, + depth = depth + 1, + parentFingerprint = null, + index = index + ) + + return try { + opt.privateKey = KMMECSecp256k1PrivateKey.tweak(privateKey, childTweak).raw + return HDKey( + privateKey = opt.privateKey, + chainCode = opt.chainCode, + depth = opt.depth, + childIndex = opt.index + ) + } catch (err: Error) { + this.deriveChild(index + 1) + } + } + + fun getKMMSecp256k1PrivateKey(): KMMECSecp256k1PrivateKey { + privateKey?.let { + return KMMECSecp256k1PrivateKey.secp256k1FromByteArray(privateKey) + } ?: throw Exception("Private key not available") + } + + private fun isValidPrivateKey(data: ByteArray): Boolean { + return (data.size == ECConfig.PRIVATE_KEY_BYTE_SIZE) + } + + companion object { + const val HARDENED_OFFSET = 2147483648 + const val BITCOIN_VERSIONS_PRIVATE = 0x0488ade4 + const val BITCOIN_VERSIONS_PUBLIC = 0x0488b21e + const val FINGERPRINT = 0 + const val MASTER_SECRET = "Atala Prism" + + fun sha512(key: ByteArray, input: ByteArray): ByteArray { + val sha512 = HmacSHA512(key) + sha512.update(input) + return sha512.doFinal() + } + } +} diff --git a/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyOptions.kt b/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyOptions.kt new file mode 100644 index 000000000..f9d649580 --- /dev/null +++ b/base-asymmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyOptions.kt @@ -0,0 +1,13 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.BigInteger + +data class HDKeyOptions( + val versions: Pair, + val chainCode: ByteArray, + val depth: Int, + val parentFingerprint: Int?, + val index: BigInteger, + var privateKey: ByteArray? = null, + var publicKey: ByteArray? = null +) diff --git a/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyTest.kt b/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyTest.kt new file mode 100644 index 000000000..794c59d09 --- /dev/null +++ b/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyTest.kt @@ -0,0 +1,127 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.BigInteger +import io.iohk.atala.prism.apollo.base64.base64UrlDecodedBytes +import io.iohk.atala.prism.apollo.derivation.HDKey.Companion.HARDENED_OFFSET +import kotlin.random.Random +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class HDKeyTest { + + lateinit var seed: ByteArray + lateinit var privateKey: String + lateinit var derivedPrivateKey: String + var childIndex = BigInteger(0) + + @BeforeTest + fun setup() { + seed = + "e8uNN7LRH5mEUcxa7FhxDAgWGLh8P94WEOD0jUdaJ2mSU1o02u-Lzao50elV32XvYT0ux9jWuBVECpFAz2ckKw".base64UrlDecodedBytes + privateKey = "96ViMAl0_N1Xm5RJesQxC2NvxhNc4ZkwPyVevZ4akDI" + derivedPrivateKey = "xURclKhT6as1Tb9vg4AJRRLPAMWb9dYTTthDvXEKjMc" + } + + @Test + fun testConstructor_whenSeedIncorrectLength_thenThrowException() { + val depth = 1 + childIndex = BigInteger(HARDENED_OFFSET) + seed = seed.sliceArray(IntRange(0, 60)) + + assertFailsWith(IllegalArgumentException::class) { + HDKey(seed, depth, childIndex) + } + } + + @Test + fun testConstructorWithSeed_thenRightPrivateKey() { + val depth = 0 + + val hdKey = HDKey(seed = seed, depth = depth, childIndex = childIndex) + + assertNotNull(hdKey.privateKey) + assertTrue(privateKey.base64UrlDecodedBytes.contentEquals(hdKey.privateKey!!)) + assertNotNull(hdKey.chainCode) + assertEquals(depth, hdKey.depth) + assertEquals(childIndex, hdKey.childIndex) + } + + @Test + fun testDerive_whenIncorrectPath_thenThrowException() { + val depth = 1 + val hdKey = HDKey(seed, depth, childIndex) + val path = "x/0" + + assertFailsWith(Error::class) { + hdKey.derive(path) + } + } + + @Test + fun testDerive_whenCorrectPath_thenDeriveOk() { + val depth = 1 + + val hdKey = HDKey(seed, depth, childIndex) + val path = "m/0'/0'/0'" + + val derPrivateKey = hdKey.derive(path) + assertTrue(derivedPrivateKey.base64UrlDecodedBytes.contentEquals(derPrivateKey.privateKey!!)) + } + + @Test + fun testDeriveChild_whenNoChainCode_thenThrowException() { + val depth = 1 + val hdKey = HDKey( + privateKey = privateKey.encodeToByteArray(), + depth = depth, + childIndex = childIndex + ) + + assertFailsWith(Exception::class) { + hdKey.deriveChild(childIndex) + } + } + + @Test + fun testDeriveChild_whenPrivateKeyNotHardened_thenThrowException() { + val depth = 1 + val hdKey = HDKey( + privateKey = privateKey.encodeToByteArray(), + depth = depth, + childIndex = childIndex + ) + + assertFailsWith(Exception::class) { + hdKey.deriveChild(childIndex) + } + } + + @Test + fun testDeriveChild_whenPrivateKeyNotRightLength_thenThrowException() { + val depth = 1 + childIndex = BigInteger(1) + + val hdKey = HDKey( + privateKey = Random.Default.nextBytes(33), + depth = depth, + childIndex = childIndex + ) + + assertFailsWith(Exception::class) { + hdKey.deriveChild(childIndex) + } + } + + @Test + fun testGetKMMSecp256k1PrivateKey_thenPrivateKeyNonNull() { + val depth = 1 + + val hdKey = HDKey(seed, depth, childIndex) + val key = hdKey.getKMMSecp256k1PrivateKey() + assertNotNull(key) + } +} diff --git a/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/utils/Secp256k1LibTests.kt b/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/utils/Secp256k1LibTests.kt index d307a69f2..cb3765148 100644 --- a/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/utils/Secp256k1LibTests.kt +++ b/base-asymmetric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/utils/Secp256k1LibTests.kt @@ -5,7 +5,6 @@ import io.iohk.atala.prism.apollo.base64.base64PadDecodedBytes import io.iohk.atala.prism.apollo.base64.base64UrlDecodedBytes import io.iohk.atala.prism.apollo.base64.base64UrlEncoded import io.iohk.atala.prism.apollo.secp256k1.Secp256k1Lib -import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -30,7 +29,7 @@ class Secp256k1LibTests { } @Test - fun testSignature() = runTest { + fun testSignature() { val privKeyBase64 = "N_JFgvYaReyRXwassz5FHg33A4I6dczzdXrjdHGksmg" val message = "Test" diff --git a/base-asymmetric-encryption/src/jsMain/kotlin/io/iohk/atala/prism/apollo/secp256k1/Secp256k1Lib.kt b/base-asymmetric-encryption/src/jsMain/kotlin/io/iohk/atala/prism/apollo/secp256k1/Secp256k1Lib.kt index e732953e1..0aa27d218 100644 --- a/base-asymmetric-encryption/src/jsMain/kotlin/io/iohk/atala/prism/apollo/secp256k1/Secp256k1Lib.kt +++ b/base-asymmetric-encryption/src/jsMain/kotlin/io/iohk/atala/prism/apollo/secp256k1/Secp256k1Lib.kt @@ -1,7 +1,6 @@ package io.iohk.atala.prism.apollo.secp256k1 import com.ionspin.kotlin.bignum.integer.BigInteger -import com.ionspin.kotlin.bignum.integer.Sign import io.iohk.atala.prism.apollo.hashing.SHA256 import io.iohk.atala.prism.apollo.hashing.internal.toHexString import io.iohk.atala.prism.apollo.utils.ECConfig @@ -17,8 +16,10 @@ actual class Secp256k1Lib actual constructor() { } actual fun derivePrivateKey(privateKeyBytes: ByteArray, derivedPrivateKeyBytes: ByteArray): ByteArray? { - val privKey = BigInteger.fromByteArray(privateKeyBytes, Sign.POSITIVE) - val derivedPrivKey = BigInteger.fromByteArray(derivedPrivateKeyBytes, Sign.POSITIVE) + val privKeyString = privateKeyBytes.toHexString() + val derivedPrivKeyString = derivedPrivateKeyBytes.toHexString() + val privKey = BigInteger.parseString(privKeyString, 16) + val derivedPrivKey = BigInteger.parseString(derivedPrivKeyString, 16) val added = (privKey + derivedPrivKey) % ECConfig.n diff --git a/settings.gradle.kts b/settings.gradle.kts index 64748ad80..134d36c23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,7 +38,7 @@ include(":base-symmetric-encryption") include(":secure-random") include(":aes") include(":base-asymmetric-encryption") -include(":rsa") +// include(":rsa") // include(":ecdsa") include(":varint") // include(":jose")