diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dc166ad0..ebb7f675 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -206,6 +206,8 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.kotlinx.coroutines.test) + // JVM-side native secp256k1 so unit tests can exercise ECDH (NIP-44 v3). + testImplementation(libs.secp256k1.jni.jvm) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.ui.test.junit4) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index 5d280896..b781c9a5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -224,6 +224,8 @@ class SignerProvider : ContentProvider() { "content://$appId.NIP44_DECRYPT", "content://$appId.NIP04_ENCRYPT", "content://$appId.NIP44_ENCRYPT", + "content://$appId.NIP44_V3_DECRYPT", + "content://$appId.NIP44_V3_ENCRYPT", "content://$appId.DECRYPT_ZAP_EVENT", -> { val content = projection?.first() ?: return null @@ -240,11 +242,30 @@ class SignerProvider : ContentProvider() { "NIP44_DECRYPT" -> SignerType.NIP44_DECRYPT "NIP04_ENCRYPT" -> SignerType.NIP04_ENCRYPT "NIP44_ENCRYPT" -> SignerType.NIP44_ENCRYPT + "NIP44_V3_DECRYPT" -> SignerType.NIP44_V3_DECRYPT + "NIP44_V3_ENCRYPT" -> SignerType.NIP44_V3_ENCRYPT "DECRYPT_ZAP_EVENT" -> SignerType.DECRYPT_ZAP_EVENT else -> null } ?: return null - val isEncrypt = type == SignerType.NIP04_ENCRYPT || type == SignerType.NIP44_ENCRYPT + val isEncrypt = type == SignerType.NIP04_ENCRYPT || + type == SignerType.NIP44_ENCRYPT || + type == SignerType.NIP44_V3_ENCRYPT + val isV3 = type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT + + // V3 carries kind and scope as projection[3] and projection[4]. + val (v3Kind, v3Scope) = if (isV3) { + val kindStr = projection.getOrNull(3) + val scopeStr = projection.getOrNull(4) ?: "" + val parsedKind = kindStr?.toIntOrNull() + if (parsedKind == null) { + Log.d(Amber.TAG, "NIP-44 v3 request missing/invalid kind") + return null + } + parsedKind to scopeStr + } else { + null to "" + } // For ENCRYPT: classify plaintext input; for DECRYPT: perform operation first then classify result val result = @@ -258,6 +279,8 @@ class SignerProvider : ContentProvider() { type, account, pubkey, + v3Kind, + v3Scope, ) ?: "Could not decrypt the message" } } catch (e: Exception) { @@ -276,18 +299,20 @@ class SignerProvider : ContentProvider() { } } - // Classify the content to determine EncryptedDataKind-based permission type - val classifyContent = if (isEncrypt) content else (result ?: content) - val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) - - var permission = permDao.getPermission(packageName, permType) - if (permission == null) { - permission = permDao.getPermission( - packageName, - type.toString(), - ) + // Permission lookup. V3 grants are scoped by (packageName, + // SignerType, kind); fall back to a kind=null "all kinds" + // grant. V3 grants do NOT satisfy V2 requests and vice versa. + var permission = if (isV3) { + permDao.getPermission(packageName, type.toString(), v3Kind!!) + ?: permDao.getPermission(packageName, type.toString()) + } else { + // Classify the content to determine EncryptedDataKind-based permission type + val classifyContent = if (isEncrypt) content else (result ?: content) + val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) + permDao.getPermission(packageName, permType) + ?: permDao.getPermission(packageName, type.toString()) } - if (permission == null) { + if (permission == null && !isV3) { val nip = when (stringType) { "NIP04_DECRYPT" -> 4 "NIP44_DECRYPT" -> 44 @@ -316,7 +341,7 @@ class SignerProvider : ContentProvider() { 0, packageName, uriString.replace("content://$appId.", ""), - null, + v3Kind, TimeUtils.now(), false, content = content, @@ -343,6 +368,8 @@ class SignerProvider : ContentProvider() { type, account, pubkey, + v3Kind, + v3Scope, ) ?: "Could not decrypt the message" } } catch (e: Exception) { @@ -367,7 +394,7 @@ class SignerProvider : ContentProvider() { 0, packageName, uriString.replace("content://$appId.", ""), - null, + v3Kind, TimeUtils.now(), true, content = if (!isEncrypt) finalResult else content, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt index 037a7d24..60ef217a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -9,8 +9,10 @@ import androidx.compose.ui.platform.Clipboard import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.DataStoreAccess import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.service.nip44v3.Nip44v3 import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -84,6 +86,10 @@ class Account( suspend fun nip04Decrypt(cipherText: String, fromPublicKey: String): String = signer.nip04Decrypt(cipherText, fromPublicKey) + fun nip44v3Encrypt(plainText: ByteArray, toPublicKey: String, kind: Int, scope: String): String = Nip44v3.encrypt(plainText, signer.keyPair.privKey!!, toPublicKey.hexToByteArray(), kind, scope) + + fun nip44v3Decrypt(cipherText: String, fromPublicKey: String, kind: Int, scope: String): ByteArray = Nip44v3.decrypt(cipherText, signer.keyPair.privKey!!, fromPublicKey.hexToByteArray(), kind, scope) + suspend fun decrypt(encryptedContent: String, fromPublicKey: String): String = signer.decrypt(encryptedContent, fromPublicKey) suspend fun signPsbt(psbtHex: String): String = signer.signPsbt(psbtHex) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt index 91962683..8763a198 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt @@ -26,6 +26,7 @@ fun EncryptedDataKind?.toPermissionType(isEncrypt: Boolean): String = when (this fun SignerType.toPermissionTypeString(encryptedData: EncryptedDataKind?): String = when (this) { SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT -> encryptedData.toPermissionType(isEncrypt = true) SignerType.NIP04_DECRYPT, SignerType.NIP44_DECRYPT -> encryptedData.toPermissionType(isEncrypt = false) + SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT -> this.toString() SignerType.DECRYPT_ZAP_EVENT -> "DECRYPT_ZAP_EVENT" else -> this.toString() } @@ -35,6 +36,8 @@ val encryptDecryptSignerTypes = setOf( SignerType.NIP44_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_DECRYPT, + SignerType.NIP44_V3_ENCRYPT, + SignerType.NIP44_V3_DECRYPT, SignerType.DECRYPT_ZAP_EVENT, ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt index fde5aaf9..f14cc9fa 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt @@ -21,4 +21,8 @@ data class IntentData( val encryptedData: EncryptedDataKind?, val isNostrConnectURI: Boolean = false, val unsignedEventKey: HexKey = "", + // NIP-44 v3 context fields. Required when [type] is one of the + // NIP44_V3_* variants; ignored otherwise. + val nip44v3Kind: Int? = null, + val nip44v3Scope: String = "", ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/SignerType.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/SignerType.kt index 9b85aca7..85fde7ea 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/SignerType.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/SignerType.kt @@ -7,6 +7,8 @@ enum class SignerType { NIP04_DECRYPT, NIP44_ENCRYPT, NIP44_DECRYPT, + NIP44_V3_ENCRYPT, + NIP44_V3_DECRYPT, GET_PUBLIC_KEY, DECRYPT_ZAP_EVENT, PING, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt index c6d245cf..0b8c2a10 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt @@ -24,11 +24,24 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils object AmberUtils { + /** + * For NIP-44 v3 callers, [data] follows the NIP-46 wire format: + * - V3 encrypt: [data] is base64-encoded plaintext bytes; the result is the + * v3 ciphertext string. + * - V3 decrypt: [data] is the v3 ciphertext string; the result is the + * base64-encoded plaintext bytes. + * + * [nip44v3Kind] and [nip44v3Scope] are required for the V3 SignerType variants + * (kind comes from the request; scope defaults to empty per spec). + */ + @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) suspend fun encryptOrDecryptData( data: String, type: SignerType, account: Account, pubKey: HexKey, + nip44v3Kind: Int? = null, + nip44v3Scope: String = "", ): String? = when (type) { SignerType.DECRYPT_ZAP_EVENT -> { account.decryptZapEvent(data) @@ -42,6 +55,16 @@ object AmberUtils { SignerType.NIP44_ENCRYPT -> { account.nip44Encrypt(data, pubKey) } + SignerType.NIP44_V3_ENCRYPT -> { + requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_ENCRYPT" } + val plaintextBytes = kotlin.io.encoding.Base64.decode(data) + account.nip44v3Encrypt(plaintextBytes, pubKey, nip44v3Kind, nip44v3Scope) + } + SignerType.NIP44_V3_DECRYPT -> { + requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_DECRYPT" } + val plaintextBytes = account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope) + kotlin.io.encoding.Base64.encode(plaintextBytes) + } else -> { account.nip44Decrypt(data, pubKey) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt index 93502bea..0ce24c9c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -228,6 +228,8 @@ object BunkerRequestUtils { "nip04_decrypt" -> SignerType.NIP04_DECRYPT "nip44_encrypt" -> SignerType.NIP44_ENCRYPT "nip44_decrypt" -> SignerType.NIP44_DECRYPT + "nip44v3_encrypt" -> SignerType.NIP44_V3_ENCRYPT + "nip44v3_decrypt" -> SignerType.NIP44_V3_DECRYPT "decrypt_zap_event" -> SignerType.DECRYPT_ZAP_EVENT "ping" -> SignerType.PING "switch_relays" -> SignerType.SWITCH_RELAYS @@ -242,11 +244,21 @@ object BunkerRequestUtils { amberEvent.toEvent().toJson() } "nip04_encrypt", "nip04_decrypt", "nip44_encrypt", "nip44_decrypt", "decrypt_zap_event" -> bunkerRequest.params.getOrElse(1) { "" } + // NIP-44 v3 NIP-46 layout: [pubkey, kind, scope, payload]. For + // `nip44v3_encrypt` the payload is base64-encoded plaintext; for + // `nip44v3_decrypt` it is the v3 ciphertext. + "nip44v3_encrypt", "nip44v3_decrypt" -> bunkerRequest.params.getOrElse(3) { "" } "ping" -> "pong" "sign_psbt" -> bunkerRequest.params.first() else -> "" } + /** Extract `kind` from a NIP-44 v3 bunker request; null if missing/invalid. */ + fun getNip44v3Kind(bunkerRequest: BunkerRequest): Int? = bunkerRequest.params.getOrNull(1)?.toIntOrNull() + + /** Extract `scope` from a NIP-44 v3 bunker request; defaults to empty per spec. */ + fun getNip44v3Scope(bunkerRequest: BunkerRequest): String = bunkerRequest.params.getOrElse(2) { "" } + fun sendResult( context: Context, account: Account, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt index 226575fb..637df52e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt @@ -455,10 +455,27 @@ class EventNotificationConsumer(private val applicationContext: Context) { } val data = BunkerRequestUtils.getDataFromBunker(bunkerRequest) - val projection = if (type == SignerType.PING) { - arrayOf(acc.npub) - } else { - arrayOf(data, pubKey, acc.npub) + val projection = when { + type == SignerType.PING -> arrayOf(acc.npub) + type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT -> { + val kind = BunkerRequestUtils.getNip44v3Kind(bunkerRequest) + val scope = BunkerRequestUtils.getNip44v3Scope(bunkerRequest) + if (kind == null) { + saveLog("NIP-44 v3 request missing/invalid kind", relay.url, acc.npub) + BunkerRequestUtils.sendBunkerResponse( + context = applicationContext, + account = acc, + bunkerRequest = request, + bunkerResponse = BunkerResponse(bunkerRequest.id, "", "kind is required for nip44v3"), + relays = relays, + onLoading = { }, + onDone = { }, + ) + return + } + arrayOf(data, pubKey, acc.npub, kind.toString(), scope) + } + else -> arrayOf(data, pubKey, acc.npub) } val cursor = applicationContext.contentResolver.query( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index 8a045e5e..cffc31c6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -138,6 +138,10 @@ object IntentUtils { SignerType.NIP44_ENCRYPT "nip44_decrypt" -> SignerType.NIP44_DECRYPT + "nip44v3_encrypt" -> + SignerType.NIP44_V3_ENCRYPT + "nip44v3_decrypt" -> + SignerType.NIP44_V3_DECRYPT "decrypt_zap_event" -> SignerType.DECRYPT_ZAP_EVENT "sign_psbt" -> @@ -187,6 +191,8 @@ object IntentUtils { var callbackUrl: String? = null var returnType = ReturnType.SIGNATURE var appName = "" + var nip44v3Kind: Int? = null + var nip44v3Scope = "" // flatMap avoids building an intermediate joined string before splitting on "&" parameters.flatMap { it.split("&") }.forEach { val params = it.split("=").toMutableList() @@ -200,6 +206,8 @@ object IntentUtils { parameter == "callbackUrl" -> callbackUrl = parameterData parameter == "returnType" -> if (parameterData == "event") returnType = ReturnType.EVENT parameter == "appName" -> appName = parameterData + parameter == "kind" -> nip44v3Kind = parameterData.toIntOrNull() + parameter == "scope" -> nip44v3Scope = parameterData } } @@ -242,7 +250,9 @@ object IntentUtils { unsignedEventKey = unsignedEventKey, ) } - SignerType.NIP04_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT -> { + SignerType.NIP04_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, + SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT, + -> { val result = try { AmberUtils.encryptOrDecryptData( @@ -250,6 +260,8 @@ object IntentUtils { type, account, pubKey, + nip44v3Kind, + nip44v3Scope, ) ?: "Could not decrypt the message" } catch (e: Exception) { Amber.instance.applicationIOScope.launch { @@ -283,6 +295,8 @@ object IntentUtils { route = route, event = null, encryptedData = encryptedDataKind, + nip44v3Kind = nip44v3Kind, + nip44v3Scope = nip44v3Scope, ) } SignerType.GET_PUBLIC_KEY -> { @@ -410,7 +424,11 @@ object IntentUtils { unsignedEventKey = unsignedEventKey, ) } - SignerType.NIP04_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, SignerType.DECRYPT_ZAP_EVENT -> { + SignerType.NIP04_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, + SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT, SignerType.DECRYPT_ZAP_EVENT, + -> { + val nip44v3Kind = intent.extras?.getString("kind")?.toIntOrNull() + val nip44v3Scope = intent.extras?.getString("scope") ?: "" val result = try { AmberUtils.encryptOrDecryptData( @@ -418,6 +436,8 @@ object IntentUtils { type, account, pubKey, + nip44v3Kind, + nip44v3Scope, ) ?: "Could not decrypt the message" } catch (e: Exception) { if (e is FailedMigrationException) throw e @@ -457,6 +477,8 @@ object IntentUtils { route = route, event = null, encryptedData = encryptedDataKind, + nip44v3Kind = nip44v3Kind, + nip44v3Scope = nip44v3Scope, ) } SignerType.GET_PUBLIC_KEY -> { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3.kt new file mode 100644 index 00000000..fe153435 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3.kt @@ -0,0 +1,261 @@ +package com.greenart7c3.nostrsigner.service.nip44v3 + +import com.vitorpamplona.quartz.nip44Encryption.crypto.ChaCha20 +import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.Secp256k1Instance +import com.vitorpamplona.quartz.utils.mac.MacInstance +import kotlin.io.encoding.Base64 + +/** + * NIP-44 v3 cipher. + * + * Implements the asymmetric encryption scheme defined in the + * `nostr-land/nip44v3` draft: ECDH(secp256k1) → HKDF-SHA256 keyed with + * `"nip44-v3\x00" || nonce`, ChaCha20 with an all-zeroes 96-bit nonce, + * HMAC-SHA256 over `nonce || kind || scope_len || scope || ciphertext`, + * and a context (`kind` + `scope`) authenticated alongside the + * ciphertext to prevent cross-context replay. + */ +object Nip44v3 { + const val VERSION: Byte = 0x03 + private const val NONCE_SIZE = 32 + private const val MAC_SIZE = 32 + private const val MIN_DECODED_SIZE = 77 // 1 + 32 + 32 + 4 + 4 + 0 + 4 + private const val MIN_PADDING = 32 + private const val PAD_CHUNK_THRESHOLD = 32768 + private const val PAD_SUBDIVS_SMALL = 4 + private const val PAD_SUBDIVS_LARGE = 8 + + private val saltPrefix = "nip44-v3\u0000".encodeToByteArray() + private val infoEncryptionKey = "encryption_key".encodeToByteArray() + private val infoMacKey = "mac_key".encodeToByteArray() + private val zeroChaChaNonce = ByteArray(12) + + private val hkdf = Hkdf() + private val chaCha = ChaCha20() + + class Nip44v3Exception(message: String, cause: Throwable? = null) : RuntimeException(message, cause) + + fun encrypt( + plaintext: ByteArray, + privKey: ByteArray, + pubKey: ByteArray, + kind: Int, + scope: String, + ): String = encryptWithNonce(plaintext, privKey, pubKey, kind, scope, RandomInstance.bytes(NONCE_SIZE)) + + /** + * Test/library-internal overload that accepts a caller-supplied nonce. The + * NIP-44 v3 spec is explicit that production code must not let callers + * choose the nonce — use [encrypt] for that. + */ + fun encryptWithNonce( + plaintext: ByteArray, + privKey: ByteArray, + pubKey: ByteArray, + kind: Int, + scope: String, + nonce: ByteArray, + ): String { + require(nonce.size == NONCE_SIZE) { "nonce must be $NONCE_SIZE bytes, got ${nonce.size}" } + require(kind >= 0) { "kind must be non-negative, got $kind" } + + val scopeBytes = scope.encodeToByteArray() + val (encryptionKey, macKey) = deriveKeys(privKey, pubKey, nonce) + + val padded = pad(plaintext) + val cipherBytes = chaCha.encrypt(padded, zeroChaChaNonce, encryptionKey) + + val mac = computeMac(macKey, nonce, kind, scopeBytes, cipherBytes) + + val payload = ByteArray(1 + NONCE_SIZE + MAC_SIZE + 4 + 4 + scopeBytes.size + cipherBytes.size) + var off = 0 + payload[off++] = VERSION + nonce.copyInto(payload, off) + off += NONCE_SIZE + mac.copyInto(payload, off) + off += MAC_SIZE + writeU32BE(payload, off, kind) + off += 4 + writeU32BE(payload, off, scopeBytes.size) + off += 4 + scopeBytes.copyInto(payload, off) + off += scopeBytes.size + cipherBytes.copyInto(payload, off) + + return Base64.encode(payload) + } + + fun decrypt( + payload: String, + privKey: ByteArray, + pubKey: ByteArray, + expectedKind: Int, + expectedScope: String, + ): ByteArray { + if (payload.isEmpty()) throw Nip44v3Exception("empty payload") + if (payload[0] == '#') throw Nip44v3Exception("unsupported future version") + + val decoded = try { + Base64.decode(payload) + } catch (e: IllegalArgumentException) { + throw Nip44v3Exception("invalid base64", e) + } + + if (decoded.size < MIN_DECODED_SIZE) { + throw Nip44v3Exception("ciphertext too short: ${decoded.size} < $MIN_DECODED_SIZE") + } + if (decoded[0] != VERSION) { + throw Nip44v3Exception("unsupported version: ${decoded[0].toInt() and 0xff}") + } + + val nonce = decoded.copyOfRange(1, 1 + NONCE_SIZE) + val mac = decoded.copyOfRange(1 + NONCE_SIZE, 1 + NONCE_SIZE + MAC_SIZE) + val kind = readU32BE(decoded, 1 + NONCE_SIZE + MAC_SIZE) + val scopeLen = readU32BE(decoded, 1 + NONCE_SIZE + MAC_SIZE + 4) + + val scopeOff = 1 + NONCE_SIZE + MAC_SIZE + 4 + 4 + if (scopeLen < 0 || scopeLen > decoded.size - scopeOff) { + throw Nip44v3Exception("scope length out-of-bounds: $scopeLen") + } + val scope = decoded.copyOfRange(scopeOff, scopeOff + scopeLen) + val cipherBytes = decoded.copyOfRange(scopeOff + scopeLen, decoded.size) + if (cipherBytes.size < 4) { + throw Nip44v3Exception("ciphertext too short") + } + + if (kind != expectedKind) { + throw Nip44v3Exception("context mismatch (kind): got $kind expected $expectedKind") + } + val expectedScopeBytes = expectedScope.encodeToByteArray() + if (!scope.contentEquals(expectedScopeBytes)) { + throw Nip44v3Exception("context mismatch (scope)") + } + + val (encryptionKey, macKey) = deriveKeys(privKey, pubKey, nonce) + + val expectedMac = computeMac(macKey, nonce, kind, scope, cipherBytes) + if (!constantTimeEq(mac, expectedMac)) { + throw Nip44v3Exception("invalid MAC") + } + + val padded = chaCha.decrypt(cipherBytes, zeroChaChaNonce, encryptionKey) + return unpad(padded) + } + + fun deriveKeys(privKey: ByteArray, pubKey: ByteArray, nonce: ByteArray): Pair { + val sharedSecret = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privKey) + return deriveKeysFromSharedSecret(sharedSecret, nonce) + } + + fun deriveKeysFromSharedSecret(sharedSecret: ByteArray, nonce: ByteArray): Pair { + val prk = extract(sharedSecret, nonce) + val encryptionKey = hkdf.expand(prk, infoEncryptionKey, 32) + val macKey = hkdf.expand(prk, infoMacKey, 32) + return encryptionKey to macKey + } + + /** Exposed for test vectors that check the intermediate `prk`. */ + fun extract(sharedSecret: ByteArray, nonce: ByteArray): ByteArray { + val salt = ByteArray(saltPrefix.size + nonce.size) + saltPrefix.copyInto(salt, 0) + nonce.copyInto(salt, saltPrefix.size) + return hkdf.extract(sharedSecret, salt) + } + + fun pad(plaintext: ByteArray): ByteArray { + val prefixedLen = 4 + plaintext.size + val targetSize = targetSize(prefixedLen) + val out = ByteArray(targetSize) + writeU32BE(out, 0, plaintext.size) + plaintext.copyInto(out, 4) + return out + } + + fun unpad(padded: ByteArray): ByteArray { + if (padded.size < 4) throw Nip44v3Exception("padded buffer too short") + val plaintextLen = readU32BE(padded, 0) + if (plaintextLen < 0) throw Nip44v3Exception("invalid plaintext length: $plaintextLen") + if (plaintextLen.toLong() + 4L > padded.size.toLong()) { + throw Nip44v3Exception("invalid padding: declared $plaintextLen, available ${padded.size - 4}") + } + // The padded buffer length is fully determined by the plaintext length; + // reject anything that does not match the canonical size so attackers + // can't smuggle data in the padding. + if (padded.size.toLong() != targetSizeLong(4L + plaintextLen)) { + throw Nip44v3Exception("invalid padding: wrong target size") + } + // Constant-time zero check over the padding region. + var diff = 0 + for (i in 4 + plaintextLen until padded.size) { + diff = diff or padded[i].toInt() + } + if (diff != 0) throw Nip44v3Exception("invalid padding: non-zero trailing bytes") + return padded.copyOfRange(4, 4 + plaintextLen) + } + + fun targetSize(len: Int): Int { + val t = targetSizeLong(len.toLong()) + if (t > Int.MAX_VALUE) throw Nip44v3Exception("padded length exceeds Int.MAX_VALUE: $t") + return t.toInt() + } + + private fun targetSizeLong(len: Long): Long { + require(len >= 0) { "negative length: $len" } + if (len == 0L) return MIN_PADDING.toLong() + // next_power = 2 ** ceil(log2(len)) + val nextPower = if (len == 1L) 1L else 1L shl ceilLog2Long(len) + val subdivs = if (nextPower >= PAD_CHUNK_THRESHOLD) PAD_SUBDIVS_LARGE.toLong() else PAD_SUBDIVS_SMALL.toLong() + val chunk = maxOf(MIN_PADDING.toLong(), nextPower / subdivs) + return chunk * ((len + chunk - 1) / chunk) + } + + private fun ceilLog2Long(n: Long): Int { + // For n >= 2; matches ceil(log2(n)). + if (n <= 1) return 0 + var v = n - 1 + var bits = 0 + while (v > 0) { + v = v ushr 1 + bits++ + } + return bits + } + + private fun computeMac(macKey: ByteArray, nonce: ByteArray, kind: Int, scope: ByteArray, ciphertext: ByteArray): ByteArray { + val mac = MacInstance("HmacSHA256", macKey) + mac.update(nonce) + val u32 = ByteArray(4) + writeU32BE(u32, 0, kind) + mac.update(u32) + writeU32BE(u32, 0, scope.size) + mac.update(u32) + if (scope.isNotEmpty()) mac.update(scope) + if (ciphertext.isNotEmpty()) mac.update(ciphertext) + return mac.doFinal() + } + + private fun writeU32BE(out: ByteArray, offset: Int, value: Int) { + out[offset] = (value ushr 24).toByte() + out[offset + 1] = (value ushr 16).toByte() + out[offset + 2] = (value ushr 8).toByte() + out[offset + 3] = value.toByte() + } + + private fun readU32BE(src: ByteArray, offset: Int): Int { + val v = ((src[offset].toLong() and 0xff) shl 24) or + ((src[offset + 1].toLong() and 0xff) shl 16) or + ((src[offset + 2].toLong() and 0xff) shl 8) or + (src[offset + 3].toLong() and 0xff) + if (v > Int.MAX_VALUE) throw Nip44v3Exception("u32 value exceeds Int.MAX_VALUE: $v") + return v.toInt() + } + + private fun constantTimeEq(a: ByteArray, b: ByteArray): Boolean { + if (a.size != b.size) return false + var diff = 0 + for (i in a.indices) diff = diff or (a[i].toInt() xor b[i].toInt()) + return diff == 0 + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt index 6fe29bfa..4b381835 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt @@ -36,7 +36,9 @@ import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.models.toPermissionType +import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.BunkerRequestUtils +import com.greenart7c3.nostrsigner.service.IntentUtils import com.greenart7c3.nostrsigner.service.PsbtDecoder import com.greenart7c3.nostrsigner.service.RelayUrlUtils import com.greenart7c3.nostrsigner.service.isPrivateEvent @@ -813,7 +815,77 @@ fun BunkerSingleEventHomeScreen( } else -> { - if (type == SignerType.DECRYPT_ZAP_EVENT) { + if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) { + // NIP-44 v3 requests arrive as a generic BunkerRequest (Quartz + // doesn't know the method name); kind/scope are at params[1..2]. + val v3Kind = BunkerRequestUtils.getNip44v3Kind(bunkerRequest.request) + val v3Scope = BunkerRequestUtils.getNip44v3Scope(bunkerRequest.request) + val permission = applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == v3Kind + } ?: applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == null + } + + val acceptOrReject = IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission) + + BunkerEncryptDecryptData( + modifier = modifier, + encryptedData = bunkerRequest.encryptedData, + shouldRunOnAccept = acceptOrReject, + appName = "$appName (kind $v3Kind${if (v3Scope.isNotEmpty()) " · scope \"$v3Scope\"" else ""})", + type = type, + account = account, + defaultScope = DecryptTypeScope.SPECIFIC, + onAccept = { rememberType, scope -> + Amber.instance.applicationIOScope.launch(Dispatchers.IO) { + val result = try { + AmberUtils.encryptOrDecryptData( + BunkerRequestUtils.getDataFromBunker(bunkerRequest.request), + type, + account, + bunkerRequest.request.params.first(), + v3Kind, + v3Scope, + ) ?: "" + } catch (e: Exception) { + "" + } + + // SPECIFIC ⇒ kind-scoped grant; ALL ⇒ broad grant (kind=null). + val grantedKind = if (scope == DecryptTypeScope.SPECIFIC) v3Kind else null + BunkerRequestUtils.sendResult( + context = context, + account = account, + key = key, + response = result, + bunkerRequest = bunkerRequest, + kind = grantedKind, + onLoading = onLoading, + permissions = null, + appName = appName, + signPolicy = null, + shouldCloseApplication = bunkerRequest.closeApplication, + rememberType = rememberType, + decryptTypeScope = scope, + ) + } + }, + onReject = { rememberType, scope -> + val grantedKind = if (scope == DecryptTypeScope.SPECIFIC) v3Kind else null + BunkerRequestUtils.sendRejection( + key = key, + account = account, + bunkerRequest = bunkerRequest, + appName = appName, + rememberType = rememberType, + signerType = type, + kind = grantedKind, + onLoading = onLoading, + decryptTypeScope = scope, + ) + }, + ) + } else if (type == SignerType.DECRYPT_ZAP_EVENT) { val permission = applicationEntity?.permissions?.firstOrNull { it.pkKey == key && it.type == type.toString() diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt index 19560315..8f9f8142 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt @@ -170,24 +170,37 @@ fun IntentSingleEventHomeScreen( ) } - SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, SignerType.DECRYPT_ZAP_EVENT -> { + SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, + SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT, SignerType.DECRYPT_ZAP_EVENT, + -> { + val isV3 = intentData.type == SignerType.NIP44_V3_ENCRYPT || intentData.type == SignerType.NIP44_V3_DECRYPT val nip = when (intentData.type) { SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT -> 4 SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT -> 44 SignerType.DECRYPT_ZAP_EVENT -> null + // V3 has its own per-kind permission model; don't fall back to a NIP-level grant. + SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT -> null else -> null } - val isEncrypt = intentData.type == SignerType.NIP04_ENCRYPT || intentData.type == SignerType.NIP44_ENCRYPT - val permType = intentData.encryptedData.toPermissionType(isEncrypt = isEncrypt) + val isEncrypt = intentData.type == SignerType.NIP04_ENCRYPT || + intentData.type == SignerType.NIP44_ENCRYPT || + intentData.type == SignerType.NIP44_V3_ENCRYPT + val permType = if (isV3) intentData.type.toString() else intentData.encryptedData.toPermissionType(isEncrypt = isEncrypt) var permission = applicationEntity?.permissions?.firstOrNull { - it.pkKey == key && it.type == permType + it.pkKey == key && it.type == permType && (!isV3 || it.kind == intentData.nip44v3Kind) } - if (permission == null) { + if (permission == null && !isV3) { permission = applicationEntity?.permissions?.firstOrNull { it.pkKey == key && it.type == intentData.type.toString() } + } else if (permission == null && isV3) { + // Fall back to a (V3, kind=null) "all kinds" grant. + permission = + applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == permType && it.kind == null + } } if (permission == null && nip != null) { permission = @@ -205,6 +218,9 @@ fun IntentSingleEventHomeScreen( packageName = packageName, type = intentData.type, account = account, + // For V3, scope toggle's "SPECIFIC vs ALL" instead controls + // whether the grant is kind-scoped — handled via [kind] below. + defaultScope = if (isV3) DecryptTypeScope.SPECIFIC else DecryptTypeScope.ALL, onAccept = { rememberType, scope -> val result = if (intentData.encryptedData?.result == "Could not decrypt the message" && (intentData.type == SignerType.DECRYPT_ZAP_EVENT)) { @@ -213,6 +229,10 @@ fun IntentSingleEventHomeScreen( intentData.encryptedData?.result ?: "" } + // V3: write the permission with the requested kind when + // the user picked SPECIFIC, or kind=null (all kinds) for ALL. + val v3Kind = if (isV3 && scope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null + IntentUtils.sendResult( context, packageName, @@ -222,7 +242,7 @@ fun IntentSingleEventHomeScreen( result, result, intentData, - null, + v3Kind, onRemoveIntentData = onRemoveIntentData, onLoading = onLoading, rememberType = rememberType, @@ -230,6 +250,7 @@ fun IntentSingleEventHomeScreen( ) }, onReject = { rememberType, scope -> + val v3Kind = if (isV3 && scope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null IntentUtils.sendRejection( key = key, account = account, @@ -238,7 +259,7 @@ fun IntentSingleEventHomeScreen( rememberType = rememberType, onLoading = onLoading, onRemoveIntentData = onRemoveIntentData, - kind = null, + kind = v3Kind, decryptTypeScope = scope, ) }, diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtilsTest.kt b/app/src/test/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtilsTest.kt index a963427a..19fba699 100644 --- a/app/src/test/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtilsTest.kt +++ b/app/src/test/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtilsTest.kt @@ -95,6 +95,16 @@ class BunkerRequestUtilsTest { assertEquals(SignerType.NIP44_DECRYPT, BunkerRequestUtils.getTypeFromBunker(mockBunkerRequest("nip44_decrypt"))) } + @Test + fun `getTypeFromBunker returns NIP44_V3_ENCRYPT for nip44v3_encrypt`() { + assertEquals(SignerType.NIP44_V3_ENCRYPT, BunkerRequestUtils.getTypeFromBunker(mockBunkerRequest("nip44v3_encrypt"))) + } + + @Test + fun `getTypeFromBunker returns NIP44_V3_DECRYPT for nip44v3_decrypt`() { + assertEquals(SignerType.NIP44_V3_DECRYPT, BunkerRequestUtils.getTypeFromBunker(mockBunkerRequest("nip44v3_decrypt"))) + } + @Test fun `getTypeFromBunker returns DECRYPT_ZAP_EVENT for decrypt_zap_event`() { assertEquals(SignerType.DECRYPT_ZAP_EVENT, BunkerRequestUtils.getTypeFromBunker(mockBunkerRequest("decrypt_zap_event"))) @@ -167,6 +177,36 @@ class BunkerRequestUtilsTest { assertEquals("ciphertext456", BunkerRequestUtils.getDataFromBunker(request)) } + @Test + fun `getDataFromBunker returns fourth param for nip44v3_encrypt`() { + val request = mockBunkerRequest("nip44v3_encrypt", arrayOf("pubkey123", "4", "dm", "cGxhaW50ZXh0Lw==")) + assertEquals("cGxhaW50ZXh0Lw==", BunkerRequestUtils.getDataFromBunker(request)) + } + + @Test + fun `getDataFromBunker returns fourth param for nip44v3_decrypt`() { + val request = mockBunkerRequest("nip44v3_decrypt", arrayOf("pubkey123", "4", "dm", "A2NpcGhlcnRleHQ=")) + assertEquals("A2NpcGhlcnRleHQ=", BunkerRequestUtils.getDataFromBunker(request)) + } + + @Test + fun `getNip44v3Kind parses second param`() { + val request = mockBunkerRequest("nip44v3_encrypt", arrayOf("pk", "42", "scope", "data")) + assertEquals(42, BunkerRequestUtils.getNip44v3Kind(request)) + } + + @Test + fun `getNip44v3Kind returns null when missing or invalid`() { + assertEquals(null, BunkerRequestUtils.getNip44v3Kind(mockBunkerRequest("nip44v3_encrypt", arrayOf("pk")))) + assertEquals(null, BunkerRequestUtils.getNip44v3Kind(mockBunkerRequest("nip44v3_encrypt", arrayOf("pk", "not-a-number", "s", "d")))) + } + + @Test + fun `getNip44v3Scope returns third param or empty`() { + assertEquals("dm", BunkerRequestUtils.getNip44v3Scope(mockBunkerRequest("nip44v3_encrypt", arrayOf("pk", "4", "dm", "data")))) + assertEquals("", BunkerRequestUtils.getNip44v3Scope(mockBunkerRequest("nip44v3_encrypt", arrayOf("pk", "4")))) + } + @Test fun `getDataFromBunker returns second param for decrypt_zap_event`() { val request = mockBunkerRequest("decrypt_zap_event", arrayOf("pubkey123", "event_json_string")) diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3Test.kt b/app/src/test/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3Test.kt new file mode 100644 index 00000000..9b32b4f4 --- /dev/null +++ b/app/src/test/java/com/greenart7c3/nostrsigner/service/nip44v3/Nip44v3Test.kt @@ -0,0 +1,236 @@ +package com.greenart7c3.nostrsigner.service.nip44v3 + +import com.fasterxml.jackson.databind.JsonNode +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import java.security.MessageDigest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class Nip44v3Test { + private fun loadVectors(): JsonNode { + val stream = Nip44v3Test::class.java.classLoader!!.getResourceAsStream("nip44v3-vectors.json") + assertNotNull("nip44v3-vectors.json not found on test classpath", stream) + return JacksonMapper.mapper.readTree(stream) + } + + private fun hex(s: String): ByteArray { + val out = ByteArray(s.length / 2) + for (i in out.indices) { + out[i] = ((Character.digit(s[i * 2], 16) shl 4) or Character.digit(s[i * 2 + 1], 16)).toByte() + } + return out + } + + private fun toHex(b: ByteArray): String { + val sb = StringBuilder(b.size * 2) + for (x in b) sb.append(String.format("%02x", x.toInt() and 0xff)) + return sb.toString() + } + + private fun pubKeyFor(privKeyHex: String): ByteArray = KeyPair(privKey = hex(privKeyHex)).pubKey + + @Test + fun `padded length vectors match`() { + val arr = loadVectors().get("padded_length") + for (entry in arr) { + val len = entry.get(0).intValue() + val expected = entry.get(1).intValue() + // The spec's target_size is over length(prefixed_plaintext); the + // table is parameterized on that prefixed length directly. + assertEquals("targetSize($len)", expected, Nip44v3.targetSize(len)) + } + } + + @Test + fun `encrypt and decrypt vectors round-trip in both directions`() { + val vectors = loadVectors().get("encrypt_decrypt") + for ((idx, v) in vectors.withIndex()) { + val priv1 = hex(v.get("secret1").textValue()) + val priv2 = hex(v.get("secret2").textValue()) + val pub1 = pubKeyFor(v.get("secret1").textValue()) + val pub2 = pubKeyFor(v.get("secret2").textValue()) + val nonce = hex(v.get("nonce").textValue()) + val kind = v.get("kind").intValue() + val scope = String(hex(v.get("scope_hex").textValue()), Charsets.UTF_8) + val plaintext = hex(v.get("plaintext_hex").textValue()) + val expectedCt = v.get("ciphertext").textValue() + val expectedPrk = v.get("prk").textValue() + val expectedEnc = v.get("encryption_key").textValue() + val expectedMac = v.get("mac_key").textValue() + + // Key derivation matches PRK / encryption_key / mac_key from spec + val prk1 = Nip44v3.extract( + com.vitorpamplona.quartz.utils.Secp256k1Instance.pubKeyTweakMulCompact(pub2, priv1), + nonce, + ) + assertEquals("vector $idx: prk", expectedPrk, toHex(prk1)) + val (enc, mac) = Nip44v3.deriveKeys(priv1, pub2, nonce) + assertEquals("vector $idx: encryption_key", expectedEnc, toHex(enc)) + assertEquals("vector $idx: mac_key", expectedMac, toHex(mac)) + + // Encrypt from secret1 → must match the spec ciphertext byte for byte. + val ct1 = Nip44v3.encryptWithNonce(plaintext, priv1, pub2, kind, scope, nonce) + assertEquals("vector $idx: ciphertext from secret1", expectedCt, ct1) + + // Encrypt from secret2 with the same nonce — same ECDH product, same ciphertext. + val ct2 = Nip44v3.encryptWithNonce(plaintext, priv2, pub1, kind, scope, nonce) + assertEquals("vector $idx: ciphertext from secret2", expectedCt, ct2) + + // Decryption recovers plaintext from each side + val dec1 = Nip44v3.decrypt(expectedCt, priv1, pub2, kind, scope) + assertArrayEquals("vector $idx: decrypt from secret1", plaintext, dec1) + val dec2 = Nip44v3.decrypt(expectedCt, priv2, pub1, kind, scope) + assertArrayEquals("vector $idx: decrypt from secret2", plaintext, dec2) + } + } + + @Test + fun `long encrypt and decrypt vectors match sha256 of ciphertext`() { + val vectors = loadVectors().get("long_encrypt_decrypt") + val sha = MessageDigest.getInstance("SHA-256") + for ((idx, v) in vectors.withIndex()) { + val priv1 = hex(v.get("secret1").textValue()) + val priv2 = hex(v.get("secret2").textValue()) + val pub1 = pubKeyFor(v.get("secret1").textValue()) + val pub2 = pubKeyFor(v.get("secret2").textValue()) + val nonce = hex(v.get("nonce").textValue()) + val kind = v.get("kind").intValue() + val scope = String(hex(v.get("scope_hex").textValue()), Charsets.UTF_8) + val pattern = hex(v.get("pattern_hex").textValue()) + val repeat = v.get("repeat").intValue() + val expectedHash = v.get("ciphertext_sha256").textValue() + + val plaintext = ByteArray(pattern.size * repeat) + for (i in 0 until repeat) pattern.copyInto(plaintext, i * pattern.size) + + val ct = Nip44v3.encryptWithNonce(plaintext, priv1, pub2, kind, scope, nonce) + sha.reset() + val hash = toHex(sha.digest(ct.toByteArray(Charsets.US_ASCII))) + assertEquals("vector $idx: ciphertext sha256", expectedHash, hash) + + // Round-trip with the other side, just to exercise the symmetric path + val recovered = Nip44v3.decrypt(ct, priv2, pub1, kind, scope) + assertArrayEquals("vector $idx: long decrypt", plaintext, recovered) + } + } + + @Test + fun `invalid decryption vectors fail`() { + val vectors = loadVectors().get("invalid_decryption") + for ((idx, v) in vectors.withIndex()) { + val priv = hex(v.get("secret").textValue()) + val pub = hex(v.get("public").textValue()) + val kind = v.get("kind").intValue() + val scope = String(hex(v.get("scope_hex").textValue()), Charsets.UTF_8) + val ct = v.get("ciphertext").textValue() + val why = v.get("why").textValue() + + assertThrows("vector $idx ($why) should fail", Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt(ct, priv, pub, kind, scope) + } + } + } + + @Test + fun `round-trip on various plaintext sizes`() { + val priv = hex("0000000000000000000000000000000000000000000000000000000000000001") + val peer = hex("0000000000000000000000000000000000000000000000000000000000000002") + val peerPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000002") + val sizes = listOf(0, 1, 31, 32, 33, 100, 1024, 65535, 65536, 100_000) + for (size in sizes) { + val plaintext = ByteArray(size) { (it and 0xff).toByte() } + val ct = Nip44v3.encrypt(plaintext, priv, peerPub, kind = 4, scope = "dm") + val ownPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000001") + val recovered = Nip44v3.decrypt(ct, peer, ownPub, expectedKind = 4, expectedScope = "dm") + assertArrayEquals("size=$size", plaintext, recovered) + } + } + + @Test + fun `context rebinding is rejected`() { + val priv = hex("0000000000000000000000000000000000000000000000000000000000000001") + val peer = hex("0000000000000000000000000000000000000000000000000000000000000002") + val peerPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000002") + val ownPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000001") + + val ct = Nip44v3.encrypt("hello".toByteArray(), priv, peerPub, kind = 4, scope = "a") + assertThrows(Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt(ct, peer, ownPub, expectedKind = 4, expectedScope = "b") + } + assertThrows(Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt(ct, peer, ownPub, expectedKind = 5, expectedScope = "a") + } + } + + @Test + fun `version byte rejection`() { + val priv = hex("0000000000000000000000000000000000000000000000000000000000000001") + val peerPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000002") + + val e1 = assertThrows(Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt("#anything", priv, peerPub, 0, "") + } + assertTrue(e1.message!!.contains("unsupported future version")) + + val e2 = assertThrows(Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt("", priv, peerPub, 0, "") + } + assertTrue(e2.message!!.contains("empty payload")) + } + + @Test + fun `pad and unpad are inverse for short and long inputs`() { + for (size in listOf(0, 1, 32, 33, 1024, 65535)) { + val plain = ByteArray(size) { (it and 0xff).toByte() } + val padded = Nip44v3.pad(plain) + assertEquals(Nip44v3.targetSize(4 + size), padded.size) + val recovered = Nip44v3.unpad(padded) + assertArrayEquals(plain, recovered) + } + } + + @Test + fun `unpad rejects non-zero padding`() { + val plain = byteArrayOf(1, 2, 3) + val padded = Nip44v3.pad(plain) + padded[padded.size - 1] = 0x42 // tamper trailing pad byte + assertThrows(Nip44v3.Nip44v3Exception::class.java) { Nip44v3.unpad(padded) } + } + + @Test + fun `constant-time MAC tamper detection`() { + val priv = hex("0000000000000000000000000000000000000000000000000000000000000001") + val peer = hex("0000000000000000000000000000000000000000000000000000000000000002") + val peerPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000002") + val ownPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000001") + + val ct = Nip44v3.encrypt("hello".toByteArray(), priv, peerPub, kind = 1, scope = "") + val decoded = kotlin.io.encoding.Base64.decode(ct) + // Flip a bit in the MAC region (bytes 33..65) + decoded[40] = (decoded[40].toInt() xor 0x01).toByte() + val tampered = kotlin.io.encoding.Base64.encode(decoded) + val ex = assertThrows(Nip44v3.Nip44v3Exception::class.java) { + Nip44v3.decrypt(tampered, peer, ownPub, 1, "") + } + assertTrue(ex.message!!.contains("invalid MAC")) + } + + @Test + fun `kind above 65535 is supported`() { + // The signer libs MUST support kinds above 65535 per implementing.md. + val priv = hex("0000000000000000000000000000000000000000000000000000000000000001") + val peer = hex("0000000000000000000000000000000000000000000000000000000000000002") + val peerPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000002") + val ownPub = pubKeyFor("0000000000000000000000000000000000000000000000000000000000000001") + val ct = Nip44v3.encrypt("ok".toByteArray(), priv, peerPub, kind = 1_000_000, scope = "") + val plain = Nip44v3.decrypt(ct, peer, ownPub, expectedKind = 1_000_000, expectedScope = "") + assertArrayEquals("ok".toByteArray(), plain) + assertFalse(plain.isEmpty()) + } +} diff --git a/app/src/test/resources/nip44v3-vectors.json b/app/src/test/resources/nip44v3-vectors.json new file mode 100644 index 00000000..1f21901a --- /dev/null +++ b/app/src/test/resources/nip44v3-vectors.json @@ -0,0 +1,461 @@ +{ + "encrypt_decrypt": [ + { + "secret1": "1b7023bb70248d8edab44658c5e2677dd7e5d7093ec062eb204975df4255fddc", + "secret2": "827844538be12d1cfa0f7fa096668cc4f2c4a25c2c8f7e92ca6cb05c3c445d17", + "nonce": "b5451a6d90ec575b4cdcedf4987429eeab1bbaa192ea3db89eafa058826885a6", + "kind": 1, + "scope_hex": "", + "prk": "3520160171dc39d75e64768d4fb667647480d458fc4d5c26d000a7cb3c8805b1", + "encryption_key": "de94e4663af538351a9b75b8af31e968ed8b88241ddbce43ad1d4ae2b984327d", + "mac_key": "70e65d5ff8769e92fbdf163b00b1b317bd4d30fe82de6b00d05cd74fb576febd", + "plaintext_hex": "efbbbf48656c6c6f20776f726c6421", + "ciphertext": "A7VFGm2Q7FdbTNzt9Jh0Ke6rG7qhkuo9uJ6voFiCaIWmMJrEDBNRRCorotVxmP7ge14Y+UtDn1/Pn3uzAaNNzHUAAAABAAAAAPJgoFXpn6mjFE0hUZrnZljeaYwSdqBKbVDXcyLgVGC8" + }, + { + "secret1": "f9869a8237c9fffd3bc175d21cc144051de4889da28b462ca1e4557adc2d2275", + "secret2": "c4c53829b9ad83682873761b71d667457935eaa84159a206dea58f18be09d05d", + "nonce": "f99a4a4a84a4906d839b62861dcd54883cccabb3616d003f27250ac00e672c50", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "prk": "7eeee2eac804eae839f64c4f2204ba6c205a65ae895bea006a45afd2ff9afee0", + "encryption_key": "56c727b1f69ff6ecb29c6cfd6469c1908da5556b0c13123b3303d5068edf03b6", + "mac_key": "f6be43893bffe64c43d56ee2692014d3e5275a78a3cd8268e2e1e0cb707a6bad", + "plaintext_hex": "6e6f7374722e6c616e64206e697034347633", + "ciphertext": "A/maSkqEpJBtg5tihh3NVIg8zKuzYW0APyclCsAOZyxQfHiK7t6u8D4JR3dRUKMpBRQzoOYtunePezG3p65AXPEAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzvgOo5isSBI06S531Yb9j9l+LpL9dA0D9/LLtorb866Y=" + }, + { + "secret1": "2f69dcb9891cf749ab0b4e07a718a9e364c44e7603d851c7c09e080b631534fb", + "secret2": "110ffc1f2ea8b15ffd5d24c59dc1b72c4b1f8180dd5ccb6a68097ff328f49e54", + "nonce": "ffffd9144f5fe48077ac672e1366d303dfebdf60b1abd07fce1ff762bb25a4aa", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "prk": "dd23c1dad51c025ed632be8b8da198517eef83a86729ccf524382f6011c9500b", + "encryption_key": "44f5de03559045aeb509d670299e7eab7f12682a7d5cc6c9a1441fd35f9484a9", + "mac_key": "ab08e3aa28d40c9376a56056dea33b3e935402da5585607b081ad166cecd8432", + "plaintext_hex": "f09f9088f09fa694", + "ciphertext": "A///2RRPX+SAd6xnLhNm0wPf699gsavQf84f92K7JaSqrm0b+bxgKBqNS04QURAmEXZlYBY9Ed4neDw2uOAqkGcAAAABAAAACeOBr+S4lueVjIEUNKR4ekMqHUoWb/ks495G0c1lD6oPQ3ZFsa4LHvRE" + }, + { + "secret1": "e945941c87478b88c8af150219ed8055692f3f01543a3dec3cb40854fdf8545b", + "secret2": "11eefd6b9a1a4d4e4b71840aa77eb47d3821d825ca8d4e45065ff563bdc342d9", + "nonce": "726cab7f363afe8c0783dc1d2d6e4700ace52a26996a53ba3928ef3c865cc235", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "prk": "7fafc5865086ba6ef1d48c93fa5e8c84dc0fd73924f23a4560d8d0f31f9ab2db", + "encryption_key": "2b6ed20127afba197082b52159120042d0bdfec6df2e657944f79a62ec90a1d0", + "mac_key": "2e72490f486fd6f0ed2ca5e508dff16b6db6936df59b77406717e6aed645d2a0", + "plaintext_hex": "e69a97e58fb7e58c96e381aee3819fe38281e381aee382a2e382afe382bbe382b9e588b6e5bea1e6a99fe883bde3818ce799bbe5a0b4e38197e381bee38197e3819fefbc81", + "ciphertext": "A3Jsq382Ov6MB4PcHS1uRwCs5SommWpTujko7zyGXMI1d8FRsRgcGnjOo+Ifry8x/QC+vDDkPCHv7WDaem7tQ10AAAABAAAABu+7v++/vm+/pDQcUXHli2Do1EEoqYFmF/67UUcl31Ks9TRy9vCwc2IUY6Ev9T+oBanqVWGbPgAWysjisi5dIPAEcndMK2Ur4m2UqTo3WVTIqKmy30ad5VOwl4v1AHweiZvJU/w+lQ==" + }, + { + "secret1": "98c7c39a4abf5f923db71a3e2c0951fa020bc5ba1555c158ebc8663e1582bb01", + "secret2": "b775d4f4ef14b1a93cc34a534a64a1ec2cd1a64a5a7b45f837af5ea4595b37dc", + "nonce": "ec64f769d99bc3c6f5231145b546334275d910e11fe9a11351ee487e4dbfd4ec", + "kind": 1, + "scope_hex": "ef8080", + "prk": "98fca3e635b9478407385a8989fb78ddb115d992a92019852953a4cd139aeb69", + "encryption_key": "9e6bdba691401f02de1403f75ebcb3516f5ff3b77c8a3918d8a3393e73eb3188", + "mac_key": "b05399edc9e6eae42cc21ec5941a73e8b7387bf10f49dfcfd740131309d81c35", + "plaintext_hex": "9b8de973ddf42a02103de24d9b7a4f0c4f551abaf7cd88f08e7a9c4d41ec5f777b45c890c112968fee50dccd3287583e9a3a33f962d78054f36dcb6f1ea9a8aa3fcb80953e04f6a2b3c3c4e26909ef7c5e84da6df3fd423215015640b249c91b28b38b18499b615bf1e92635e1df15aeeba2063692ce7cc8296582ceed25ceda", + "ciphertext": "A+xk92nZm8PG9SMRRbVGM0J12RDhH+mhE1HuSH5Nv9Tsg6J943ljpXnIaVIuHaXrWfa99RkqZOW6NGy6oqm2HocAAAABAAAAA++AgNCkiGZgN5Uzx1HVpcoLQQisIwWD32PqBoQ4T598/KmHsxUAGEARiXh9ikGXtwKuH8a8EzTcobkr4OEXfPs0h5u0A1HUJ3M/Hc/orcqZgeA0RhfZe3IASVmQfU9/pge+nTPjJVK5ZHOlEBnt7tmYcT8vqv9bpxbyhCBGMO6nEFhUtrr2IKCW3Z6vljg7T3FDr7aVIY/cxniq4E+e5ec9pZ+wn3j9PAibWgEANCDK5nyiH6B348lnqxfmu8bvzzyPhA==" + }, + { + "secret1": "b0e73a57d65972a4276879cb8604f683dfd9197cc236f299ea55acb66bfa8ff0", + "secret2": "ebf87d9858227055ac9f789911edad1b55777edc99dd4b8634f52bb8c0922edc", + "nonce": "c027624d50656a34add75cec7e476e6287bc919cacf0ebbda6d3277c02b0a239", + "kind": 1, + "scope_hex": "", + "prk": "a76ecc57266a24238761cf79c9909e27af6adfa523fb914a1a54e17d15e26287", + "encryption_key": "3c5be4141db8d4e4bfd998b6a4f995922070b9dc4af41c5d50c89c7ccd437f0e", + "mac_key": "92704765cd32cbe3beb21f347541184fc0cff839c8d2077d198d1f91103bdd22", + "plaintext_hex": "6120646563656e7472616c697a65642070726f746f636f6c20666f7220616e797468696e67", + "ciphertext": "A8AnYk1QZWo0rddc7H5HbmKHvJGcrPDrvabTJ3wCsKI5ZMf+aMW7P7Iz5qDghY+87TL5pZjNiykm0xpMKlkwITgAAAABAAAAAE2F94qgXOR+co8R41Vu04wLtkrI3Y5QJbVmutA5v1MkCgrLCmZAwNXhQsUnzUOuAPloXVQQdgQL4gmVgIz0rqQ=" + }, + { + "secret1": "0bac57d63af3e6650152577f7d5515062270b68cd2cda1250604ab70b7cdf091", + "secret2": "ddb09a891ef13bbf1b9ed8fb403afce4eea2197428da805dc85d90eee76e20f2", + "nonce": "0da18d3ebcc5f269f6415e3e3fcb5e1a8d76318fe439ec83cfdf99ef8eaacee9", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "prk": "d084d04cb7e61bb0c8fc7fbfaf48b58863fc01c1a4cebc7d48cad3b3853ac7a7", + "encryption_key": "be6eb2e4aa213dc2260abc2414e763057ab7df785e33338f1a3167ac280ee7fb", + "mac_key": "59f55d61316dee4f7f71c7b3d9704ef822d5d31bcfda30d02a267f29cb20d92e", + "plaintext_hex": "efbbbf48656c6c6f20776f726c6421", + "ciphertext": "Aw2hjT68xfJp9kFePj/LXhqNdjGP5Dnsg8/fme+Oqs7p+xyexXUdk8ZJ2rtLWT1xQ9lXxWSiagEVpRg35PndmKQAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzuKZ6xxWsljlgBA/i6yz7+dmE6dyszU9qkR7f2xDUQdg=" + }, + { + "secret1": "b69f38d981ad22b1fd25473756b2dd9c69d1554c6d31ae2a64c0fc82aafd86ac", + "secret2": "c916f18fd08a90c1d20bdfe27f31c53d33ebefdbe28e3da8797632b4b474b9df", + "nonce": "8b3c3f3aaf575328259ac5e3c08191dde308c573e3f4e7cda7042f82133143fb", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "prk": "93c97bd637c3fc60d9dbdb410df34c4a614c52db57ed0f2a218e8a973e125265", + "encryption_key": "e0a45a9306cc404aea91687e2b3c26abe23b0e12945799279e332c1880cacd78", + "mac_key": "b21748a15b40d53bbdaeb81c419c160994780d141324fcb1ac65bcd8be6bb6f4", + "plaintext_hex": "6e6f7374722e6c616e64206e697034347633", + "ciphertext": "A4s8PzqvV1MoJZrF48CBkd3jCMVz4/TnzacEL4ITMUP7b2QxXAKNEKp93ebvTrmrJ4aeJtLvqRokEeGXPBLE9UsAAAABAAAACeOBr+S4lueVjO04T51hx+sZw9n3gheEAyVOP0w/pWFvFtCuolpBkHvk" + }, + { + "secret1": "20d7e7e95a8e6376438182425c33c9445055fa4a8bd2c57e5c7902015433e18d", + "secret2": "d38139efe4118dd5862c2556600ef7914d1659cabbd1a3d5fd9f2a0abe9dcbb3", + "nonce": "20c635f2f795178ea0bbf9856dd99da02138ba79337d2511d887f2a065b917c9", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "prk": "4f9c75fe7c850a79f83000901ef8f020301c06e413a84de01784971ec249bb7b", + "encryption_key": "4345c818ddb2793427d8f5bb056e663cd941f910165601ff6806866cb7fb0fc3", + "mac_key": "4eb9ee0ea464574336446a8aae961f05b6a65cd5feb2087417eabf5344c554da", + "plaintext_hex": "f09f9088f09fa694", + "ciphertext": "AyDGNfL3lReOoLv5hW3ZnaAhOLp5M30lEdiH8qBluRfJTmsWPfIzALsx5OokjdKYAWkgDkES88FoC4k6wtgxUK8AAAABAAAABu+7v++/vmSE/qHW8+XDY97+8EQCRVPzORPYKrnLM6mNRp+zl2C6" + }, + { + "secret1": "1a2c6e81b5f1038fdda1f555d0431d1bd3efb22d57f608708fa46d7d7b96f1f5", + "secret2": "c18596eac499c94e04334021c1b6952757d83aeda2aa84f90ab47357cdd29fdb", + "nonce": "a05a11dcd50aa1e855b7e11a816158a1a4827d21a00b60105ed3c8e802770d77", + "kind": 1, + "scope_hex": "ef8080", + "prk": "c043b08590fcc2ef03e299633af842deffd1b5dbd2bf598606bf02abb898303d", + "encryption_key": "ba927e27a656a34369920ce7be028b6f6cb5878890123d1d3ba6b9f7ef4ab9c4", + "mac_key": "71163bde93ea8fa3b5574e81869416bcb8f6954a3b746e1b2ed24546949e208c", + "plaintext_hex": "e69a97e58fb7e58c96e381aee3819fe38281e381aee382a2e382afe382bbe382b9e588b6e5bea1e6a99fe883bde3818ce799bbe5a0b4e38197e381bee38197e3819fefbc81", + "ciphertext": "A6BaEdzVCqHoVbfhGoFhWKGkgn0hoAtgEF7TyOgCdw13O273WC9FSDyMtfOYNFvOlZQcaSrLdo6WBQ7ZI2UWn5MAAAABAAAAA++AgPPJWHFZya+M6arLz4wrWMHfL4Wyv4gYZBkicAvVBX0dMsr5tBcTP5xaM4lJZZnokEvMZRzYbjrfNTjT2gCWBapNdr/QrHxlTDa54nRmVR/2GBLkmQ5QeIiDm6OhfjXyYA==" + } + ], + "long_encrypt_decrypt": [ + { + "secret1": "e35f016acdf0bec26f9f0e97fd813aa042727cb1e5ac2adf1c7b8d18d393f455", + "secret2": "e3c47278057365a1007414224f54ee99e6198ac6b6a82917e635375a1f9afa8e", + "nonce": "0598a9aa024df86e1e532e8cd3ed412e5b8bc914ff0340aa8868f9fd2fe2871f", + "kind": 1, + "scope_hex": "ef8080", + "pattern_hex": "9b8de973ddf42a02103de24d9b7a4f0c4f551abaf7cd88f08e7a9c4d41ec5f777b45c890c112968fee50dccd3287583e9a3a33f962d78054f36dcb6f1ea9a8aa3fcb80953e04f6a2b3c3c4e26909ef7c5e84da6df3fd423215015640b249c91b28b38b18499b615bf1e92635e1df15aeeba2063692ce7cc8296582ceed25ceda", + "repeat": 511, + "ciphertext_sha256": "cf2c183f974c2601c0ec5d4fad0f0f98f18b0c83bc4e988c53ec1e496528deb1" + }, + { + "secret1": "69efe21ce6ffe00d4126a019542e61324ff59fef06c81798cf1ec9810bfa5566", + "secret2": "91749344a212c2168587ad46197ef2eb026e3fa6839cb4286599c4f24820431c", + "nonce": "8c9f02398c9c5c11260b9ec27292bd32f0127c3e5366b255e0878ecb82e81eeb", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "pattern_hex": "9b8de973ddf42a02103de24d9b7a4f0c4f551abaf7cd88f08e7a9c4d41ec5f777b45c890c112968fee50dccd3287583e9a3a33f962d78054f36dcb6f1ea9a8aa3fcb80953e04f6a2b3c3c4e26909ef7c5e84da6df3fd423215015640b249c91b28b38b18499b615bf1e92635e1df15aeeba2063692ce7cc8296582ceed25ceda", + "repeat": 1023, + "ciphertext_sha256": "8538f11d334dee64c561961ab7b371a90cb5ded3d1bf6c544d93aa4c72e5b1c0" + }, + { + "secret1": "9ed97778c4bcaf3b5c66c41d3b97ec62e89e2bb9ead5d27a980a1c268a24a2c9", + "secret2": "58cba97d2985b001a7367a088a2e868a85158bd218f2a5858eb23a43de4ea382", + "nonce": "d8212d54ca0a36a7a5ed9f33656aebcd995f64dc6c4551a54b0dad5b897e254c", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "pattern_hex": "9b8de973ddf42a02103de24d9b7a4f0c4f551abaf7cd88f08e7a9c4d41ec5f777b45c890c112968fee50dccd3287583e9a3a33f962d78054f36dcb6f1ea9a8aa3fcb80953e04f6a2b3c3c4e26909ef7c5e84da6df3fd423215015640b249c91b28b38b18499b615bf1e92635e1df15aeeba2063692ce7cc8296582ceed25ceda", + "repeat": 2047, + "ciphertext_sha256": "66c5b453cc7123d9826115d437902db5226dd25f41aea9519f986938709c2901" + }, + { + "secret1": "77930acaf9d28607482f0d329d65eea04fd218a957f25004d6606414dcdea848", + "secret2": "c31571de3b9b8053d5477a8dd090d7ed7ffbed98f8e9c904b8034ba77f74e232", + "nonce": "b40a2f2ae51c8355ed8bce7f810628c5fd3a4c5d4fe9170c159b9c7e9d1d5f87", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "pattern_hex": "f09fa694", + "repeat": 16383, + "ciphertext_sha256": "ccd2942a398aaab22845d6d65599a82fa9ee5fc1caecb5a8cc358771b1b7ba7b" + }, + { + "secret1": "dc0ded78a0d133195b49429aadc6d424fde9b98a0e9ee12b4382bc0f08125a1c", + "secret2": "fac11ede5498f415f3a48cdf052a4e0d2f77fc4012baaf77de70a3f2cb4bc195", + "nonce": "7f2506e82ad6d97fa2cbbc2cf9f3a02bb61ce65bbe72a891c07c7bde23ade06b", + "kind": 1, + "scope_hex": "", + "pattern_hex": "f09fa694", + "repeat": 32767, + "ciphertext_sha256": "d84d643bda0d6f11dfc165eff69ab7c12c31f7f651603fe9e3397fb2ebb44a24" + }, + { + "secret1": "b585991901f19ca1353b4122a591c3ade793338174f70326ee351d54b2b4c9be", + "secret2": "cdbc4210a142109928087965a6e47922ed65763165cebdb54498770da448d5b0", + "nonce": "aef913a704ce90355a134dd5f4ea253115d9d426269f371f45a33de3c79a90d7", + "kind": 1, + "scope_hex": "ef8080", + "pattern_hex": "f09fa694", + "repeat": 65535, + "ciphertext_sha256": "75d4386e1e5bf39c2775486c066274fbceb9e0aff2880b513ec1593ce8a68f74" + }, + { + "secret1": "57420cd8c43b789a506e7dd1eb433b010e5323eb219de7c6a6a6f6976aa80693", + "secret2": "370091dce8dce4c3cf6b1a67ec8f41f9ae78a69ea902c67bdfe8930d90b2b7d6", + "nonce": "15ecf921c0e227f5199523de99193626087d506d998b8abd3c086e66fb25af0e", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "pattern_hex": "e38193e38293e381abe381a1e381afe4b896e7958c", + "repeat": 3120, + "ciphertext_sha256": "0577e05453f458e700740be3d849096f3d9d46924333ce3aa27c342a786a6cc4" + }, + { + "secret1": "aeda4666950455c2e038d6b7d9e000be92be6aaecbb57b7f0f980ba29da52453", + "secret2": "9d819437f4eb60290bfb9aa547d426516a3ea07a2149e46f3310acc85dd45a18", + "nonce": "56146d9c3caf5c118288754d2caabd142eb45e1f2d80f3caf5183888ca2ee416", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "pattern_hex": "e38193e38293e381abe381a1e381afe4b896e7958c", + "repeat": 6241, + "ciphertext_sha256": "6a0c112e126aa897f80ac73227fbb062ac7ded96f641a05fa4634a9f4a8c702b" + }, + { + "secret1": "f153b2eadf9aa51bedf90ac8804dbe2bc4fed1ddd3861a857ae80a20d5c55f27", + "secret2": "22c42e7079ae57313d99c2f025c1be2f5dd999a3898d9aecbed7a207a1018510", + "nonce": "a7db4437442ca0ffbd836822c622115bc001de197c9f1a1d3d67a1e63c044d30", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "pattern_hex": "e38193e38293e381abe381a1e381afe4b896e7958c", + "repeat": 12482, + "ciphertext_sha256": "c066419c9ee88e7c8a77dbe25fac1dd8e9720fa4f481924058189062b12e3fe1" + }, + { + "secret1": "217d14ae21140584417aa9bbcab4bdf4adccb5e74b191d65d2b357794f7f7143", + "secret2": "cccdef797a083bea633fcff31f255b57b5d5c99b682eda8ff132066dc3cd9127", + "nonce": "143cb408e61cf7b4281b0ccf300284a68d7df282f71667df5740b5f282424880", + "kind": 1, + "scope_hex": "", + "pattern_hex": "21", + "repeat": 65532, + "ciphertext_sha256": "e5360cffeef8c31c88a3455b920d9d9a98c8669b77d38ac232e0f67420467e5d" + }, + { + "secret1": "625bbf8de97b71e8d70092bb6c576ee95895b7e6d2acf924790c80dd69785e8f", + "secret2": "13e7e6925ef02a644ea1c7bf8b68ff7de8c676ecf7f22d49a78a1802a6933189", + "nonce": "1bbbfd8803eac5c844e96ade2fdacf18fe3bda62312bdda102a29b52dd0c97bf", + "kind": 1, + "scope_hex": "ef8080", + "pattern_hex": "21", + "repeat": 131068, + "ciphertext_sha256": "8dd9a3157906d3a4e3de2b4d552cddae489a0996812bacca7728996a2605cae9" + }, + { + "secret1": "266503c3818aa70800280c53be6f7f8156273c6c606cfbb0803d8eb63dca65f9", + "secret2": "9846126fad398ea9b6fe7ebddd7e28c021691eaff5355847cca42a2caacacb68", + "nonce": "9815cf89d4ab86023b1a427166fd8db49681a077d5724d1b743a57cd6fb96b81", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "pattern_hex": "21", + "repeat": 262140, + "ciphertext_sha256": "0bd33e75b6bb9be42a1fce897801d064943945a930977551437031a413e07443" + }, + { + "secret1": "39506d419c1dede09cba910247457d66be7b213adbd981e3af0ef69b46c790f5", + "secret2": "5294e3ad19298a304753a32a42be2d44f3438f553677f30aab591f8a19ba4fd4", + "nonce": "8ccdf24876123afdbce42e59d4e03c53589150b4f23b087d3e3bfe89d96a54a6", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "pattern_hex": "00", + "repeat": 65532, + "ciphertext_sha256": "c4c030de28d8b5fa9aafb1961aab296f078aced2988897c7907fa67cae073de1" + }, + { + "secret1": "86bbd84e3e9e2db2e7ec37fb16900723aac499f1074b66586b44a3ed63022a3e", + "secret2": "17d3ff292c4a65e6181ecc201b9c5ed0c7e8408f36183659181414ddbb17c3d5", + "nonce": "f7ce68c00fb546c23efb88f778819c131797705abfc222886406b8547a9c244c", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "pattern_hex": "00", + "repeat": 131068, + "ciphertext_sha256": "3992c52ad7811ba181f572d37a0e1c9a4dbc1d9b127caa171896eb861d1e818c" + }, + { + "secret1": "d38c5d7c0ba3b3e103318eb12952a481a204ab8dfd1d440b8882e9f0649ac0ee", + "secret2": "484475e3653226fd962fb9ca9ea8c8d4929473308eb53fd73a8ccd95e5eb8b98", + "nonce": "47fd0420348c7c4dead0c52874f2efe9ba9ecc9c4f9ee82319e1f33338d3ab86", + "kind": 1, + "scope_hex": "", + "pattern_hex": "00", + "repeat": 262140, + "ciphertext_sha256": "e03e2f16a5ee7e874d3cdc52e36ae8e73791d664540cdb7ac36f65687c7fc2a5" + }, + { + "secret1": "fd20231a23032bde7acc638eb7086784670dcf8bc90e57a65c1d03d68594d3f0", + "secret2": "b47f3ba965fbea51c927b6846543480aeb25275c0e01c5c4d286ea087e70ee92", + "nonce": "6c9820a69d021d86a695eb3d5cc11ec638a799aca10ecfb9819b1efa3b9731ff", + "kind": 1, + "scope_hex": "ef8080", + "pattern_hex": "ff", + "repeat": 65532, + "ciphertext_sha256": "6621fc89a79464d19cbef605d962c9f43af90568c8c352007829445d1886c9bf" + }, + { + "secret1": "38c961d0c8289789cec189e8db140f740abf34c8251eb16ad93cc5f9021abeb3", + "secret2": "e985cda5e08979c17a2c148532434c6f830bc2243e7a5592704972328f24d62d", + "nonce": "3957e9cc3be20433aff61993558572099e186312e714050cd3a589ff675f0e07", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "pattern_hex": "ff", + "repeat": 131068, + "ciphertext_sha256": "e41d7c737462f18b653683796d739c3b93f284c9bc15182bd94d278d13b84dea" + }, + { + "secret1": "0666935a88d5196746c799ce0593dede7a8e8044930e62e07793950b9dab9b4d", + "secret2": "e3505f4e3b8fd37da2a9b4f3cb67864819e6272cdcdcf198f422598fa1114d21", + "nonce": "56fef50b564913b040a9ee83c9c1eb36ac6553a9e5ad699ac036fb4338a38e35", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "pattern_hex": "ff", + "repeat": 262140, + "ciphertext_sha256": "086aaa494e3a4ca5bc573d91fbed292d9abe05fb730c54660bf5813250744aab" + } + ], + "padded_length": [[0,32],[1,32],[32,32],[33,64],[34,64],[64,64],[65,96],[66,96],[96,96],[97,128],[98,128],[128,128],[129,192],[130,192],[192,192],[193,256],[194,256],[256,256],[257,384],[258,384],[384,384],[385,512],[386,512],[512,512],[513,768],[514,768],[768,768],[769,1024],[770,1024],[1024,1024],[1025,1536],[1026,1536],[1536,1536],[1537,2048],[1538,2048],[2048,2048],[2049,3072],[2050,3072],[3072,3072],[3073,4096],[3074,4096],[4096,4096],[4097,6144],[4098,6144],[6144,6144],[6145,8192],[6146,8192],[8192,8192],[8193,12288],[8194,12288],[12288,12288],[12289,16384],[12290,16384],[16384,16384],[16385,20480],[16386,20480],[20480,20480],[20481,24576],[20482,24576],[24576,24576],[24577,28672],[24578,28672],[28672,28672],[28673,32768],[28674,32768],[32768,32768],[32769,40960],[32770,40960],[40960,40960],[40961,49152],[40962,49152],[49152,49152],[49153,57344],[49154,57344],[57344,57344],[57345,65536],[57346,65536],[65536,65536],[65537,81920],[65538,81920],[81920,81920],[81921,98304],[81922,98304],[98304,98304],[98305,114688],[98306,114688],[114688,114688],[114689,131072],[114690,131072],[131072,131072],[131073,163840],[131074,163840],[163840,163840],[163841,196608],[163842,196608],[196608,196608],[196609,229376],[196610,229376],[229376,229376],[229377,262144],[229378,262144],[262144,262144],[262145,327680],[262146,327680],[327680,327680],[327681,393216],[327682,393216],[393216,393216],[393217,458752],[393218,458752],[458752,458752],[458753,524288],[458754,524288],[524288,524288],[524289,655360],[524290,655360],[655360,655360],[655361,786432],[655362,786432],[786432,786432],[786433,917504],[786434,917504],[917504,917504],[917505,1048576],[917506,1048576],[1048576,1048576],[1048577,1310720],[1048578,1310720],[1310720,1310720],[1310721,1572864],[1310722,1572864],[1572864,1572864],[1572865,1835008],[1572866,1835008],[1835008,1835008],[1835009,2097152],[1835010,2097152],[2097152,2097152],[2097153,2621440],[2097154,2621440],[2621440,2621440],[2621441,3145728],[2621442,3145728],[3145728,3145728],[3145729,3670016],[3145730,3670016],[3670016,3670016],[3670017,4194304],[3670018,4194304],[4194304,4194304],[4194305,5242880],[4194306,5242880],[5242880,5242880],[5242881,6291456],[5242882,6291456],[6291456,6291456],[6291457,7340032],[6291458,7340032],[7340032,7340032],[7340033,8388608],[7340034,8388608],[8388608,8388608],[8388609,10485760],[8388610,10485760],[10485760,10485760],[10485761,12582912],[10485762,12582912],[12582912,12582912],[12582913,14680064],[12582914,14680064],[14680064,14680064],[14680065,16777216],[14680066,16777216],[16777216,16777216],[16777217,20971520],[16777218,20971520]], + "invalid_decryption": [ + { + "secret": "b2a4cca9347992d235fe115382098e313f6eaa3680248443b90c64e4e2ab039e", + "public": "dc62907f84a35acecfc55b6d82961399f019981be0cd7d5e6a5a0620f9158870", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "Awx1nilOH4b0PT+ZszAS4TqOfADUQxWfAHAUVyJmy7c8EvrgFmKouWAVFZyjYN2XuuGSWHlKeuo9bF9t7MwMGfwAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzVeOxtyTClFO2/OPL6lpuSi3WFTdQgbhX6g/f1Iv2K6o=", + "why": "invalid MAC" + }, + { + "secret": "83ed5a7ae0494831e938a0a8226472954be9daffb4bf5d7641473b35e959cf90", + "public": "90ecf0dd8a793c74809735cc37cc3b9de20ffc9aae0eae7a1a0c740ecf09e395", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "A90ZzLN2HaQRTrzzLoobOtW+c9GyPxVp64fhIygpEpLaYYbCN6Pq1rjptBbN5S2vCFPCsE3wmU5u3Wx6L8oZxJoAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYz8Sxek35q19YWqmhyQNRHVZ+sNTtdgXO3MCnvjw0nanb233a39sc969Lm5DaUPN+yTKV0NbtYYN5hIWDOMOXj63XtzT7S7i/LAhv/l8y1zLTc0aUUoIjEg0EHi/FvlakK", + "why": "invalid MAC" + }, + { + "secret": "efd2ac18f500ac0fa1b9639149432ff2d309d1b49c7f683c9ca4613d14449dce", + "public": "84194beab56b44c426b866261772bc0a447ea34f94f2317ce1350cb714021a25", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "A5vkMZfJPQuWeOQsEiuZX1M4VJLY/k2G1mL8EKHK/yq7gifeC4V4zQ4L1iiQ1oVgmTmhwb/vd21Fm3YZrpYGEvYAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzQlapufh4trECMJjdb8m/TNFFcFJJSmiM/XQpnKXM/wc=", + "why": "invalid padding" + }, + { + "secret": "2926c352495ce986639ccbb263ad2221df731bae6ee8ec329cbb5d00c5b9ca87", + "public": "3c4b835fc7de0dd3a02971b559ccd5d3bcd2eb3cce7c1023b93892918effb71d", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "AzW5Wy/bvTYdcVOLIL8W26mfgTFG19S0H2BC9kyqgqK+TZ8oI00WJTNqwJIg8JE7DqDnOY+Q40Yd3G8Hi4GobQ8AAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYz3cI/rKzcQ22idVIBuFgLEVJaK1W5uxh6M6AdMoKKtdxO+lESYYWCEh/zzj35MFMwSJTn8z+XFvv9f2jYgQXHvPmY13wLOpIrpoS2W8luUbOVf9fip5mZXHw3neYvc7jA", + "why": "invalid padding" + }, + { + "secret": "47c04c4c6d385ddaaef691bd58dab94f81f4ba9eb1ab832339c3d0042f998dc5", + "public": "cb6a3aa6d94a58c9f03354a2a8723c3449e06b19a0ccfe19353195b28ddf7694", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "ciphertext": "#A2A/tbfDDqn4qx267aPFZDwyH78j9zZV8g8ekZKonH8bDR7vYhp7zzh3oJAlJWem/Z5OVrRvUAJQrx8q289PqsEAAAABAAAACeOBr+S4lueVjASdOe8pTxevoZoYq1Y8rRarB6+yzRlquT4RZlmHH3jLEQmAbBjQGrOXi1uWPbaKC8j/VpjW5S9BAtyMSMpUcHg=", + "why": "unsupported future version" + }, + { + "secret": "751321afdaaf76aeb87851ca8f35eab87d0f50e1ec595c67e7e37ced7d6b7b9a", + "public": "a340bd34205bb0f3dc2f9aa3fcb70fc52c326cc4566c457bb9cfb9ae17c239af", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "ciphertext": "AP9SHg4CFoD4fy22vSMNZV+efP7Ld7GCOpIKeZANqL+Kspe380sGxRQGKyy/liCuMi8DbcfQJypivkS+Y/bz3sIAAAABAAAACeOBr+S4lueVjMM0jKIQIfjKuEBBmjWPFmQsB20qe5pcJiLpnnXmp12z", + "why": "unsupported version 0" + }, + { + "secret": "c4ca1bf68b1f768bbc3670d554036b5c892303319ed6f7228e8dc2f6c99dd0b3", + "public": "537986db4ffe564eb7d565643e78dbc24e86650957e6e7c2c8d0124fd04fd68f", + "kind": 1, + "scope_hex": "", + "ciphertext": "Ap2Mv1HTQrVArE2UVevKq7rQ+a0FMw8OBuiAMnA81jJit7c0QzkEMr/o+5++t0/FbXFABfQaTRpF+dBuISyw3rEAAAABAAAAAC5e+Y9lfvgD1trmXL2Jv5H3Khi8ayWJJQMrVOEMd9tlJNj1b7k/ZzIG71f2GBzzoeBImA2fk+q6Iix4v5jUulo=", + "why": "unsupported version 2" + }, + { + "secret": "9460c5781801b35f01c1093434266cab5ba997014c52d9a16c3772f8580ac61b", + "public": "375cf44101e7d77699da6b8d7e633f507eaa82720d4d3cea1c0db85447975fa3", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "BAahtRKP0WL8luCz9m6TydiQtWUfoIWvkRlg2tatPVOCAwYrO8Dw3DZMgeTGjaehohfAmVyZ6SneuTQF3Ho+EUIAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzpHJCeyhgMaqIsPgWO635BaIwmRU4cfe9aA6gGqGI7SY=", + "why": "unsupported version 4" + }, + { + "secret": "b46bd96a998b00b55e48ad76c1fa0c68bc64b94174a189983b689fc05a0cec52", + "public": "797b06ba5dd8ca23a8c8cae4e9ee8963b25d9bf92618fa0b62db1ddc7b611453", + "kind": 1, + "scope_hex": "ef8080", + "ciphertext": "", + "why": "empty payload" + }, + { + "secret": "42602869f3d35e0bb04f72e3053d8de9cccf1e78dd6926c9311134784a5e70d6", + "public": "94a4ac3a4bf998dc64ab07cbb573ecb045e028f9b19c88f02af8f73d334ba0bd", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "ciphertext": "Aw==", + "why": "payload only has 1 byte" + }, + { + "secret": "d2dccc683567cd0cc6d9ec907703ddb362ecb61ef8e6c8ebb7411a293230935a", + "public": "294da03d650400e766953c9fb78600b788598a35e47b9755a891802de76f7a09", + "kind": 1, + "scope_hex": "ef8080", + "ciphertext": "A6oBqSSHPckKYt8Doymo2s1ku7LJxSNSfSuQdBoXaRe8q+5FQmvwbejIzJEaVKdUhbykNt5VFxno+sZtreNhrHgAAAABAAAVCO+AgAnTQMJrGJX7muna+wwIyM82vR398H+fhL6XxOam03jQErUC9W+klrNflk6oJ4mmyO88x6FJcf6n6LfDpGjMogNuMxoR1crWfknPMJuHggEUfOU6AjN7CgGiFPdnfcgbUPP487vxX9iw7U8WhQCfnh46vQTdwCDIm0C3aUp2/fJ0xNTVIZkFVKV1CyvyFpVicY9Zc7fmeEIDAsJvM1beK9sWqp9CI/sMU9OXmTfjdugSuisDRevchLkr6h5kB/rXDw==", + "why": "scope length out-of-bounds" + }, + { + "secret": "e1ef663aeb335452c9670d8a9f1f75cae5077f216314e9b49761610b4eb98538", + "public": "b729446e5cdbd2e4ac7967b056c17d5e4735035f2d2cf829722a0a85ca1fb6cb", + "kind": 1, + "scope_hex": "efbbbfefbfbe", + "ciphertext": "A/uzxqp0UwE6j7p8PIRKJDa0ah39GGyMbM0fOivlqESqbfFnp5OD2FSHR9TOTeJwiAfLXcXkoZPwiNKjB4ZgXV4AAAABAAAABu+7v++/vm7l8A==", + "why": "ciphertext too short" + }, + { + "secret": "327ffef01143a4dae30e201add671183424b39b1f33f64d78c19c22d3a7e790c", + "public": "039f6ce0144af03f0f3caf8a070a6a36519e2942286c2295a23785b0668ea621", + "kind": 30078, + "scope_hex": "737065632e6e6f7374722e6c616e642f6e697034347633", + "ciphertext": "A5jnLthci6SRC9V9Ak/AKGyB7xAGPLGZx+fW9wjfvOKQuug5cUUGX4R0mmxFHl5/TtcQ4syIiTtgXL3uVveIP3MAAHV+AAAAF3NwZWMubm9zdHIubGFuZC9uaXA0NHYzS3rEqFHqpL5Yqh/xY+a9i0XAyY960LDSfzQjSZh4UHzbuxmMEk92jAczsFkI9cWqd+xzahX59yD9l+UnCw3o9yXmzZIaA6UYPFI20f2VnH+G6F0Zt917fgt0bJwR3QUwblT63eOKLJGYhXqC11dweuKORW6oGJagRFo7P8r3UIFTHPkL5xMhUtZS7TS7GDKTB7kmEp5trwfzboiWxzp12LSlkUU9Nctyf6KE4iEQbk2jDndbC9npCn1rFpsFiHsd!", + "why": "invalid base64 (trailing)" + }, + { + "secret": "53d492157f17e6804bacdef3942fbb220a7239aab0e6bd6355eb3ff989ea0076", + "public": "ad2e1f00a4d5985810072cea336b0670f8e924368bca5b15c84ba8f3d7cae478", + "kind": 1, + "scope_hex": "e381afe4b896e7958c", + "ciphertext": "A3oG29gbEA%8QXjVLA5JeYOl1Hj5bJVaNcl2tAnfEHm5pzS0V+3eF8Tns8+A+TkfxrSc3DuAbkxc9SgWC+214cBIAAAABAAAACeOBr+S4lueVjGfB4CLk22vLao5NE6OH5KlgzSy++iyD7FZEmAkCVOfQkrnbj9kzyLF7HRygI5E2FJdeQkX6WDiHtwzP/UWwd+cnMaXTYS7vL0Zh6Lvz/PKicCecxB0NvkAdYM3hOpodhXEYd2nano+37mU1Cahp2uwyygJQTb427cHBucQiVpIadVoKqMeIA7EGvO9HTgTxgE93vyT26NqZFO9aniV1bFc7y9nq1OYHFfNQgzdxVMTQ88SwMbq2TSpU5uJc/cphNA==", + "why": "invalid base64 (middle)" + }, + { + "secret": "d4db7f6dcf6a45843739a806876a9da849f75f7a36702e7b4f7a10a986bf76cd", + "public": "0377961328c3bb0db459cd22033d7a7ca1e29d7f42fd3f1b038530a2402879c8", + "kind": 1, + "scope_hex": "", + "ciphertext": "Ay1pBSibePLV49S4vfkgB4GCHR0Xywd7acm1WoC1ZaX6Jg38sM0PQAshZtniNKpUjQWZGw4e7kSKEgFIGhT6SxQAAAADAAAAABuabdmj5F6vlFssb3CHu/ndTMpdcPSWXklapkGwxJRS", + "why": "context mismatch (kind)" + }, + { + "secret": "b00e9f068deb4b69c474109502839c981bd429075eeab9e5f41db9022c0cd869", + "public": "6de2d6a91eb75f5e6038d633da8695f35fad807d5bad6b9e25ae8f526e10c05f", + "kind": 1, + "scope_hex": "68656c6c6f20776f726c6421", + "ciphertext": "A49dkIXX4dVAn6A9ql4cQ3MQfoU7rPrsg9/8V8d5NL9+A7Ntb9VIbBQ4V2ORFXS5rzOkHhZkKmtnn3dpMUiQWwsAACcQAAAADGhlbGxvIHdvcmxkIRb3rRdfIPuJvtEnjn6dj7RgKg1OUvGmCQoXmp+32EN6XL+vEHnbIbvLWLqIV7eCAmOqrPVGngCJEppHzzFMuhM=", + "why": "context mismatch (kind)" + }, + { + "secret": "e150f3f5fd2eb47c49e8c1ee0ca51d3502d370108a98519d212731746fc513c2", + "public": "901f54e63d0d1df5bf120e12faf00d6e4ec3c6d9fe81e0dc0f35cf1d5a489ac2", + "kind": 30078, + "scope_hex": "6170706c69636174696f6e2d64617461", + "ciphertext": "A/JaJhlnuKYJhr6LdZ46lkK7cw0nxGvA4rp112e70q0O9jMnXxaWqqNE7nZnllwl7yuGWRtcLU2q6uWAUZ6wthIAAHV+AAAAD2luY29ycmVjdC1zY29wZZ++z1BEvaKntWN9G+9EIT0da0luVAV/hAvOoUg90pXE4C6nsAKmpZ3Q2YfAvtUDlkMpxPv4u10Rlz0VGlPo9GtcGJp4X2aq/5gdbTZJF8mxnkhZ4zQqwRibV0JJ0ZNZxg==", + "why": "context mismatch (scope)" + }, + { + "secret": "1c9c9ff5df8a9ff99e50b3dac27d987422567e9122075097edea2915e12580ca", + "public": "32178882654a70441f1c507902ae3a89888ecd2266930a45f8d6b2f578643307", + "kind": 4, + "scope_hex": "efbbbf", + "ciphertext": "A3c7VODzMr9mQXQNRtw3MQsntesGokh3g8nQwdAK4iWqL/nV6HPOhnz5xFf1UjyvTrHJC8BrGjRefguMrJLrfy8AAAAEAAAAD2luY29ycmVjdC1zY29wZfvq9zBo8LQrHyulrPg0swuPZs4W3Xy1TSQlmczZ643aOllm+9DveIcqoXoQaGQTs1RDLeekOMNquv9a5XMWZ431ysJ44L3ET4Ztw5Eevnxx3pgaob8bCikyuGT+5CyR2A==", + "why": "context mismatch (scope)" + }, + { + "secret": "18a8c52a7d94c36bf08ec04336247ab2c67014b964a3e33f3d6677e697f2008c", + "public": "d1069a731110484f212fa9fe2fa4dcd2e2c0857b5e0dda1bd5667901ce303d81", + "kind": 4, + "scope_hex": "ff", + "ciphertext": "A3PSjybYp19qos4LlMKEdlPQShJIBOJBGjjOI6etumpU4VaN0LYMB8qECxbwF7ebuv2Lxu5h0yXtNpdJtX4Z5fgAAAAEAAAAAf+AeTttkffcDMHUCDRxKYA3p7nnmK2hNvB04X0VP2H27Q==", + "why": "invalid scope (not valid utf8)" + } + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 329e36fa..23ce4f5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ mockk = "1.14.9" coroutinesTest = "1.10.2" kmpTor = "2.6.0" kmpTorResource = "409.5.0" +secp256k1Jni = "0.23.0" [libraries] datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } @@ -81,6 +82,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" } kmptor-runtime = { module = "io.matthewnelson.kmp-tor:runtime", version.ref = "kmpTor" } kmptor-resource-exec = { module = "io.matthewnelson.kmp-tor:resource-exec-tor", version.ref = "kmpTorResource" } +secp256k1-jni-jvm = { module = "fr.acinq.secp256k1:secp256k1-kmp-jni-jvm", version.ref = "secp256k1Jni" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" }