Moves to NIP-44v2

This commit is contained in:
Vitor Pamplona
2023-12-20 18:07:37 -05:00
parent a22b6db130
commit e794ff44a1
17 changed files with 1550 additions and 319 deletions
@@ -1,32 +1,26 @@
package com.vitorpamplona.quartz.crypto
import android.util.Log
import android.util.LruCache
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
private val sharedKeyCache04 = LruCache<Int, ByteArray>(200)
private val sharedKeyCache44 = LruCache<Int, ByteArray>(200)
private val secp256k1 = Secp256k1.get()
private val libSodium = SodiumAndroid()
private val random = SecureRandom()
private val h02 = Hex.decode("02")
private val nip04 = Nip04(secp256k1, random)
private val nip44v1 = Nip44v1(secp256k1, random)
private val nip44v2 = Nip44v2(secp256k1, random)
fun clearCache() {
sharedKeyCache04.evictAll()
sharedKeyCache44.evictAll()
nip04.clearCache()
nip44v1.clearCache()
nip44v2.clearCache()
}
fun randomInt(bound: Int): Int {
@@ -63,242 +57,174 @@ object CryptoUtils {
return MessageDigest.getInstance("SHA-256").digest(data)
}
/**
* NIP 04 Utils
*/
fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
val encryptionInfo = EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
return "${encryptionInfo.ciphertext}?iv=${encryptionInfo.nonce}"
return nip04.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP04Json(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
return encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
}
fun encryptNIP04(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val iv = ByteArray(16)
random.nextBytes(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
//val ivBase64 = Base64.getEncoder().encodeToString(iv)
val encryptedMsg = cipher.doFinal(msg.toByteArray())
//val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg)
return EncryptedInfo(encryptedMsg, iv, Nip44Version.NIP04.versionCode)
fun encryptNIP04(msg: String, sharedSecret: ByteArray): Nip04.EncryptedInfo {
return nip04.encrypt(msg, sharedSecret)
}
fun decryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecretNIP04(privateKey, pubKey)
return decryptNIP04(msg, sharedSecret)
return nip04.decrypt(msg, privateKey, pubKey)
}
fun decryptNIP04(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecretNIP04(privateKey, pubKey)
return decryptNIP04(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret)
fun decryptNIP04(encryptedInfo: Nip04.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String {
return nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP04(msg: String, sharedSecret: ByteArray): String {
val parts = msg.split("?iv=")
return decryptNIP04(parts[0], parts[1], sharedSecret)
return nip04.decrypt(msg, sharedSecret)
}
private fun decryptNIP04(cipher: String, nonce: String, sharedSecret: ByteArray): String {
val iv = Base64.getDecoder().decode(nonce)
val encryptedMsg = Base64.getDecoder().decode(cipher)
return decryptNIP04(encryptedMsg, iv, sharedSecret)
return nip04.decrypt(cipher, nonce, sharedSecret)
}
private fun decryptNIP04(encryptedMsg: ByteArray, iv: ByteArray, sharedSecret: ByteArray): String {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
return String(cipher.doFinal(encryptedMsg))
return nip04.decrypt(encryptedMsg, iv, sharedSecret)
}
fun encryptNIP44(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return encryptNIP44(msg, sharedSecret)
}
fun encryptNIP44(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val nonce = ByteArray(24)
random.nextBytes(nonce)
val cipher = cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = msg.toByteArray(),//compress(msg),
nonce = nonce,
key = Key.fromBytes(sharedSecret)
)
return EncryptedInfo(
ciphertext = cipher ?: ByteArray(0),
nonce = nonce,
v = Nip44Version.NIP44.versionCode
)
}
fun decryptNIP44(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return decryptNIP44(encryptedInfo, sharedSecret)
}
fun decryptNIP44(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? {
return cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret)
)?.decodeToString() //?.let { decompress(it) }
}
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache04[hash]
if (preComputed != null) return preComputed
return nip04.getSharedSecret(privateKey, pubKey)
}
val computed = computeSharedSecretNIP04(privateKey, pubKey)
sharedKeyCache04.put(hash, computed)
return computed
fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
return nip04.computeSharedSecret(privateKey, pubKey)
}
/**
* NIP 44v1 Utils
*/
fun encryptNIP44v1(msg: String, privateKey: ByteArray, pubKey: ByteArray): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP44v1(msg: String, sharedSecret: ByteArray): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, sharedSecret)
}
fun decryptNIP44v1(encryptedInfo: Nip44v1.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(encryptedInfo: String, privateKey: ByteArray, pubKey: ByteArray): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(encryptedInfo: Nip44v1.EncryptedInfo, sharedSecret: ByteArray): String? {
return nip44v1.decrypt(encryptedInfo, sharedSecret)
}
fun getSharedSecretNIP44v1(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
return nip44v1.getSharedSecret(privateKey, pubKey)
}
fun computeSharedSecretNIP44v1(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
return nip44v1.computeSharedSecret(privateKey, pubKey)
}
/**
* @return 32B shared secret
* NIP 44v2 Utils
*/
fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache44[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP44(privateKey, pubKey)
sharedKeyCache44.put(hash, computed)
return computed
fun encryptNIP44v2(msg: String, privateKey: ByteArray, pubKey: ByteArray): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, privateKey, pubKey)
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
}
data class EncryptedInfo(val ciphertext: ByteArray, val nonce: ByteArray, val v: Int)
data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: Int)
enum class Nip44Version(val versionCode: Int) {
NIP04(0),
NIP44(1)
}
fun encodeNIP44(info: EncryptedInfo): String {
return encodeByteArray(info)
}
fun decodeNIP44(str: String): EncryptedInfo? {
if (str.isEmpty()) return null
return if (str[0] == '{') {
decodeJackson(str)
} else {
decodeByteArray(str)
fun encryptNIP44v2(msg: String, sharedSecret: ByteArray): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, sharedSecret)
}
}
fun encodeByteArray(info: EncryptedInfo): String {
return Base64.getEncoder().encodeToString(byteArrayOf(info.v.toByte()) + info.nonce + info.ciphertext)
}
fun decodeByteArray(base64: String): EncryptedInfo? {
return try {
val byteArray = Base64.getDecoder().decode(base64)
return EncryptedInfo(
v = byteArray[0].toInt(),
nonce = byteArray.copyOfRange(1, 25),
ciphertext = byteArray.copyOfRange(25, byteArray.size)
)
} catch (e: Exception) {
Log.w("CryptoUtils", "Unable to Parse encrypted payload: ${base64}")
null
fun decryptNIP44v2(encryptedInfo: Nip44v2.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
}
fun encodeJackson(info: EncryptedInfo): String {
return Event.mapper.writeValueAsString(
EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
)
}
fun decryptNIP44v2(encryptedInfo: String, privateKey: ByteArray, pubKey: ByteArray): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decodeJackson(json: String): EncryptedInfo {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
return EncryptedInfo(
v = info.v,
nonce = Base64.getDecoder().decode(info.nonce),
ciphertext = Base64.getDecoder().decode(info.ciphertext)
)
}
fun decryptNIP44v2(encryptedInfo: Nip44v2.EncryptedInfo, sharedSecret: ByteArray): String? {
return nip44v2.decrypt(encryptedInfo, sharedSecret)
}
fun combinedHashCode(a: ByteArray, b: ByteArray): Int {
var result = 1
for (element in a) result = 31 * result + element
for (element in b) result = 31 * result + element
return result
}
fun getSharedSecretNIP44v2(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
return nip44v2.getConversationKey(privateKey, pubKey)
}
/*
OLD Versions used for the Benchmark
fun computeSharedSecretNIP44v2(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
return nip44v2.computeConversationKey(privateKey, pubKey)
}
fun encodeKotlin(info: EncryptedInfo): String {
return Json.encodeToString(
EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
)
}
fun decryptNIP44(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? {
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
fun decodeKotlin(json: String): EncryptedInfo {
val info = Json.decodeFromString<EncryptedInfoString>(json)
return EncryptedInfo(
v = info.v,
nonce = Base64.getDecoder().decode(info.nonce),
ciphertext = Base64.getDecoder().decode(info.ciphertext)
)
}
data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: Int, val mac: String?)
fun encodeCSV(info: EncryptedInfo): String {
return "${info.v},${Base64.getEncoder().encodeToString(info.nonce)},${Base64.getEncoder().encodeToString(info.ciphertext)}"
}
fun decryptNIP44FromJackson(json: String, privateKey: ByteArray, pubKey: ByteArray): String? {
return try {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
fun decodeCSV(base64: String): EncryptedInfo {
val parts = base64.split(",")
return EncryptedInfo(
v = parts[0].toInt(),
nonce = Base64.getDecoder().decode(parts[1]),
ciphertext = Base64.getDecoder().decode(parts[2])
)
}
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
fun compress(input: String): ByteArray {
return DeflaterInputStream(input.toByteArray().inputStream()).readBytes()
}
return try {
val byteArray = Base64.getDecoder().decode(payload)
fun decompress(inputBytes: ByteArray): String {
return InflaterInputStream(inputBytes.inputStream()).bufferedReader().use { it.readText() }
}
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
}
}
*/
}
@@ -0,0 +1,37 @@
package com.vitorpamplona.quartz.crypto
import java.nio.ByteBuffer
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class Hkdf(val algorithm: String = "HmacSHA256", val hashLen: Int = 32) {
fun extract(key: ByteArray, salt: ByteArray): ByteArray {
val mac = Mac.getInstance(algorithm)
mac.init(SecretKeySpec(salt, algorithm))
return mac.doFinal(key)
}
fun expand(key: ByteArray, nonce: ByteArray, outputLength: Int): ByteArray {
check(key.size == hashLen)
check(nonce.size == hashLen)
val n = if (outputLength % hashLen == 0) outputLength / hashLen else outputLength / hashLen + 1
var hashRound = ByteArray(0)
val generatedBytes = ByteBuffer.allocate(Math.multiplyExact(n, hashLen))
val mac = Mac.getInstance(algorithm)
mac.init(SecretKeySpec(key, algorithm))
for (roundNum in 1..n) {
mac.reset()
val t = ByteBuffer.allocate(hashRound.size + nonce.size + 1)
t.put(hashRound)
t.put(nonce)
t.put(roundNum.toByte())
hashRound = mac.doFinal(t.array())
generatedBytes.put(hashRound)
}
val result = ByteArray(outputLength)
generatedBytes.rewind()
generatedBytes[result, 0, outputLength]
return result
}
}
@@ -0,0 +1,130 @@
package com.vitorpamplona.quartz.crypto
import android.util.Log
import android.util.LruCache
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
fun clearCache() {
sharedKeyCache.clearCache()
}
fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
return encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
}
fun encrypt(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val iv = ByteArray(16)
random.nextBytes(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
//val ivBase64 = Base64.getEncoder().encodeToString(iv)
val encryptedMsg = cipher.doFinal(msg.toByteArray())
//val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg)
return EncryptedInfo(encryptedMsg, iv)
}
fun decrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(msg, sharedSecret)
}
fun decrypt(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret)
}
fun decrypt(msg: String, sharedSecret: ByteArray): String {
val decoded = EncryptedInfo.decodeFromNIP04(msg)
check(decoded != null) {
"Unable to decode msg $msg as NIP04"
}
return decrypt(decoded.ciphertext, decoded.nonce, sharedSecret)
}
fun decrypt(cipher: String, nonce: String, sharedSecret: ByteArray): String {
val iv = Base64.getDecoder().decode(nonce)
val encryptedMsg = Base64.getDecoder().decode(cipher)
return decrypt(encryptedMsg, iv, sharedSecret)
}
fun decrypt(encryptedMsg: ByteArray, iv: ByteArray, sharedSecret: ByteArray): String {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
return String(cipher.doFinal(encryptedMsg))
}
fun getSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val preComputed = sharedKeyCache.get(privateKey, pubKey)
if (preComputed != null) return preComputed
val computed = computeSharedSecret(privateKey, pubKey)
sharedKeyCache.add(privateKey, pubKey, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
class EncryptedInfo(
val ciphertext: ByteArray,
val nonce: ByteArray
) {
companion object {
const val v: Int = 0
fun decodePayload(payload: String): EncryptedInfo? {
return try {
val byteArray = Base64.getDecoder().decode(payload)
check(byteArray[0].toInt() == Nip44v1.EncryptedInfo.v)
return EncryptedInfo(
nonce = byteArray.copyOfRange(1, 25),
ciphertext = byteArray.copyOfRange(25, byteArray.size)
)
} catch (e: Exception) {
Log.w("NIP04", "Unable to Parse encrypted payload: ${payload}")
null
}
}
fun decodeFromNIP04(payload: String): EncryptedInfo? {
return try {
val parts = payload.split("?iv=")
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(parts[0]),
nonce = Base64.getDecoder().decode(parts[1])
)
} catch (e: Exception) {
Log.w("NIP04", "Unable to Parse encrypted payload: ${payload}")
null
}
}
}
fun encodePayload(): String {
return Base64.getEncoder().encodeToString(
byteArrayOf(v.toByte()) + nonce + ciphertext
)
}
fun encodeToNIP04(): String {
val nonce = Base64.getEncoder().encodeToString(nonce)
val ciphertext = Base64.getEncoder().encodeToString(ciphertext)
return "${ciphertext}?iv=${nonce}"
}
}
}
@@ -0,0 +1,117 @@
package com.vitorpamplona.quartz.crypto
import android.util.Log
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
private val libSodium = SodiumAndroid()
fun clearCache() {
sharedKeyCache.clearCache()
}
fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return encrypt(msg, sharedSecret)
}
fun encrypt(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val nonce = ByteArray(24)
random.nextBytes(nonce)
val cipher = cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = msg.toByteArray(),
nonce = nonce,
key = Key.fromBytes(sharedSecret)
)
return EncryptedInfo(
ciphertext = cipher ?: ByteArray(0),
nonce = nonce,
)
}
fun decrypt(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(payload, sharedSecret)
}
fun decrypt(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(encryptedInfo, sharedSecret)
}
fun decrypt(payload: String, sharedSecret: ByteArray): String? {
val encryptedInfo = EncryptedInfo.decodePayload(payload) ?: return null
return decrypt(encryptedInfo, sharedSecret)
}
fun decrypt(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? {
return cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret)
)?.decodeToString()
}
fun getSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val preComputed = sharedKeyCache.get(privateKey, pubKey)
if (preComputed != null) return preComputed
val computed = computeSharedSecret(privateKey, pubKey)
sharedKeyCache.add(privateKey, pubKey, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
sha256(
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
class EncryptedInfo(
val ciphertext: ByteArray,
val nonce: ByteArray
) {
companion object {
const val v: Int = 1
fun decodePayload(payload: String): EncryptedInfo? {
return try {
val byteArray = Base64.getDecoder().decode(payload)
check(byteArray[0].toInt() == v)
return EncryptedInfo(
nonce = byteArray.copyOfRange(1, 25),
ciphertext = byteArray.copyOfRange(25, byteArray.size)
)
} catch (e: Exception) {
Log.w("NIP44v1", "Unable to Parse encrypted payload: ${payload}")
null
}
}
}
fun encodePayload(): String {
return Base64.getEncoder().encodeToString(
byteArrayOf(v.toByte()) + nonce + ciphertext
)
}
}
}
@@ -0,0 +1,223 @@
package com.vitorpamplona.quartz.crypto
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.SecureRandom
import java.util.Base64
import kotlin.experimental.and
import kotlin.math.floor
import kotlin.math.log2
class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
private val sharedKeyCache = SharedKeyCache()
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
private val hkdf = Hkdf()
private val h02 = Hex.decode("02")
private val hashLength = 32
private val minPlaintextSize: Int = 0x0001 // 1b msg => padded to 32b
private val maxPlaintextSize: Int = 0xffff // 65535 (64kb-1) => padded to 64kb
fun clearCache() {
sharedKeyCache.clearCache()
}
fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
return encrypt(msg, getConversationKey(privateKey, pubKey))
}
fun encrypt(plaintext: String, conversationKey: ByteArray): EncryptedInfo {
val nonce = ByteArray(hashLength)
random.nextBytes(nonce)
return encryptWithNonce(plaintext, conversationKey, nonce)
}
fun encryptWithNonce(plaintext: String, conversationKey: ByteArray, nonce: ByteArray): EncryptedInfo {
val messageKeys = getMessageKeys(conversationKey, nonce)
val padded = pad(plaintext)
val ciphertext = ByteArray(padded.size)
lazySodium.cryptoStreamChaCha20IetfXor(
ciphertext, padded, padded.size.toLong(), messageKeys.chachaNonce, messageKeys.chachaKey
)
val mac = hmacAad(messageKeys.hmacKey, ciphertext, nonce)
return EncryptedInfo(
nonce = nonce,
ciphertext = ciphertext,
mac = mac
)
}
fun decrypt(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? {
return decrypt(payload, getConversationKey(privateKey, pubKey))
}
fun decrypt(decoded: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
return decrypt(decoded, getConversationKey(privateKey, pubKey))
}
fun decrypt(payload: String, conversationKey: ByteArray): String? {
val decoded = EncryptedInfo.decodePayload(payload) ?: return null
return decrypt(decoded, conversationKey)
}
fun decrypt(decoded: EncryptedInfo, conversationKey: ByteArray): String? {
val messageKey = getMessageKeys(conversationKey, decoded.nonce)
val calculatedMac = hmacAad(messageKey.hmacKey, decoded.ciphertext, decoded.nonce)
check(calculatedMac.contentEquals(decoded.mac)) {
"Invalid Mac: Calculated ${calculatedMac.toHexKey()}, decoded: ${decoded.mac.toHexKey()}"
}
val mLen = decoded.ciphertext.size.toLong()
val padded = ByteArray(decoded.ciphertext.size)
lazySodium.cryptoStreamChaCha20IetfXor(
padded, decoded.ciphertext, mLen, messageKey.chachaNonce, messageKey.chachaKey
)
return unpad(padded)
}
fun getConversationKey(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val preComputed = sharedKeyCache.get(privateKey, pubKey)
if (preComputed != null) return preComputed
val computed = computeConversationKey(privateKey, pubKey)
sharedKeyCache.add(privateKey, pubKey, computed)
return computed
}
fun calcPaddedLen(len: Int): Int {
check(len > 0) {
"expected positive integer"
}
if (len <= 32) return 32
val nextPower = 1 shl (floor(log2(len - 1f)) + 1).toInt()
val chunk = if (nextPower <= 256) 32 else nextPower / 8
return chunk * (floor((len - 1f) / chunk).toInt() + 1)
}
fun pad(plaintext: String): ByteArray {
val unpadded = plaintext.toByteArray(Charsets.UTF_8)
val unpaddedLen = unpadded.size
check(unpaddedLen > 0) {
"Message is empty ($unpaddedLen): $plaintext"
}
check(unpaddedLen <= maxPlaintextSize) {
"Message is too long ($unpaddedLen): $plaintext"
}
val prefix = ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(unpaddedLen.toShort()).array()
val suffix = ByteArray(calcPaddedLen(unpaddedLen) - unpaddedLen)
return ByteBuffer.wrap(prefix + unpadded + suffix).array()
}
private fun bytesToInt(byte1: Byte, byte2: Byte, bigEndian: Boolean): Int {
return if (bigEndian)
(byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF))
else
(byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF))
}
fun unpad(padded: ByteArray): String {
val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true)
val unpadded = padded.sliceArray(2 until 2 + unpaddedLen)
check(
unpaddedLen in minPlaintextSize..maxPlaintextSize
&& unpadded.size == unpaddedLen
&& padded.size == 2 + calcPaddedLen(unpaddedLen)) {
"invalid padding ${unpadded.size} != $unpaddedLen"
}
return unpadded.decodeToString()
}
fun hmacAad(key: ByteArray, message: ByteArray, aad: ByteArray): ByteArray {
check (aad.size == hashLength) {
"AAD associated data must be 32 bytes, but it was ${aad.size} bytes"
}
return hkdf.extract(aad + message, key)
}
fun getMessageKeys(conversationKey: ByteArray, nonce: ByteArray): MessageKey {
val keys = hkdf.expand(conversationKey, nonce, 76)
return MessageKey(
chachaKey = keys.copyOfRange(0, 32),
chachaNonce = keys.copyOfRange(32, 44),
hmacKey = keys.copyOfRange(44, 76),
)
}
class MessageKey(
val chachaKey: ByteArray,
val chachaNonce: ByteArray,
val hmacKey: ByteArray
)
/**
* @return 32B shared secret
*/
fun computeConversationKey(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val sharedX = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
return hkdf.extract(sharedX, "nip44-v2".toByteArray(Charsets.UTF_8))
}
class EncryptedInfo(
val nonce: ByteArray,
val ciphertext: ByteArray,
val mac: ByteArray
) {
companion object {
const val v: Int = 2
fun decodePayload(payload: String): EncryptedInfo? {
check(payload.length >= 132 || payload.length <= 87472) {
"Invalid payload length ${payload.length} for ${payload}"
}
check(payload[0] != '#') {
"Unknown encryption version ${payload.get(0)}"
}
return try {
val byteArray = Base64.getDecoder().decode(payload)
check(byteArray[0].toInt() == v)
return EncryptedInfo(
nonce = byteArray.copyOfRange(1, 33),
ciphertext = byteArray.copyOfRange(33, byteArray.size-32),
mac = byteArray.copyOfRange(byteArray.size-32, byteArray.size)
)
} catch (e: Exception) {
Log.w("NIP44v2", "Unable to Parse encrypted payload: $payload")
null
}
}
}
fun encodePayload(): String {
return Base64.getEncoder().encodeToString(
byteArrayOf(v.toByte()) + nonce + ciphertext + mac
)
}
}
}
@@ -0,0 +1,26 @@
package com.vitorpamplona.quartz.crypto
import android.util.LruCache
class SharedKeyCache {
private val sharedKeyCache = LruCache<Int, ByteArray>(200)
fun clearCache() {
sharedKeyCache.evictAll()
}
fun combinedHashCode(a: ByteArray, b: ByteArray): Int {
var result = 1
for (element in a) result = 31 * result + element
for (element in b) result = 31 * result + element
return result
}
fun get(privateKey: ByteArray, pubKey: ByteArray): ByteArray? {
return sharedKeyCache[combinedHashCode(privateKey, pubKey)]
}
fun add(privateKey: ByteArray, pubKey: ByteArray, secret: ByteArray) {
sharedKeyCache.put(combinedHashCode(privateKey, pubKey), secret)
}
}
@@ -90,7 +90,7 @@ class ChatMessageEvent(
subject?.let {
tags.add(arrayOf("subject", it))
}
tags.add(arrayOf("alt", alt))
//tags.add(arrayOf("alt", alt))
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
}
@@ -1,15 +1,8 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.crypto.Nip44Version
import com.vitorpamplona.quartz.crypto.decodeNIP44
import com.vitorpamplona.quartz.crypto.encodeNIP44
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.signers.NostrSignerInternal
@@ -31,7 +24,6 @@ class GiftWrapEvent(
onReady(it)
return
}
unwrap(signer) { gift ->
if (gift is WrappedEvent) {
gift.host = this
@@ -72,7 +64,7 @@ class GiftWrapEvent(
) {
val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key
val serializedContent = toJson(event)
val tags = arrayOf(arrayOf("p", recipientPubKey), arrayOf("alt", alt))
val tags = arrayOf(arrayOf("p", recipientPubKey))
signer.nip44Encrypt(serializedContent, recipientPubKey) {
signer.sign(createdAt, kind, tags, it, onReady)
@@ -4,15 +4,9 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.Nip44Version
import com.vitorpamplona.quartz.crypto.decodeNIP44
import com.vitorpamplona.quartz.crypto.encodeNIP44
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import java.util.UUID
@Immutable
class SealedGossipEvent(
@@ -3,19 +3,13 @@ package com.vitorpamplona.quartz.signers
import android.util.Log
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.crypto.Nip44Version
import com.vitorpamplona.quartz.crypto.decodeNIP44
import com.vitorpamplona.quartz.crypto.encodeNIP44
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHexKey()) {
override fun <T: Event> sign(
@@ -94,28 +88,23 @@ class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHe
override fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) {
if (keyPair.privKey == null) return
val sharedSecret = CryptoUtils.getSharedSecretNIP44(keyPair.privKey, toPublicKey.hexToByteArray())
onReady(
encodeNIP44(
CryptoUtils.encryptNIP44(
decryptedContent,
sharedSecret
)
)
CryptoUtils.encryptNIP44v2(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray()
).encodePayload()
)
}
override fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) {
if (keyPair.privKey == null) return
val toDecrypt = decodeNIP44(encryptedContent) ?: return
when (toDecrypt.v) {
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray())
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray())
else -> null
}?.let {
CryptoUtils.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray()
)?.let {
onReady(it)
}
}