Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bip32): add hd derivation for jvm/android, ios/macos and js #84

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading