Skip to content

Commit

Permalink
feat(bip32): add hd derivation for jvm/android, ios/macos and js (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianIOHK authored Jul 19, 2023
1 parent c18c0eb commit d564507
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 7 deletions.
3 changes: 2 additions & 1 deletion base-asymmetric-encryption/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.iohk.atala.prism.apollo.derivation

import com.ionspin.kotlin.bignum.integer.BigInteger

data class HDKeyOptions(
val versions: Pair<Int, Int>,
val chainCode: ByteArray,
val depth: Int,
val parentFingerprint: Int?,
val index: BigInteger,
var privateKey: ByteArray? = null,
var publicKey: ByteArray? = null
)
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +29,7 @@ class Secp256k1LibTests {
}

@Test
fun testSignature() = runTest {
fun testSignature() {
val privKeyBase64 = "N_JFgvYaReyRXwassz5FHg33A4I6dczzdXrjdHGksmg"
val message = "Test"

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit d564507

Please sign in to comment.