Merge pull request #448 from greenart7c3/claude/implement-nip44-v3-0G3I9

Add NIP-44 v3 encryption/decryption support
This commit is contained in:
greenart7c3
2026-06-01 10:25:49 -03:00
committed by GitHub
23 changed files with 1626 additions and 26 deletions
+2
View File
@@ -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)
+1 -1
View File
@@ -58,7 +58,7 @@
<provider
android:name=".SignerProvider"
android:authorities="${applicationId}.PING;${applicationId}.SIGN_EVENT;${applicationId}.NIP04_ENCRYPT;${applicationId}.NIP04_DECRYPT;${applicationId}.NIP44_ENCRYPT;${applicationId}.NIP44_DECRYPT;${applicationId}.GET_PUBLIC_KEY;${applicationId}.DECRYPT_ZAP_EVENT"
android:authorities="${applicationId}.PING;${applicationId}.SIGN_EVENT;${applicationId}.NIP04_ENCRYPT;${applicationId}.NIP04_DECRYPT;${applicationId}.NIP44_ENCRYPT;${applicationId}.NIP44_DECRYPT;${applicationId}.NIP44_V3_ENCRYPT;${applicationId}.NIP44_V3_DECRYPT;${applicationId}.GET_PUBLIC_KEY;${applicationId}.DECRYPT_ZAP_EVENT"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedContentProvider" />
@@ -26,6 +26,16 @@ import kotlinx.coroutines.runBlocking
class SignerProvider : ContentProvider() {
private val scope get() = Amber.instance.applicationIOScope
private fun rejectedCursor(): Cursor = MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) }
// Decodes the Base64 v3 wire value to readable plaintext for history.
@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
private fun nip44v3Plaintext(wireValue: String): String = try {
kotlin.io.encoding.Base64.decode(wireValue).toString(Charsets.UTF_8)
} catch (_: Exception) {
wireValue
}
override fun delete(
uri: Uri,
selection: String?,
@@ -224,6 +234,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 +252,32 @@ 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].
// A missing/invalid kind is a malformed request: auto-reject
// instead of prompting the user.
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 rejectedCursor()
}
parsedKind to scopeStr
} else {
null to ""
}
// For ENCRYPT: classify plaintext input; for DECRYPT: perform operation first then classify result
val result =
@@ -258,6 +291,8 @@ class SignerProvider : ContentProvider() {
type,
account,
pubkey,
v3Kind,
v3Scope,
) ?: "Could not decrypt the message"
}
} catch (e: Exception) {
@@ -272,22 +307,32 @@ class SignerProvider : ContentProvider() {
),
)
}
// A V3 decrypt that throws cannot succeed (wrong
// version/context, bad MAC, corrupt padding, ...):
// reject it rather than prompting the user.
if (isV3) return rejectedCursor()
"Could not decrypt the message"
}
}
// 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) {
// V3 grants are kind-scoped; fall back to the explicit
// "all kinds" (kind IS NULL) grant only, never to any
// other kind — otherwise e.g. a kind-A reject would
// leak to a kind-B request.
permDao.getPermission(packageName, type.toString(), v3Kind!!)
?: permDao.getPermissionAllKinds(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 +361,7 @@ class SignerProvider : ContentProvider() {
0,
packageName,
uriString.replace("content://$appId.", ""),
null,
v3Kind,
TimeUtils.now(),
false,
content = content,
@@ -343,6 +388,8 @@ class SignerProvider : ContentProvider() {
type,
account,
pubkey,
v3Kind,
v3Scope,
) ?: "Could not decrypt the message"
}
} catch (e: Exception) {
@@ -360,6 +407,13 @@ class SignerProvider : ContentProvider() {
"Could not decrypt the message"
}
// For v3 the wire value is Base64; this auto-accept path has no
// EncryptedDataKind, so decode it once for the readable log.
val historyContent = if (isV3) {
nip44v3Plaintext(if (!isEncrypt) finalResult else content)
} else {
if (!isEncrypt) finalResult else content
}
scope.launch {
historyDatabase.dao().addHistory(
listOf(
@@ -367,10 +421,10 @@ class SignerProvider : ContentProvider() {
0,
packageName,
uriString.replace("content://$appId.", ""),
null,
v3Kind,
TimeUtils.now(),
true,
content = if (!isEncrypt) finalResult else content,
content = historyContent,
),
),
account.npub,
@@ -78,6 +78,18 @@ interface ApplicationDao {
type: String,
): ApplicationPermissionsEntity?
/**
* Match an "all kinds" grant — the row whose `kind` column is `NULL`.
* Distinct from [getPermission] (no kind), which matches any row of the
* given type regardless of kind: V3's kind-scoped permission model needs
* the explicit `IS NULL` to avoid kind-A rejects leaking to kind-B requests.
*/
@Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind IS NULL AND relay = '' LIMIT 1")
fun getPermissionAllKinds(
key: String,
type: String,
): ApplicationPermissionsEntity?
@Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = :relay LIMIT 1")
fun getPermissionForRelay(
key: String,
@@ -21,7 +21,7 @@ import androidx.paging.PagingSource
class CachingApplicationDao(
private val delegate: ApplicationDao,
) : ApplicationDao {
private enum class Method { SIGN_POLICY, PERM, PERM_KIND, PERM_RELAY, PERM_WILDCARD }
private enum class Method { SIGN_POLICY, PERM, PERM_KIND, PERM_ALL_KINDS, PERM_RELAY, PERM_WILDCARD }
private data class Key(
val method: Method,
@@ -81,6 +81,14 @@ class CachingApplicationDao(
return result
}
override fun getPermissionAllKinds(key: String, type: String): ApplicationPermissionsEntity? {
val k = Key(Method.PERM_ALL_KINDS, key, type, null, null)
lookup(k)?.let { return it.permission }
val result = delegate.getPermissionAllKinds(key, type)
store(k, Value(null, result))
return result
}
override fun getPermissionForRelay(key: String, type: String, kind: Int, relay: String): ApplicationPermissionsEntity? {
val k = Key(Method.PERM_RELAY, key, type, kind, relay)
lookup(k)?.let { return it.permission }
@@ -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)
@@ -26,15 +26,25 @@ 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()
}
/**
* The readable NIP-44 v3 plaintext. For v3 the data kind stores the real
* plaintext in `text` and the Base64 wire value in `result`, so history/display
* read `text` and never touch the wire encoding.
*/
fun EncryptedDataKind?.nip44v3Plaintext(): String = (this as? ClearTextEncryptedDataKind)?.text ?: this?.result ?: ""
val encryptDecryptSignerTypes = setOf(
SignerType.NIP04_ENCRYPT,
SignerType.NIP44_ENCRYPT,
SignerType.NIP04_DECRYPT,
SignerType.NIP44_DECRYPT,
SignerType.NIP44_V3_ENCRYPT,
SignerType.NIP44_V3_DECRYPT,
SignerType.DECRYPT_ZAP_EVENT,
)
@@ -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 = "",
)
@@ -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,
@@ -24,11 +24,21 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.TimeUtils
object AmberUtils {
/**
* Produces the wire value for a request. Per the nip44v3 NIP-46 draft, v3
* plaintext travels Base64-encoded: encrypt receives base64(plaintext) and
* returns the ciphertext; decrypt returns base64(plaintext). The readable
* plaintext for logs/display comes from the EncryptedDataKind instead (see
* [nip44v3Plaintext]). [nip44v3Kind]/[nip44v3Scope] are required for V3.
*/
@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 +52,14 @@ object AmberUtils {
SignerType.NIP44_ENCRYPT -> {
account.nip44Encrypt(data, pubKey)
}
SignerType.NIP44_V3_ENCRYPT -> {
requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_ENCRYPT" }
account.nip44v3Encrypt(kotlin.io.encoding.Base64.decode(data), pubKey, nip44v3Kind, nip44v3Scope)
}
SignerType.NIP44_V3_DECRYPT -> {
requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_DECRYPT" }
kotlin.io.encoding.Base64.encode(account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope))
}
else -> {
account.nip44Decrypt(data, pubKey)
}
@@ -16,6 +16,7 @@ import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
import com.greenart7c3.nostrsigner.models.EncryptionType
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.nip44v3Plaintext
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.ui.RememberType
@@ -228,6 +229,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 +245,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,
@@ -420,6 +433,11 @@ object BunkerRequestUtils {
SignerType.NIP44_DECRYPT,
SignerType.DECRYPT_ZAP_EVENT,
-> response
// v3 wire values are Base64; log the readable plaintext
// already decoded into encryptedData.
SignerType.NIP44_V3_ENCRYPT,
SignerType.NIP44_V3_DECRYPT,
-> bunkerRequest.encryptedData.nip44v3Plaintext()
else -> getDataFromBunker(bunkerRequest.request)
},
),
@@ -455,10 +455,32 @@ 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)
// A v3 request that can never succeed — missing/invalid kind, a
// decrypt whose context (kind/scope) does not match the
// ciphertext, a bad MAC/padding, or a non-base64 encrypt payload
// — is rejected here, before any approval screen is shown.
val validationError = validateNip44v3Request(type, kind, scope, data, pubKey, acc)
if (validationError != null) {
saveLog("Rejecting invalid NIP-44 v3 request: $validationError", relay.url, acc.npub)
BunkerRequestUtils.sendBunkerResponse(
context = applicationContext,
account = acc,
bunkerRequest = request,
bunkerResponse = BunkerResponse(bunkerRequest.id, "", validationError),
relays = relays,
onLoading = { },
onDone = { },
)
return
}
arrayOf(data, pubKey, acc.npub, kind.toString(), scope)
}
else -> arrayOf(data, pubKey, acc.npub)
}
val cursor =
applicationContext.contentResolver.query(
@@ -585,7 +607,11 @@ class EventNotificationConsumer(private val applicationContext: Context) {
bunkerRequest: BunkerRequest?,
acc: Account,
url: String,
): EncryptedDataKind? = if (bunkerRequest is BunkerRequestNip44Decrypt) {
): EncryptedDataKind? = if (bunkerRequest != null && (bunkerRequest.method == "nip44v3_encrypt" || bunkerRequest.method == "nip44v3_decrypt")) {
// V3 arrives as a generic BunkerRequest; build the preview the dedicated
// approval screen renders (plaintext, like the v2 path).
nip44v3EncryptedDataKind(bunkerRequest, acc)
} else if (bunkerRequest is BunkerRequestNip44Decrypt) {
val result = acc.nip44Decrypt(bunkerRequest.ciphertext, bunkerRequest.pubKey)
if (result.startsWith("{")) {
@@ -755,5 +781,65 @@ class EventNotificationConsumer(private val applicationContext: Context) {
null
}
/**
* Builds the preview [EncryptedDataKind] for a NIP-44 v3 bunker request,
* mirroring v2: encrypt stores the plaintext as `text` and the ciphertext
* as `result`; decrypt stores the ciphertext as `text` and the decrypted
* plaintext as `result`. The Base64 wire payload is decoded here once so
* history/display can use the readable plaintext. Returns null if it can't
* be produced (the request is auto-rejected elsewhere in that case).
*/
@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
private fun nip44v3EncryptedDataKind(
bunkerRequest: BunkerRequest,
acc: Account,
): EncryptedDataKind? {
val kind = BunkerRequestUtils.getNip44v3Kind(bunkerRequest) ?: return null
val scope = BunkerRequestUtils.getNip44v3Scope(bunkerRequest)
val data = BunkerRequestUtils.getDataFromBunker(bunkerRequest)
val pubKey = bunkerRequest.params.firstOrNull() ?: return null
return try {
// text = readable plaintext (for display/history); result = wire value.
if (bunkerRequest.method == "nip44v3_encrypt") {
val plainBytes = kotlin.io.encoding.Base64.decode(data)
val ciphertext = acc.nip44v3Encrypt(plainBytes, pubKey, kind, scope)
ClearTextEncryptedDataKind(plainBytes.toString(Charsets.UTF_8), ciphertext)
} else {
val plainBytes = acc.nip44v3Decrypt(data, pubKey, kind, scope)
ClearTextEncryptedDataKind(plainBytes.toString(Charsets.UTF_8), kotlin.io.encoding.Base64.encode(plainBytes))
}
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) throw e
null
}
}
/**
* Validates a NIP-44 v3 bunker request. Returns null when it can proceed, or
* a generic error when it is malformed and must be rejected without
* prompting the user (missing kind, or a decrypt whose context/MAC/padding
* doesn't check out).
*/
private fun validateNip44v3Request(
type: SignerType,
kind: Int?,
scope: String,
data: String,
pubKey: String,
acc: Account,
): String? {
if (kind == null) return "kind is required for nip44v3"
if (type != SignerType.NIP44_V3_DECRYPT) return null
return try {
acc.nip44v3Decrypt(data, pubKey, kind, scope)
null
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) throw e
// Keep the response generic so we don't leak the ciphertext's
// embedded context back to the requester.
"could not decrypt the message"
}
}
fun notificationManager(): NotificationManager = ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) as NotificationManager
}
@@ -35,6 +35,7 @@ import com.greenart7c3.nostrsigner.models.ReturnType
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind
import com.greenart7c3.nostrsigner.models.containsNip
import com.greenart7c3.nostrsigner.models.nip44v3Plaintext
import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.components.DecryptTypeScope
@@ -138,6 +139,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 +192,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 +207,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 +251,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,8 +261,18 @@ object IntentUtils {
type,
account,
pubKey,
nip44v3Kind,
nip44v3Scope,
) ?: "Could not decrypt the message"
} catch (e: Exception) {
// A NIP-44 v3 failure (missing/invalid kind, context
// mismatch, bad MAC, corrupt payload) means the request
// can never succeed: reject it without showing the user
// an approval screen.
if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) {
emitInvalid(intent, packageName, "Invalid NIP-44 v3 request: ${e.message}", e)
return null
}
Amber.instance.applicationIOScope.launch {
val database = Amber.instance.getLogDatabase(account.npub)
database.dao().insertLog(
@@ -283,6 +304,8 @@ object IntentUtils {
route = route,
event = null,
encryptedData = encryptedDataKind,
nip44v3Kind = nip44v3Kind,
nip44v3Scope = nip44v3Scope,
)
}
SignerType.GET_PUBLIC_KEY -> {
@@ -410,7 +433,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,9 +445,19 @@ object IntentUtils {
type,
account,
pubKey,
nip44v3Kind,
nip44v3Scope,
) ?: "Could not decrypt the message"
} catch (e: Exception) {
if (e is FailedMigrationException) throw e
// A NIP-44 v3 failure (missing/invalid kind, context
// mismatch, bad MAC, corrupt payload) means the request
// can never succeed: reject it without showing the user
// an approval screen.
if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) {
emitInvalid(intent, packageName, "Invalid NIP-44 v3 request: ${e.message}", e)
return null
}
Amber.instance.applicationIOScope.launch {
val database = Amber.instance.getLogDatabase(account.npub)
database.dao().insertLog(
@@ -457,6 +494,8 @@ object IntentUtils {
route = route,
event = null,
encryptedData = encryptedDataKind,
nip44v3Kind = nip44v3Kind,
nip44v3Scope = nip44v3Scope,
)
}
SignerType.GET_PUBLIC_KEY -> {
@@ -507,6 +546,7 @@ object IntentUtils {
}
}
@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
private suspend fun getEncryptedDataKind(
type: SignerType,
result: String,
@@ -517,6 +557,25 @@ object IntentUtils {
PrivateZapEncryptedDataKind(result)
}
// v3 wire values are Base64; decode once so display/history can read the
// plaintext from `text`, while `result` keeps the wire value.
SignerType.NIP44_V3_ENCRYPT -> {
val plaintext = try {
kotlin.io.encoding.Base64.decode(data).toString(Charsets.UTF_8)
} catch (_: Exception) {
data
}
ClearTextEncryptedDataKind(plaintext, result)
}
SignerType.NIP44_V3_DECRYPT -> {
val plaintext = try {
kotlin.io.encoding.Base64.decode(result).toString(Charsets.UTF_8)
} catch (_: Exception) {
result
}
ClearTextEncryptedDataKind(plaintext, result)
}
else -> {
if (type.name.contains("ENCRYPT")) {
if (data.startsWith("{")) {
@@ -819,6 +878,12 @@ object IntentUtils {
SignerType.DECRYPT_ZAP_EVENT,
-> value
// v3 wire values are Base64; log the readable
// plaintext already decoded into encryptedData.
SignerType.NIP44_V3_ENCRYPT,
SignerType.NIP44_V3_DECRYPT,
-> intentData.encryptedData.nip44v3Plaintext()
else -> intentData.data
},
),
@@ -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<ByteArray, ByteArray> {
val sharedSecret = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privKey)
return deriveKeysFromSharedSecret(sharedSecret, nonce)
}
fun deriveKeysFromSharedSecret(sharedSecret: ByteArray, nonce: ByteArray): Pair<ByteArray, ByteArray> {
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
}
}
@@ -37,6 +37,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.kindToNip
import com.greenart7c3.nostrsigner.models.toPermissionType
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 +814,69 @@ 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)
Nip44v3ApprovalData(
modifier = modifier,
isBunker = true,
appName = appName,
packageName = null,
type = type,
account = account,
kind = v3Kind,
scope = v3Scope,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
onAccept = { rememberType, scope ->
// encryptedData.result holds the Base64 wire value (ciphertext
// for encrypt, base64 plaintext for decrypt), computed when the
// request arrived — same shape as the v2 path.
val result = bunkerRequest.encryptedData?.result ?: ""
// 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()
@@ -170,6 +170,67 @@ fun IntentSingleEventHomeScreen(
)
}
SignerType.NIP44_V3_ENCRYPT, SignerType.NIP44_V3_DECRYPT -> {
// V3 grants are scoped by (app, type, kind); fall back to a
// kind=null "all kinds" grant. They never satisfy v2/v4 requests.
val permType = intentData.type.toString()
val permission =
applicationEntity?.permissions?.firstOrNull {
it.pkKey == key && it.type == permType && it.kind == intentData.nip44v3Kind
} ?: applicationEntity?.permissions?.firstOrNull {
it.pkKey == key && it.type == permType && it.kind == null
}
val acceptOrReject = IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission)
Nip44v3ApprovalData(
modifier = modifier,
isBunker = false,
appName = appName,
packageName = packageName,
type = intentData.type,
account = account,
kind = intentData.nip44v3Kind,
scope = intentData.nip44v3Scope,
encryptedData = intentData.encryptedData,
shouldRunOnAccept = acceptOrReject,
onAccept = { rememberType, scope ->
val result = intentData.encryptedData?.result ?: ""
// SPECIFIC ⇒ kind-scoped grant; ALL ⇒ all-kinds grant (kind=null).
val v3Kind = if (scope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null
IntentUtils.sendResult(
context,
packageName,
account,
key,
clipboardManager,
result,
result,
intentData,
v3Kind,
onRemoveIntentData = onRemoveIntentData,
onLoading = onLoading,
rememberType = rememberType,
decryptTypeScope = scope,
)
},
onReject = { rememberType, scope ->
val v3Kind = if (scope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null
IntentUtils.sendRejection(
key = key,
account = account,
intentData = intentData,
appName = appName,
rememberType = rememberType,
onLoading = onLoading,
onRemoveIntentData = onRemoveIntentData,
kind = v3Kind,
decryptTypeScope = scope,
)
},
)
}
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, SignerType.DECRYPT_ZAP_EVENT -> {
val nip = when (intentData.type) {
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT -> 4
@@ -0,0 +1,183 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.EncryptedDataKind
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.nip44v3Plaintext
import com.greenart7c3.nostrsigner.ui.RememberType
/**
* Dedicated approval screen for NIP-44 v3 encrypt/decrypt requests.
*
* Unlike the v2/v4 [EncryptDecryptData] screen, v3 authenticates an event
* `kind` and a `scope`, so this screen surfaces that context explicitly. The
* scope toggle grants either a single kind or all kinds.
*/
@Composable
fun Nip44v3ApprovalData(
modifier: Modifier,
isBunker: Boolean,
appName: String,
packageName: String?,
type: SignerType,
account: Account,
kind: Int?,
scope: String,
encryptedData: EncryptedDataKind?,
shouldRunOnAccept: Boolean?,
defaultScope: DecryptTypeScope = DecryptTypeScope.SPECIFIC,
onAccept: (RememberType, DecryptTypeScope) -> Unit,
onReject: (RememberType, DecryptTypeScope) -> Unit,
) {
var rememberType by remember { mutableStateOf(RememberType.NEVER) }
var grantScope by remember { mutableStateOf(defaultScope) }
val isEncrypt = type == SignerType.NIP44_V3_ENCRYPT
val messageRes = if (isEncrypt) R.string.nip44_v3_wants_to_encrypt else R.string.nip44_v3_wants_to_decrypt
val displayContent = encryptedData.nip44v3Plaintext()
Column(modifier) {
if (isBunker) {
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(appName)
}
append(" ")
append(stringResource(messageRes))
},
fontSize = 18.sp,
)
} else {
LocalAppIcon(packageName)
Text(
stringResource(messageRes),
fontSize = 18.sp,
)
}
Spacer(Modifier.size(8.dp))
Nip44v3ContextBox(kind = kind, scope = scope)
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
colors = CardDefaults.cardColors().copy(
containerColor = MaterialTheme.colorScheme.background,
),
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
Text(
displayContent,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
}
Spacer(Modifier.size(16.dp))
SigningAs(account)
Spacer(modifier = Modifier.weight(1f))
Spacer(Modifier.size(8.dp))
LabeledBorderBox(
label = stringResource(R.string.encryption_scope),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
AmberToggles(
selected = grantScope,
options = listOf(DecryptTypeScope.SPECIFIC, DecryptTypeScope.ALL),
onSelected = { grantScope = it },
label = {
stringResource(
when (it) {
DecryptTypeScope.SPECIFIC -> R.string.for_this_kind_only
DecryptTypeScope.ALL -> R.string.for_all_kinds
},
)
},
)
}
Spacer(Modifier.size(8.dp))
RememberMyChoice(
shouldRunOnAccept,
if (isBunker) null else packageName,
isBunker,
onAccept = { onAccept(it, grantScope) },
onReject = { onReject(it, grantScope) },
) {
rememberType = it
}
AcceptRejectButtons(
onAccept = { onAccept(rememberType, grantScope) },
onReject = { onReject(rememberType, grantScope) },
)
}
}
/**
* Shows the NIP-44 v3 context (event kind + scope) being authenticated by the
* request, so the user can see what they are granting access to.
*/
@Composable
fun Nip44v3ContextBox(
kind: Int?,
scope: String,
) {
LabeledBorderBox(
label = stringResource(R.string.nip44_v3_context),
modifier = Modifier.fillMaxWidth(),
) {
Column {
val kindLabel = if (kind != null) {
val translation = Permission("sign_event", kind).toLocalizedString(Amber.instance)
val unknown = stringResource(R.string.event_kind, kind.toString())
// toLocalizedString returns the "Event kind N" fallback when the
// kind has no translation; only append the name when one exists.
if (translation != unknown) "$kind ($translation)" else kind.toString()
} else {
"?"
}
Text(stringResource(R.string.nip44_v3_kind, kindLabel))
Text(
stringResource(
R.string.nip44_v3_scope,
scope.ifEmpty { stringResource(R.string.nip44_v3_no_scope) },
),
)
}
}
}
+8
View File
@@ -647,6 +647,14 @@
<string name="encryption_scope">Encryption scope</string>
<string name="for_this_method_only">This method only</string>
<string name="for_all_methods">All methods</string>
<string name="nip44_v3_context">NIP-44 v3 context</string>
<string name="nip44_v3_kind">Kind: %1$s</string>
<string name="nip44_v3_scope">Scope: %1$s</string>
<string name="nip44_v3_no_scope">(none)</string>
<string name="for_this_kind_only">This kind only</string>
<string name="for_all_kinds">All kinds</string>
<string name="nip44_v3_wants_to_encrypt">wants to encrypt with NIP-44 v3</string>
<string name="nip44_v3_wants_to_decrypt">wants to read encrypted content with NIP-44 v3</string>
<string name="event_kind_10086">Indexer relays</string>
<string name="event_kind_10087">Proxy relays</string>
<string name="event_kind_10088">Broadcast relays</string>
+1 -1
View File
@@ -25,7 +25,7 @@
<provider
android:name=".SignerProvider"
android:authorities="${applicationId}.PING;${applicationId}.SIGN_EVENT;${applicationId}.NIP04_ENCRYPT;${applicationId}.NIP04_DECRYPT;${applicationId}.NIP44_ENCRYPT;${applicationId}.NIP44_DECRYPT;${applicationId}.GET_PUBLIC_KEY;${applicationId}.DECRYPT_ZAP_EVENT"
android:authorities="${applicationId}.PING;${applicationId}.SIGN_EVENT;${applicationId}.NIP04_ENCRYPT;${applicationId}.NIP04_DECRYPT;${applicationId}.NIP44_ENCRYPT;${applicationId}.NIP44_DECRYPT;${applicationId}.NIP44_V3_ENCRYPT;${applicationId}.NIP44_V3_DECRYPT;${applicationId}.GET_PUBLIC_KEY;${applicationId}.DECRYPT_ZAP_EVENT"
android:enabled="true"
android:exported="true" />
@@ -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"))
@@ -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())
}
}
+461
View File
@@ -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)"
}
]
}
+2
View File
@@ -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" }