Adds support for NIP-06 seed word key derivation (bip32 and bip39)

This commit is contained in:
Vitor Pamplona
2024-05-24 14:54:38 -04:00
parent 7426600dc6
commit 04c449072a
16 changed files with 2924 additions and 19 deletions
@@ -56,8 +56,14 @@ object CryptoUtils {
return bytes
}
fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun isPrivKeyValid(il: ByteArray): Boolean {
return secp256k1.secKeyVerify(il)
}
fun sign(
data: ByteArray,
privKey: ByteArray,
@@ -351,4 +357,11 @@ object CryptoUtils {
null
}
}
fun sum(
first: ByteArray,
second: ByteArray,
): ByteArray {
return secp256k1.privKeyTweakAdd(first, second)
}
}
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/*
Simplified from: https://github.com/ACINQ/bitcoin-kmp/
*/
object Bip32SeedDerivation {
class ExtendedPrivateKey(
val secretkeybytes: ByteArray,
val chaincode: ByteArray,
)
/**
* @param seed random seed
* @return a "master" private key
*/
fun generate(seed: ByteArray): ExtendedPrivateKey {
val i = hmac512("Bitcoin seed".encodeToByteArray(), seed)
val il = i.take(32).toByteArray()
val ir = i.takeLast(32).toByteArray()
return ExtendedPrivateKey(il, ir)
}
fun hmac512(
key: ByteArray,
data: ByteArray,
): ByteArray {
val mac = Mac.getInstance("HmacSHA512")
mac.init(SecretKeySpec(key, "HmacSHA512"))
return mac.doFinal(data)
}
fun derivePrivateKey(
parent: ExtendedPrivateKey,
index: Long,
): ExtendedPrivateKey {
val i =
if (Hardener.isHardened(index)) {
val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
} else {
val data = CryptoUtils.pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
}
val il = i.take(32).toByteArray()
val ir = i.takeLast(32).toByteArray()
require(CryptoUtils.isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" }
val key = CryptoUtils.sum(il, parent.secretkeybytes)
require(CryptoUtils.isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
return ExtendedPrivateKey(key, ir)
}
public fun writeInt32BE(n: Int): ByteArray = ByteArray(Int.SIZE_BYTES).also { writeInt32BE(n, it) }
public fun writeInt32BE(
n: Int,
bs: ByteArray,
off: Int = 0,
) {
require(bs.size - off >= Int.SIZE_BYTES)
bs[off] = (n ushr 24).toByte()
bs[off + 1] = (n ushr 16).toByte()
bs[off + 2] = (n ushr 8).toByte()
bs[off + 3] = n.toByte()
}
fun derivePrivateKey(
parent: ExtendedPrivateKey,
chain: List<Long>,
): ExtendedPrivateKey = chain.fold(parent, Bip32SeedDerivation::derivePrivateKey)
fun derivePrivateKey(
parent: ExtendedPrivateKey,
keyPath: KeyPath,
): ByteArray = derivePrivateKey(parent, keyPath.path).secretkeybytes
fun derivePrivateKey(
parent: ExtendedPrivateKey,
keyPath: String,
): ByteArray = derivePrivateKey(parent, KeyPath.fromPath(keyPath))
}
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.crypto.nip06
class KeyPath(val path: List<Long>) {
constructor(path: String) : this(computePath(path))
val lastChildNumber: Long get() = if (path.isEmpty()) 0L else path.last()
fun derive(number: Long): KeyPath = KeyPath(path + listOf(number))
fun append(index: Long): KeyPath {
return KeyPath(path + listOf(index))
}
fun append(indexes: List<Long>): KeyPath {
return KeyPath(path + indexes)
}
fun append(that: KeyPath): KeyPath {
return KeyPath(path + that.path)
}
override fun toString(): String = asString('\'')
fun asString(hardenedSuffix: Char): String = path.map { childNumberToString(it, hardenedSuffix) }.fold("m") { a, b -> "$a/$b" }
companion object {
val empty: KeyPath = KeyPath(listOf())
fun computePath(path: String): List<Long> {
fun toNumber(value: String): Long = if (value.last() == '\'' || value.last() == 'h') Hardener.hardened(value.dropLast(1).toLong()) else value.toLong()
val path1 = path.removePrefix("m").removePrefix("/")
return if (path1.isEmpty()) {
listOf()
} else {
path1.split('/').map { toNumber(it) }
}
}
fun fromPath(path: String): KeyPath = KeyPath(path)
fun childNumberToString(
childNumber: Long,
hardenedSuffix: Char = '\'',
): String =
if (Hardener.isHardened(childNumber)) {
Hardener.unharden(childNumber).toString() + hardenedSuffix
} else {
childNumber.toString()
}
}
}
object Hardener {
val hardenedKeyIndex: Long = 0x80000000L
fun hardened(index: Long): Long = hardenedKeyIndex + index
fun unharden(index: Long): Long = index - hardenedKeyIndex
fun isHardened(index: Long): Boolean = index >= hardenedKeyIndex
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.crypto.nip06
class Nip06 {
// m/44'/1237'/<account>'/0/0
private val nip6Base: KeyPath =
KeyPath("")
.derive(Hardener.hardened(44L))
.derive(Hardener.hardened(1237L))
private fun nip6Path(account: Long): KeyPath {
return nip6Base.derive(Hardener.hardened(account))
.derive(0L)
.derive(0L)
}
fun isValidMnemonic(mnemonic: String): Boolean {
return Bip39Mnemonics.isValid(mnemonic)
}
fun privateKeyFromSeed(
seed: ByteArray,
account: Long = 0,
): ByteArray {
return Bip32SeedDerivation.derivePrivateKey(Bip32SeedDerivation.generate(seed), nip6Path(account))
}
fun privateKeyFromMnemonic(
mnemonic: String,
account: Long = 0,
): ByteArray {
val seed = Bip39Mnemonics.toSeed(mnemonic, "")
return privateKeyFromSeed(seed, account)
}
}