Merge branch 'main' into amber

This commit is contained in:
greenart7c3
2023-08-21 08:50:56 -03:00
committed by GitHub
66 changed files with 1211 additions and 175 deletions
+2 -2
View File
@@ -6,11 +6,11 @@ plugins {
android {
namespace 'com.vitorpamplona.quartz'
compileSdk 33
compileSdk 34
defaultConfig {
minSdk 26
targetSdk 33
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -1,10 +1,11 @@
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.events.Event
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
@@ -15,11 +16,19 @@ import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
private val sharedKeyCache04 = LruCache<Int, ByteArray>(200)
private val sharedKeyCache24 = LruCache<Int, ByteArray>(200)
private val secp256k1 = Secp256k1.get()
private val libSodium = SodiumAndroid()
private val random = SecureRandom()
private val h02 = Hex.decode("02")
fun clearCache() {
sharedKeyCache04.evictAll()
sharedKeyCache24.evictAll()
}
fun randomInt(bound: Int): Int {
return random.nextInt(bound)
}
@@ -27,8 +36,10 @@ object CryptoUtils {
/**
* Provides a 32B "private key" aka random number
*/
fun privkeyCreate(): ByteArray {
val bytes = ByteArray(32)
fun privkeyCreate() = random(32)
fun random(size: Int): ByteArray {
val bytes = ByteArray(size)
random.nextBytes(bytes)
return bytes
}
@@ -52,12 +63,6 @@ object CryptoUtils {
return MessageDigest.getInstance("SHA-256").digest(data)
}
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
val encryptionInfo = EncryptedInfoString(
@@ -150,7 +155,39 @@ object CryptoUtils {
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache04[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP04(privateKey, pubKey)
sharedKeyCache04.put(hash, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache24[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP24(privateKey, pubKey)
sharedKeyCache24.put(hash, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
}
@@ -213,6 +250,13 @@ fun decodeJackson(json: String): EncryptedInfo {
)
}
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
}
/*
OLD Versions used for the Benchmark
@@ -30,6 +30,7 @@ private typealias Int5 = Byte
*/
object Bech32 {
const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
const val alphabetUpperCase: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L"
enum class Encoding(public val constant: Int) {
Bech32(1),
@@ -44,20 +45,26 @@ object Bech32 {
for (i in 0..alphabet.lastIndex) {
map[alphabet[i].code] = i.toByte()
}
}
private fun expand(hrp: String): Array<Int5> {
val result = Array<Int5>(hrp.length + 1 + hrp.length) { 0 }
for (i in hrp.indices) {
result[i] = hrp[i].code.shr(5).toByte()
result[hrp.length + 1 + i] = (hrp[i].code and 31).toByte()
for (i in 0..alphabetUpperCase.lastIndex) {
map[alphabetUpperCase[i].code] = i.toByte()
}
result[hrp.length] = 0
return result
}
private fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
fun expand(hrp: String): Array<Int5> {
val half = hrp.length + 1
val size = half + hrp.length
return Array<Int5>(size) {
when (it) {
in hrp.indices -> hrp[it].code.shr(5).toByte()
in half until size -> (hrp[it - half].code and 31).toByte()
else -> 0
}
}
}
private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
var chk = 1
values.forEach { v ->
val b = chk shr 25
@@ -83,14 +90,18 @@ object Bech32 {
* @return hrp + data encoded as a Bech32 string
*/
@JvmStatic
public fun encode(hrp: String, int5s: Array<Int5>, encoding: Encoding): String {
public fun encode(hrp: String, int5s: ArrayList<Int5>, encoding: Encoding): String {
require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" }
val data = int5s.toByteArray().toTypedArray()
val checksum = when (encoding) {
Encoding.Beck32WithoutChecksum -> arrayOf()
else -> checksum(hrp, data, encoding)
val dataWithChecksum = when (encoding) {
Encoding.Beck32WithoutChecksum -> int5s
else -> addChecksum(hrp, int5s, encoding)
}
return hrp + "1" + (data + checksum).map { i -> alphabet[i.toInt()] }.toCharArray().concatToString()
val charArray = CharArray(dataWithChecksum.size) {
alphabet[dataWithChecksum[it].toInt()]
}.concatToString()
return hrp + "1" + charArray
}
/**
@@ -110,14 +121,21 @@ object Bech32 {
*/
@JvmStatic
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
require(bech32.lowercase() == bech32 || bech32.uppercase() == bech32) { "mixed case strings are not valid bech32" }
bech32.forEach { require(it.code in 33..126) { "invalid character " } }
val input = bech32.lowercase()
val pos = input.lastIndexOf('1')
val hrp = input.take(pos)
var pos = 0
bech32.forEachIndexed { index, char ->
require( char.code in 33..126 ) { "invalid character ${char}" }
if (char == '1') {
pos = index
}
}
val hrp = bech32.take(pos)
require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
val data = Array<Int5>(input.length - pos - 1) { 0 }
for (i in 0..data.lastIndex) data[i] = map[input[pos + 1 + i].code]
val data = Array(bech32.length - pos - 1) {
map[bech32[pos + 1 + it].code]
}
return if (noChecksum) {
Triple(hrp, data, Encoding.Beck32WithoutChecksum)
} else {
@@ -126,7 +144,7 @@ object Bech32 {
Encoding.Bech32m.constant -> Encoding.Bech32m
else -> throw IllegalArgumentException("invalid checksum for $bech32")
}
Triple(hrp, data.dropLast(6).toTypedArray(), encoding)
Triple(hrp, data.copyOfRange(0, data.size-6), encoding)
}
}
@@ -142,16 +160,23 @@ object Bech32 {
return Triple(hrp, five2eight(int5s, 0), encoding)
}
val ZEROS = arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())
/**
* @param hrp Human Readable Part
* @param data data (a sequence of 5 bits integers)
* @param encoding encoding to use (bech32 or bech32m)
* @return a checksum computed over hrp and data
*/
private fun checksum(hrp: String, data: Array<Int5>, encoding: Encoding): Array<Int5> {
private fun addChecksum(hrp: String, data: ArrayList<Int5>, encoding: Encoding): ArrayList<Int5> {
val values = expand(hrp) + data
val poly = polymod(values, arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())) xor encoding.constant
return Array(6) { i -> (poly.shr(5 * (5 - i)) and 31).toByte() }
val poly = polymod(values, ZEROS) xor encoding.constant
for (i in 0 until 6) {
data.add((poly.shr(5 * (5 - i)) and 31).toByte())
}
return data
}
/**
@@ -159,9 +184,9 @@ object Bech32 {
* @return a sequence of 5 bits integers
*/
@JvmStatic
public fun eight2five(input: ByteArray): Array<Int5> {
public fun eight2five(input: ByteArray): ArrayList<Int5> {
var buffer = 0L
val output = ArrayList<Int5>()
val output = ArrayList<Int5>(input.size * 2) //larger array on purpose. Checksum is added later.
var count = 0
input.forEach { b ->
buffer = (buffer shl 8) or (b.toLong() and 0xff)
@@ -172,7 +197,7 @@ object Bech32 {
}
}
if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
return output.toTypedArray()
return output
}
/**
@@ -182,7 +207,7 @@ object Bech32 {
@JvmStatic
public fun five2eight(input: Array<Int5>, offset: Int): ByteArray {
var buffer = 0L
val output = ArrayList<Byte>()
val output = ArrayList<Byte>(input.size)
var count = 0
for (i in offset..input.lastIndex) {
val b = input[i]
@@ -0,0 +1,62 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class AudioHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1)
fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1)
fun wavefrom() = tags.firstOrNull { it.size > 1 && it[0] == WAVEFORM }?.get(1)?.let {
mapper.readValue<List<Int>>(it)
}
companion object {
const val kind = 1808
private const val DOWNLOAD_URL = "download_url"
private const val STREAM_URL = "stream_url"
private const val WAVEFORM = "waveform"
fun create(
description: String,
downloadUrl: String,
streamUrl: String? = null,
wavefront: String? = null,
sensitiveContent: Boolean? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): AudioHeaderEvent {
val tags = listOfNotNull(
downloadUrl.let { listOf(DOWNLOAD_URL, it) },
streamUrl?.let { listOf(STREAM_URL, it) },
wavefront?.let { listOf(WAVEFORM, it) },
sensitiveContent?.let {
if (it) {
listOf("content-warning", "")
} else {
null
}
}
)
val content = description
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return AudioHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -17,7 +17,7 @@ class ChatMessageEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
) : WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
/**
* Recepients intended to receive this conversation
*/
@@ -215,7 +215,7 @@ class ContactListEvent(
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) },
privateKey = privateKey,
createdAt = createdAt
)
@@ -362,6 +362,22 @@ open class Event(
}
}
@Immutable
open class WrappedEvent(
id: HexKey,
@JsonProperty("pubkey")
pubKey: HexKey,
@JsonProperty("created_at")
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
var host: Event? = null // host event to broadcast when needed
}
@Immutable
interface AddressableEvent {
fun dTag(): String
@@ -16,6 +16,7 @@ class EventFactory {
AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AudioHeaderEvent.kind -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
@@ -28,6 +28,10 @@ class GiftWrapEvent(
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val myInnerEvent = unwrap(privKey = privKey)
if (myInnerEvent is WrappedEvent) {
myInnerEvent.host = this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
return myInnerEvent
}
@@ -40,6 +40,10 @@ class LiveActivitiesEvent(
}
}
fun participantsIntersect(keySet: Set<String>): Boolean {
return tags.any { it.size > 1 && it[0] == "p" && it[1] in keySet }
}
companion object {
const val kind = 30311
@@ -20,7 +20,7 @@ class SealedGossipEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
): WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
@@ -30,6 +30,10 @@ class SealedGossipEvent(
val gossip = unseal(privKey = privKey)
val event = gossip?.mergeWith(this)
if (event is WrappedEvent) {
event.host = host ?: this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
return event
}