Skip to content

Commit

Permalink
Patch 2022-01-05 (#26)
Browse files Browse the repository at this point in the history
Patch 2022-01-05
- Update Kotlin to 1.5.32 + dependencies
- Pin common Kotlin dependencies to version with kotlin-bom
- Add value type for Bech32 Address
- Add more comments
- Small reorg of extension methods
- Replaced deprecated methods
- Convert ExtKey* classes to value classes
- Fix CODEOWNERS format
  • Loading branch information
mwoods-figure authored Jan 7, 2022
1 parent 9fb1e29 commit e88bddb
Show file tree
Hide file tree
Showing 27 changed files with 199 additions and 132 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @mtps @jhorecny-figure
* @mtps @jhorecny-figure @scirner22 @mwoods-figure
2 changes: 1 addition & 1 deletion bech32/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
dependencies {
implementation(Deps.commonsCodec)
implementation("commons-codec", "commons-codec", Versions.commonsCodec)
}
16 changes: 16 additions & 0 deletions bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Address.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.provenance.hdwallet.bech32

import kotlin.jvm.JvmInline

/**
* A typed Bech32 address.
*/
@JvmInline
value class Address(val value: String) {

companion object {
fun fromString(value: String): Address = Address(value)
}

override fun toString() = value
}
17 changes: 11 additions & 6 deletions bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ object Bech32 {

/**
* Decodes a Bech32 String
*
* @param bech32 The bech32 encoded string to decode
* @return [Bech32Data]]
*/
fun decode(bech32: String): Bech32Data {
require(bech32.length in MIN_VALID_LENGTH..MAX_VALID_LENGTH) { "invalid bech32 string length" }
require(bech32.toCharArray().all { c -> c.toInt() in MIN_VALID_CODEPOINT..MAX_VALID_CODEPOINT }) {
require(bech32.toCharArray().all { c -> c.code in MIN_VALID_CODEPOINT..MAX_VALID_CODEPOINT }) {
val invalidChars = bech32.toCharArray()
.filter { it.toInt() !in MIN_VALID_CODEPOINT..MAX_VALID_CODEPOINT }
.filter { it.code !in MIN_VALID_CODEPOINT..MAX_VALID_CODEPOINT }
"invalid characters in bech32: $invalidChars"
}
require(bech32 == bech32.toLowerCase() || bech32 == bech32.toUpperCase()) {
require(bech32 == bech32.lowercase() || bech32 == bech32.uppercase()) {
"bech32 must be either all upper or lower case"
}
require(bech32.substring(1).dropLast(CHECKSUM_SIZE).contains('1')) {
"invalid index of '1'"
}

val hrp = bech32.substringBeforeLast('1').toLowerCase()
val dataString = bech32.substringAfterLast('1').toLowerCase()
val hrp = bech32.substringBeforeLast('1').lowercase()
val dataString = bech32.substringAfterLast('1').lowercase()

require(charset.toList().containsAll(dataString.toList())) {
"invalid data encoding character in bech32"
Expand Down Expand Up @@ -120,6 +123,8 @@ object Bech32 {

/**
* Calculates a bech32 checksum based on BIP 173 specification
*
* @param hrp The human-readable prefix string used in the bech32 address.
*/
fun checksum(hrp: String, data: ByteArray): ByteArray {
var values = expandHrp(hrp)
Expand All @@ -137,7 +142,7 @@ object Bech32 {
* Expands the human readable prefix per BIP173 for Checksum encoding
*/
private fun expandHrp(hrp: String) = let {
hrp.map { c -> c.toInt() shr 5 } + 0 + hrp.map { c -> c.toInt() and 31 }
hrp.map { c -> c.code shr 5 } + 0 + hrp.map { c -> c.code and 31 }
}.toIntArray()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,18 @@ class Bech32Data(val hrp: String, fiveBitData: ByteArray) {
/**
* The encapsulated data as typical 8bit bytes.
*/
val data = Bech32.convertBits(fiveBitData, 5, 8, false)
val data: ByteArray = Bech32.convertBits(fiveBitData, 5, 8, false)

/**
* Checksum for encapsulated data + hrp
*/
val checksum = Bech32.checksum(hrp, fiveBitData)
val checksum: ByteArray = Bech32.checksum(hrp, fiveBitData)

/**
* Address is the Bech32 encoded value of the data prefixed with the human readable portion and
* protected by an appended checksum.
*/
val address = Bech32.encode(hrp, fiveBitData)
val address: Address = Address(Bech32.encode(hrp, fiveBitData))

/**
* The Bech32 Address toString prints state information for debugging purposes.
Expand Down
32 changes: 19 additions & 13 deletions bip32/src/main/kotlin/io/provenance/hdwallet/bip32/MasterKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import io.provenance.hdwallet.bip39.DeterministicSeed
import io.provenance.hdwallet.bip44.BIP44_HARDENING_FLAG
import io.provenance.hdwallet.bip44.parseBIP44Path
import io.provenance.hdwallet.common.hashing.sha256hash160
import io.provenance.hdwallet.ec.CURVE
import io.provenance.hdwallet.ec.DEFAULT_CURVE
import io.provenance.hdwallet.ec.Curve
import io.provenance.hdwallet.ec.ECKeyPair
import io.provenance.hdwallet.ec.PrivateKey
import io.provenance.hdwallet.ec.PublicKey
import io.provenance.hdwallet.ec.decompressPublicKey
import io.provenance.hdwallet.ec.toBigInteger
import io.provenance.hdwallet.ec.toBytesPadded
import io.provenance.hdwallet.ec.toECKeyPair
import io.provenance.hdwallet.ec.extensions.toBigInteger
import io.provenance.hdwallet.ec.extensions.toBytesPadded
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.ByteOrder
Expand All @@ -27,25 +26,28 @@ private const val CHAINCODE_SIZE = 32
private const val EXTENDED_KEY_SIZE: Int = 78

private fun hmacSha512(key: ByteArray, input: ByteArray): ByteArray =
Mac.getInstance(HMAC_SHA512).let {
Mac.getInstance(HMAC_SHA512).run {
val spec = SecretKeySpec(key, HMAC_SHA512)
it.init(spec)
it.doFinal(input)
init(spec)
doFinal(input)
}

class ExtKeyVersion(val bytes: ByteArray) {
@JvmInline
value class ExtKeyVersion(val bytes: ByteArray) {
init {
require(bytes.size == 4) { "invalid version len" }
}
}

class ExtKeyFingerprint(val bytes: ByteArray = byteArrayOf(0, 0, 0, 0)) {
@JvmInline
value class ExtKeyFingerprint(val bytes: ByteArray = byteArrayOf(0, 0, 0, 0)) {
init {
require(bytes.size == 4) { "invalid fingerprint len" }
}
}

class ExtKeyChainCode(val bytes: ByteArray) {
@JvmInline
value class ExtKeyChainCode(val bytes: ByteArray) {
init {
require(bytes.size == 32) { "invalid chaincode len" }
}
Expand Down Expand Up @@ -90,7 +92,7 @@ data class ExtKey(
}

return bb.array()
}
}

fun childKey(path: String): ExtKey =
path.parseBIP44Path().fold(this) { acc, i -> acc.childKey(i.number, i.hardened) }
Expand Down Expand Up @@ -158,7 +160,7 @@ data class ExtKey(
}

companion object {
fun deserialize(bip32: ByteArray, curve: Curve = CURVE): ExtKey {
fun deserialize(bip32: ByteArray, curve: Curve = DEFAULT_CURVE): ExtKey {
val bb = ByteBuffer.wrap(bip32)
val ver = bb.getByteArray(4)
val depth = bb.get()
Expand Down Expand Up @@ -200,7 +202,11 @@ data class ExtKey(
}

// https://en.bitcoin.it/wiki/BIP_0032
fun DeterministicSeed.toRootKey(publicKeyOnly: Boolean = false, testnet: Boolean = false, curve: Curve = CURVE): ExtKey {
fun DeterministicSeed.toRootKey(
publicKeyOnly: Boolean = false,
testnet: Boolean = false,
curve: Curve = DEFAULT_CURVE
): ExtKey {
val i = hmacSha512(BITCOIN_SEED, value)
val il = i.copyOfRange(0, PRIVATE_KEY_SIZE)
val ir = i.copyOfRange(PRIVATE_KEY_SIZE, PRIVATE_KEY_SIZE + CHAINCODE_SIZE)
Expand Down
5 changes: 2 additions & 3 deletions bip39/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
dependencies {
implementation(project(":common"))

listOf(Deps.jacksonKotlin, Deps.commonsCodec)
.map(::testImplementation)
testImplementation("commons-codec", "commons-codec", Versions.commonsCodec)
testImplementation("com.fasterxml.jackson.module", "jackson-module-kotlin", Versions.jackson)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package io.provenance.hdwallet.bip39
import javax.crypto.SecretKey
import javax.security.auth.Destroyable

class DeterministicSeed(val value: ByteArray) : Destroyable {
@JvmInline
value class DeterministicSeed(val value: ByteArray) : Destroyable {
/**
* Zero out the contents of this seed.
*/
override fun destroy() {
value.fill(0x00)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MnemonicWords(val words: List<CharArray>) {
}

// Assuming UTF8
private fun CharArray.toByteArray(): ByteArray = map { (it.toInt() and 0xFF).toByte() }.toByteArray()
private fun CharArray.toByteArray(): ByteArray = map { (it.code and 0xFF).toByte() }.toByteArray()

private fun CharArray.normalizeNKFD(): CharArray {
val dest = CharArrayBuffer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ open class TestBIP39 {

private fun getTestVectors(lang: String): List<TV> {
val json = javaClass
.getResourceAsStream("/bip39_vectors_${lang.toLowerCase()}.json")!!
.getResourceAsStream("/bip39_vectors_${lang.lowercase()}.json")!!
.readAllBytes()
.toString(Charsets.UTF_8)
.asTree()
Expand All @@ -40,7 +40,7 @@ open class TestBIP39 {

private fun getWordList(lang: String): WordList {
return javaClass
.getResourceAsStream("/wordlist_${lang.toLowerCase()}.txt")!!
.getResourceAsStream("/wordlist_${lang.lowercase()}.txt")!!
.readAllBytes()
.toString(Charsets.UTF_8)
.split("\n")
Expand Down
8 changes: 7 additions & 1 deletion bip44/src/main/kotlin/io/provenance/hdwallet/bip44/Paths.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package io.provenance.hdwallet.bip44

object PathElements {
/**
* Given a BIP-32 style path like "m/44'/1'/0'/420'", generate a list of [PathElement] represented the parsed path.
*
* @param path The BIP-32 style derivation path to parse.
* @return The parsed path as a list of [PathElement] instances.
*/
fun from(path: String): List<PathElement> {
val s = path.split("/")
if (s.isEmpty()) {
Expand Down Expand Up @@ -47,7 +53,7 @@ fun String.parseBIP44Path(): List<PathElement> {
val l = it.takeWhile { c -> c.isDigit() }
val n = l.toInt()
val r = it.substring(l.length, it.length)
val hard = r == "\'" || r.toLowerCase() == "h"
val hard = r == "\'" || r.lowercase() == "h"
require(r.isEmpty() || hard) { "Invalid hardening: $r" }
PathElement(n, hard)
}
Expand Down
29 changes: 18 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ val projectVersion = project.property("version")?.takeIf { it != "unspecified" }
group = projectGroup
version = projectVersion


subprojects {
group = projectGroup
version = projectVersion
Expand All @@ -50,6 +49,11 @@ subprojects {
plugin("signing")
}

jacoco {
// Workaround for https://youtrack.jetbrains.com/issue/KT-44757
toolVersion = "0.8.7"
}

project.ext.properties["kotlin_version"] = Versions.kotlin

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
Expand All @@ -73,16 +77,19 @@ subprojects {
}

dependencies {
listOf(
Deps.bouncycastle,
Deps.kotlinStdLibJdk8, Deps.kotlinStdLib, Deps.kotlinReflect
).map(::implementation)

listOf(
Deps.junitJupiterApi,
Deps.junitJupiterEngine,
Deps.coroutines
).map(::testImplementation)
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${Versions.kotlin}"))
implementation("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin", "kotlin-stdlib")
implementation("org.jetbrains.kotlin", "kotlin-reflect")

implementation("commons-codec", "commons-codec", Versions.commonsCodec)
implementation("com.fasterxml.jackson.module", "jackson-module-kotlin", Versions.jackson)

implementation("org.bouncycastle", "bcprov-jdk15on", Versions.bouncyCastle)

testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", Versions.coroutines)
testImplementation("org.junit.jupiter", "junit-jupiter-api", Versions.junit)
testImplementation("org.junit.jupiter", "junit-jupiter-engine", Versions.junit)
}

tasks {
Expand Down
23 changes: 0 additions & 23 deletions buildSrc/src/main/kotlin/Deps.kt

This file was deleted.

8 changes: 4 additions & 4 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ object Versions {
val projectSnapshot = "1.0-SNAPSHOT"

const val commonsCodec = "1.15"
const val bouncyCastle = "1.63"
const val kotlin = "1.4.32"
const val jackson = "2.9.7"
const val bouncyCastle = "1.70"
const val kotlin = "1.5.32"
const val jackson = "2.13.1"
const val junit = "5.8.2"
const val coroutines = "1.4.3"
const val coroutines = "1.5.2"
const val nexusPublishPlugin = "1.1.0"
}
1 change: 1 addition & 0 deletions ec/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dependencies {
implementation(project(":common"))
implementation(project(":bech32"))
}
11 changes: 0 additions & 11 deletions ec/src/main/kotlin/io/provenance/hdwallet/ec/Compression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,3 @@ fun decompressPublicKey(compressedBytes: ByteArray, curve: Curve): BigInteger {
return BigInteger(encoded.copyOfRange(1, encoded.size))
}

fun BigInteger.toBytesPadded(length: Int): ByteArray {
val result = ByteArray(length)
val bytes = toByteArray()
val offset = if (bytes[0].toInt() == 0) 1 else 0
if (bytes.size - offset > length) {
throw RuntimeException("Input is too large to put in byte array of size $length")
}

val destOffset = length - bytes.size + offset
return bytes.copyInto(result, destinationOffset = destOffset, startIndex = offset)
}
Loading

0 comments on commit e88bddb

Please sign in to comment.