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

View File

@ -50,7 +50,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair() val keyPair2 = KeyPair()
benchmarkRule.measureRepeated { 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() val keyPair2 = KeyPair()
benchmarkRule.measureRepeated { 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 { benchmarkRule.measureRepeated {
assertNotNull( assertNotNull(
CryptoUtils.decryptNIP44v2( CryptoUtils.decryptNIP44(
wrap.content, wrap.content,
receiver.keyPair.privKey!!, receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(), wrap.pubKey.hexToByteArray(),
@ -182,7 +182,7 @@ class GiftWrapReceivingBenchmark {
val wrap = createWrap(sender, receiver) val wrap = createWrap(sender, receiver)
val innerJson = val innerJson =
CryptoUtils.decryptNIP44v2( CryptoUtils.decryptNIP44(
wrap.content, wrap.content,
receiver.keyPair.privKey!!, receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(), wrap.pubKey.hexToByteArray(),
@ -200,7 +200,7 @@ class GiftWrapReceivingBenchmark {
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
assertNotNull( assertNotNull(
CryptoUtils.decryptNIP44v2( CryptoUtils.decryptNIP44(
seal.content, seal.content,
receiver.keyPair.privKey!!, receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(), seal.pubKey.hexToByteArray(),
@ -217,7 +217,7 @@ class GiftWrapReceivingBenchmark {
val seal = createSeal(sender, receiver) val seal = createSeal(sender, receiver)
val innerJson = val innerJson =
CryptoUtils.decryptNIP44v2( CryptoUtils.decryptNIP44(
seal.content, seal.content,
receiver.keyPair.privKey!!, receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(), seal.pubKey.hexToByteArray(),

View File

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

View File

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

View File

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

View File

@ -18,12 +18,13 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
@ -79,8 +80,7 @@ class NIP44v2Test {
v.plaintext!!, v.plaintext!!,
conversationKey1, conversationKey1,
v.nonce!!.hexToByteArray(), v.nonce!!.hexToByteArray(),
) ).encodePayload()
.encodePayload()
assertEquals(v.payload, ciphertext) assertEquals(v.payload, ciphertext)
@ -107,8 +107,7 @@ class NIP44v2Test {
plaintext, plaintext,
conversationKey, conversationKey,
v.nonce!!.hexToByteArray(), v.nonce!!.hexToByteArray(),
) ).encodePayload()
.encodePayload()
assertEquals(v.payloadSha256, sha256Hex(ciphertext.toByteArray(Charsets.UTF_8))) 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 * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey 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 * 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. * 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 com.vitorpamplona.quartz.crypto.CryptoUtils
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -31,8 +31,8 @@ class HexEncodingTest {
fun testHexEncodeDecodeOurs() { fun testHexEncodeDecodeOurs() {
assertEquals( assertEquals(
testHex, testHex,
com.vitorpamplona.quartz.encoders.Hex.encode( Hex.encode(
com.vitorpamplona.quartz.encoders.Hex.decode(testHex), Hex.decode(testHex),
), ),
) )
} }
@ -42,7 +42,8 @@ class HexEncodingTest {
assertEquals( assertEquals(
testHex, testHex,
fr.acinq.secp256k1.Hex.encode( 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() { fun testRandoms() {
for (i in 0..1000) { for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate() val bytes = CryptoUtils.privkeyCreate()
val hex = fr.acinq.secp256k1.Hex.encode(bytes) val hex =
fr.acinq.secp256k1.Hex
.encode(bytes)
assertEquals( assertEquals(
fr.acinq.secp256k1.Hex.encode(bytes), fr.acinq.secp256k1.Hex
com.vitorpamplona.quartz.encoders.Hex.encode(bytes), .encode(bytes),
Hex.encode(bytes),
) )
assertEquals( assertEquals(
bytes.toList(), 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 * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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 * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair 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.Event
import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FhirResourceEvent
import com.vitorpamplona.quartz.events.TextNoteEvent 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 * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

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

View File

@ -18,15 +18,12 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey 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.events.LnZapRequestEvent.Companion.createEncryptionPrivateKey
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import junit.framework.TestCase.assertNotNull 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 * 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. * 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair

View File

@ -20,35 +20,32 @@
*/ */
package com.vitorpamplona.quartz.crypto 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.encoders.HexKey
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Base64
object CryptoUtils { object CryptoUtils {
private val secp256k1 = Secp256k1.get() private val secp256k1 = Secp256k1.get()
private val random = SecureRandom() private val random = SecureRandom()
private val nip04 = Nip04(secp256k1, random) public val nip01 = Nip01(secp256k1, random)
private val nip44v1 = Nip44v1(secp256k1, random) public val nip06 = Nip06(secp256k1)
private val nip44v2 = Nip44v2(secp256k1, random) public val nip04 = Nip04(secp256k1, random)
private val nip49 = Nip49(secp256k1, random) public val nip44 = Nip44(secp256k1, random, nip04)
public val nip49 = Nip49(secp256k1, random)
fun clearCache() { fun clearCache() {
nip04.clearCache() nip04.clearCache()
nip44v1.clearCache() nip44.clearCache()
nip44v2.clearCache()
} }
fun randomInt(bound: Int): Int { fun randomInt(bound: Int): Int = random.nextInt(bound)
return random.nextInt(bound)
}
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random(32)
fun random(size: Int): ByteArray { fun random(size: Int): ByteArray {
val bytes = ByteArray(size) val bytes = ByteArray(size)
@ -56,39 +53,32 @@ object CryptoUtils {
return bytes 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 pubkeyCreate(privKey: ByteArray) = nip01.pubkeyCreate(privKey)
fun isPrivKeyValid(il: ByteArray): Boolean {
return secp256k1.secKeyVerify(il)
}
fun signString( fun signString(
message: String, message: String,
privKey: ByteArray, privKey: ByteArray,
auxrand32: ByteArray = random(32), auxrand32: ByteArray = random(32),
): ByteArray { ): ByteArray = nip01.signString(message, privKey, auxrand32)
return sign(sha256(message.toByteArray()), privKey, auxrand32)
}
fun sign( fun sign(
data: ByteArray, data: ByteArray,
privKey: ByteArray, privKey: ByteArray,
auxrand32: ByteArray? = null, auxrand32: ByteArray? = null,
): ByteArray = secp256k1.signSchnorr(data, privKey, auxrand32) ): ByteArray = nip01.sign(data, privKey, auxrand32)
fun verifySignature( fun verifySignature(
signature: ByteArray, signature: ByteArray,
hash: ByteArray, hash: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): Boolean { ): Boolean = nip01.verify(signature, hash, pubKey)
return secp256k1.verifySchnorr(signature, hash, pubKey)
}
fun sha256(data: ByteArray): ByteArray { fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time // Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data) return nip01.sha256(data)
} }
/** NIP 04 Utils */ /** NIP 04 Utils */
@ -96,189 +86,84 @@ object CryptoUtils {
msg: String, msg: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String { ): String = nip04.encrypt(msg, privateKey, pubKey)
return nip04.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP04( fun encryptNIP04(
msg: String, msg: String,
sharedSecret: ByteArray, sharedSecret: ByteArray,
): Nip04.EncryptedInfo { ): Nip04.EncryptedInfo = nip04.encrypt(msg, sharedSecret)
return nip04.encrypt(msg, sharedSecret)
}
fun decryptNIP04( fun decryptNIP04(
msg: String, msg: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String { ): String = nip04.decrypt(msg, privateKey, pubKey)
return nip04.decrypt(msg, privateKey, pubKey)
}
fun decryptNIP04( fun decryptNIP04(
encryptedInfo: Nip04.EncryptedInfo, encryptedInfo: Nip04.EncryptedInfo,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String { ): String = nip04.decrypt(encryptedInfo, privateKey, pubKey)
return nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP04( fun decryptNIP04(
msg: String, msg: String,
sharedSecret: ByteArray, sharedSecret: ByteArray,
): String { ): String = nip04.decrypt(msg, sharedSecret)
return nip04.decrypt(msg, sharedSecret)
}
private fun decryptNIP04( private fun decryptNIP04(
cipher: String, cipher: String,
nonce: String, nonce: String,
sharedSecret: ByteArray, sharedSecret: ByteArray,
): String { ): String = nip04.decrypt(cipher, nonce, sharedSecret)
return nip04.decrypt(cipher, nonce, sharedSecret)
}
private fun decryptNIP04( private fun decryptNIP04(
encryptedMsg: ByteArray, encryptedMsg: ByteArray,
iv: ByteArray, iv: ByteArray,
sharedSecret: ByteArray, sharedSecret: ByteArray,
): String { ): String = nip04.decrypt(encryptedMsg, iv, sharedSecret)
return nip04.decrypt(encryptedMsg, iv, sharedSecret)
}
fun getSharedSecretNIP04( fun getSharedSecretNIP04(
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): ByteArray { ): ByteArray = nip04.getSharedSecret(privateKey, pubKey)
return nip04.getSharedSecret(privateKey, pubKey)
}
fun computeSharedSecretNIP04( fun computeSharedSecretNIP04(
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): ByteArray { ): ByteArray = nip04.computeSharedSecret(privateKey, pubKey)
return nip04.computeSharedSecret(privateKey, pubKey)
}
/** NIP 44v1 Utils */ /** NIP 06 Utils */
fun encryptNIP44v1( 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, msg: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): Nip44v1.EncryptedInfo { ): Nip44v2.EncryptedInfo = nip44.encrypt(msg, privateKey, pubKey)
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)
}
fun decryptNIP44( fun decryptNIP44(
payload: String, payload: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String? { ): String? = nip44.decrypt(payload, privateKey, pubKey)
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
/** NIP 49 Utils */
fun decryptNIP49( fun decryptNIP49(
payload: String, payload: String,
password: String, password: String,
@ -290,87 +175,5 @@ object CryptoUtils {
fun encryptNIP49( fun encryptNIP49(
key: HexKey, key: HexKey,
password: String, password: String,
): String? { ): String = nip49.encrypt(key, password)
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)
}
} }

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

View File

@ -20,14 +20,16 @@
*/ */
package com.vitorpamplona.quartz.crypto.nip06 package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils import fr.acinq.secp256k1.Secp256k1
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
/* /*
Simplified from: https://github.com/ACINQ/bitcoin-kmp/ Simplified from: https://github.com/ACINQ/bitcoin-kmp/
*/ */
object Bip32SeedDerivation { class Bip32SeedDerivation(
val secp256k1: Secp256k1,
) {
class ExtendedPrivateKey( class ExtendedPrivateKey(
val secretkeybytes: ByteArray, val secretkeybytes: ByteArray,
val chaincode: ByteArray, val chaincode: ByteArray,
@ -53,6 +55,15 @@ object Bip32SeedDerivation {
return mac.doFinal(data) 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( fun derivePrivateKey(
parent: ExtendedPrivateKey, parent: ExtendedPrivateKey,
index: Long, index: Long,
@ -62,17 +73,17 @@ object Bip32SeedDerivation {
val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt()) val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data) hmac512(parent.chaincode, data)
} else { } else {
val data = CryptoUtils.pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt()) val data = pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data) hmac512(parent.chaincode, data)
} }
val il = i.take(32).toByteArray() val il = i.take(32).toByteArray()
val ir = i.takeLast(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) return ExtendedPrivateKey(key, ir)
} }
@ -94,7 +105,7 @@ object Bip32SeedDerivation {
fun derivePrivateKey( fun derivePrivateKey(
parent: ExtendedPrivateKey, parent: ExtendedPrivateKey,
chain: List<Long>, chain: List<Long>,
): ExtendedPrivateKey = chain.fold(parent, Bip32SeedDerivation::derivePrivateKey) ): ExtendedPrivateKey = chain.fold(parent, this::derivePrivateKey)
fun derivePrivateKey( fun derivePrivateKey(
parent: ExtendedPrivateKey, parent: ExtendedPrivateKey,

View File

@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.quartz.crypto.nip06 package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.nip49.PBKDF
import com.vitorpamplona.quartz.crypto.PBKDF import java.security.MessageDigest
// CODE FROM: https://github.com/ACINQ/bitcoin-kmp/ // CODE FROM: https://github.com/ACINQ/bitcoin-kmp/
@ -37,6 +37,11 @@ object Bip39Mnemonics {
return zeroes + digits 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 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 } 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>, items: List<Boolean>,
size: Int, size: Int,
acc: List<List<Boolean>> = emptyList(), acc: List<List<Boolean>> = emptyList(),
): List<List<Boolean>> { ): List<List<Boolean>> =
return when { when {
items.isEmpty() -> acc items.isEmpty() -> acc
items.size < size -> acc + listOf(items) items.size < size -> acc + listOf(items)
else -> group(items.drop(size), size, acc + listOf(items.take(size))) else -> group(items.drop(size), size, acc + listOf(items.take(size)))
} }
}
/** /**
* @param mnemonics list of mnemonic words * @param mnemonics list of mnemonic words
@ -80,20 +84,19 @@ object Bip39Mnemonics {
val databits = bits.subList(0, bitlength) val databits = bits.subList(0, bitlength)
val checksumbits = bits.subList(bitlength, bits.size) val checksumbits = bits.subList(bitlength, bits.size)
val data = group(databits, 8).map { fromBinary(it) }.map { it.toByte() }.toByteArray() 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" } require(check == checksumbits) { "invalid checksum" }
} }
fun validate(mnemonics: String): Unit = validate(mnemonics.split(" ")) fun validate(mnemonics: String): Unit = validate(mnemonics.split(" "))
fun isValid(mnemonics: String): Boolean { fun isValid(mnemonics: String): Boolean =
return try { try {
validate(mnemonics) validate(mnemonics)
true true
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
}
/** /**
* BIP39 entropy encoding * BIP39 entropy encoding
@ -108,7 +111,7 @@ object Bip39Mnemonics {
wordlist: Array<String>, wordlist: Array<String>,
): List<String> { ): List<String> {
require(wordlist.size == 2048) { "invalid word list (size should be 2048)" } 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] } return group(digits, 11).map(Bip39Mnemonics::fromBinary).map { wordlist[it] }
} }

View File

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

View File

@ -18,13 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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 java.nio.ByteBuffer
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec 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( fun extract(
key: ByteArray, key: ByteArray,
salt: 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 * 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. * 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 android.util.Log
import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Base64 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 sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02") private val h02 = Hex.decode("02")
private val libSodium = SodiumAndroid() private val libSodium = SodiumAndroid()
@ -97,15 +101,13 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
fun decrypt( fun decrypt(
encryptedInfo: EncryptedInfo, encryptedInfo: EncryptedInfo,
sharedSecret: ByteArray, sharedSecret: ByteArray,
): String? { ): String? =
return cryptoStreamXChaCha20Xor( cryptoStreamXChaCha20Xor(
libSodium = libSodium, libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext, messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce, nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret), key = Key.fromBytes(sharedSecret),
) )?.decodeToString()
?.decodeToString()
}
fun getSharedSecret( fun getSharedSecret(
privateKey: ByteArray, privateKey: ByteArray,
@ -155,11 +157,11 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
} }
} }
fun encodePayload(): String { fun encodePayload(): String =
return Base64.getEncoder() Base64
.getEncoder()
.encodeToString( .encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext, 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 * 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. * 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 android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
@ -33,7 +34,10 @@ import java.util.Base64
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.log2 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 sharedKeyCache = SharedKeyCache()
private val libSodium = SodiumAndroid() private val libSodium = SodiumAndroid()
@ -55,9 +59,7 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
msg: String, msg: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): EncryptedInfo { ): EncryptedInfo = encrypt(msg, getConversationKey(privateKey, pubKey))
return encrypt(msg, getConversationKey(privateKey, pubKey))
}
fun encrypt( fun encrypt(
plaintext: String, plaintext: String,
@ -99,17 +101,13 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
payload: String, payload: String,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String? { ): String? = decrypt(payload, getConversationKey(privateKey, pubKey))
return decrypt(payload, getConversationKey(privateKey, pubKey))
}
fun decrypt( fun decrypt(
decoded: EncryptedInfo, decoded: EncryptedInfo,
privateKey: ByteArray, privateKey: ByteArray,
pubKey: ByteArray, pubKey: ByteArray,
): String? { ): String? = decrypt(decoded, getConversationKey(privateKey, pubKey))
return decrypt(decoded, getConversationKey(privateKey, pubKey))
}
fun decrypt( fun decrypt(
payload: String, payload: String,
@ -173,7 +171,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
check(unpaddedLen <= maxPlaintextSize) { "Message is too long ($unpaddedLen): $plaintext" } check(unpaddedLen <= maxPlaintextSize) { "Message is too long ($unpaddedLen): $plaintext" }
val prefix = 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) val suffix = ByteArray(calcPaddedLen(unpaddedLen) - unpaddedLen)
return ByteBuffer.wrap(prefix + unpadded + suffix).array() return ByteBuffer.wrap(prefix + unpadded + suffix).array()
} }
@ -182,13 +184,12 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
byte1: Byte, byte1: Byte,
byte2: Byte, byte2: Byte,
bigEndian: Boolean, bigEndian: Boolean,
): Int { ): Int =
return if (bigEndian) { if (bigEndian) {
(byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF)) (byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF))
} else { } else {
(byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF)) (byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF))
} }
}
fun unpad(padded: ByteArray): String { fun unpad(padded: ByteArray): String {
val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true) val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true)
@ -273,11 +274,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
} }
} }
fun encodePayload(): String { fun encodePayload(): String =
return Base64.getEncoder() Base64
.getEncoder()
.encodeToString( .encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext + mac, 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 * 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. * 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.SodiumAndroid
import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.Key
@ -66,9 +66,7 @@ fun cryptoStreamXchacha20Xor(
messageLen: Long, messageLen: Long,
nonce: ByteArray, nonce: ByteArray,
key: ByteArray, key: ByteArray,
): Int { ): Int = cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
return cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
}
fun cryptoStreamXChaCha20Xor( fun cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid, libSodium: SodiumAndroid,

View File

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

View File

@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved. // 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.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;

View File

@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved. // 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.Mac;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;

View File

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

View File

@ -18,10 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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 junit.framework.TestCase
import org.junit.Test 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. """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. 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 atTheMiddle = DualCase("Lorem Ipsum".lowercase(), "Lorem Ipsum".uppercase())
val atTheBeginning = DualCase("contrAry".lowercase(), "contrAry".uppercase()) val atTheBeginning = DualCase("contrAry".lowercase(), "contrAry".uppercase())