Skip to content

Commit 3fef8f9

Browse files
feat: add bip32 pub key derivation (#204)
Signed-off-by: goncalo-frade-iohk <goncalo.frade@iohk.io>
1 parent 0917cb6 commit 3fef8f9

File tree

8 files changed

+329
-1
lines changed

8 files changed

+329
-1
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
import com.ionspin.kotlin.bignum.integer.toBigInteger
4+
import uniffi.ed25519_bip32_wrapper.deriveBytesPub
5+
6+
/**
7+
* Represents and HDKey with its derive methods
8+
*/
9+
actual class EdHDPubKey actual constructor(
10+
actual val publicKey: ByteArray,
11+
actual val chainCode: ByteArray,
12+
actual val depth: Int,
13+
actual val index: BigIntegerWrapper
14+
) {
15+
/**
16+
* Method to derive an HDKey by a path
17+
*
18+
* @param path value used to derive a key
19+
*/
20+
actual fun derive(path: String): EdHDPubKey {
21+
if (!path.matches(Regex("^[mM].*"))) {
22+
throw Error("Path must start with \"m\" or \"M\"")
23+
}
24+
if (Regex("^[mM]'?$").matches(path)) {
25+
return this
26+
}
27+
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
28+
var child = this
29+
30+
for (c in parts) {
31+
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
32+
if (m == null || m.size != 3) {
33+
throw Error("Invalid child index: $c")
34+
}
35+
val idx = m[1].toBigInteger()
36+
if (idx >= HDKey.HARDENED_OFFSET) {
37+
throw Error("Invalid index")
38+
}
39+
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx
40+
41+
child = child.deriveChild(BigIntegerWrapper(finalIdx))
42+
}
43+
44+
return child
45+
}
46+
47+
/**
48+
* Method to derive an HDKey child by index
49+
*
50+
* @param wrappedIndex value used to derive a key
51+
*/
52+
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
53+
val index = wrappedIndex.value.uintValue()
54+
val derived = deriveBytesPub(publicKey, chainCode, index)
55+
val publicKey = derived["public_key"]
56+
val chainCode = derived["chain_code"]
57+
58+
if (publicKey == null || chainCode == null) {
59+
throw Error("Unable to derive key")
60+
}
61+
62+
return EdHDPubKey(
63+
publicKey = publicKey,
64+
chainCode = chainCode,
65+
depth = depth + 1,
66+
index = wrappedIndex
67+
)
68+
}
69+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
import com.ionspin.kotlin.bignum.integer.toBigInteger
4+
import uniffi.ed25519_bip32_wrapper.deriveBytesPub
5+
6+
/**
7+
* Represents and HDKey with its derive methods
8+
*/
9+
actual class EdHDPubKey actual constructor(
10+
actual val publicKey: ByteArray,
11+
actual val chainCode: ByteArray,
12+
actual val depth: Int,
13+
actual val index: BigIntegerWrapper
14+
) {
15+
/**
16+
* Method to derive an HDKey by a path
17+
*
18+
* @param path value used to derive a key
19+
*/
20+
actual fun derive(path: String): EdHDPubKey {
21+
if (!path.matches(Regex("^[mM].*"))) {
22+
throw Error("Path must start with \"m\" or \"M\"")
23+
}
24+
if (Regex("^[mM]'?$").matches(path)) {
25+
return this
26+
}
27+
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
28+
var child = this
29+
30+
for (c in parts) {
31+
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
32+
if (m == null || m.size != 3) {
33+
throw Error("Invalid child index: $c")
34+
}
35+
val idx = m[1].toBigInteger()
36+
if (idx >= HDKey.HARDENED_OFFSET) {
37+
throw Error("Invalid index")
38+
}
39+
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx
40+
41+
child = child.deriveChild(BigIntegerWrapper(finalIdx))
42+
}
43+
44+
return child
45+
}
46+
47+
/**
48+
* Method to derive an HDKey child by index
49+
*
50+
* @param wrappedIndex value used to derive a key
51+
*/
52+
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
53+
val index = wrappedIndex.value.uintValue()
54+
val derived = deriveBytesPub(publicKey, chainCode, index)
55+
val publicKey = derived["public_key"]
56+
val chainCode = derived["chain_code"]
57+
58+
if (publicKey == null || chainCode == null) {
59+
throw Error("Unable to derive key")
60+
}
61+
62+
return EdHDPubKey(
63+
publicKey = publicKey,
64+
chainCode = chainCode,
65+
depth = depth + 1,
66+
index = wrappedIndex
67+
)
68+
}
69+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
expect class EdHDPubKey constructor(
4+
publicKey: ByteArray,
5+
chainCode: ByteArray,
6+
depth: Int = 0,
7+
index: BigIntegerWrapper = BigIntegerWrapper(0)
8+
) {
9+
val publicKey: ByteArray
10+
val chainCode: ByteArray
11+
val depth: Int
12+
val index: BigIntegerWrapper
13+
14+
/**
15+
* Method to derive an HDKey by a path
16+
*
17+
* @param path value used to derive a key
18+
*/
19+
fun derive(path: String): EdHDPubKey
20+
21+
/**
22+
* Method to derive an HDKey child by index
23+
*
24+
* @param wrappedIndex value used to derive a key
25+
*/
26+
fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
import org.hyperledger.identus.apollo.Platform
4+
import org.hyperledger.identus.apollo.utils.decodeHex
5+
import org.hyperledger.identus.apollo.utils.toHexString
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
9+
class EdHDPubKeyTest {
10+
@Test
11+
fun test_derive_m_1852_1815_0() {
12+
if (!Platform.OS.contains("Android")) {
13+
val publicKey = "6fd8d9c696b01525cc45f15583fc9447c66e1c71fd1a11c8885368404cd0a4ab".decodeHex()
14+
val chainCode = "00b5f1652f5cbe257e567c883dc2b16e0a9568b19c5b81ea8bd197fc95e8bdcf".decodeHex()
15+
16+
val key = EdHDPubKey(publicKey, chainCode)
17+
val derivationPath = listOf(1852, 1815, 0)
18+
val pathString = derivationPath.joinToString(separator = "/", prefix = "m/") { "$it" }
19+
val derived = key.derive(pathString)
20+
21+
assertEquals(
22+
derived.publicKey.toHexString(),
23+
"b857a8cd1dbbfed1824359d9d9e58bc8ffb9f66812b404f4c6ffc315629835bf"
24+
)
25+
assertEquals(derived.chainCode.toHexString(), "9db12d11a3559131a47f51f854a6234725ab8767d3fcc4c9908be55508f3c712")
26+
}
27+
}
28+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
import com.ionspin.kotlin.bignum.integer.toBigInteger
4+
import org.hyperledger.identus.apollo.utils.external.ed25519_bip32
5+
6+
/**
7+
* Represents and HDKey with its derive methods
8+
*/
9+
@OptIn(ExperimentalJsExport::class)
10+
@JsExport
11+
actual class EdHDPubKey actual constructor(
12+
actual val publicKey: ByteArray,
13+
actual val chainCode: ByteArray,
14+
actual val depth: Int,
15+
actual val index: BigIntegerWrapper
16+
) {
17+
/**
18+
* Method to derive an HDKey by a path
19+
*
20+
* @param path value used to derive a key
21+
*/
22+
actual fun derive(path: String): EdHDPubKey {
23+
if (!path.matches(Regex("^[mM].*"))) {
24+
throw Error("Path must start with \"m\" or \"M\"")
25+
}
26+
if (Regex("^[mM]'?$").matches(path)) {
27+
return this
28+
}
29+
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
30+
var child = this
31+
32+
for (c in parts) {
33+
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
34+
if (m == null || m.size != 3) {
35+
throw Error("Invalid child index: $c")
36+
}
37+
val idx = m[1].toBigInteger()
38+
if (idx >= HDKey.HARDENED_OFFSET) {
39+
throw Error("Invalid index")
40+
}
41+
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx
42+
43+
child = child.deriveChild(BigIntegerWrapper(finalIdx))
44+
}
45+
46+
return child
47+
}
48+
49+
/**
50+
* Method to derive an HDKey child by index
51+
*
52+
* @param wrappedIndex value used to derive a key
53+
*/
54+
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
55+
val derived = ed25519_bip32.derive_bytes_pub(publicKey, chainCode, wrappedIndex.value.uintValue())
56+
57+
return EdHDPubKey(
58+
publicKey = derived[0],
59+
chainCode = derived[1],
60+
depth = depth + 1,
61+
index = wrappedIndex
62+
)
63+
}
64+
}

apollo/src/jsMain/kotlin/org/hyperledger/identus/apollo/utils/external/Ed25519_Bip32.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ external interface ed25519_bip32_export {
44
fun from_nonextended(key: ByteArray, chain_code: ByteArray): Array<ByteArray>
55

66
fun derive_bytes(key: ByteArray, chain_code: ByteArray, index: Any): Array<ByteArray>
7+
8+
fun derive_bytes_pub(key: ByteArray, chain_code: ByteArray, index: Any): Array<ByteArray>
79
}
810

911
@JsModule("./ed25519_bip32_wasm.js")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.hyperledger.identus.apollo.derivation
2+
3+
import com.ionspin.kotlin.bignum.integer.toBigInteger
4+
import uniffi.ed25519_bip32_wrapper.deriveBytesPub
5+
6+
/**
7+
* Represents and HDKey with its derive methods
8+
*/
9+
actual class EdHDPubKey actual constructor(
10+
actual val publicKey: ByteArray,
11+
actual val chainCode: ByteArray,
12+
actual val depth: Int,
13+
actual val index: BigIntegerWrapper
14+
) {
15+
/**
16+
* Method to derive an HDKey by a path
17+
*
18+
* @param path value used to derive a key
19+
*/
20+
actual fun derive(path: String): EdHDPubKey {
21+
if (!path.matches(Regex("^[mM].*"))) {
22+
throw Error("Path must start with \"m\" or \"M\"")
23+
}
24+
if (Regex("^[mM]'?$").matches(path)) {
25+
return this
26+
}
27+
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
28+
var child = this
29+
30+
for (c in parts) {
31+
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
32+
if (m == null || m.size != 3) {
33+
throw Error("Invalid child index: $c")
34+
}
35+
val idx = m[1].toBigInteger()
36+
if (idx >= HDKey.HARDENED_OFFSET) {
37+
throw Error("Invalid index")
38+
}
39+
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx
40+
41+
child = child.deriveChild(BigIntegerWrapper(finalIdx))
42+
}
43+
44+
return child
45+
}
46+
47+
/**
48+
* Method to derive an HDKey child by index
49+
*
50+
* @param wrappedIndex value used to derive a key
51+
*/
52+
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
53+
val index = wrappedIndex.value.uintValue()
54+
val derived = deriveBytesPub(publicKey, chainCode, index)
55+
val publicKey = derived["public_key"]
56+
val chainCode = derived["chain_code"]
57+
58+
if (publicKey == null || chainCode == null) {
59+
throw Error("Unable to derive key")
60+
}
61+
62+
return EdHDPubKey(
63+
publicKey = publicKey,
64+
chainCode = chainCode,
65+
depth = depth + 1,
66+
index = wrappedIndex
67+
)
68+
}
69+
}

0 commit comments

Comments
 (0)