Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
curtis-h committed Mar 28, 2024
1 parent 922d037 commit b1e9040
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 56 deletions.
2 changes: 2 additions & 0 deletions apollo/build.gradle.kts
Expand Up @@ -244,6 +244,8 @@ kotlin {
implementation("com.ionspin.kotlin:bignum:0.3.9")
implementation("org.kotlincrypto.macs:hmac-sha2:0.3.0")
implementation("org.kotlincrypto.hash:sha2:0.4.0")

implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
}
}
val commonTest by getting {
Expand Down
Expand Up @@ -3,7 +3,6 @@ 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.ExperimentalJsExport
Expand Down Expand Up @@ -133,26 +132,13 @@ class HDKey(
* @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("/")
val indexes = pathToIndexes(path)
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(BigIntegerWrapper(finalIdx))

for (i in indexes) {
child = child.deriveChild(BigIntegerWrapper(i))
}

return child
}

Expand All @@ -162,45 +148,16 @@ class HDKey(
* @param index value used to derive a key
*/
fun deriveChild(index: BigIntegerWrapper): HDKey {
@Suppress("NAME_SHADOWING")
val index = index.value
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 derived = HDKey.deriveChild(this, index.value)
val keyData = privateKey ?: throw Error("Missing Key data")
val derivedData = derived.privateKey ?: throw Error("Missing Derived data")
val tweakedKey = KMMECSecp256k1PrivateKey.tweak(keyData, derivedData).raw

val opt =
HDKeyOptions(
versions = Pair(BITCOIN_VERSIONS_PRIVATE, BITCOIN_VERSIONS_PUBLIC),
chainCode = newChainCode,
depth = depth + 1,
parentFingerprint = null,
index = index
)

opt.privateKey = KMMECSecp256k1PrivateKey.tweak(privateKey, childTweak).raw
return HDKey(
privateKey = opt.privateKey,
chainCode = opt.chainCode,
depth = opt.depth,
childIndex = BigIntegerWrapper(opt.index)
privateKey = tweakedKey,
chainCode = derived.chainCode,
depth = derived.depth,
childIndex = derived.childIndex
)
}

Expand Down Expand Up @@ -268,5 +225,98 @@ class HDKey(
sha512.update(input)
return sha512.doFinal()
}

/**
* Initialize a HDKey from a curve and seed
*/
fun fromSeed(curve: String, seed: ByteArray) : HDKey {
val init = "$curve seed".encodeToByteArray()
val result = sha512(init, seed)
val privateKey = result.sliceArray(IntRange(0, 31))
val chainCode = result.sliceArray(32 until result.size)

return HDKey(privateKey = privateKey, chainCode = chainCode)
}

/**
* Derive a HDKey for curve with path
*/
fun deriveCurveFromPath(curve: String, key: HDKey, path: String) : HDKey {
val indexes = pathToIndexes(path)
var current = key

for (i in indexes) {
if(curve == "ed25519") {
current = deriveChild(key = current, index = i)
}
else {
current = key.deriveChild(BigIntegerWrapper(i))
}
}

return current
}

/**
* Convert a path string to an index array
*/
private fun pathToIndexes(path: String) : List<BigInteger> {
if (!path.matches(Regex("^[mM].*"))) {
throw Error("Path must start with \"m\" or \"M\"")
}
if (Regex("^[mM]'?$").matches(path)) {
return emptyList()
}

val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
val indexes = mutableListOf<BigInteger>()

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
indexes.add(finalIdx)
}

return indexes
}

/**
* Method to derive a HDKey child by index
*
* @param index value used to derive a key
*/
fun deriveChild(key: HDKey, index: BigInteger): HDKey {
if (key.chainCode == null) {
throw Exception("No chainCode set")
}

val data =
if (index >= HARDENED_OFFSET) {
val priv = key.privateKey ?: throw Error("Could not derive hardened child key")
byteArrayOf(0) + priv + index.toByteArray()
} else {
throw Exception("Not supported")
}

val i = sha512(key.chainCode, data)
val childTweak = i.sliceArray(IntRange(0, 31))
val newChainCode = i.sliceArray(32 until i.size)

return HDKey(
privateKey = childTweak,
chainCode = newChainCode,
childIndex = BigIntegerWrapper(index),
depth = key.depth + 1
)
}

}
}
@@ -0,0 +1,75 @@
package io.iohk.atala.prism.apollo.derivation

import com.ionspin.kotlin.bignum.integer.util.toBigEndianUByteArray
import com.ionspin.kotlin.crypto.box.Box
import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
import org.kotlincrypto.macs.hmac.sha2.HmacSHA512
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport

// this is just for testing, actual code needs to be in HDKey
// have been using HDKey companion functions to avoid breaking

@OptIn(ExperimentalJsExport::class)
@JsExport
data class Key(val key: ByteArray, val chainCode: ByteArray)

@OptIn(ExperimentalJsExport::class)
@JsExport
class Sodium {
private val hardenedOffset = 0x80000000
fun getMasterKeyFromSeed(seed: ByteArray): Key {
val init = "ed25519 seed".encodeToByteArray()
val hmac = HmacSHA512(init)
hmac.update(seed)
val result = hmac.doFinal()
val key = result.copyOfRange(0, 32)
val chainCode = result.copyOfRange(32, 64)

return Key(key, chainCode)
}

@OptIn(ExperimentalUnsignedTypes::class)
fun derive(seed: ByteArray, index: Int): Key {
val masterKey = getMasterKeyFromSeed(seed)
val offset = (index + hardenedOffset).toUInt()
val bytes = offset.toBigEndianUByteArray().toByteArray()
val data = byteArrayOf(0).plus(masterKey.key.plus(bytes))
val hmac = HmacSHA512(masterKey.chainCode)
val result = hmac.doFinal(data)
val key = result.copyOfRange(0, 32)
val chainCode = result.copyOfRange(32, 64)

return Key(key, chainCode)
}


@OptIn(ExperimentalStdlibApi::class)
fun keygen(seed: ByteArray): Key {
// this KeyExchange.seedKeyPair might be usable for X25519
// would need to be ported to HDKey
val kkp = KeyExchange.seedKeypair(seed.toUByteArray())
// no idea what Box is, just used it for testing to simulate another Key
val bkp = Box.seedKeypair(seed.toUByteArray())
// val skp = Signature.seedKeypair(seed.toUByteArray())

val clientSessionKeyPair = KeyExchange.clientSessionKeys(
bkp.publicKey,
bkp.secretKey,
kkp.publicKey
)
val serverSessionKeyPair = KeyExchange.serverSessionKeys(
kkp.publicKey,
kkp.secretKey,
bkp.publicKey
)

val a = clientSessionKeyPair.sendKey.toHexString()
val b = serverSessionKeyPair.receiveKey.toHexString()
// matched should be true
val matched = a == b

return Key(kkp.secretKey.toByteArray(), kkp.publicKey.toByteArray())
}

}
Expand Up @@ -3,6 +3,7 @@ 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 io.iohk.atala.prism.apollo.utils.toHexString
import kotlin.random.Random
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -126,4 +127,46 @@ class HDKeyTest {
val key = hdKey.getKMMSecp256k1PrivateKey()
assertNotNull(key)
}

// test vectors: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testFromSeed() {
val seedHex = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
val seed = seedHex.hexToByteArray()
val key = HDKey.fromSeed("ed25519", seed)

assertEquals("171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", key.privateKey?.toHexString())
assertEquals("ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b", key.chainCode?.toHexString())
}

@OptIn(ExperimentalStdlibApi::class)
@Test
fun testFromSeed_Derive_1() {
val seedHex = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
val seed = seedHex.hexToByteArray()
val key = HDKey.fromSeed("ed25519", seed)
val derived = HDKey.deriveCurveFromPath("ed25519", key, "m/0'")

assertEquals("1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635", derived.privateKey?.toHexString())
assertEquals("0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d", derived.chainCode?.toHexString())
}

@OptIn(ExperimentalStdlibApi::class)
@Test
fun testFromSeed_Derive_2() {
val seedHex = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
val seed = seedHex.hexToByteArray()
val curve = "ed25519"
val key = HDKey.fromSeed(curve, seed)
val path = "m/0'/2147483647'"

val d2 = HDKey.deriveCurveFromPath(curve, key, path)
val d2key = d2.privateKey?.toHexString()
val d2code = d2.chainCode?.toHexString()

assertEquals("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4", d2key)
assertEquals("138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f", d2code)
}

}
@@ -0,0 +1,24 @@
package io.iohk.atala.prism.apollo.derivation

import com.ionspin.kotlin.crypto.LibsodiumInitializer
import io.iohk.atala.prism.apollo.utils.toHexString
import kotlin.test.Test
import kotlin.test.assertEquals

class SodiumTest {
@OptIn(ExperimentalStdlibApi::class)
@Test
fun test() {
LibsodiumInitializer.initializeWithCallback {
val seedHex = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
val seed = seedHex.hexToByteArray()
val result = Sodium().getMasterKeyFromSeed(seed)

val a = Sodium().keygen(seed)
val b = Sodium().keygen("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a".hexToByteArray())

val expected = result.chainCode.toHexString()
assertEquals("171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", result.key.toHexString())
}
}
}

0 comments on commit b1e9040

Please sign in to comment.