Skip to content

Commit

Permalink
Improve BIP44 API - no more companion object
Browse files Browse the repository at this point in the history
  • Loading branch information
ligi committed Aug 16, 2018
1 parent a8a0bf9 commit 804fdaa
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 46 deletions.
4 changes: 2 additions & 2 deletions bip32/src/main/kotlin/org/kethereum/bip32/BIP32.kt
Expand Up @@ -5,11 +5,11 @@ package org.kethereum.bip32
import org.kethereum.bip44.BIP44


fun generateKey(seed: ByteArray, path: String): ExtendedKey {
fun generateKey(seed: ByteArray, pathString: String): ExtendedKey {
val master = ExtendedKey.createFromSeed(seed)

var child = master
BIP44.fromPath(path).toIntList().forEach {
BIP44(pathString).path.forEach {
child = child.generateChildKey(it)
}

Expand Down
18 changes: 9 additions & 9 deletions bip32/src/main/kotlin/org/kethereum/bip32/ExtendedKey.kt
@@ -1,6 +1,6 @@
package org.kethereum.bip32

import org.kethereum.bip44.BIP44.Companion.isHardened
import org.kethereum.bip44.BIP44Element
import org.kethereum.crypto.*
import org.kethereum.encodings.decodeBase58WithChecksum
import org.kethereum.encodings.encodeToBase58WithChecksum
Expand All @@ -25,9 +25,9 @@ data class ExtendedKey(val keyPair: ECKeyPair,
private val parentFingerprint: Int,
private val sequence: Int) {

fun generateChildKey(element: Int): ExtendedKey {
fun generateChildKey(element: BIP44Element): ExtendedKey {
try {
if (isHardened(element) && keyPair.privateKey == BigInteger.ZERO) {
if (element.hardened && keyPair.privateKey == BigInteger.ZERO) {
throw IllegalArgumentException("need private key for private generation using hardened paths")
}
val mac = Mac.getInstance("HmacSHA512")
Expand All @@ -36,23 +36,23 @@ data class ExtendedKey(val keyPair: ECKeyPair,

val extended: ByteArray
val pub = keyPair.getCompressedPublicKey()
if (isHardened(element)) {
if (element.hardened) {
val privateKeyPaddedBytes = keyPair.privateKey.toBytesPadded(PRIVATE_KEY_SIZE)

extended = ByteBuffer
.allocate(privateKeyPaddedBytes.size + 5)
.order(ByteOrder.BIG_ENDIAN)
.put(0)
.put(privateKeyPaddedBytes)
.putInt(element)
.putInt(element.numberWithHardeningFlag)
.array()
} else {
//non-hardened
extended = ByteBuffer
.allocate(pub.size + 4)
.order(ByteOrder.BIG_ENDIAN)
.put(pub)
.putInt(element)
.putInt(element.numberWithHardeningFlag)
.array()
}
val lr = mac.doFinal(extended)
Expand All @@ -69,15 +69,15 @@ data class ExtendedKey(val keyPair: ECKeyPair,
if (k == BigInteger.ZERO) {
throw KeyException("Child key derivation resulted in zeros. Suggest deriving the next increment.")
}
ExtendedKey(ECKeyPair.create(k), r, (depth + 1).toByte(), computeFingerPrint(keyPair), element)
ExtendedKey(ECKeyPair.create(k), r, (depth + 1).toByte(), computeFingerPrint(keyPair), element.numberWithHardeningFlag)
} else {
val q = CURVE.g.multiply(m).add(CURVE.curve.decodePoint(pub)).normalize()
if (q.isInfinity) {
throw KeyException("Child key derivation resulted in zeros. Suggest deriving the next increment.")
}
val point = CURVE.curve.createPoint(q.xCoord.toBigInteger(), q.yCoord.toBigInteger())

ExtendedKey(ECKeyPair(BigInteger.ZERO, point.toPublicKey()), r, (depth + 1).toByte(), computeFingerPrint(keyPair), element)
ExtendedKey(ECKeyPair(BigInteger.ZERO, point.toPublicKey()), r, (depth + 1).toByte(), computeFingerPrint(keyPair), element.numberWithHardeningFlag)
}
} catch (e: NoSuchAlgorithmException) {
throw KeyException(e)
Expand Down Expand Up @@ -115,7 +115,7 @@ data class ExtendedKey(val keyPair: ECKeyPair,
}

fun serialize(publicKeyOnly: Boolean = false): String {
val out = ByteBuffer.allocate(Companion.EXTENDED_KEY_SIZE)
val out = ByteBuffer.allocate(EXTENDED_KEY_SIZE)
try {
if (publicKeyOnly || keyPair.privateKey == BigInteger.ZERO) {
out.put(xpub)
Expand Down
54 changes: 26 additions & 28 deletions bip44/src/main/kotlin/org/kethereum/bip44/BIP44.kt
Expand Up @@ -4,40 +4,38 @@ package org.kethereum.bip44
BIP44 as in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
*/

data class BIP44Element(val hardened: Boolean, val number: Int)
data class BIP44(val path: List<BIP44Element>) {
companion object {

const val HARDENING_FLAG = 0x80000000.toInt()
fun isHardened(element: Int): Boolean = (element and HARDENING_FLAG != 0)

fun fromPath(path: String): BIP44 {
if (!path.trim().startsWith("m/")) {
throw (IllegalArgumentException("Must start with m/"))
}
val cleanPath = path.replace("m/", "").replace(" ", "")
return BIP44(cleanPath.split("/")
.filter { it.isNotEmpty() }
.map {
BIP44Element(
hardened = it.contains("'"),
number = it.replace("'", "").toIntOrNull() ?:
throw IllegalArgumentException("not a number " + it)
)
})
}
const val BIP44_HARDENING_FLAG = 0x80000000.toInt()

private fun getEnsuredCleanPath(path: String): String {
if (!path.trim().startsWith("m/")) {
throw (IllegalArgumentException("Must start with m/"))
}
return path.replace("m/", "").replace(" ", "")
}

override fun equals(other: Any?) = path == path
override fun hashCode() = path.hashCode()
data class BIP44Element(val hardened: Boolean, val number: Int) {
val numberWithHardeningFlag = if (hardened) number or BIP44_HARDENING_FLAG else number
}

fun toIntList() = path.map { if (it.hardened) it.number or HARDENING_FLAG else it.number }
data class BIP44(val path: List<BIP44Element>) {
constructor(path: String) : this(getEnsuredCleanPath(path).split("/")
.filter { it.isNotEmpty() }
.map {
BIP44Element(
hardened = it.contains("'"),
number = it.replace("'", "").toIntOrNull()
?: throw IllegalArgumentException("not a number $it")
)
})

override fun equals(other: Any?) = (other as? BIP44)?.path == path
override fun hashCode() = path.hashCode()
override fun toString() = "m/" + path.joinToString("/") {
if (it.hardened) "${it.number}'" else "${it.number}"
}

fun increment()
= BIP44(path.subList(0, path.size - 1) +
fun increment() = BIP44(path.subList(0, path.size - 1) +
path.last().let { BIP44Element(it.hardened, it.number + 1) })
}

}

14 changes: 7 additions & 7 deletions bip44/src/test/kotlin/org/kethereum/bip44/TheBIP44.kt
Expand Up @@ -7,18 +7,18 @@ class TheBIP44 {

@Test(expected = IllegalArgumentException::class)
fun parsingFailsForBadInput() {
BIP44.fromPath("abc")
BIP44("abc")
}

@Test(expected = IllegalArgumentException::class)
fun parsingFailsForEmptyInput() {
BIP44.fromPath("")
BIP44("")
}


@Test(expected = IllegalArgumentException::class)
fun parsingFailsForPseudoCorrect() {
BIP44.fromPath("m")
BIP44("m")
}

val stringProbes = mapOf(
Expand Down Expand Up @@ -47,14 +47,14 @@ class TheBIP44 {
@Test
fun fromPathWork() {
for ((key, value) in (stringProbes + dirtyStringProbes)) {
assertThat(BIP44.fromPath(key).path).isEqualTo(value)
assertThat(BIP44(key).path).isEqualTo(value)
}
}

@Test
fun toStringFromIntoWorks() {
for ((path, ints) in (intProbes)) {
assertThat(BIP44.fromPath(path).toIntList()).isEqualTo(ints)
assertThat(BIP44(path).path.map { it.numberWithHardeningFlag }).isEqualTo(ints)
}
}

Expand All @@ -68,7 +68,7 @@ class TheBIP44 {

@Test
fun incrementWorks() {
assertThat(BIP44.fromPath("m/0/1/2").increment())
.isEqualTo(BIP44.fromPath("m/0/1/3"))
assertThat(BIP44("m/0/1/2").increment())
.isEqualTo(BIP44("m/0/1/3"))
}
}

0 comments on commit 804fdaa

Please sign in to comment.