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

View File

@ -80,8 +80,6 @@ class Split<T>() {
} else {
splitItem.percentage = percentage
println("Update ${items[index].key} to $percentage")
val othersMustShare = 1.0f - splitItem.percentage
val othersHave =
@ -89,8 +87,6 @@ class Split<T>() {
if (abs(othersHave - othersMustShare) < 0.01) return // nothing to do
println("Others Must Share $othersMustShare but have $othersHave")
bottomUpAdjustment(othersMustShare, othersHave, index)
}
}
@ -109,14 +105,10 @@ class Split<T>() {
val oldValue = items[i].percentage
items[i].percentage -= needToRemove
needToRemove = 0f
println(
"- Updating ${items[i].key} from $oldValue to ${items[i].percentage - needToRemove}. $needToRemove left",
)
} else {
val oldValue = items[i].percentage
needToRemove -= items[i].percentage
items[i].percentage = 0f
println("- Updating ${items[i].key} from $oldValue to ${0}. $needToRemove left")
}
if (needToRemove < 0.01) {

View File

@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.crypto.nip06.Nip06
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.bechToBytes
@ -127,6 +128,14 @@ class AccountStateViewModel() : ViewModel() {
proxyPort = proxyPort,
signer = NostrSignerInternal(keyPair),
)
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
val keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key))
Account(
keyPair,
proxy = proxy,
proxyPort = proxyPort,
signer = NostrSignerInternal(keyPair),
)
} else if (pubKeyParsed != null) {
val keyPair = KeyPair(pubKey = pubKeyParsed)
Account(
@ -137,7 +146,7 @@ class AccountStateViewModel() : ViewModel() {
)
} else if (EMAIL_PATTERN.matcher(key).matches()) {
val keyPair = KeyPair()
// Evaluate NIP-5
// TODO: Evaluate NIP-5
Account(
keyPair,
proxy = proxy,

View File

@ -0,0 +1,124 @@
{
"english": [
[
"00000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607"
],
[
"80808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069"
],
[
"000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd"
],
[
"808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad"
],
[
"77c2b00716cec7213839159e404db50d",
"jelly better achieve collect unaware mountain thought cargo oxygen act hood bridge",
"b5b6d0127db1a9d2226af0c3346031d77af31e918dba64287a1b44b8ebf63cdd52676f672a290aae502472cf2d602c051f3e6f18055e84e4c43897fc4e51a6ff"
],
[
"b63a9c59a6e641f288ebc103017f1da9f8290b3da6bdef7b",
"renew stay biology evidence goat welcome casual join adapt armor shuffle fault little machine walk stumble urge swap",
"9248d83e06f4cd98debf5b6f010542760df925ce46cf38a1bdb4e4de7d21f5c39366941c69e1bdbf2966e0f6e6dbece898a0e2f0a4c2b3e640953dfe8b7bbdc5"
],
[
"3e141609b97933b66a060dcddc71fad1d91677db872031e85f4c015c5e7e8982",
"dignity pass list indicate nasty swamp pool script soccer toe leaf photo multiply desk host tomato cradle drill spread actor shine dismiss champion exotic",
"ff7f3184df8696d8bef94b6c03114dbee0ef89ff938712301d27ed8336ca89ef9635da20af07d4175f2bf5f3de130f39c9d9e8dd0472489c19b1a020a940da67"
],
[
"0460ef47585604c5660618db2e6a7e7f",
"afford alter spike radar gate glance object seek swamp infant panel yellow",
"65f93a9f36b6c85cbe634ffc1f99f2b82cbb10b31edc7f087b4f6cb9e976e9faf76ff41f8f27c99afdf38f7a303ba1136ee48a4c1e7fcd3dba7aa876113a36e4"
],
[
"72f60ebac5dd8add8d2a25a797102c3ce21bc029c200076f",
"indicate race push merry suffer human cruise dwarf pole review arch keep canvas theme poem divorce alter left",
"3bbf9daa0dfad8229786ace5ddb4e00fa98a044ae4c4975ffd5e094dba9e0bb289349dbe2091761f30f382d4e35c4a670ee8ab50758d2c55881be69e327117ba"
],
[
"2c85efc7f24ee4573d2b81a6ec66cee209b2dcbd09d8eddc51e0215b0b68e416",
"clutch control vehicle tonight unusual clog visa ice plunge glimpse recipe series open hour vintage deposit universe tip job dress radar refuse motion taste",
"fe908f96f46668b2d5b37d82f558c77ed0d69dd0e7e043a5b0511c48c2f1064694a956f86360c93dd04052a8899497ce9e985ebe0c8c52b955e6ae86d4ff4449"
],
[
"eaebabb2383351fd31d703840b32e9e2",
"turtle front uncle idea crush write shrug there lottery flower risk shell",
"bdfb76a0759f301b0b899a1e3985227e53b3f51e67e3f2a65363caedf3e32fde42a66c404f18d7b05818c95ef3ca1e5146646856c461c073169467511680876c"
],
[
"7ac45cfe7722ee6c7ba84fbc2d5bd61b45cb2fe5eb65aa78",
"kiss carry display unusual confirm curtain upgrade antique rotate hello void custom frequent obey nut hole price segment",
"ed56ff6c833c07982eb7119a8f48fd363c4a9b1601cd2de736b01045c5eb8ab4f57b079403485d1c4924f0790dc10a971763337cb9f9c62226f64fff26397c79"
],
[
"4fa1a8bc3e6d80ee1316050e862c1812031493212b7ec3f3bb1b08f168cabeef",
"exile ask congress lamp submit jacket era scheme attend cousin alcohol catch course end lucky hurt sentence oven short ball bird grab wing top",
"095ee6f817b4c2cb30a5a797360a81a40ab0f9a4e25ecd672a3f58a0b5ba0687c096a6b14d2c0deb3bdefce4f61d01ae07417d502429352e27695163f7447a8c"
],
[
"18ab19a9f54a9274f03e5209a2ac8a91",
"board flee heavy tunnel powder denial science ski answer betray cargo cat",
"6eff1bb21562918509c73cb990260db07c0ce34ff0e3cc4a8cb3276129fbcb300bddfe005831350efd633909f476c45c88253276d9fd0df6ef48609e8bb7dca8"
],
[
"18a2e1d81b8ecfb2a333adcb0c17a5b9eb76cc5d05db91a4",
"board blade invite damage undo sun mimic interest slam gaze truly inherit resist great inject rocket museum chief",
"f84521c777a13b61564234bf8f8b62b3afce27fc4062b51bb5e62bdfecb23864ee6ecf07c1d5a97c0834307c5c852d8ceb88e7c97923c0a3b496bedd4e5f88a9"
],
[
"15da872c95a13dd738fbf50e427583ad61f18fd99f628c417a61cf8343c90419",
"beyond stage sleep clip because twist token leaf atom beauty genius food business side grid unable middle armed observe pair crouch tonight away coconut",
"b15509eaa2d09d3efd3e006ef42151b30367dc6e3aa5e44caba3fe4d3e352e65101fbdb86a96776b91946ff06f8eac594dc6ee1d3e82a42dfe1b40fef6bcc3fd"
]
]
}

View File

@ -18,11 +18,9 @@
* 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
package com.vitorpamplona.quartz.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import org.junit.Assert.assertEquals

View File

@ -18,7 +18,7 @@
* 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
package com.vitorpamplona.quartz.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation

View File

@ -18,13 +18,12 @@
* 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
package com.vitorpamplona.quartz.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.Nip44v2
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
@ -70,7 +69,7 @@ class NIP44v2Test {
@Test
fun encryptDecryptTest() {
for (v in vectors.v2?.valid?.encryptDecrypt!!) {
val pub2 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec2!!.hexToByteArray())
val pub2 = KeyPair(v.sec2!!.hexToByteArray())
val conversationKey1 = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey)
assertEquals(v.conversationKey, conversationKey1.toHexKey())
@ -85,7 +84,7 @@ class NIP44v2Test {
assertEquals(v.payload, ciphertext)
val pub1 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec1.hexToByteArray())
val pub1 = KeyPair(v.sec1.hexToByteArray())
val conversationKey2 = nip44v2.getConversationKey(v.sec2.hexToByteArray(), pub1.pubKey)
assertEquals(v.conversationKey, conversationKey2.toHexKey())

View File

@ -18,10 +18,9 @@
* 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
package com.vitorpamplona.quartz.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.Nip49
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals

View File

@ -0,0 +1,79 @@
/**
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip32SeedDerivationTest {
val masterBitcoin =
Bip32SeedDerivation.generate(
Bip39Mnemonics.toSeed("gun please vital unable phone catalog explain raise erosion zoo truly exist", ""),
)
val nostrMnemonic0 =
Bip32SeedDerivation.generate(
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", ""),
)
val nostrMnemonic1 =
Bip32SeedDerivation.generate(
Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", ""),
)
@Test
fun restoreBIP44Wallet() {
val privateKey = Bip32SeedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/44'/1'/0'"))
assertEquals("50b3e7905c642309c8a8b73df5a49757a10f2bebb5804571b9db9004cce8a190", privateKey.toHexKey())
}
@Test
fun restoreBIP49Wallet() {
val privateKey = Bip32SeedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'"))
assertEquals("154c02c0b66899291a19012207642ba096a2d3ebf51baf153c9495976feb1b30", privateKey.toHexKey())
}
@Test
fun restoreBIP84Wallet() {
val privateKey = Bip32SeedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'"))
assertEquals("53e8c09a0e3ddcd8d68821c1e99e823966e99df91fb253e1f453a443ba543cb2", privateKey.toHexKey())
}
@Test
fun testGenerateMasterKeyForNostrMnemonics0() {
assertEquals(
"dbbcc0e112894d1430d5bc348d1bd72e8ac339952702be1fe572de80fe1b7fcb",
nostrMnemonic0.secretkeybytes.toHexKey(),
)
}
@Test
fun testGenerateMasterKeyForNostrMnemonics1() {
assertEquals(
"d58d40d5724435552fa442350b75e0ff95a19d990e908e3a516bcc88f780108f",
nostrMnemonic1.secretkeybytes.toHexKey(),
)
}
}

View File

@ -0,0 +1,68 @@
/**
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip39KeyPathTest {
// 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)
}
@Test
fun testKeyPath() {
// m/44'/1237'/<account>'/0/0
val path = nip6Path(0).path
assertEquals(2147483692L, path[0])
assertEquals(2147484885L, path[1])
assertEquals(2147483648L, path[2])
assertEquals(0L, path[3])
assertEquals(0L, path[4])
}
@Test
fun testKeyDecompiledPath() {
// m/44'/1237'/<account>'/0/0
val path = KeyPath.computePath("m/44'/1237'/0'/0/0")
assertEquals(2147483692L, path[0])
assertEquals(2147484885L, path[1])
assertEquals(2147483648L, path[2])
assertEquals(0L, path[3])
assertEquals(0L, path[4])
}
@Test
fun testHardening() {
assertEquals(2147483692, Hardener.hardened(44))
}
}

View File

@ -0,0 +1,120 @@
/**
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip39MnemonicsTest {
private val tests =
jacksonObjectMapper()
.readTree(
InstrumentationRegistry.getInstrumentation().context.assets.open("bip39.vectors.json"),
)
@Test
fun toSeed() {
val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
val passphrase = ""
val seed = Bip39Mnemonics.toSeed(mnemonics, passphrase)
assertEquals(
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
Hex.encode(seed),
)
}
@Test
fun referenceTests() {
tests.get("english").map {
val raw = it.get(0).asText()
val mnemonics = it.get(1).asText()
val seed = it.get(2).asText()
assertEquals(Bip39Mnemonics.toMnemonics(Hex.decode(raw)).joinToString(" "), mnemonics)
assertEquals(Hex.encode(Bip39Mnemonics.toSeed(Bip39Mnemonics.toMnemonics(Hex.decode(raw)), "TREZOR")), seed)
}
}
@Test
fun validateMnemonicsValid() {
for (i in 0..99) {
for (length in listOf(16, 20, 24, 28, 32, 36, 40)) {
val mnemonics = Bip39Mnemonics.toMnemonics(CryptoUtils.random(length))
Bip39Mnemonics.validate(mnemonics)
}
}
}
@Test
fun testNostrMnemonics1() {
assertEquals(
"173b9c5f0d165502d08a4d122b2c9bf1e33e27806eac119713600a263c1241101dc55fb7cffb8f48a59b19a5ba65b037904f907bb8d08eb5bff8a17e85c2ee93",
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", "").toHexKey(),
)
}
@Test
fun testNostrMnemonics2() {
assertEquals(
"5e2bd11b4d371f25098ed95ded029e2b9268cf188e6b764023bafbbd8fe843244fb72ca8f66c9378085d69fcb4d4224e709ffe071acafa7b7d5eb54b2905d553",
Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", "").toHexKey(),
)
}
@Test
fun testSnortMnemonics() {
assertEquals(
"b8fb0abca0a032d74b6a58def8ba51402810528d0a7bb9acec8fa1880e1003e13c4421b916cb4c8a861f2694b255010fdc91c2d1ac24c8ce51287ac65d1eb399",
Bip39Mnemonics.toSeed("clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless", "").toHexKey(),
)
}
@Test
fun validateMnemonicsInvalid() {
val invalidMnemonics =
listOf(
"",
// one word missing
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow",
// one extra word
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog fog",
// wrong word
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fig",
)
invalidMnemonics.map {
try {
Bip39Mnemonics.validate(it)
fail()
} catch (e: Exception) {
assertTrue(true)
}
}
}
}

View File

@ -0,0 +1,68 @@
/**
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey
import junit.framework.TestCase.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip06Test {
// private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
// nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
private val menemonic0 = "leader monkey parrot ring guide accident before fence cannon height naive bean"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val menemonic1 = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val snortTest = "clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless"
@Test
fun fromSeedNip06TestVector0() {
val privateKeyHex = Nip06().privateKeyFromMnemonic(menemonic0).toHexKey()
assertEquals("7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a", privateKeyHex)
val privateKeyHex21 = Nip06().privateKeyFromMnemonic(menemonic0, 21).toHexKey()
assertEquals("576390ec69951fcfbf159f2aac0965bb2e6d7a07da2334992af3225c57eaefca", privateKeyHex21)
}
@Test
fun fromSeedNip06TestVector1() {
val privateKeyHex = Nip06().privateKeyFromMnemonic(menemonic1).toHexKey()
assertEquals("c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add", privateKeyHex)
val privateKeyHex21 = Nip06().privateKeyFromMnemonic(menemonic1, 42).toHexKey()
assertEquals("ad993054383da74e955f8b86346365b5ffd6575992e1de3738dda9f94407052b", privateKeyHex21)
}
@Test
@Ignore("Snort is not correctly implemented")
fun fromSeedNip06FromSnort() {
val privateKeyNsec = Nip06().privateKeyFromMnemonic(snortTest).toHexKey()
assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec)
}
}

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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

View File

@ -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)
}
}