mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Merge pull request #448 from greenart7c3/claude/implement-nip44-v3-0G3I9
Add NIP-44 v3 encryption/decryption support
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+64
-1
@@ -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()
|
||||
|
||||
+61
@@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user