First idea without changing NIP-44

This commit is contained in:
Vitor Pamplona
2025-03-12 09:41:18 -04:00
parent 40bc651860
commit 5766bb1887
11 changed files with 466 additions and 0 deletions
@@ -0,0 +1,112 @@
/**
* 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.experimental.decoupling
import com.vitorpamplona.quartz.experimental.decoupling.setup.EncryptionKeyListEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip44Encryption.Nip44
class DecoupledCipher {
fun innerEncrypt(
content: String,
privKey: ByteArray,
toPublicKey: HexKey,
) = Nip44
.encrypt(content, privKey, toPublicKey.hexToByteArray())
.encodePayload()
fun innerDecrypt(
ciphertext: String,
privKey: ByteArray,
fromPublicKey: HexKey,
) = Nip44.decrypt(
payload = ciphertext,
privateKey = privKey,
pubKey = fromPublicKey.hexToByteArray(),
)
fun encrypt(
decryptedContent: String,
toPublicKey: HexKey,
fromKeyList: EncryptionKeyListEvent,
toKeyList: EncryptionKeyListEvent,
signer: NostrSigner,
onReady: (String) -> Unit,
) {
val toKeys = toKeyList.keys()
val sendToKey = if (toKeys.isEmpty()) toKeyList.pubKey else toKeys.random().pubkey
val fromKeys = fromKeyList.keys()
// uses the main key
if (fromKeys.isEmpty()) {
signer.nip44Encrypt(decryptedContent, sendToKey, onReady)
} else {
val keyToUse = fromKeys.random()
EncryptionKeyCache.getOrLoad(
deriveFromPubKey = signer.pubKey,
nonce = keyToUse.nonce,
load = { onLoaded ->
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
onLoaded(newPrivKey.hexToByteArray())
}
},
) { derivedPrivKey ->
onReady(innerEncrypt(decryptedContent, derivedPrivKey, sendToKey))
}
}
}
fun decrypt(
encryptedContent: String,
fromPublicKey: HexKey,
toPublicKey: HexKey,
fromKeyList: EncryptionKeyListEvent,
toEncryptedKeyList: EncryptionKeyListEvent,
signer: NostrSigner,
onReady: (String) -> Unit,
) {
val fromKeys = fromKeyList.keys()
val sentFromKey = if (fromKeys.isEmpty()) fromKeyList.pubKey else fromKeys.random().pubkey
val keyToUse = toEncryptedKeyList.keys().firstOrNull { it.pubkey == toPublicKey }
// uses the main key
if (signer.pubKey == toPublicKey) {
signer.nip44Decrypt(encryptedContent, sentFromKey, onReady)
} else if (keyToUse != null) {
EncryptionKeyCache.getOrLoad(
deriveFromPubKey = signer.pubKey,
nonce = keyToUse.nonce,
load = { onLoaded ->
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
onLoaded(newPrivKey.hexToByteArray())
}
},
) { derivedPrivKey ->
innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey)?.let { onReady(it) }
}
}
}
}
@@ -0,0 +1,68 @@
/**
* 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.experimental.decoupling
import android.util.LruCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
object EncryptionKeyCache {
private val sharedNonceKeyCache = LruCache<EncryptionKeyCacheKey, ByteArray>(50)
private fun idx(
key: HexKey,
nonce: HexKey,
) = EncryptionKeyCacheKey(key, nonce)
fun put(
deriveFromPubKey: HexKey,
nonce: HexKey,
privKey: ByteArray,
) = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey)
fun get(
deriveFromPubKey: HexKey,
nonce: HexKey,
) = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce))
inline fun getOrLoad(
deriveFromPubKey: HexKey,
nonce: HexKey,
load: (onLoaded: (privKey: ByteArray) -> Unit) -> Unit,
crossinline whenReady: (privKey: ByteArray) -> Unit,
) {
val cachedPrivKey = get(deriveFromPubKey, nonce)
if (cachedPrivKey != null) {
whenReady(cachedPrivKey)
return
}
load { newPrivKey ->
put(deriveFromPubKey, nonce, newPrivKey)
whenReady(newPrivKey)
}
}
}
// keeps a cache per signed-in user
private data class EncryptionKeyCacheKey(
val pubkey: HexKey,
val nonce: HexKey,
)
@@ -0,0 +1,46 @@
/**
* 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.experimental.decoupling
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf
import com.vitorpamplona.quartz.utils.sha256.sha256
class EncryptionKeyDerivation {
companion object {
private val saltPrefix = "nip44kd".toByteArray(Charsets.UTF_8)
fun derivePrivateKey(
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray {
if (nonce == null) {
return privKey
}
return sha256(Hkdf().extract(privKey, saltPrefix + nonce))
}
fun deriveKeyPair(
privKey: ByteArray,
nonce: ByteArray?,
): KeyPair = KeyPair(privKey = derivePrivateKey(privKey, nonce))
}
}
@@ -0,0 +1,80 @@
/**
* 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.experimental.decoupling.setup
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.decoupling.setup.tags.KeyTag
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventUpdate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class EncryptionKeyListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun keys() = tags.mapNotNull(KeyTag::parse)
companion object {
const val KIND = 10044
const val ALT = "Encryption keys"
fun add(
key: KeyTag,
currentEvent: EncryptionKeyListEvent,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
) = eventUpdate(currentEvent, createdAt) {
alt(ALT)
key(key)
initializer()
}
fun remove(
key: KeyTag,
currentEvent: EncryptionKeyListEvent,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
) = eventUpdate(currentEvent, createdAt) {
alt(ALT)
removeIf(KeyTag::isSameKey, key.toTagArray())
initializer()
}
fun build(
keys: List<KeyTag>,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT)
keys(keys)
initializer()
}
}
}
@@ -0,0 +1,34 @@
/**
* 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.experimental.decoupling.setup
import com.vitorpamplona.quartz.experimental.decoupling.setup.tags.KeyTag
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<EncryptionKeyListEvent>.key(
key: HexKey,
nonce: HexKey,
) = add(KeyTag.assemble(key, nonce))
fun TagArrayBuilder<EncryptionKeyListEvent>.key(key: KeyTag) = add(key.toTagArray())
fun TagArrayBuilder<EncryptionKeyListEvent>.keys(keys: List<KeyTag>) = addAll(keys.map { it.toTagArray() })
@@ -0,0 +1,62 @@
/**
* 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.experimental.decoupling.setup.tags
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class KeyTag(
val pubkey: HexKey,
val nonce: HexKey,
) {
fun toTagArray() = assemble(pubkey, nonce)
companion object {
const val TAG_NAME = "n"
fun isSameKey(
tag1: Array<String>,
tag2: Array<String>,
): Boolean {
ensure(tag1.has(1)) { return false }
ensure(tag2.has(1)) { return false }
ensure(tag1[0] == tag2[0]) { return false }
ensure(tag1[1] == tag2[1]) { return false }
return true
}
@JvmStatic
fun parse(tag: Array<String>): KeyTag? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(tag[2].isNotEmpty()) { return null }
return KeyTag(tag[1], tag[2])
}
@JvmStatic
fun assemble(
key: HexKey,
nonce: HexKey,
) = arrayOf(TAG_NAME, key, nonce)
}
}
@@ -73,6 +73,11 @@ abstract class NostrSigner(
onReady: (LnZapPrivateEvent) -> Unit,
)
abstract fun deriveKey(
nonce: HexKey,
onReady: (HexKey) -> Unit,
)
fun decrypt(
encryptedContent: String,
fromPublicKey: HexKey,
@@ -80,4 +80,11 @@ class NostrSignerInternal(
) {
signerSync.decryptZapEvent(event)?.let { onReady(it) }
}
override fun deriveKey(
nonce: HexKey,
onReady: (HexKey) -> Unit,
) {
signerSync.deriveKey(nonce)?.let { onReady(it) }
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip01Core.signers
import android.util.Log
import com.vitorpamplona.quartz.experimental.decoupling.EncryptionKeyDerivation
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
@@ -128,4 +129,10 @@ class NostrSignerSync(
}
fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? = PrivateZapRequestBuilder().decryptZapEvent(event, this)
fun deriveKey(nonce: HexKey): HexKey? {
if (keyPair.privKey == null) return null
return EncryptionKeyDerivation.derivePrivateKey(keyPair.privKey, nonce.hexToByteArray()).toHexKey()
}
}
@@ -46,6 +46,7 @@ enum class SignerType {
NIP44_DECRYPT,
GET_PUBLIC_KEY,
DECRYPT_ZAP_EVENT,
DERIVE_KEY,
}
class Permission(
@@ -219,6 +220,7 @@ class ExternalSignerLauncher(
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
SignerType.GET_PUBLIC_KEY -> "get_public_key"
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
SignerType.DERIVE_KEY -> "derive_key"
}
intent.putExtra("type", signerType)
intent.putExtra("pubKey", pubKey)
@@ -318,6 +320,15 @@ class ExternalSignerLauncher(
return kotlin.Result.success(null)
}
fun hashCodeFields(
str1: String,
onReady: (String) -> Unit,
): Int {
var result = str1.hashCode()
result = 31 * result + onReady.hashCode()
return result
}
fun hashCodeFields(
str1: String,
str2: String,
@@ -398,4 +409,27 @@ class ExternalSignerLauncher(
},
)
}
fun deriveKey(
nonce: HexKey,
signerType: SignerType = SignerType.DERIVE_KEY,
onReady: (String) -> Unit,
) {
getDataFromResolver(signerType, arrayOf(nonce)).fold(
onFailure = { },
onSuccess = {
if (it == null) {
openSignerApp(
nonce,
signerType,
"",
hashCodeFields(nonce, onReady).toString(),
onReady,
)
} else {
onReady(it)
}
},
)
}
}
@@ -133,6 +133,17 @@ class NostrSignerExternal(
)
}
override fun deriveKey(
nonce: HexKey,
onReady: (HexKey) -> Unit,
) {
launcher.deriveKey(
nonce,
SignerType.DERIVE_KEY,
onReady,
)
}
override fun decryptZapEvent(
event: LnZapRequestEvent,
onReady: (LnZapPrivateEvent) -> Unit,