Adds support for NIP-06 seed word key derivation (bip32 and bip39)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user