-
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.
- Loading branch information
1 parent
36c5c5f
commit e4b6ca7
Showing
4 changed files
with
196 additions
and
4 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
109 changes: 107 additions & 2 deletions
109
...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 |
---|---|---|
@@ -1,4 +1,109 @@ | ||
package io.iohk.atala.prism.apollo.derivation | ||
|
||
class HDKey { | ||
} | ||
import com.ionspin.kotlin.bignum.integer.BigInteger | ||
import com.ionspin.kotlin.bignum.integer.Sign | ||
import com.ionspin.kotlin.bignum.integer.toBigInteger | ||
import io.iohk.atala.prism.apollo.hashing.SHA512 | ||
import io.iohk.atala.prism.apollo.utils.ECConfig | ||
import io.iohk.atala.prism.apollo.utils.ECPrivateKeyDecodingException | ||
import io.iohk.atala.prism.apollo.utils.KMMECSecp256k1PrivateKey | ||
|
||
class HDKey( | ||
val privateKey: ByteArray? = null, | ||
val publicKey: ByteArray? = null, | ||
val chainCode: ByteArray? = null, | ||
val depth: Int, | ||
val childIndex: BigInteger | ||
) { | ||
|
||
constructor(seed: ByteArray, depth: Int, childIndex: BigInteger) : this( | ||
privateKey = seed.sliceArray(IntRange(0, 31)), | ||
chainCode = seed.sliceArray(listOf(32)), | ||
depth = depth, | ||
childIndex = childIndex | ||
) | ||
|
||
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") | ||
} | ||
// TODO: Null check?? | ||
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 | ||
} | ||
|
||
fun deriveChild(index: BigInteger): HDKey { | ||
if (chainCode == null) { | ||
throw Error("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().hmac(key = chainCode, input = data) | ||
val childTweak = I.sliceArray(IntRange(0, 31)) | ||
val newChainCode = I.sliceArray(listOf(32)) | ||
|
||
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 = FINGERPRINT, | ||
index = index | ||
) | ||
return try { | ||
val added = BigInteger.fromByteArray(privateKey + childTweak, Sign.POSITIVE) % ECConfig.n | ||
if (!isValidPrivateKey(added.toByteArray())) { | ||
throw Error("The tweak was out of range or the resulted private key is invalid") | ||
} | ||
opt.privateKey = added.toByteArray() | ||
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.secp256k1FromBytes(privateKey) | ||
} ?: throw Exception("") | ||
} | ||
|
||
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 | ||
} | ||
} |
12 changes: 11 additions & 1 deletion
12
...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 |
---|---|---|
@@ -1,3 +1,13 @@ | ||
package io.iohk.atala.prism.apollo.derivation | ||
|
||
data class HDKeyOptions() | ||
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 | ||
) |
78 changes: 77 additions & 1 deletion
78
...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 |
---|---|---|
@@ -1,4 +1,80 @@ | ||
package io.iohk.atala.prism.apollo.derivation | ||
|
||
import com.ionspin.kotlin.bignum.integer.BigInteger | ||
import kotlin.random.Random | ||
import kotlin.test.Ignore | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertFailsWith | ||
import kotlin.test.assertNotNull | ||
|
||
class HDKeyTest { | ||
} | ||
@Test | ||
fun testConstructorWithSeed_thenNonNullValues() { | ||
val seed = Random.Default.nextBytes(64) | ||
val depth = 1 | ||
val childIndex = BigInteger(0) | ||
|
||
val hdKey = HDKey(seed, depth, childIndex) | ||
|
||
assertNotNull(hdKey.privateKey) | ||
assertNotNull(hdKey.chainCode) | ||
assertEquals(depth, hdKey.depth) | ||
assertEquals(childIndex, hdKey.childIndex) | ||
} | ||
|
||
@Test | ||
fun testDerive_whenIncorrectPath_thenThrowError() { | ||
val seed = Random.Default.nextBytes(64) | ||
val depth = 1 | ||
val childIndex = BigInteger(0) | ||
|
||
val hdKey = HDKey(seed, depth, childIndex) | ||
val path = "x/0" | ||
|
||
assertFailsWith(Error::class) { | ||
hdKey.derive(path) | ||
} | ||
} | ||
|
||
@Ignore | ||
@Test | ||
fun testDerive_thenHDDeriveOk() { | ||
val seed = Random.Default.nextBytes(64) | ||
val depth = 1 | ||
val childIndex = BigInteger(0) | ||
|
||
val hdKey = HDKey(seed, depth, childIndex) | ||
val path = "m/44'/0'/0'/0/0" | ||
|
||
val hdKeyResult = hdKey.derive(path) | ||
assertNotNull(hdKeyResult.privateKey) | ||
assertNotNull(hdKeyResult.chainCode) | ||
assertEquals(depth, hdKeyResult.depth) | ||
assertEquals(childIndex, hdKeyResult.childIndex) | ||
} | ||
|
||
@Test | ||
fun testDeriveChild_whenIncorrectPath_thenThrowError() { | ||
val seed = Random.Default.nextBytes(64) | ||
val depth = 1 | ||
val childIndex = BigInteger(HDKey.HARDENED_OFFSET) | ||
|
||
val hdKey = HDKey(seed, depth, childIndex) | ||
|
||
assertFailsWith(Exception::class) { | ||
hdKey.deriveChild(childIndex) | ||
} | ||
} | ||
|
||
@Test | ||
fun testGetKMMSecp256k1PrivateKey_thenPrivateKeyNonNull() { | ||
val seed = Random.Default.nextBytes(64) | ||
val depth = 1 | ||
val childIndex = BigInteger(0) | ||
|
||
val hdKey = HDKey(seed, depth, childIndex) | ||
val key = hdKey.getKMMSecp256k1PrivateKey() | ||
assertNotNull(key) | ||
} | ||
} |