rearranges the crypto package into separate nips and reduces the amount of circular dependencies.

This commit is contained in:
Vitor Pamplona 2024-06-25 12:13:17 -04:00
parent 79ace7f18c
commit a8a2bda9af
34 changed files with 468 additions and 461 deletions

View File

@ -33,7 +33,6 @@ import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.service.HttpClientManager
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
@ -129,8 +128,8 @@ class AccountStateViewModel : ViewModel() {
proxyPort = proxyPort,
signer = NostrSignerInternal(keyPair),
)
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
val keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key))
} else if (key.contains(" ") && CryptoUtils.isValidMnemonic(key)) {
val keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key))
Account(
keyPair,
proxy = proxy,

View File

@ -50,7 +50,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.getSharedSecretNIP44v1(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(CryptoUtils.nip44.v1.getSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@ -70,7 +70,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.computeSharedSecretNIP44v1(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(CryptoUtils.nip44.v1.computeSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
}
}

View File

@ -165,7 +165,7 @@ class GiftWrapReceivingBenchmark {
benchmarkRule.measureRepeated {
assertNotNull(
CryptoUtils.decryptNIP44v2(
CryptoUtils.decryptNIP44(
wrap.content,
receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(),
@ -182,7 +182,7 @@ class GiftWrapReceivingBenchmark {
val wrap = createWrap(sender, receiver)
val innerJson =
CryptoUtils.decryptNIP44v2(
CryptoUtils.decryptNIP44(
wrap.content,
receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(),
@ -200,7 +200,7 @@ class GiftWrapReceivingBenchmark {
benchmarkRule.measureRepeated {
assertNotNull(
CryptoUtils.decryptNIP44v2(
CryptoUtils.decryptNIP44(
seal.content,
receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(),
@ -217,7 +217,7 @@ class GiftWrapReceivingBenchmark {
val seal = createSeal(sender, receiver)
val innerJson =
CryptoUtils.decryptNIP44v2(
CryptoUtils.decryptNIP44(
seal.content,
receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(),

View File

@ -43,7 +43,7 @@ class CryptoUtilsTest {
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key =
CryptoUtils.getSharedSecretNIP44v1(
CryptoUtils.nip44.v1.getSharedSecret(
privateKey = privateKey.hexToByteArray(),
pubKey = publicKey.hexToByteArray(),
)
@ -56,8 +56,8 @@ class CryptoUtilsTest {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = CryptoUtils.getSharedSecretNIP44v1(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.getSharedSecretNIP44v1(receiver.privKey!!, sender.pubKey)
val sharedSecret1 = CryptoUtils.nip44.v1.getSharedSecret(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.nip44.v1.getSharedSecret(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
@ -88,8 +88,8 @@ class CryptoUtilsTest {
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP44v1(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP44v1(encrypted, privateKey, publicKey)
val encrypted = CryptoUtils.nip44.v1.encrypt(msg, privateKey, publicKey)
val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@ -113,10 +113,10 @@ class CryptoUtilsTest {
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val sharedSecret = CryptoUtils.getSharedSecretNIP44v1(privateKey, publicKey)
val sharedSecret = CryptoUtils.nip44.v1.getSharedSecret(privateKey, publicKey)
val encrypted = CryptoUtils.encryptNIP44v1(msg, sharedSecret)
val decrypted = CryptoUtils.decryptNIP44v1(encrypted, sharedSecret)
val encrypted = CryptoUtils.nip44.v1.encrypt(msg, sharedSecret)
val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}

View File

@ -22,42 +22,45 @@ package com.vitorpamplona.quartz.crypto.nip06
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip32SeedDerivationTest {
val seedDerivation = Bip32SeedDerivation(Secp256k1.get())
val masterBitcoin =
Bip32SeedDerivation.generate(
seedDerivation.generate(
Bip39Mnemonics.toSeed("gun please vital unable phone catalog explain raise erosion zoo truly exist", ""),
)
val nostrMnemonic0 =
Bip32SeedDerivation.generate(
seedDerivation.generate(
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", ""),
)
val nostrMnemonic1 =
Bip32SeedDerivation.generate(
seedDerivation.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'"))
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/44'/1'/0'"))
assertEquals("50b3e7905c642309c8a8b73df5a49757a10f2bebb5804571b9db9004cce8a190", privateKey.toHexKey())
}
@Test
fun restoreBIP49Wallet() {
val privateKey = Bip32SeedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'"))
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'"))
assertEquals("154c02c0b66899291a19012207642ba096a2d3ebf51baf153c9495976feb1b30", privateKey.toHexKey())
}
@Test
fun restoreBIP84Wallet() {
val privateKey = Bip32SeedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'"))
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'"))
assertEquals("53e8c09a0e3ddcd8d68821c1e99e823966e99df91fb253e1f453a443ba543cb2", privateKey.toHexKey())
}

View File

@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.crypto.nip06
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals
import org.junit.Ignore
import org.junit.Test
@ -29,6 +30,8 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip06Test {
val nip06 = Nip06(Secp256k1.get())
// private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
// nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
private val menemonic0 = "leader monkey parrot ring guide accident before fence cannon height naive bean"
@ -43,26 +46,26 @@ class Nip06Test {
@Test
fun fromSeedNip06TestVector0() {
val privateKeyHex = Nip06().privateKeyFromMnemonic(menemonic0).toHexKey()
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic0).toHexKey()
assertEquals("7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a", privateKeyHex)
val privateKeyHex21 = Nip06().privateKeyFromMnemonic(menemonic0, 21).toHexKey()
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic0, 21).toHexKey()
assertEquals("576390ec69951fcfbf159f2aac0965bb2e6d7a07da2334992af3225c57eaefca", privateKeyHex21)
}
@Test
fun fromSeedNip06TestVector1() {
val privateKeyHex = Nip06().privateKeyFromMnemonic(menemonic1).toHexKey()
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic1).toHexKey()
assertEquals("c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add", privateKeyHex)
val privateKeyHex21 = Nip06().privateKeyFromMnemonic(menemonic1, 42).toHexKey()
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()
val privateKeyNsec = nip06.privateKeyFromMnemonic(snortTest).toHexKey()
assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec)
}
}

View File

@ -18,12 +18,13 @@
* 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
package com.vitorpamplona.quartz.crypto.nip44
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.KeyPair
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
@ -79,8 +80,7 @@ class NIP44v2Test {
v.plaintext!!,
conversationKey1,
v.nonce!!.hexToByteArray(),
)
.encodePayload()
).encodePayload()
assertEquals(v.payload, ciphertext)
@ -107,8 +107,7 @@ class NIP44v2Test {
plaintext,
conversationKey,
v.nonce!!.hexToByteArray(),
)
.encodePayload()
).encodePayload()
assertEquals(v.payloadSha256, sha256Hex(ciphertext.toByteArray(Charsets.UTF_8)))

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.crypto
package com.vitorpamplona.quartz.crypto.nip49
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey

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.encoders
import com.vitorpamplona.quartz.crypto.CryptoUtils
import org.junit.Assert.assertEquals
@ -31,8 +31,8 @@ class HexEncodingTest {
fun testHexEncodeDecodeOurs() {
assertEquals(
testHex,
com.vitorpamplona.quartz.encoders.Hex.encode(
com.vitorpamplona.quartz.encoders.Hex.decode(testHex),
Hex.encode(
Hex.decode(testHex),
),
)
}
@ -42,7 +42,8 @@ class HexEncodingTest {
assertEquals(
testHex,
fr.acinq.secp256k1.Hex.encode(
fr.acinq.secp256k1.Hex.decode(testHex),
fr.acinq.secp256k1.Hex
.decode(testHex),
),
)
}
@ -51,14 +52,17 @@ class HexEncodingTest {
fun testRandoms() {
for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate()
val hex = fr.acinq.secp256k1.Hex.encode(bytes)
val hex =
fr.acinq.secp256k1.Hex
.encode(bytes)
assertEquals(
fr.acinq.secp256k1.Hex.encode(bytes),
com.vitorpamplona.quartz.encoders.Hex.encode(bytes),
fr.acinq.secp256k1.Hex
.encode(bytes),
Hex.encode(bytes),
)
assertEquals(
bytes.toList(),
com.vitorpamplona.quartz.encoders.Hex.decode(hex).toList(),
Hex.decode(hex).toList(),
)
}
}

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.encoders
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith

View File

@ -18,14 +18,10 @@
* 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.encoders
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.decodePrivateKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FhirResourceEvent
import com.vitorpamplona.quartz.events.TextNoteEvent

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.events
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Assert.assertEquals
import org.junit.Test

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.events
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.TextNoteEvent
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

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.events
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.Event
import org.junit.Test
import org.junit.runner.RunWith

View File

@ -18,18 +18,13 @@
* 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.events
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.NIP17Factory
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@ -276,14 +271,12 @@ class GiftWrapEventTest {
assertTrue(unwrappedMsgForReceiverByReceiver is SealedGossipEvent)
if (unwrappedMsgForReceiverByReceiver is SealedGossipEvent) {
unwrappedMsgForReceiverByReceiver.cachedGossip(receiver) {
unwrappedGossipToReceiverByReceiver ->
unwrappedMsgForReceiverByReceiver.cachedGossip(receiver) { unwrappedGossipToReceiverByReceiver ->
assertEquals("Hi There!", unwrappedGossipToReceiverByReceiver?.content)
countDownDecryptLatch.countDown()
}
unwrappedMsgForReceiverByReceiver.cachedGossip(sender) { unwrappedGossipToReceiverBySender,
->
unwrappedMsgForReceiverByReceiver.cachedGossip(sender) { unwrappedGossipToReceiverBySender ->
fail(
"Should not be able to decrypt msg for the receiver by the receiver but decrypted with the sender",
)
@ -422,13 +415,11 @@ class GiftWrapEventTest {
assertEquals(SealedGossipEvent.KIND, unwrappedMsgForSenderBySender.kind)
if (unwrappedMsgForSenderBySender is SealedGossipEvent) {
unwrappedMsgForSenderBySender.cachedGossip(receiverA) { unwrappedGossipToSenderByReceiverA,
->
unwrappedMsgForSenderBySender.cachedGossip(receiverA) { unwrappedGossipToSenderByReceiverA ->
fail()
}
unwrappedMsgForSenderBySender.cachedGossip(receiverB) { unwrappedGossipToSenderByReceiverB,
->
unwrappedMsgForSenderBySender.cachedGossip(receiverB) { unwrappedGossipToSenderByReceiverB ->
fail()
}
@ -459,21 +450,18 @@ class GiftWrapEventTest {
assertEquals(SealedGossipEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind)
if (unwrappedMsgForReceiverAByReceiverA is SealedGossipEvent) {
unwrappedMsgForReceiverAByReceiverA.cachedGossip(receiverA) {
unwrappedGossipToReceiverAByReceiverA ->
unwrappedMsgForReceiverAByReceiverA.cachedGossip(receiverA) { unwrappedGossipToReceiverAByReceiverA ->
assertEquals(
"Who is going to the party tonight?",
unwrappedGossipToReceiverAByReceiverA.content,
)
}
unwrappedMsgForReceiverAByReceiverA.cachedGossip(sender) {
unwrappedGossipToReceiverABySender ->
unwrappedMsgForReceiverAByReceiverA.cachedGossip(sender) { unwrappedGossipToReceiverABySender ->
fail()
}
unwrappedMsgForReceiverAByReceiverA.cachedGossip(receiverB) {
unwrappedGossipToReceiverAByReceiverB ->
unwrappedMsgForReceiverAByReceiverA.cachedGossip(receiverB) { unwrappedGossipToReceiverAByReceiverB ->
fail()
}
}
@ -495,13 +483,11 @@ class GiftWrapEventTest {
assertEquals(SealedGossipEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind)
if (unwrappedMsgForReceiverBByReceiverB is SealedGossipEvent) {
unwrappedMsgForReceiverBByReceiverB.cachedGossip(receiverA) {
unwrappedGossipToReceiverBByReceiverA ->
unwrappedMsgForReceiverBByReceiverB.cachedGossip(receiverA) { unwrappedGossipToReceiverBByReceiverA ->
fail()
}
unwrappedMsgForReceiverBByReceiverB.cachedGossip(receiverB) {
unwrappedGossipToReceiverBByReceiverB ->
unwrappedMsgForReceiverBByReceiverB.cachedGossip(receiverB) { unwrappedGossipToReceiverBByReceiverB ->
assertEquals(
"Who is going to the party tonight?",
unwrappedGossipToReceiverBByReceiverB.content,
@ -510,8 +496,7 @@ class GiftWrapEventTest {
countDownDecryptLatch.countDown()
}
unwrappedMsgForReceiverBByReceiverB.cachedGossip(sender) {
unwrappedGossipToReceiverBBySender ->
unwrappedMsgForReceiverBByReceiverB.cachedGossip(sender) { unwrappedGossipToReceiverBBySender ->
fail()
}
}
@ -538,8 +523,7 @@ class GiftWrapEventTest {
]
]
}
"""
.trimIndent()
""".trimIndent()
var gossip: Event? = null
@ -573,8 +557,7 @@ class GiftWrapEventTest {
]
]
}
"""
.trimIndent()
""".trimIndent()
val privateKey = "409ff7654141eaa16cd2161fe5bd127aeaef71f270c67587474b78998a8e3533"
@ -612,8 +595,7 @@ class GiftWrapEventTest {
"wss://relay.damus.io/"
]
}
"""
.trimIndent()
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
var gossip: Event? = null
@ -647,8 +629,7 @@ class GiftWrapEventTest {
]
]
}
"""
.trimIndent()
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
@ -687,8 +668,7 @@ class GiftWrapEventTest {
"id": "d9fc85ece892ce45ffa737b3ddc0f8b752623181d75363b966191f8c03d2debe",
"sig": "1b20416b83f4b5b8eead11e29c185f46b5e76d1960e4505210ddd00f7a6973cc11268f52a8989e3799b774d5f3a55db95bed4d66a1b6e88ab54becec5c771c17"
}
"""
.trimIndent()
""".trimIndent()
val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700"
@ -753,8 +733,7 @@ class GiftWrapEventTest {
"id": "ae625fd43612127d63bfd1967ba32ae915100842a205fc2c3b3fc02ab3827f08",
"sig": "2807a7ab5728984144676fd34686267cbe6fe38bc2f65a3640ba9243c13e8a1ae5a9a051e8852aa0c997a3623d7fa066cf2073a233c6d7db46fb1a0d4c01e5a3"
}
"""
.trimIndent()
""".trimIndent()
val wrap = Event.fromJson(msg) as GiftWrapEvent
wrap.checkSignature()

View File

@ -18,15 +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.events
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent.Companion.createEncryptionPrivateKey
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import junit.framework.TestCase.assertNotNull

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.ots
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair

View File

@ -20,35 +20,32 @@
*/
package com.vitorpamplona.quartz.crypto
import android.util.Log
import com.vitorpamplona.quartz.crypto.nip01.Nip01
import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.crypto.nip06.Nip06
import com.vitorpamplona.quartz.crypto.nip44.Nip44
import com.vitorpamplona.quartz.crypto.nip44.Nip44v2
import com.vitorpamplona.quartz.crypto.nip49.Nip49
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
object CryptoUtils {
private val secp256k1 = Secp256k1.get()
private val random = SecureRandom()
private val nip04 = Nip04(secp256k1, random)
private val nip44v1 = Nip44v1(secp256k1, random)
private val nip44v2 = Nip44v2(secp256k1, random)
private val nip49 = Nip49(secp256k1, random)
public val nip01 = Nip01(secp256k1, random)
public val nip06 = Nip06(secp256k1)
public val nip04 = Nip04(secp256k1, random)
public val nip44 = Nip44(secp256k1, random, nip04)
public val nip49 = Nip49(secp256k1, random)
fun clearCache() {
nip04.clearCache()
nip44v1.clearCache()
nip44v2.clearCache()
nip44.clearCache()
}
fun randomInt(bound: Int): Int {
return random.nextInt(bound)
}
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random(32)
fun randomInt(bound: Int): Int = random.nextInt(bound)
fun random(size: Int): ByteArray {
val bytes = ByteArray(size)
@ -56,39 +53,32 @@ object CryptoUtils {
return bytes
}
fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = nip01.privkeyCreate()
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun isPrivKeyValid(il: ByteArray): Boolean {
return secp256k1.secKeyVerify(il)
}
fun pubkeyCreate(privKey: ByteArray) = nip01.pubkeyCreate(privKey)
fun signString(
message: String,
privKey: ByteArray,
auxrand32: ByteArray = random(32),
): ByteArray {
return sign(sha256(message.toByteArray()), privKey, auxrand32)
}
): ByteArray = nip01.signString(message, privKey, auxrand32)
fun sign(
data: ByteArray,
privKey: ByteArray,
auxrand32: ByteArray? = null,
): ByteArray = secp256k1.signSchnorr(data, privKey, auxrand32)
): ByteArray = nip01.sign(data, privKey, auxrand32)
fun verifySignature(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean {
return secp256k1.verifySchnorr(signature, hash, pubKey)
}
): Boolean = nip01.verify(signature, hash, pubKey)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
return nip01.sha256(data)
}
/** NIP 04 Utils */
@ -96,189 +86,84 @@ object CryptoUtils {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.encrypt(msg, privateKey, pubKey)
}
): String = nip04.encrypt(msg, privateKey, pubKey)
fun encryptNIP04(
msg: String,
sharedSecret: ByteArray,
): Nip04.EncryptedInfo {
return nip04.encrypt(msg, sharedSecret)
}
): Nip04.EncryptedInfo = nip04.encrypt(msg, sharedSecret)
fun decryptNIP04(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.decrypt(msg, privateKey, pubKey)
}
): String = nip04.decrypt(msg, privateKey, pubKey)
fun decryptNIP04(
encryptedInfo: Nip04.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
): String = nip04.decrypt(encryptedInfo, privateKey, pubKey)
fun decryptNIP04(
msg: String,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(msg, sharedSecret)
}
): String = nip04.decrypt(msg, sharedSecret)
private fun decryptNIP04(
cipher: String,
nonce: String,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(cipher, nonce, sharedSecret)
}
): String = nip04.decrypt(cipher, nonce, sharedSecret)
private fun decryptNIP04(
encryptedMsg: ByteArray,
iv: ByteArray,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(encryptedMsg, iv, sharedSecret)
}
): String = nip04.decrypt(encryptedMsg, iv, sharedSecret)
fun getSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip04.getSharedSecret(privateKey, pubKey)
}
): ByteArray = nip04.getSharedSecret(privateKey, pubKey)
fun computeSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip04.computeSharedSecret(privateKey, pubKey)
}
): ByteArray = nip04.computeSharedSecret(privateKey, pubKey)
/** NIP 44v1 Utils */
fun encryptNIP44v1(
/** NIP 06 Utils */
fun isValidMnemonic(mnemonic: String): Boolean = nip06.isValidMnemonic(mnemonic)
fun privateKeyFromMnemonic(
mnemonic: String,
account: Long = 0,
) = nip06.privateKeyFromMnemonic(mnemonic, account)
/** NIP 44 Utils */
fun getSharedSecretNIP44(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip44.getSharedSecret(privateKey, pubKey)
fun computeSharedSecretNIP44(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip44.computeSharedSecret(privateKey, pubKey)
fun encryptNIP44(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP44v1(
msg: String,
sharedSecret: ByteArray,
): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, sharedSecret)
}
fun decryptNIP44v1(
encryptedInfo: Nip44v1.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(
encryptedInfo: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(
encryptedInfo: Nip44v1.EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, sharedSecret)
}
fun getSharedSecretNIP44v1(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v1.getSharedSecret(privateKey, pubKey)
}
fun computeSharedSecretNIP44v1(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v1.computeSharedSecret(privateKey, pubKey)
}
/** NIP 44v2 Utils */
fun encryptNIP44v2(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP44v2(
msg: String,
sharedSecret: ByteArray,
): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, sharedSecret)
}
fun decryptNIP44v2(
encryptedInfo: Nip44v2.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v2(
encryptedInfo: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v2(
encryptedInfo: Nip44v2.EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, sharedSecret)
}
fun getSharedSecretNIP44v2(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v2.getConversationKey(privateKey, pubKey)
}
fun computeSharedSecretNIP44v2(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v2.computeConversationKey(privateKey, pubKey)
}
): Nip44v2.EncryptedInfo = nip44.encrypt(msg, privateKey, pubKey)
fun decryptNIP44(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
): String? = nip44.decrypt(payload, privateKey, pubKey)
/** NIP 49 Utils */
fun decryptNIP49(
payload: String,
password: String,
@ -290,87 +175,5 @@ object CryptoUtils {
fun encryptNIP49(
key: HexKey,
password: String,
): String? {
return nip49.encrypt(key, password, 16, Nip49.EncryptedInfo.CLIENT_DOES_NOT_TRACK)
}
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
fun decryptNIP44FromJackson(
json: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return try {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
when (info.v) {
Nip04.EncryptedInfo.V -> {
val encryptedInfo =
Nip04.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
decryptNIP04(encryptedInfo, privateKey, pubKey)
}
Nip44v1.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v1.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
decryptNIP44v1(encryptedInfo, privateKey, pubKey)
}
Nip44v2.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v2.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
mac = Base64.getDecoder().decode(info.mac),
)
decryptNIP44v2(encryptedInfo, privateKey, pubKey)
}
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $json")
e.printStackTrace()
null
}
}
fun decryptNIP44FromBase64(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return try {
val byteArray = Base64.getDecoder().decode(payload)
when (byteArray[0].toInt()) {
Nip04.EncryptedInfo.V -> decryptNIP04(payload, privateKey, pubKey)
Nip44v1.EncryptedInfo.V -> decryptNIP44v1(payload, privateKey, pubKey)
Nip44v2.EncryptedInfo.V -> decryptNIP44v2(payload, privateKey, pubKey)
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $payload")
e.printStackTrace()
null
}
}
fun sum(
first: ByteArray,
second: ByteArray,
): ByteArray {
return secp256k1.privKeyTweakAdd(first, second)
}
): String = nip49.encrypt(key, password)
}

View File

@ -0,0 +1,60 @@
/**
* 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.nip01
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.CryptoUtils.random
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
class Nip01(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random(32)
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun sign(
data: ByteArray,
privKey: ByteArray,
auxrand32: ByteArray? = null,
): ByteArray = secp256k1.signSchnorr(data, privKey, auxrand32)
fun verify(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
fun signString(
message: String,
privKey: ByteArray,
auxrand32: ByteArray = random(32),
): ByteArray = sign(CryptoUtils.sha256(message.toByteArray()), privKey, auxrand32)
}

View File

@ -18,9 +18,11 @@
* 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
package com.vitorpamplona.quartz.crypto.nip04
import android.util.Log
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.crypto.nip44.Nip44v1
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
@ -29,7 +31,10 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip04(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
@ -41,9 +46,7 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
}
): String = encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
fun encrypt(
msg: String,
@ -146,8 +149,8 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun decodeFromNIP04(payload: String): EncryptedInfo? {
return try {
fun decodeFromNIP04(payload: String): EncryptedInfo? =
try {
val parts = payload.split("?iv=")
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(parts[0]),
@ -157,15 +160,14 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
Log.w("NIP04", "Unable to Parse encrypted payload: $payload")
null
}
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext,
)
}
fun encodeToNIP04(): String {
val nonce = Base64.getEncoder().encodeToString(nonce)

View File

@ -20,14 +20,16 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils
import fr.acinq.secp256k1.Secp256k1
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/*
Simplified from: https://github.com/ACINQ/bitcoin-kmp/
*/
object Bip32SeedDerivation {
class Bip32SeedDerivation(
val secp256k1: Secp256k1,
) {
class ExtendedPrivateKey(
val secretkeybytes: ByteArray,
val chaincode: ByteArray,
@ -53,6 +55,15 @@ object Bip32SeedDerivation {
return mac.doFinal(data)
}
fun isPrivKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
fun sum(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
fun derivePrivateKey(
parent: ExtendedPrivateKey,
index: Long,
@ -62,17 +73,17 @@ object Bip32SeedDerivation {
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())
val data = 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" }
require(isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" }
val key = CryptoUtils.sum(il, parent.secretkeybytes)
val key = sum(il, parent.secretkeybytes)
require(CryptoUtils.isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
require(isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
return ExtendedPrivateKey(key, ir)
}
@ -94,7 +105,7 @@ object Bip32SeedDerivation {
fun derivePrivateKey(
parent: ExtendedPrivateKey,
chain: List<Long>,
): ExtendedPrivateKey = chain.fold(parent, Bip32SeedDerivation::derivePrivateKey)
): ExtendedPrivateKey = chain.fold(parent, this::derivePrivateKey)
fun derivePrivateKey(
parent: ExtendedPrivateKey,

View File

@ -20,8 +20,8 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.PBKDF
import com.vitorpamplona.quartz.crypto.nip49.PBKDF
import java.security.MessageDigest
// CODE FROM: https://github.com/ACINQ/bitcoin-kmp/
@ -37,6 +37,11 @@ object Bip39Mnemonics {
return zeroes + digits
}
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
private fun toBinary(x: ByteArray): List<Boolean> = x.map(Bip39Mnemonics::toBinary).flatten()
private fun fromBinary(bin: List<Boolean>): Int = bin.fold(0) { acc, flag -> if (flag) 2 * acc + 1 else 2 * acc }
@ -45,13 +50,12 @@ object Bip39Mnemonics {
items: List<Boolean>,
size: Int,
acc: List<List<Boolean>> = emptyList(),
): List<List<Boolean>> {
return when {
): List<List<Boolean>> =
when {
items.isEmpty() -> acc
items.size < size -> acc + listOf(items)
else -> group(items.drop(size), size, acc + listOf(items.take(size)))
}
}
/**
* @param mnemonics list of mnemonic words
@ -80,20 +84,19 @@ object Bip39Mnemonics {
val databits = bits.subList(0, bitlength)
val checksumbits = bits.subList(bitlength, bits.size)
val data = group(databits, 8).map { fromBinary(it) }.map { it.toByte() }.toByteArray()
val check = toBinary(CryptoUtils.sha256(data)).take(data.size / 4)
val check = toBinary(sha256(data)).take(data.size / 4)
require(check == checksumbits) { "invalid checksum" }
}
fun validate(mnemonics: String): Unit = validate(mnemonics.split(" "))
fun isValid(mnemonics: String): Boolean {
return try {
fun isValid(mnemonics: String): Boolean =
try {
validate(mnemonics)
true
} catch (e: Exception) {
false
}
}
/**
* BIP39 entropy encoding
@ -108,7 +111,7 @@ object Bip39Mnemonics {
wordlist: Array<String>,
): List<String> {
require(wordlist.size == 2048) { "invalid word list (size should be 2048)" }
val digits = toBinary(entropy) + toBinary(CryptoUtils.sha256(entropy)).take(entropy.size / 4)
val digits = toBinary(entropy) + toBinary(sha256(entropy)).take(entropy.size / 4)
return group(digits, 11).map(Bip39Mnemonics::fromBinary).map { wordlist[it] }
}

View File

@ -20,29 +20,31 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
class Nip06 {
import fr.acinq.secp256k1.Secp256k1
class Nip06(
val secp256k1: Secp256k1,
) {
val derivation = Bip32SeedDerivation(secp256k1)
// 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))
private fun nip6Path(account: Long): KeyPath =
nip6Base
.derive(Hardener.hardened(account))
.derive(0L)
.derive(0L)
}
fun isValidMnemonic(mnemonic: String): Boolean {
return Bip39Mnemonics.isValid(mnemonic)
}
fun isValidMnemonic(mnemonic: String): Boolean = Bip39Mnemonics.isValid(mnemonic)
fun privateKeyFromSeed(
seed: ByteArray,
account: Long = 0,
): ByteArray {
return Bip32SeedDerivation.derivePrivateKey(Bip32SeedDerivation.generate(seed), nip6Path(account))
}
): ByteArray = derivation.derivePrivateKey(derivation.generate(seed), nip6Path(account))
fun privateKeyFromMnemonic(
mnemonic: String,

View File

@ -18,13 +18,16 @@
* 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
package com.vitorpamplona.quartz.crypto.nip44
import java.nio.ByteBuffer
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class Hkdf(val algorithm: String = "HmacSHA256", val hashLen: Int = 32) {
class Hkdf(
val algorithm: String = "HmacSHA256",
val hashLen: Int = 32,
) {
fun extract(
key: ByteArray,
salt: ByteArray,

View File

@ -0,0 +1,148 @@
/**
* 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.nip44
import android.util.Log
import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import java.util.Base64
class Nip44(
secp256k1: Secp256k1,
random: SecureRandom,
val nip04: Nip04,
) {
public val v1 = Nip44v1(secp256k1, random)
public val v2 = Nip44v2(secp256k1, random)
fun clearCache() {
v1.clearCache()
v2.clearCache()
}
/** NIP 44v2 Utils */
fun getSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = v2.getConversationKey(privateKey, pubKey)
fun computeSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = v2.computeConversationKey(privateKey, pubKey)
fun encrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): Nip44v2.EncryptedInfo {
// current version should be used.
return v2.encrypt(msg, privateKey, pubKey)
}
// Always decrypt from any version/any encoding
fun decrypt(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
fun decryptNIP44FromJackson(
json: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? =
try {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
when (info.v) {
Nip04.EncryptedInfo.V -> {
val encryptedInfo =
Nip04.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
Nip44v1.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v1.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
v1.decrypt(encryptedInfo, privateKey, pubKey)
}
Nip44v2.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v2.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
mac = Base64.getDecoder().decode(info.mac),
)
v2.decrypt(encryptedInfo, privateKey, pubKey)
}
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $json")
e.printStackTrace()
null
}
fun decryptNIP44FromBase64(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return try {
val byteArray = Base64.getDecoder().decode(payload)
when (byteArray[0].toInt()) {
Nip04.EncryptedInfo.V -> nip04.decrypt(payload, privateKey, pubKey)
Nip44v1.EncryptedInfo.V -> v1.decrypt(payload, privateKey, pubKey)
Nip44v2.EncryptedInfo.V -> v2.decrypt(payload, privateKey, pubKey)
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $payload")
e.printStackTrace()
null
}
}
}

View File

@ -18,18 +18,22 @@
* 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
package com.vitorpamplona.quartz.crypto.nip44
import android.util.Log
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip44v1(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
private val libSodium = SodiumAndroid()
@ -97,15 +101,13 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
fun decrypt(
encryptedInfo: EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return cryptoStreamXChaCha20Xor(
): String? =
cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret),
)
?.decodeToString()
}
)?.decodeToString()
fun getSharedSecret(
privateKey: ByteArray,
@ -155,11 +157,11 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext,
)
}
}
}

View File

@ -18,11 +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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
@ -33,7 +34,10 @@ import java.util.Base64
import kotlin.math.floor
import kotlin.math.log2
class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip44v2(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val libSodium = SodiumAndroid()
@ -55,9 +59,7 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): EncryptedInfo {
return encrypt(msg, getConversationKey(privateKey, pubKey))
}
): EncryptedInfo = encrypt(msg, getConversationKey(privateKey, pubKey))
fun encrypt(
plaintext: String,
@ -99,17 +101,13 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return decrypt(payload, getConversationKey(privateKey, pubKey))
}
): String? = decrypt(payload, getConversationKey(privateKey, pubKey))
fun decrypt(
decoded: EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return decrypt(decoded, getConversationKey(privateKey, pubKey))
}
): String? = decrypt(decoded, getConversationKey(privateKey, pubKey))
fun decrypt(
payload: String,
@ -173,7 +171,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
check(unpaddedLen <= maxPlaintextSize) { "Message is too long ($unpaddedLen): $plaintext" }
val prefix =
ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(unpaddedLen.toShort()).array()
ByteBuffer
.allocate(2)
.order(ByteOrder.BIG_ENDIAN)
.putShort(unpaddedLen.toShort())
.array()
val suffix = ByteArray(calcPaddedLen(unpaddedLen) - unpaddedLen)
return ByteBuffer.wrap(prefix + unpadded + suffix).array()
}
@ -182,13 +184,12 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
byte1: Byte,
byte2: Byte,
bigEndian: Boolean,
): Int {
return if (bigEndian) {
): Int =
if (bigEndian) {
(byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF))
} else {
(byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF))
}
}
fun unpad(padded: ByteArray): String {
val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true)
@ -273,11 +274,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext + mac,
)
}
}
}

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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
@ -66,9 +66,7 @@ fun cryptoStreamXchacha20Xor(
messageLen: Long,
nonce: ByteArray,
key: ByteArray,
): Int {
return cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
}
): Int = cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
fun cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,

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.crypto
package com.vitorpamplona.quartz.crypto.nip49
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
@ -32,16 +32,17 @@ import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import java.text.Normalizer
class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip49(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
fun decrypt(
nCryptSec: String,
password: String,
): HexKey {
return decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
}
): HexKey = decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
fun decrypt(
encryptedInfo: EncryptedInfo?,
@ -75,11 +76,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
fun encrypt(
secretKeyHex: String,
password: String,
logn: Int,
ksb: Byte,
): String? {
return encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
}
logn: Int = 16,
ksb: Byte = EncryptedInfo.CLIENT_DOES_NOT_TRACK,
): String = encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
fun encrypt(
secretKey: ByteArray,
@ -104,9 +103,12 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
// byte[] ad, long adLen,
// byte[] nSec, byte[] nPub, byte[] k
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext, longArrayOf(48),
secretKey, secretKey.size.toLong(),
byteArrayOf(ksb), 1,
ciphertext,
longArrayOf(48),
secretKey,
secretKey.size.toLong(),
byteArrayOf(ksb),
1,
key,
nonce,
key,
@ -163,8 +165,8 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
}
// ln(n.toDouble()).toInt().toByte(),
fun encodePayload(): String {
return Bech32.encodeBytes(
fun encodePayload(): String =
Bech32.encodeBytes(
hrp = "ncryptsec",
byteArrayOf(
version,
@ -172,6 +174,5 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
) + salt + nonce + keySecurity + encryptedKey,
Bech32.Encoding.Bech32,
)
}
}
}

View File

@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved.
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

View File

@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved.
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import javax.crypto.Mac;
import java.security.GeneralSecurityException;

View File

@ -23,7 +23,7 @@
* questions.
*/
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import java.security.MessageDigest;
import java.security.spec.KeySpec;

View File

@ -31,7 +31,9 @@ import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toHexKey()) {
class NostrSignerInternal(
val keyPair: KeyPair,
) : NostrSigner(keyPair.pubKey.toHexKey()) {
override fun <T : Event> sign(
createdAt: Long,
kind: Int,
@ -52,10 +54,9 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
fun isUnsignedPrivateEvent(
kind: Int,
tags: Array<Array<String>>,
): Boolean {
return kind == LnZapRequestEvent.KIND &&
): Boolean =
kind == LnZapRequestEvent.KIND &&
tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() }
}
fun <T : Event> signNormal(
createdAt: Long,
@ -123,12 +124,12 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
if (keyPair.privKey == null) return
onReady(
CryptoUtils.encryptNIP44v2(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
)
.encodePayload(),
CryptoUtils
.encryptNIP44(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
).encodePayload(),
)
}
@ -139,12 +140,12 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
) {
if (keyPair.privKey == null) return
CryptoUtils.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)
?.let { onReady(it) }
CryptoUtils
.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)?.let { onReady(it) }
}
private fun <T> signPrivateZap(

View File

@ -18,10 +18,8 @@
* 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.encoders
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.utils.DualCase
import com.vitorpamplona.quartz.utils.containsAny
import junit.framework.TestCase
import org.junit.Test
@ -30,8 +28,7 @@ class TimeUtilsTest {
"""Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
"""
.intern()
""".intern()
val atTheMiddle = DualCase("Lorem Ipsum".lowercase(), "Lorem Ipsum".uppercase())
val atTheBeginning = DualCase("contrAry".lowercase(), "contrAry".uppercase())