From e88bddbf2583088a625e5f250388cc838ea669f9 Mon Sep 17 00:00:00 2001 From: Mike Woods <87540692+mwoods-figure@users.noreply.github.com> Date: Fri, 7 Jan 2022 09:45:40 -0800 Subject: [PATCH] Patch 2022-01-05 (#26) 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 --- .github/CODEOWNERS | 2 +- bech32/build.gradle.kts | 2 +- .../io/provenance/hdwallet/bech32/Address.kt | 16 ++++++++++ .../io/provenance/hdwallet/bech32/Bech32.kt | 17 ++++++---- .../provenance/hdwallet/bech32/Bech32Data.kt | 6 ++-- .../io/provenance/hdwallet/bip32/MasterKey.kt | 32 +++++++++++-------- bip39/build.gradle.kts | 5 ++- .../hdwallet/bip39/DeterministicSeed.kt | 6 +++- .../hdwallet/bip39/MnemonicWords.kt | 2 +- .../io/provenance/hdwallet/bip39/TestBIP39.kt | 4 +-- .../io/provenance/hdwallet/bip44/Paths.kt | 8 ++++- build.gradle.kts | 29 ++++++++++------- buildSrc/src/main/kotlin/Deps.kt | 23 ------------- buildSrc/src/main/kotlin/Versions.kt | 8 ++--- ec/build.gradle.kts | 1 + .../io/provenance/hdwallet/ec/Compression.kt | 11 ------- .../kotlin/io/provenance/hdwallet/ec/Curve.kt | 17 ++++++++-- .../kotlin/io/provenance/hdwallet/ec/Keys.kt | 27 ++++++++++++---- .../provenance/hdwallet/ec/extensions/BC.kt | 29 +++++++++++++++-- .../hdwallet/ec/extensions/Bytes.kt | 28 ++++++++++++++++ .../hdwallet/ec/extensions/JavaKeys.kt | 24 -------------- .../kotlin/io/provenance/hdwallet/hrp/Hrp.kt | 3 +- .../hdwallet/wallet/DefaultWallet.kt | 7 ++-- .../io/provenance/hdwallet/wallet/Wallet.kt | 11 ++++--- .../provenance/hdwallet/wallet/TestWallet.kt | 6 ++-- .../provenance/hdwallet/signer/BCECSigner.kt | 1 + .../hdwallet/signer/ECDSASignature.kt | 6 ++-- 27 files changed, 199 insertions(+), 132 deletions(-) create mode 100644 bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Address.kt delete mode 100644 buildSrc/src/main/kotlin/Deps.kt create mode 100644 ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/Bytes.kt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7533b25..82399ee 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @mtps @jhorecny-figure +* @mtps @jhorecny-figure @scirner22 @mwoods-figure diff --git a/bech32/build.gradle.kts b/bech32/build.gradle.kts index 7848ff1..570b896 100644 --- a/bech32/build.gradle.kts +++ b/bech32/build.gradle.kts @@ -1,3 +1,3 @@ dependencies { - implementation(Deps.commonsCodec) + implementation("commons-codec", "commons-codec", Versions.commonsCodec) } diff --git a/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Address.kt b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Address.kt new file mode 100644 index 0000000..cbff550 --- /dev/null +++ b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Address.kt @@ -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 +} diff --git a/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32.kt b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32.kt index ebfc10d..6d97e9a 100644 --- a/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32.kt +++ b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32.kt @@ -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" @@ -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) @@ -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() /** diff --git a/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32Data.kt b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32Data.kt index 2e916bf..49cb0b6 100644 --- a/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32Data.kt +++ b/bech32/src/main/kotlin/io/provenance/hdwallet/bech32/Bech32Data.kt @@ -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. diff --git a/bip32/src/main/kotlin/io/provenance/hdwallet/bip32/MasterKey.kt b/bip32/src/main/kotlin/io/provenance/hdwallet/bip32/MasterKey.kt index 005ef25..8e5b427 100644 --- a/bip32/src/main/kotlin/io/provenance/hdwallet/bip32/MasterKey.kt +++ b/bip32/src/main/kotlin/io/provenance/hdwallet/bip32/MasterKey.kt @@ -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 @@ -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" } } @@ -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) } @@ -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() @@ -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) diff --git a/bip39/build.gradle.kts b/bip39/build.gradle.kts index 04be368..8337c30 100644 --- a/bip39/build.gradle.kts +++ b/bip39/build.gradle.kts @@ -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) } diff --git a/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/DeterministicSeed.kt b/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/DeterministicSeed.kt index 50d5396..c21bf55 100644 --- a/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/DeterministicSeed.kt +++ b/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/DeterministicSeed.kt @@ -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) } diff --git a/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/MnemonicWords.kt b/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/MnemonicWords.kt index e9aaab3..e07047b 100644 --- a/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/MnemonicWords.kt +++ b/bip39/src/main/kotlin/io/provenance/hdwallet/bip39/MnemonicWords.kt @@ -28,7 +28,7 @@ class MnemonicWords(val words: List) { } // 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() diff --git a/bip39/src/test/kotlin/io/provenance/hdwallet/bip39/TestBIP39.kt b/bip39/src/test/kotlin/io/provenance/hdwallet/bip39/TestBIP39.kt index c4b0dba..3e94f5e 100644 --- a/bip39/src/test/kotlin/io/provenance/hdwallet/bip39/TestBIP39.kt +++ b/bip39/src/test/kotlin/io/provenance/hdwallet/bip39/TestBIP39.kt @@ -30,7 +30,7 @@ open class TestBIP39 { private fun getTestVectors(lang: String): List { val json = javaClass - .getResourceAsStream("/bip39_vectors_${lang.toLowerCase()}.json")!! + .getResourceAsStream("/bip39_vectors_${lang.lowercase()}.json")!! .readAllBytes() .toString(Charsets.UTF_8) .asTree() @@ -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") diff --git a/bip44/src/main/kotlin/io/provenance/hdwallet/bip44/Paths.kt b/bip44/src/main/kotlin/io/provenance/hdwallet/bip44/Paths.kt index 4559b4c..6ee2d66 100644 --- a/bip44/src/main/kotlin/io/provenance/hdwallet/bip44/Paths.kt +++ b/bip44/src/main/kotlin/io/provenance/hdwallet/bip44/Paths.kt @@ -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 { val s = path.split("/") if (s.isEmpty()) { @@ -47,7 +53,7 @@ fun String.parseBIP44Path(): List { 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) } diff --git a/build.gradle.kts b/build.gradle.kts index a236b02..16d0e82 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,7 +36,6 @@ val projectVersion = project.property("version")?.takeIf { it != "unspecified" } group = projectGroup version = projectVersion - subprojects { group = projectGroup version = projectVersion @@ -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().configureEach { @@ -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 { diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt deleted file mode 100644 index 79da485..0000000 --- a/buildSrc/src/main/kotlin/Deps.kt +++ /dev/null @@ -1,23 +0,0 @@ -import org.gradle.api.artifacts.ModuleDependency -import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency - -object Deps { - private fun mvn(group: String, name: String, version: String): ModuleDependency = - DefaultExternalModuleDependency(group, name, version) - - // General deps. - val bouncycastle = mvn("org.bouncycastle", "bcprov-jdk15on", Versions.bouncyCastle) - val commonsCodec = mvn("commons-codec", "commons-codec", Versions.commonsCodec) - val jacksonKotlin = mvn("com.fasterxml.jackson.module", "jackson-module-kotlin", Versions.jackson) - - // Kotlin deps. - val kotlinStdLibJdk8 = mvn("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", Versions.kotlin) - val kotlinStdLib = mvn("org.jetbrains.kotlin", "kotlin-stdlib", Versions.kotlin) - val kotlinReflect = mvn("org.jetbrains.kotlin", "kotlin-reflect", Versions.kotlin) - - // Test deps. - val junitJupiterApi = mvn("org.junit.jupiter", "junit-jupiter-api", Versions.junit) - val junitJupiterEngine = mvn("org.junit.jupiter", "junit-jupiter-engine", Versions.junit) - - val coroutines = mvn("org.jetbrains.kotlinx", "kotlinx-coroutines-core", Versions.coroutines) -} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index e3e9619..d9ee919 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -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" } diff --git a/ec/build.gradle.kts b/ec/build.gradle.kts index 4b920f8..a354389 100644 --- a/ec/build.gradle.kts +++ b/ec/build.gradle.kts @@ -1,3 +1,4 @@ dependencies { implementation(project(":common")) + implementation(project(":bech32")) } diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Compression.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Compression.kt index 64defe1..bac564c 100644 --- a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Compression.kt +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Compression.kt @@ -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) -} diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Curve.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Curve.kt index 01930af..c39362f 100644 --- a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Curve.kt +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Curve.kt @@ -13,6 +13,9 @@ import java.security.spec.ECParameterSpec import org.bouncycastle.jce.spec.ECParameterSpec as BCECParameterSpec import java.security.spec.EllipticCurve +/** + * Defines an elliptic curve point. + */ class CurvePoint(val ecPoint: ECPoint) { val x: BigInteger = ecPoint.xCoord.toBigInteger() val y: BigInteger = ecPoint.yCoord.toBigInteger() @@ -30,6 +33,13 @@ class CurvePoint(val ecPoint: ECPoint) { EC5Util.convertPoint(curve, toJavaECPoint()) } +/** + * Defines an elliptic curve. + * + * @property n + * @property g + * @property ecCurve + */ data class Curve(val n: BigInteger, val g: CurvePoint, private val ecCurve: ECCurve) { val ecDomainParameters: ECDomainParameters = ECDomainParameters(ecCurve, g.ecPoint, n) @@ -61,10 +71,13 @@ data class Curve(val n: BigInteger, val g: CurvePoint, private val ecCurve: ECCu } val secp256k1Curve = Curve.lookup("secp256k1") + val secp256r1Curve = Curve.lookup("secp256r1") -// Provenance defaults to the secp256k1 EC curve for keys and signatures. -val CURVE = secp256k1Curve +/** + * Provenance defaults to the secp256k1 EC curve for keys and signatures. + */ +val DEFAULT_CURVE = secp256k1Curve val Curve.ecParameterSpec: ECParameterSpec get() = ECParameterSpec(toJavaEllipticCurve(), g.toJavaECPoint(), n, ecDomainParameters.h.toInt()) diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Keys.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Keys.kt index 2d8e4bc..f41ff7c 100644 --- a/ec/src/main/kotlin/io/provenance/hdwallet/ec/Keys.kt +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/Keys.kt @@ -1,27 +1,40 @@ package io.provenance.hdwallet.ec +import io.provenance.hdwallet.bech32.Address +import io.provenance.hdwallet.bech32.toBech32 +import io.provenance.hdwallet.common.hashing.sha256hash160 +import io.provenance.hdwallet.ec.extensions.toBigInteger +import io.provenance.hdwallet.ec.extensions.toBytesPadded import java.math.BigInteger -fun ByteArray.toBigInteger() = BigInteger(1, this) - /** + * Elliptic curve (EC) private key. * + * @property key The private key `d` value. + * @property curve The underlying elliptic curve. */ class PrivateKey(val key: BigInteger, val curve: Curve) { + companion object { fun fromBytes(bytes: ByteArray, curve: Curve): PrivateKey = PrivateKey(bytes.toBigInteger(), curve) } fun toPublicKey(): PublicKey = PublicKey(curve.publicFromPrivate(key), curve) + + fun toECKeyPair() = ECKeyPair(this, toPublicKey()) } fun BigInteger.toPrivateKey(curve: Curve) = PrivateKey(this, curve) /** + * Elliptic curve (EC) public key. * + * @property key The public key `q` value. + * @property curve The underlying elliptic curve. */ class PublicKey(val key: BigInteger, val curve: Curve) { + override fun toString() = key.toString() fun point(): CurvePoint { @@ -30,6 +43,8 @@ class PublicKey(val key: BigInteger, val curve: Curve) { return curve.decodePoint(dest) } + fun address(hrp: String): Address = compressed().sha256hash160().toBech32(hrp).address + fun compressed() = point().encoded(true) companion object { @@ -38,11 +53,9 @@ class PublicKey(val key: BigInteger, val curve: Curve) { } /** + * An elliptic curve key pair. * + * @property privateKey The private key. + * @property publicKey The public key. */ data class ECKeyPair(val privateKey: PrivateKey, val publicKey: PublicKey) - -/** - * - */ -fun PrivateKey.toECKeyPair() = ECKeyPair(this, toPublicKey()) diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/BC.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/BC.kt index fb34611..b7cb9e6 100644 --- a/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/BC.kt +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/BC.kt @@ -2,10 +2,14 @@ package io.provenance.hdwallet.ec.extensions import io.provenance.hdwallet.ec.Curve import io.provenance.hdwallet.ec.CurvePoint +import java.math.BigInteger import java.security.KeyFactory +import java.security.interfaces.ECPublicKey import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import org.bouncycastle.asn1.x9.X9ECParameters +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECParameterSpec import org.bouncycastle.math.ec.ECPoint @@ -19,7 +23,28 @@ fun ECPoint.toCurvePoint(): CurvePoint = CurvePoint(this) fun ECParameterSpec.toCurve() = Curve(n, g.toCurvePoint(), curve) /** - * Interprets a byte array as an X.509 encoded elliptical curve (EC) public key. + * Convert a BouncyCastle [BCECPublicKey] to a [Pair] of ([BigInteger], [Curve]). + * + * See kethereum's EllipticCurveKeyPairGenerator for conversion of [JavaECPublicKey] > [BCECPublicKey] > [org.kethereum.model.PublicKey]] + * + * @param compressed True if the encoding of the key should be compressed before being packed into a [BigInteger] + * + * @return Pair + */ +fun BCECPublicKey.toBigIntegerPair(compressed: Boolean = false): Pair = + Pair(BigInteger(1, q.getEncoded(compressed)), parameters.toCurve()) + +/** + * Convert a BouncyCastle [BCECPublicKey] to a [Pair] of ([BigInteger], [Curve]). + * + * See kethereum's EllipticCurveKeyPairGenerator for conversion of [JavaECPublicKey] > [BCECPublicKey] > [org.kethereum.model.PublicKey]] + * + * @return Pair + */ +fun BCECPrivateKey.toBigIntegerPair(): Pair = Pair(d, parameters.toCurve()) + +/** + * Interprets a byte array as an X.509 encoded elliptic curve (EC) public key. * * @return A reconstructed [JavaPublicKey]. */ @@ -29,7 +54,7 @@ fun ByteArray.asJavaX509ECPublicKey(): JavaPublicKey = .generatePublic(X509EncodedKeySpec(this)) /** - * Interprets a byte array as an X.509 encoded elliptical curve (EC) public key. + * Interprets a byte array as an X.509 encoded elliptic curve (EC) public key. * * @return A reconstructed [JavaPrivateKey]. */ diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/Bytes.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/Bytes.kt new file mode 100644 index 0000000..2ed1241 --- /dev/null +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/Bytes.kt @@ -0,0 +1,28 @@ +package io.provenance.hdwallet.ec.extensions + +import java.math.BigInteger + +/** + * Pack a byte array into an unsigned [BigInteger]. + * + * @return [BigInteger] + */ +fun ByteArray.toBigInteger() = BigInteger(1, this) + +/** + * Unpack a [BigInteger] into a padded [ByteArray]. + * + * @param length The final length of the returned byte array. + * @return The padded [ByteArray]. + */ +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) +} diff --git a/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/JavaKeys.kt b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/JavaKeys.kt index 4f36a0d..d8191a0 100644 --- a/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/JavaKeys.kt +++ b/ec/src/main/kotlin/io/provenance/hdwallet/ec/extensions/JavaKeys.kt @@ -1,12 +1,9 @@ package io.provenance.hdwallet.ec.extensions -import io.provenance.hdwallet.ec.Curve import io.provenance.hdwallet.ec.PrivateKey import io.provenance.hdwallet.ec.PublicKey import io.provenance.hdwallet.ec.bcecParameterSpec import io.provenance.hdwallet.ec.ecParameterSpec -import io.provenance.hdwallet.ec.toBigInteger -import java.math.BigInteger import java.security.KeyFactory import java.security.spec.ECPublicKeySpec import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey @@ -37,18 +34,6 @@ fun JavaPublicKey.toBCECPublicKey(): BCECPublicKey? = else -> null } -/** - * Convert a BouncyCastle [BCECPublicKey] to a [Pair] of ([BigInteger], [Curve]). - * - * See kethereum's EllipticCurveKeyPairGenerator for conversion of [JavaECPublicKey] > [BCECPublicKey] > [org.kethereum.model.PublicKey]] - * - * @param compressed True if the encoding of the key should be compressed before being packed into a [BigInteger] - * - * @return Pair - */ -fun BCECPublicKey.toBigIntegerPair(compressed: Boolean = false): Pair = - Pair(BigInteger(1, q.getEncoded(compressed)), parameters.toCurve()) - /** * Convert a Java cryptographic [JavaPublicKey] to a hdwallet library EC [PublicKey]. * @@ -88,15 +73,6 @@ fun PrivateKey.toJavaECPrivateKey(): JavaPrivateKey = */ fun JavaECPrivateKey.toBCECPrivateKey(): BCECPrivateKey = BCECPrivateKey(this, BouncyCastlePQCProvider.CONFIGURATION) -/** - * Convert a BouncyCastle [BCECPublicKey] to a [Pair] of ([BigInteger], [Curve]). - * - * See kethereum's EllipticCurveKeyPairGenerator for conversion of [JavaECPublicKey] > [BCECPublicKey] > [org.kethereum.model.PublicKey]] - * - * @return Pair - */ -fun BCECPrivateKey.toBigIntegerPair(): Pair = Pair(d, parameters.toCurve()) - /** * Convert a Sun Security Provider [JavaPublicKey] to a Bouncy Castle elliptic curve (EC) public key, [BCECPublicKey]. * diff --git a/hdwallet/src/main/kotlin/io/provenance/hdwallet/hrp/Hrp.kt b/hdwallet/src/main/kotlin/io/provenance/hdwallet/hrp/Hrp.kt index b862221..d9624c8 100644 --- a/hdwallet/src/main/kotlin/io/provenance/hdwallet/hrp/Hrp.kt +++ b/hdwallet/src/main/kotlin/io/provenance/hdwallet/hrp/Hrp.kt @@ -3,6 +3,5 @@ package io.provenance.hdwallet.hrp enum class Hrp(val mainnet: String, val testnet: String) { CosmosHub("cosmos", "cosmos"), CryptoOrg("cro", "tcro"), - ProvenanceBlockchain("pb", "tp"), - ; + ProvenanceBlockchain("pb", "tp") } \ No newline at end of file diff --git a/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/DefaultWallet.kt b/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/DefaultWallet.kt index 7adee6f..f9c8498 100644 --- a/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/DefaultWallet.kt +++ b/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/DefaultWallet.kt @@ -1,10 +1,9 @@ package io.provenance.hdwallet.wallet -import io.provenance.hdwallet.bech32.toBech32 +import io.provenance.hdwallet.bech32.Address import io.provenance.hdwallet.bip32.AccountType.ROOT import io.provenance.hdwallet.bip32.ExtKey import io.provenance.hdwallet.bip44.PathElement -import io.provenance.hdwallet.common.hashing.sha256hash160 import io.provenance.hdwallet.ec.ECKeyPair import io.provenance.hdwallet.encoding.base58.base58EncodeChecked import io.provenance.hdwallet.signer.BCECSigner @@ -25,8 +24,8 @@ class DefaultAccount( private val key: ExtKey, private val signer: Signer = BCECSigner() ) : Account { - override val address: String = - key.keyPair.publicKey.compressed().sha256hash160().toBech32(hrp).address + + override val address: Address = key.keyPair.publicKey.address(hrp) override fun serializeExtKey(publicOnly: Boolean): String = key.serialize(publicOnly).base58EncodeChecked() diff --git a/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/Wallet.kt b/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/Wallet.kt index a478dcf..d861e11 100644 --- a/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/Wallet.kt +++ b/hdwallet/src/main/kotlin/io/provenance/hdwallet/wallet/Wallet.kt @@ -1,5 +1,6 @@ package io.provenance.hdwallet.wallet +import io.provenance.hdwallet.bech32.Address import io.provenance.hdwallet.bip32.ExtKey import io.provenance.hdwallet.bip32.toRootKey import io.provenance.hdwallet.bip39.DeterministicSeed @@ -7,7 +8,7 @@ import io.provenance.hdwallet.bip39.MnemonicWords import io.provenance.hdwallet.bip44.PathElement import io.provenance.hdwallet.bip44.PathElements import io.provenance.hdwallet.common.hashing.sha256 -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.encoding.base58.base58DecodeChecked @@ -35,7 +36,7 @@ interface Wallet { seed: DeterministicSeed, publicKeyOnly: Boolean = false, testnet: Boolean = false, - curve: Curve = CURVE, + curve: Curve = DEFAULT_CURVE, ): Wallet = DefaultWallet( hrp = hrp, key = seed.toRootKey(publicKeyOnly = publicKeyOnly, testnet = testnet, curve = curve) @@ -67,7 +68,7 @@ interface Wallet { mnemonicWords: MnemonicWords, publicKeyOnly: Boolean = false, testnet: Boolean = false, - curve: Curve = CURVE, + curve: Curve = DEFAULT_CURVE, ): Wallet = fromSeed( hrp = hrp, seed = mnemonicWords.toSeed(passphrase), @@ -102,7 +103,7 @@ interface Wallet { mnemonicWords: MnemonicWords, publicKeyOnly: Boolean = false, testnet: Boolean = false, - curve: Curve = CURVE, + curve: Curve = DEFAULT_CURVE, ): Wallet = fromMnemonic( hrp = hrp, passphrase = passphrase.toCharArray(), @@ -121,7 +122,7 @@ interface Account { /** * Bech32 encoded address for this account's extended key. */ - val address: String + val address: Address /** * Elliptic curve keypair for this account. diff --git a/hdwallet/src/test/kotlin/io/provenance/hdwallet/wallet/TestWallet.kt b/hdwallet/src/test/kotlin/io/provenance/hdwallet/wallet/TestWallet.kt index 2a7f0a2..5414561 100644 --- a/hdwallet/src/test/kotlin/io/provenance/hdwallet/wallet/TestWallet.kt +++ b/hdwallet/src/test/kotlin/io/provenance/hdwallet/wallet/TestWallet.kt @@ -32,18 +32,20 @@ class TestWallet { vectors.map { runFromSeedTest(seed, it) } } + @Suppress("UNUSED_VARIABLE") private fun runBip32Test(seed: DeterministicSeed, walletData: WalletData) { val encodedKey = seed.toRootKey().childKey(walletData.path).serialize().base58EncodeChecked() val childKey: Account = Account.fromBip32("cosmos", encodedKey) val sig = childKey.sign("test".toByteArray().sha256()) - assertEquals(childKey.address, walletData.address) + assertEquals(childKey.address.toString(), walletData.address) } + @Suppress("UNUSED_VARIABLE") private fun runFromSeedTest(seed: DeterministicSeed, walletData: WalletData) { val wallet = Wallet.fromSeed("cosmos", seed) val childKey = wallet[walletData.path] val sig = childKey.sign("test".toByteArray().sha256()) - assertEquals(childKey.address, walletData.address) + assertEquals(childKey.address.toString(), walletData.address) } private fun runBIP32AsyncTestVectors(vectors: List) { diff --git a/signer/src/main/kotlin/io/provenance/hdwallet/signer/BCECSigner.kt b/signer/src/main/kotlin/io/provenance/hdwallet/signer/BCECSigner.kt index ed4dff3..6adb2cd 100644 --- a/signer/src/main/kotlin/io/provenance/hdwallet/signer/BCECSigner.kt +++ b/signer/src/main/kotlin/io/provenance/hdwallet/signer/BCECSigner.kt @@ -12,6 +12,7 @@ import org.bouncycastle.crypto.signers.HMacDSAKCalculator * */ open class BCECSigner : SignAndVerify { + private fun signer(fn: ECDSASigner.() -> T): T { return ECDSASigner(HMacDSAKCalculator(SHA256Digest())).fn() } diff --git a/signer/src/main/kotlin/io/provenance/hdwallet/signer/ECDSASignature.kt b/signer/src/main/kotlin/io/provenance/hdwallet/signer/ECDSASignature.kt index 2751b1f..c1641e5 100644 --- a/signer/src/main/kotlin/io/provenance/hdwallet/signer/ECDSASignature.kt +++ b/signer/src/main/kotlin/io/provenance/hdwallet/signer/ECDSASignature.kt @@ -1,10 +1,10 @@ package io.provenance.hdwallet.signer -import io.provenance.hdwallet.ec.CURVE +import io.provenance.hdwallet.ec.DEFAULT_CURVE import io.provenance.hdwallet.ec.Curve import java.math.BigInteger -data class ECDSASignature(val r: BigInteger, val s: BigInteger, val curve: Curve = CURVE) { +data class ECDSASignature(val r: BigInteger, val s: BigInteger, val curve: Curve = DEFAULT_CURVE) { private val halfCurveOrder = curve.n.shiftRight(1) companion object { @@ -12,7 +12,7 @@ data class ECDSASignature(val r: BigInteger, val s: BigInteger, val curve: Curve * decodeAsBTC returns an ECDSASignature where the 64 byte array is divided * into r || s with each being a 32 byte big endian integer. */ - fun decodeAsBTC(bytes: ByteArray, curveParams: Curve = CURVE): ECDSASignature { + fun decodeAsBTC(bytes: ByteArray, curveParams: Curve = DEFAULT_CURVE): ECDSASignature { val halfCurveOrder = curveParams.n.shiftRight(1) require(bytes.size == 64) { "malformed BTC encoded signature, expected 64 bytes" }