-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(bip32): add hd derivation for jvm/android, ios/macos and js (#84)
- Loading branch information
1 parent
c18c0eb
commit d564507
Showing
7 changed files
with
282 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
...symmetric-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...ic-encryption/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyOptions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
127 changes: 127 additions & 0 deletions
127
...etric-encryption/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/HDKeyTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters