Add NIP-44 v3 encryption

Implements the draft NIP-44 v3 cipher (nostr-land/nip44v3) alongside
the existing v2 path, with kind+scope context binding for cross-context
replay protection. The cipher is a self-contained class that reuses
Quartz's existing primitives (secp256k1 ECDH, HKDF-SHA256, ChaCha20,
HMAC-SHA256), so no new runtime dependency is needed.

Wires v3 through all three ingestion paths (NIP-46 bunker via
`nip44v3_encrypt` / `nip44v3_decrypt` methods, `nostrsigner://` intents,
and the ContentProvider IPC at `content://*.NIP44_V3_{EN,DE}CRYPT`),
threading kind and scope from the request through the approval UI to
the cipher. Permissions for v3 are stored per (app, type, kind) so
users can grant a peer access to a single event kind rather than the
whole NIP. V3 grants are kept distinct from v2 grants.

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