Quartz support for NIP-49 encrypted nsecs

This commit is contained in:
Vitor Pamplona
2024-02-13 19:51:32 -05:00
parent 74f0393706
commit b94c1e2792
6 changed files with 292 additions and 1 deletions
@@ -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) {