mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 21:31:57 +01:00
Quartz support for NIP-49 encrypted nsecs
This commit is contained in:
parent
74f0393706
commit
b94c1e2792
@ -59,7 +59,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [ ] Nostr Connect (NIP-46)
|
||||
- [x] Wallet Connect API (NIP-47)
|
||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||
- [ ] Private key encryption for import/export (NIP-49)
|
||||
- [x] Private key encryption for import/export (NIP-49)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
- [x] Lists (NIP-51)
|
||||
- [ ] Calendar Events (NIP-52)
|
||||
|
@ -54,6 +54,8 @@ dependencies {
|
||||
// immutable collections to avoid recomposition
|
||||
api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7')
|
||||
|
||||
api('com.lambdaworks:scrypt:1.4.0')
|
||||
|
||||
// Parses URLs from Text:
|
||||
api "io.github.url-detector:url-detector:0.1.23"
|
||||
|
||||
|
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.crypto.Nip49
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.security.SecureRandom
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
public class NIP49Test {
|
||||
companion object {
|
||||
val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p"
|
||||
|
||||
val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"
|
||||
val TEST_CASE_PASSWORD = "nostr"
|
||||
|
||||
val MAIN_TEST_CASES =
|
||||
listOf<Nip49TestCase>(
|
||||
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00),
|
||||
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01),
|
||||
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02),
|
||||
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00),
|
||||
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01),
|
||||
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02),
|
||||
)
|
||||
}
|
||||
|
||||
val random = SecureRandom()
|
||||
val nip49 = Nip49(Secp256k1.get(), random)
|
||||
|
||||
@Test
|
||||
fun decodeBech32() {
|
||||
val data =
|
||||
Nip49.EncryptedInfo.decodePayload(
|
||||
TEST_CASE,
|
||||
)!!
|
||||
|
||||
assertEquals(2, data.version)
|
||||
assertEquals(16, data.logn)
|
||||
assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey())
|
||||
assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey())
|
||||
assertEquals(0, data.keySecurity)
|
||||
assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decrypt() {
|
||||
val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD)
|
||||
assertEquals(TEST_CASE_EXPECTED, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptDecryptTestCase() {
|
||||
val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0)
|
||||
val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD)
|
||||
|
||||
assertEquals(TEST_CASE_EXPECTED, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptDecrypt() {
|
||||
MAIN_TEST_CASES.forEach {
|
||||
val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb)
|
||||
|
||||
assertNotNull(encrypted)
|
||||
|
||||
val decrypted = nip49.decrypt(encrypted!!, it.password)
|
||||
|
||||
assertEquals(it.secretKey, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
class Nip49TestCase(
|
||||
val password: String,
|
||||
val secretKey: String,
|
||||
val logn: Int,
|
||||
val ksb: Byte,
|
||||
)
|
||||
}
|
173
quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt
Normal file
173
quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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
|
||||
|
||||
import android.util.Log
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.lambdaworks.crypto.SCrypt
|
||||
import com.vitorpamplona.quartz.encoders.Bech32
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
import java.security.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)
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
encryptedInfo: EncryptedInfo?,
|
||||
password: String = "",
|
||||
): String? {
|
||||
check(encryptedInfo != null) { "Couldn't decode key" }
|
||||
check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" }
|
||||
|
||||
val n = Math.pow(2.0, encryptedInfo.logn.toDouble()).toInt()
|
||||
val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), encryptedInfo.salt, n, 8, 1, 32)
|
||||
val m = ByteArray(32)
|
||||
|
||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
m,
|
||||
longArrayOf(32L),
|
||||
key,
|
||||
encryptedInfo.encryptedKey,
|
||||
encryptedInfo.encryptedKey.size.toLong(),
|
||||
byteArrayOf(encryptedInfo.keySecurity),
|
||||
1,
|
||||
encryptedInfo.nonce,
|
||||
key,
|
||||
)
|
||||
|
||||
return m.toHexKey()
|
||||
}
|
||||
|
||||
fun encrypt(
|
||||
secretKeyHex: String,
|
||||
password: String,
|
||||
logn: Int,
|
||||
ksb: Byte,
|
||||
): String? {
|
||||
return encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
|
||||
}
|
||||
|
||||
fun encrypt(
|
||||
secretKey: ByteArray,
|
||||
password: String,
|
||||
logn: Int,
|
||||
ksb: Byte,
|
||||
): String {
|
||||
check(secretKey.size == 32) { "invalid secret key" }
|
||||
val salt = ByteArray(16)
|
||||
random.nextBytes(salt)
|
||||
|
||||
val nonce = ByteArray(24)
|
||||
random.nextBytes(nonce)
|
||||
|
||||
val n = Math.pow(2.0, logn.toDouble()).toInt()
|
||||
val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), salt, n, 8, 1, 32)
|
||||
val ciphertext = ByteArray(48)
|
||||
|
||||
// byte[] c, long[] cLen,
|
||||
// byte[] m, long mLen,
|
||||
// byte[] ad, long adLen,
|
||||
// byte[] nSec, byte[] nPub, byte[] k
|
||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
||||
ciphertext, longArrayOf(48),
|
||||
secretKey, secretKey.size.toLong(),
|
||||
byteArrayOf(ksb), 1,
|
||||
key,
|
||||
nonce,
|
||||
key,
|
||||
)
|
||||
|
||||
return EncryptedInfo(
|
||||
EncryptedInfo.V,
|
||||
logn.toByte(),
|
||||
salt,
|
||||
nonce,
|
||||
ksb,
|
||||
ciphertext,
|
||||
).encodePayload()
|
||||
}
|
||||
|
||||
class EncryptedInfo(
|
||||
val version: Byte,
|
||||
val logn: Byte,
|
||||
val salt: ByteArray,
|
||||
val nonce: ByteArray,
|
||||
val keySecurity: Byte,
|
||||
val encryptedKey: ByteArray,
|
||||
) {
|
||||
companion object {
|
||||
const val V: Byte = 0x02
|
||||
|
||||
const val UNSAFE_HANDLING = 0x00
|
||||
const val SAFE_HANDLING = 0x01
|
||||
const val CLIENT_DOES_NOT_TRACK = 0x02
|
||||
|
||||
fun decodePayload(nCryptSec: String): EncryptedInfo? {
|
||||
val byteArray =
|
||||
try {
|
||||
nCryptSec.bechToBytes()
|
||||
} catch (e: Throwable) {
|
||||
Log.e("NIP19 Parser", "Issue trying to Decode NIP49 $nCryptSec: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
return EncryptedInfo(
|
||||
version = byteArray[0],
|
||||
logn = byteArray[1],
|
||||
salt = byteArray.copyOfRange(2, 2 + 16),
|
||||
nonce = byteArray.copyOfRange(2 + 16, 2 + 16 + 24),
|
||||
keySecurity = byteArray.copyOfRange(2 + 16 + 24, 2 + 16 + 24 + 1).get(0),
|
||||
encryptedKey = byteArray.copyOfRange(2 + 16 + 24 + 1, byteArray.size),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP44v2", "Unable to Parse encrypted ncryptsec payload")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ln(n.toDouble()).toInt().toByte(),
|
||||
fun encodePayload(): String {
|
||||
return Bech32.encodeBytes(
|
||||
hrp = "ncryptsec",
|
||||
byteArrayOf(
|
||||
version,
|
||||
logn,
|
||||
) + salt + nonce + keySecurity + encryptedKey,
|
||||
Bech32.Encoding.Bech32,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ object Nip19 {
|
||||
EVENT,
|
||||
RELAY,
|
||||
ADDRESS,
|
||||
NSEC,
|
||||
}
|
||||
|
||||
enum class TlvTypes(val id: Byte) {
|
||||
|
@ -24,6 +24,18 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class NIP19ParserTest {
|
||||
@Test
|
||||
fun nCryptSecParser() {
|
||||
val result =
|
||||
Nip19.uriToRoute(
|
||||
"nostr:ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p",
|
||||
)
|
||||
assertEquals(
|
||||
"30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:",
|
||||
result?.hex,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nAddrParser() {
|
||||
val result =
|
||||
|
Loading…
x
Reference in New Issue
Block a user