multiple fixes

This commit is contained in:
particle-box
2026-01-23 00:34:27 +05:30
parent 18f280cfd7
commit 2c5f54368c
26 changed files with 1160 additions and 758 deletions

3
announcements.txt Normal file
View File

@@ -0,0 +1,3 @@
# PurrfectSnap Announcements
- Welcome to PurrfectSnap announcements.

View File

@@ -149,6 +149,7 @@ class HomeRootSection : Routes.Route() {
private val changelogClient by lazy { OkHttpClient() }
private val changelogStableUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-stable.txt"
private val changelogPrereleaseUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-prerelease.txt"
private val announcementsUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/announcements.txt"
private val heroGradientColors = listOf(
Color(0xFF5C4B99),
@@ -748,6 +749,10 @@ class HomeRootSection : Routes.Route() {
var changelogError by remember { mutableStateOf<String?>(null) }
var changelogText by remember { mutableStateOf<String?>(null) }
var changelogVersion by remember { mutableStateOf<String?>(null) }
var showAnnouncementsDialog by remember { mutableStateOf(false) }
var announcementsLoading by remember { mutableStateOf(false) }
var announcementsError by remember { mutableStateOf<String?>(null) }
var announcementsText by remember { mutableStateOf<String?>(null) }
val handleUpdateAction: () -> Unit = {
latestUpdate?.let { latest ->
@@ -827,6 +832,31 @@ class HomeRootSection : Routes.Route() {
}
}
fun loadAnnouncements() {
if (announcementsText != null) return
announcementsLoading = true
announcementsError = null
coroutineScope.launch(Dispatchers.IO) {
runCatching {
changelogClient.newCall(Request.Builder().url(announcementsUrl).build()).execute().use { response ->
if (!response.isSuccessful) throw IllegalStateException("Failed to fetch announcements (${response.code})")
val body = response.body?.string() ?: throw IllegalStateException("Empty announcements body")
body.trim()
}
}.onSuccess { text ->
withContext(Dispatchers.Main) {
announcementsText = text
announcementsLoading = false
}
}.onFailure { error ->
withContext(Dispatchers.Main) {
announcementsError = error.message ?: "Failed to load announcements"
announcementsLoading = false
}
}
}
}
val onUpdateButtonClick: () -> Unit = {
latestUpdate?.let {
showChangelogDialog = true
@@ -859,6 +889,13 @@ class HomeRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
TopBarActionChip(
icon = Icons.Filled.Info,
label = "Announcements"
) {
showAnnouncementsDialog = true
loadAnnouncements()
}
Spacer(modifier = Modifier.weight(1f))
HomeActionChips()
}
@@ -1143,6 +1180,68 @@ class HomeRootSection : Routes.Route() {
)
}
if (showAnnouncementsDialog) {
AestheticDialog(
onDismissRequest = { showAnnouncementsDialog = false },
title = "Announcements",
text = "",
icon = Icons.Filled.Info,
confirmButtonText = "Close",
onConfirm = { showAnnouncementsDialog = false },
dismissButtonText = "Dismiss",
onDismiss = { showAnnouncementsDialog = false },
confirmEnabled = !announcementsLoading,
customContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 120.dp, max = 340.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
when {
announcementsLoading -> {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(28.dp),
strokeWidth = 3.dp,
color = Color.White
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Loading announcements...",
color = Color.White,
fontWeight = FontWeight.SemiBold
)
}
}
announcementsError != null -> {
Text(
text = announcementsError ?: "Failed to load announcements",
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.SemiBold
)
}
else -> {
Text(
text = announcementsText ?: "Announcements not available",
color = PurrfectPalette.textPrimary,
fontSize = 14.sp,
lineHeight = 20.sp
)
}
}
}
}
)
}
if (showQuickActionsMenu) {
QuickActionsDialog(
quickActions = cards,

View File

@@ -339,7 +339,7 @@ class PatchSnapchatScreen : SetupScreen() {
textAlign = TextAlign.Start
)
Text(
text = "Fix: Download and install JingMatrix LSPatch, then patch Snapchat 13.64.0.52 in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.",
text = "Fix: Download and install JingMatrix LSPatch, then patch Snapchat 13.65.1.0 to 13.71.0.51 in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.",
style = bodyStyle,
textAlign = TextAlign.Start
)

View File

@@ -4,40 +4,36 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -48,30 +44,17 @@ import me.eternal.purrfectsnap.common.data.FriendLinkType
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
import me.eternal.purrfectsnap.core.action.AbstractAction
import me.eternal.purrfectsnap.core.event.events.impl.ActivityResultEvent
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver
import me.eternal.purrfectsnap.core.features.impl.experiments.AddFriendSourceSpoof
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
import me.eternal.purrfectsnap.core.util.EvictingMap
import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver
import kotlin.random.Random
import java.text.SimpleDateFormat
import java.util.*
class ManageFriendList : AbstractAction() {
companion object {
private var openSuggestedOnLaunch = false
@Synchronized
fun requestOpenSuggestedOnLaunch() {
openSuggestedOnLaunch = true
}
@Synchronized
private fun consumeOpenSuggestedOnLaunch(): Boolean {
val shouldOpen = openSuggestedOnLaunch
openSuggestedOnLaunch = false
return shouldOpen
}
}
private val translation by lazy { context.translation.getCategory("friend_list") }
private val dialogBackground = Brush.verticalGradient(
listOf(
@@ -92,211 +75,7 @@ class ManageFriendList : AbstractAction() {
)
)
private var pendingPickerAction: Pair<Int, (data: Uri) -> Unit>? = null
private val uuidRegex by lazy {
Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
}
private fun loadStaticDefaultField(classLoader: ClassLoader, className: String, fieldName: String): Any? {
return try {
val clazz = classLoader.loadClass(className)
try {
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
field.get(null)
} catch (e: Exception) {
clazz.declaredFields
.filter { java.lang.reflect.Modifier.isStatic(it.modifiers) }
.firstOrNull()
?.apply { isAccessible = true }
?.get(null)
}
} catch (e: Exception) {
context.log.warn("Could not load $className: ${e.message}")
null
}
}
private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
if (friendRelationshipChangerInstance == null) {
context.log.error("friendRelationshipChangerInstance is null")
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available")
return
}
runCatching {
val classLoader = context.androidContext.classLoader
val classNamesToTry = listOf("EnumC3559qC", "qC", "com.snapchat.android.EnumC3559qC", "LC", "EnumC0886LC", "EnumC1539TC", "TC")
val enumC3559qCClass = classNamesToTry.firstNotNullOfOrNull { className ->
try {
classLoader.loadClass(className).takeIf { it.isEnum }?.also {
context.log.verbose("Successfully loaded enum class: $className")
}
} catch (e: Exception) {
null
}
}
if (enumC3559qCClass == null) {
context.log.error("Friend source enum class not found after trying: ${classNamesToTry.joinToString()}")
context.longToast("Failed to add friend: Required enum class not found")
return@runCatching
}
val enumConstants = enumC3559qCClass.enumConstants
?: enumC3559qCClass.getMethod("values").invoke(null) as? Array<*>
?: run {
context.log.error("Could not retrieve enum constants from ${enumC3559qCClass.name}")
context.longToast("Failed to add friend: Enum constants not accessible")
return@runCatching
}
context.log.verbose("Found ${enumConstants.size} enum constants")
val addedByUsername = enumConstants.firstOrNull { it.toString() == "ADDED_BY_USERNAME" }
?: enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) }
?: enumConstants.firstOrNull()
?: run {
context.log.error("No enum constants available")
context.longToast("Failed to add friend: No valid enum constant found")
return@runCatching
}
context.log.verbose("Using enum constant: $addedByUsername")
val sQ7Default = loadStaticDefaultField(classLoader, "sQ7", "f288251e0")
val zQ7Default = loadStaticDefaultField(classLoader, "ZQ7", "f159958F0")
val iBgClass = try {
classLoader.loadClass("iBg")
} catch (e: Exception) {
context.log.error("Could not load iBg class: ${e.message}")
null
}
if (iBgClass == null) {
context.log.error("iBg class not found")
return@runCatching
}
val m51157aMethod = iBgClass.declaredMethods.firstOrNull { it.name == "m51157a" }
?: iBgClass.methods.firstOrNull { it.name == "m51157a" }
?: iBgClass.declaredMethods.firstOrNull { method ->
java.lang.reflect.Modifier.isStatic(method.modifiers) &&
method.parameterTypes.size == 14 &&
method.parameterTypes[0].isAssignableFrom(friendRelationshipChangerInstance.javaClass) &&
method.parameterTypes[1] == String::class.java &&
method.parameterTypes[2] == enumC3559qCClass
}
if (m51157aMethod == null) {
context.log.error("Could not find iBg.m51157a method")
context.log.error("Available static methods with 14 params:")
iBgClass.declaredMethods.filter {
java.lang.reflect.Modifier.isStatic(it.modifiers) && it.parameterTypes.size == 14
}.forEach { method ->
context.log.error(" ${method.name}: ${method.parameterTypes.joinToString { it.simpleName }}")
}
return@runCatching
}
m51157aMethod.isAccessible = true
try {
m51157aMethod.invoke(
null,
friendRelationshipChangerInstance,
userId,
addedByUsername,
sQ7Default,
zQ7Default,
null,
null,
null,
null,
null,
null,
null,
null,
4064
)?.also { result ->
result.javaClass.methods
.find { it.name == "subscribe" && it.parameterCount == 0 }
?.apply { isAccessible = true }
?.invoke(result)
}
} catch (e: Exception) {
context.log.error("Exception during method invocation: ${e.javaClass.name}: ${e.message}")
e.cause?.let { cause ->
context.log.error("Cause: ${cause.javaClass.name}: ${cause.message}")
cause.stackTrace?.take(5)?.forEach {
context.log.error(" at $it")
}
}
throw e
}
}.onFailure {
context.log.error("Failed to add friend $userId", it)
context.longToast("Failed to add friend: ${it.message}")
}
}
private fun loadSuggestedFriends(
coroutineScope: CoroutineScope,
onLoaded: (List<String>) -> Unit
) {
coroutineScope.launch(Dispatchers.IO) {
val blacklist = getUserIdBlacklist()
val suggestedFriends = context.database.getAllFriends()
.filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }
.sortedByDescending { it.addedTimestamp }
.mapNotNull { it.userId }
withContext(Dispatchers.Main) {
onLoaded(suggestedFriends)
}
}
}
override fun onActivityCreate() {
context.event.subscribe(ActivityResultEvent::class) { event ->
if (event.requestCode == pendingPickerAction?.first) {
val pendingAction = pendingPickerAction ?: return@subscribe
this.pendingPickerAction = null
event.canceled = true
pendingAction.second(event.intent.data!!)
}
}
}
private fun exportFriends(
userIds: List<String>
) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
context.androidContext.contentResolver.openOutputStream(data).use { output ->
output?.bufferedWriter()?.use { writer ->
userIds.forEach {
writer.write(it)
writer.newLine()
}
}
context.longToast("Exported ${userIds.size} friends!")
}
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "my_friends.txt")
},
"Select a location to save the file"
),
pendingPickerAction!!.first
)
}
private val userIdToSnapchatter = mutableMapOf<String, Snapchatter>()
private val uuidRegex = Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
private fun getUserIdBlacklist() = arrayOf(
context.database.myUserId,
@@ -304,21 +83,131 @@ class ManageFriendList : AbstractAction() {
"84ee8839-3911-492d-8b94-72dd80f3713a",
)
private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
?: run {
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available")
return
}
runCatching {
val classLoader = context.androidContext.classLoader
val friendRelationshipChangerClass = friendRelationshipChangerInstance.javaClass
// Helper function to find static field by trying multiple field names
fun findStaticField(clazz: Class<*>, fieldNames: List<String>): Any? {
fieldNames.forEach { fieldName ->
runCatching {
clazz.getDeclaredField(fieldName).apply { isAccessible = true }.get(null)
}.getOrNull()?.let { return it }
}
// Fallback: find any static field of the same type
return clazz.declaredFields.firstOrNull { field ->
java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz
}?.let { field ->
runCatching {
field.isAccessible = true
field.get(null)?.takeIf { it.javaClass == clazz }
}.getOrNull()
}
}
// Load F9l class
val f9lClass = classLoader.loadClass("F9l")
// Find the add friend method by matching signature
val method = (f9lClass.declaredMethods + f9lClass.methods).firstOrNull { method ->
if (!java.lang.reflect.Modifier.isStatic(method.modifiers) || method.parameterTypes.size != 14) return@firstOrNull false
val params = method.parameterTypes
params[0].isAssignableFrom(friendRelationshipChangerClass) &&
params[1] == String::class.java &&
params[2].isEnum
} ?: return@runCatching context.log.error("Could not find F9l.m8344a method")
// Extract classes from method signature
val enumClass = method.parameterTypes[2]
val tZ7Class = method.parameterTypes[3]
val g08Class = method.parameterTypes[4]
// Get enum constant for USERNAME
val enumConstants = enumClass.enumConstants ?: enumClass.getMethod("values").invoke(null) as? Array<*>
?: return@runCatching context.log.error("Could not get enum constants")
val addedByUsername = enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) }
?: return@runCatching context.log.error("Could not find ADDED_BY_USERNAME enum")
// Get static field instances
val tZ7Default = findStaticField(tZ7Class, listOf("f301161j0", "f301158a", "f301165n0", "f301160c", "f301171t"))
?: return@runCatching context.log.error("Could not find tZ7 static field")
val g08Default = findStaticField(g08Class, listOf("f211750K0", "f211807x1", "f211805w1", "f211774f1", "f211775g1", "f211760U0", "f211783l1", "f211785m1", "f211787n1", "f211772d1"))
?: return@runCatching context.log.error("Could not find g08 static field")
// Invoke method with 14 parameters
method.isAccessible = true
val result = method.invoke(
null,
friendRelationshipChangerInstance,
userId,
addedByUsername,
tZ7Default,
g08Default,
null, null, null, null, null, // String params 6-10
null, // InteractionPlacementInfo
null, // String str7
null, // Integer num
4064 // int flags
)
// Subscribe to the Completable result
result?.javaClass?.methods?.firstOrNull {
it.name == "subscribe" && it.parameterCount == 0
}?.let { subscribeMethod ->
subscribeMethod.isAccessible = true
subscribeMethod.invoke(result)
}
}.onFailure {
context.log.error("Failed to add friend $userId", it)
context.longToast("Failed to add friend: ${it.message}")
}
}
override fun onActivityCreate() {
context.event.subscribe(ActivityResultEvent::class) { event ->
pendingPickerAction?.takeIf { it.first == event.requestCode }?.let {
pendingPickerAction = null
event.canceled = true
it.second(event.intent.data!!)
}
}
}
private fun exportFriends(userIds: List<String>) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
context.androidContext.contentResolver.openOutputStream(data)?.bufferedWriter()?.use { writer ->
userIds.forEach { writer.write(it); writer.newLine() }
}
context.longToast("Exported ${userIds.size} friends!")
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "my_friends.txt")
}, "Select a location to save the file"),
pendingPickerAction!!.first
)
}
private val userIdToSnapchatter = mutableMapOf<String, Snapchatter>()
@Composable
private fun ManagerDialog() {
val pendingFriendRequests = remember { mutableStateMapOf<String, Job>() }
var fetchedFriends by remember { mutableStateOf<List<String>?>(null) }
val coroutineScope = rememberCoroutineScope()
val openSuggestedOnLaunch = remember { consumeOpenSuggestedOnLaunch() }
val bitmojiCache = remember { me.eternal.purrfectsnap.core.util.EvictingMap<String, Bitmap>(50) }
val bitmojiCache = remember { EvictingMap<String, Bitmap>(50) }
val noBitmojiBitmap = remember { BitmapFactory.decodeResource(context.resources, android.R.drawable.ic_menu_report_image).asImageBitmap() }
LaunchedEffect(openSuggestedOnLaunch) {
if (openSuggestedOnLaunch) {
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
}
}
Box(
modifier = Modifier
.fillMaxWidth()
@@ -425,50 +314,38 @@ class ManageFriendList : AbstractAction() {
) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
runCatching {
fetchedFriends = null
context.androidContext.contentResolver.openInputStream(data).use { input ->
fetchedFriends = input?.bufferedReader()?.readLines()?.filter {
it.matches(uuidRegex)
}?.map { it.trim() }?.toMutableList() ?: mutableListOf()
}
fetchedFriends = context.androidContext.contentResolver.openInputStream(data)?.bufferedReader()?.readLines()?.filter { it.matches(uuidRegex) }?.map { it.trim() }?.toMutableList() ?: mutableListOf()
}.onFailure {
context.log.error("Failed to import friends", it)
context.longToast("Failed to import friends: ${it.message}")
}
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" },
"Select a file"
),
pendingPickerAction!!.first
)
context.mainActivity?.startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, "Select a file"), pendingPickerAction!!.first)
}
}
PrimaryButton(
text = "Load Suggested Friends",
text = translation.get("load_suggested_friends"),
modifier = Modifier.fillMaxWidth()
) {
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
coroutineScope.launch(Dispatchers.IO) {
val blacklist = getUserIdBlacklist()
val suggestedFriends = context.database.getAllFriends().filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }.sortedByDescending { it.addedTimestamp }.mapNotNull { it.userId }
withContext(Dispatchers.Main) { fetchedFriends = suggestedFriends }
}
}
}
} else {
var searchQuery by remember { mutableStateOf("") }
val filteredFriends = remember(fetchedFriends, searchQuery) {
val friends = fetchedFriends ?: emptyList()
if (searchQuery.isBlank()) {
friends.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
} else {
friends.filter { userId ->
val friendInfo = context.database.getFriendInfo(userId)
friendInfo?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true ||
friendInfo?.displayName?.contains(searchQuery, ignoreCase = true) == true ||
userId.contains(searchQuery, ignoreCase = true)
}.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
}
val sorted = { list: List<String> -> list.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L } }
if (searchQuery.isBlank()) sorted(friends) else sorted(friends.filter { userId ->
val info = context.database.getFriendInfo(userId)
info?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true || info?.displayName?.contains(searchQuery, ignoreCase = true) == true || userId.contains(searchQuery, ignoreCase = true)
})
}
Column(
modifier = Modifier
.fillMaxWidth()
@@ -511,7 +388,7 @@ class ManageFriendList : AbstractAction() {
}
Spacer(modifier = Modifier.size(46.dp))
}
BasicTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
@@ -572,64 +449,52 @@ class ManageFriendList : AbstractAction() {
}
}
items(filteredFriends) { userId ->
val friendInfo = remember(userId) { context.database.getFriendInfo(userId) }
val linkType = remember(friendInfo) {
friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
}
val isActuallyAdded = remember(friendInfo, linkType) {
friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
}
var friendInfo by remember(userId) { mutableStateOf(context.database.getFriendInfo(userId)) }
var friendLinkType by remember(userId) { mutableStateOf(friendInfo?.let { FriendLinkType.fromValue(it.friendLinkType) }) }
val isActuallyAdded = friendInfo?.let { info ->
friendLinkType != null && info.addedTimestamp > 0 &&
(friendLinkType == FriendLinkType.MUTUAL || friendLinkType == FriendLinkType.OUTGOING)
} ?: false
var friendSnapchatter by remember(userId) { mutableStateOf<Snapchatter?>(null) }
var friendLinkType by remember(userId) { mutableStateOf(linkType) }
var actuallyAdded by remember(userId) { mutableStateOf(isActuallyAdded) }
var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) {
mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] })
}
LaunchedEffect(userId) {
if (friendSnapchatter == null && !userIdToSnapchatter.containsKey(userId)) {
friendSnapchatter = userIdToSnapchatter[userId] ?: run {
withContext(Dispatchers.IO) {
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.let {
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.also {
userIdToSnapchatter[userId] = it
friendSnapchatter = it
}
}
} else {
friendSnapchatter = userIdToSnapchatter[userId]
}
while (true) {
delay(2000)
val newLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
if (newLinkType != friendLinkType) {
friendLinkType = newLinkType
}
val newActuallyAdded = friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
if (newActuallyAdded != actuallyAdded) {
actuallyAdded = newActuallyAdded
}
}
}
LaunchedEffect(Unit) {
while (true) {
delay(2000)
context.database.getFriendInfo(userId)?.let {
friendInfo = it
FriendLinkType.fromValue(it.friendLinkType)?.let { newType ->
if (newType != friendLinkType) friendLinkType = newType
}
}
}
}
var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) {
mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] })
}
LaunchedEffect(userId, friendInfo?.bitmojiAvatarId, friendInfo?.bitmojiSelfieId) {
if (bitmojiBitmap != null || friendInfo?.bitmojiAvatarId == null || friendInfo?.bitmojiSelfieId == null) return@LaunchedEffect
withContext(Dispatchers.IO) {
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(
friendInfo.bitmojiSelfieId,
friendInfo.bitmojiAvatarId,
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
) ?: return@withContext
runCatching {
RemoteMediaResolver.downloadMedia(bitmojiUrl) { inputStream, length ->
val avatarId = friendInfo.bitmojiAvatarId ?: return@downloadMedia
bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also {
bitmojiBitmap = it
val info = friendInfo ?: return@LaunchedEffect
val avatarId = info.bitmojiAvatarId ?: return@LaunchedEffect
val selfieId = info.bitmojiSelfieId ?: return@LaunchedEffect
if (bitmojiBitmap != null) return@LaunchedEffect
BitmojiSelfie.getBitmojiSelfie(selfieId, avatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D)?.let { url ->
withContext(Dispatchers.IO) {
runCatching {
RemoteMediaResolver.downloadMedia(url) { inputStream, _ ->
bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also { bitmojiBitmap = it }
}
}
}
@@ -652,7 +517,7 @@ class ManageFriendList : AbstractAction() {
contentDescription = null,
modifier = Modifier.size(35.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp)
@@ -680,39 +545,38 @@ class ManageFriendList : AbstractAction() {
color = if (type == FriendLinkType.MUTUAL) Color(0xFF8EF0F3) else Color(0xFFD9D3FF)
)
}
if (friendSnapchatter != null) {
val isPending = pendingFriendRequests.containsKey(userId) && pendingFriendRequests[userId]?.isActive != false
friendSnapchatter?.let {
val isFollowing = friendLinkType == FriendLinkType.FOLLOWING
val isPending = pendingFriendRequests[userId]?.isActive == true
val canAdd = !isActuallyAdded && !isPending && !isFollowing
PrimaryButton(
text = when {
isFollowing -> "Following"
actuallyAdded -> context.translation["common.added"]
isPending -> translation.get("adding") ?: "Adding..."
isActuallyAdded -> context.translation["common.added"]
isPending -> translation.get("adding")
else -> translation.get("add")
},
modifier = Modifier.widthIn(min = 110.dp),
enabled = !actuallyAdded && !isPending && !isFollowing
enabled = canAdd
) {
if (actuallyAdded || isPending || isFollowing) return@PrimaryButton
if (!canAdd) return@PrimaryButton
val prevLinkType = friendLinkType
addFriend(userId)
val job = coroutineScope.launch {
pendingFriendRequests[userId] = coroutineScope.launch(Dispatchers.IO) {
withTimeout(10000) {
while (friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }?.value == prevLinkType?.value) {
while (true) {
context.database.getFriendInfo(userId)?.let { updated ->
FriendLinkType.fromValue(updated.friendLinkType)?.takeIf { it != prevLinkType }?.let {
friendInfo = updated
friendLinkType = it
return@withTimeout
}
}
delay(500)
}
}
}.apply {
invokeOnCompletion {
pendingFriendRequests.remove(userId)
friendLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
actuallyAdded = isActuallyAdded || (friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING))
}
}
pendingFriendRequests[userId] = job
}.apply { invokeOnCompletion { pendingFriendRequests.remove(userId) } }
}
if (isPending) {
CircularProgressIndicator(
@@ -805,4 +669,4 @@ class ManageFriendList : AbstractAction() {
}
}
}
}
}

View File

@@ -2,7 +2,9 @@ package me.eternal.purrfectsnap.core.features.impl.messaging
import me.eternal.purrfectsnap.common.data.NotificationType
import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.util.hook.HookStage
@@ -12,9 +14,9 @@ class PreventMessageSending : Feature("Prevent message sending") {
override fun init() {
val preventMessageSending by context.config.messaging.preventMessageSending
context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event ->
if (event.uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return@subscribe
event.buffer = ProtoEditor(event.buffer).apply {
fun handleUpdateContentMessage(uri: String, buffer: ByteArray): ByteArray? {
if (uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return null
return ProtoEditor(buffer).apply {
edit(3) {
// replace replayed to read receipt
if (firstOrNull(13) != null) {
@@ -25,6 +27,36 @@ class PreventMessageSending : Feature("Prevent message sending") {
}.toByteArray()
}
fun handleCreateContentMessage(uri: String, buffer: ByteArray): Boolean {
if (uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return false
val reader = ProtoReader(buffer)
// check for missed audio/video call in MessageContent (field 4)
val contentReader = reader.followPath(4) ?: return false
// try both possible field IDs based on SnapEnums and ContentType.java
val contentType = contentReader.getVarInt(2)
val isMissedAudio = contentType == 13L || contentType == 18L
val isMissedVideo = contentType == 12L || contentType == 17L
if (isMissedAudio && preventMessageSending.contains("abandon_audio")) return true
if (isMissedVideo && preventMessageSending.contains("abandon_video")) return true
return false
}
arrayOf(NativeUnaryCallEvent::class, UnaryCallEvent::class).forEach { eventClass ->
context.event.subscribe(eventClass) { event ->
val uri = if (event is NativeUnaryCallEvent) event.uri else (event as UnaryCallEvent).uri
if (!uri.startsWith("/messagingcoreservice.MessagingCoreService/")) return@subscribe
if (handleCreateContentMessage(uri, event.buffer)) {
event.canceled = true
}
handleUpdateContentMessage(uri, event.buffer)?.let { event.buffer = it }
}
}
context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param ->
val messageUpdate = param.arg<Any>(2).toString()
if (messageUpdate == "SCREENSHOT" && preventMessageSending.contains("chat_screenshot")) {
@@ -34,6 +66,10 @@ class PreventMessageSending : Feature("Prevent message sending") {
if (messageUpdate == "SCREEN_RECORD" && preventMessageSending.contains("chat_screen_record")) {
param.setResult(null)
}
if ((messageUpdate == "REPLAY" || messageUpdate == "replay") && preventMessageSending.contains("snap_replay")) {
param.setResult(null)
}
}
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
@@ -41,9 +77,8 @@ class PreventMessageSending : Feature("Prevent message sending") {
val associatedType = NotificationType.fromContentType(contentType ?: return@subscribe) ?: return@subscribe
if (preventMessageSending.contains(associatedType.key)) {
context.log.verbose("Preventing message sending for $associatedType")
event.canceled = true
}
}
}
}
}

View File

@@ -94,6 +94,7 @@ class MessageSender(
it.conversations = conversations.toCollection(ArrayList())
it.mPhoneNumbers = arrayListOf<Any>()
it.stories = arrayListOf<Any>()
it.massSnaps = arrayListOf<Any>()
}
sendMessageWithContentMethod.invoke(context.feature(Messaging::class).conversationManager?.instanceNonNull(), messageDestinations.instanceNonNull(), localMessageContent, callback)

View File

@@ -1,6 +1,6 @@
package me.eternal.purrfectsnap.core.wrapper.impl
import me.eternal.purrfectsnap.core.PurrfectSnap
import me.eternal.purrfectsnap.core.purrfectsnap
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper
import java.nio.ByteBuffer
@@ -29,12 +29,14 @@ class SnapUUID(
obj
}
obj is UUID -> obj.toBytes()
PurrfectSnap.classCache.snapUUID.isInstance(obj) -> {
purrfectsnap.classCache.snapUUID.isInstance(obj) -> {
obj?.getObjectField("mId") as ByteArray
}
PurrfectSnap.classCache.snapShimsUUID?.isInstance(obj) == true -> {
purrfectsnap.classCache.snapShimsUUID?.isInstance(obj) == true -> {
val any = obj as Any
any::class.java.methods.firstOrNull { it.name == "getId" }?.invoke(any) as? ByteArray ?: ByteArray(16)
runCatching { any.javaClass.getMethod("getId").invoke(any) as ByteArray }.getOrElse {
any.getObjectField("mId") as ByteArray
}
}
else -> ByteArray(16)
}
@@ -44,7 +46,7 @@ class SnapUUID(
override var instance: Any?
set(_) {}
get() = PurrfectSnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes)
get() = purrfectsnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes)
override fun toString(): String {
return uuidString
@@ -60,3 +62,4 @@ class SnapUUID(
return uuidBytes.contentHashCode()
}
}

436
native/rust/Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
@@ -21,24 +21,18 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_log-sys"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
@@ -62,9 +56,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
@@ -74,15 +68,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "2.7.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
@@ -95,9 +89,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.16.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "byteorder"
@@ -107,9 +101,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "bzip2"
@@ -133,10 +127,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.9"
version = "1.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -150,9 +145,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
@@ -162,14 +157,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.39"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.52.6",
"windows-link",
]
[[package]]
@@ -221,9 +215,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@@ -268,7 +262,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.114",
]
[[package]]
@@ -343,9 +337,9 @@ dependencies = [
[[package]]
name = "env_filter"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
"regex",
@@ -353,12 +347,12 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.10"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -368,10 +362,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "flate2"
version = "1.0.35"
name = "find-msvc-tools"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "flate2"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -389,15 +389,38 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.15"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "goblin"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "goldberg"
version = "0.1.0"
@@ -428,14 +451,15 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.61"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
@@ -460,9 +484,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.14"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
@@ -488,18 +512,19 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -507,9 +532,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "linux-raw-sys"
@@ -519,23 +544,24 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "miniz_oxide"
version = "0.8.3"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
@@ -578,9 +604,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.20.2"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "password-hash"
@@ -623,9 +649,15 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "powerfmt"
@@ -635,18 +667,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -686,6 +718,7 @@ dependencies = [
"crc32fast",
"dobby-rs",
"ed25519-dalek",
"goblin",
"goldberg",
"jni",
"libc",
@@ -699,18 +732,24 @@ dependencies = [
"serde_json",
"sha2",
"zip",
"zstd 0.13.2",
"zstd 0.13.3",
]
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
@@ -738,14 +777,14 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.17",
]
[[package]]
name = "regex"
version = "1.11.1"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -755,9 +794,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -766,9 +805,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustc_version"
@@ -781,9 +820,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.43"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
@@ -794,15 +833,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.19"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
@@ -813,6 +846,26 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scroll"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "semver"
version = "1.0.27"
@@ -821,34 +874,45 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.114",
]
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -888,6 +952,12 @@ dependencies = [
"rand_core",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "spki"
version = "0.7.3"
@@ -917,9 +987,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.96"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -943,27 +1013,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.114",
]
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]]
name = "typenum"
@@ -973,9 +1043,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.14"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "version_check"
@@ -995,41 +1065,37 @@ dependencies = [
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.96",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1037,42 +1103,92 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.96",
"wasm-bindgen-backend",
"syn 2.0.114",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.52.0"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-targets 0.52.6",
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
@@ -1093,6 +1209,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -1215,24 +1340,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.7.35"
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zerocopy"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"syn 2.0.114",
]
[[package]]
@@ -1261,6 +1391,12 @@ dependencies = [
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "zmij"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
@@ -1272,11 +1408,11 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.13.2"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe 7.2.1",
"zstd-safe 7.2.4",
]
[[package]]
@@ -1291,18 +1427,18 @@ dependencies = [
[[package]]
name = "zstd-safe"
version = "7.2.1"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",

View File

@@ -27,6 +27,7 @@ sha2 = "0.10"
zip = "0.6.6"
base64 = "0.22.1"
ed25519-dalek = { version = "2.1.1", default-features = true, features = ["std"] }
goblin = "0.10.4"
[build-dependencies]
cc = "1.0"

View File

@@ -24,11 +24,16 @@ pub static CLIENT_MODULE: Lazy<MappedLib> = Lazy::new(|| {
pub fn set_native_lib_instance(instance: GlobalRef) {
NATIVE_LIB_INSTANCE.set(instance).expect("NativeLib instance already set");
NATIVE_LIB_INSTANCE
.set(instance)
.expect("NativeLib instance already set");
}
pub fn native_lib_instance() -> GlobalRef {
NATIVE_LIB_INSTANCE.get().expect("NativeLib instance not set").clone()
NATIVE_LIB_INSTANCE
.get()
.expect("NativeLib instance not set")
.clone()
}
pub fn set_java_vm(vm: *mut jni::sys::JavaVM) {
@@ -37,13 +42,16 @@ pub fn set_java_vm(vm: *mut jni::sys::JavaVM) {
pub fn java_vm() -> JavaVM {
unsafe {
JavaVM::from_raw(*JAVA_VM.get().expect("JavaVM not set") as *mut jni::sys::JavaVM).expect("Failed to get JavaVM")
JavaVM::from_raw(*JAVA_VM.get().expect("JavaVM not set") as *mut jni::sys::JavaVM)
.expect("Failed to get JavaVM")
}
}
pub fn attach_jni_env(block: impl FnOnce(&mut jni::JNIEnv)) {
let jvm = java_vm();
let mut env: jni::AttachGuard = jvm.attach_current_thread().expect("Failed to attach to current thread");
let mut env: jni::AttachGuard = jvm
.attach_current_thread()
.expect("Failed to attach to current thread");
block(&mut env);
}
}

View File

@@ -1,11 +1,16 @@
use std::{error::Error, sync::Mutex};
use jni::{objects::JObject, JNIEnv};
use crate::{secstrings, util::get_jni_string};
use jni::{objects::JObject, JNIEnv};
use std::{error::Error, sync::Mutex};
static NATIVE_CONFIG: Mutex<Option<NativeConfig>> = Mutex::new(None);
pub fn native_config() -> NativeConfig {
NATIVE_CONFIG.lock().unwrap().as_ref().expect("NativeConfig not loaded").clone()
NATIVE_CONFIG
.lock()
.unwrap()
.as_ref()
.expect("NativeConfig not loaded")
.clone()
}
#[derive(Debug, Clone)]
@@ -27,11 +32,13 @@ impl NativeConfig {
macro_rules! get_string {
($field:expr) => {
match env.get_field(&obj, $field, "Ljava/lang/String;")?.l()? {
jstring => if !jstring.is_null() {
Some(get_jni_string(env, jstring.into())?)
} else {
None
},
jstring => {
if !jstring.is_null() {
Some(get_jni_string(env, jstring.into())?)
} else {
None
}
}
}
};
}
@@ -73,10 +80,11 @@ pub fn get_blocker_config() -> BlockerConfig {
}
}
pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
NATIVE_CONFIG.lock().unwrap().replace(
NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig")
);
pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
NATIVE_CONFIG
.lock()
.unwrap()
.replace(NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig"));
info!("Config loaded {:?}", native_config());
}

View File

@@ -36,15 +36,3 @@ macro_rules! dobby_hook {
};
}
#[macro_export]
macro_rules! dobby_hook_sym {
($lib:expr, $sym:expr, $hook:expr) => {
if let Some(hook_symbol) = dobby_rs::resolve_symbol($lib, $sym) {
crate::dobby_hook!(hook_symbol, $hook);
debug!("hooked symbol: {}", $sym);
} else {
panic!("Failed to resolve symbol: {}", $sym);
}
};
}

View File

@@ -3,11 +3,11 @@ extern crate log;
mod common;
mod hook;
mod util;
mod mapped_lib;
mod config;
mod hook;
mod mapped_lib;
mod sig;
mod util;
mod modules;
mod security;
@@ -15,7 +15,10 @@ mod secstrings;
use android_logger::Config;
use log::LevelFilter;
use modules::{valdi_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
use modules::{
custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook,
valdi_hook,
};
use jni::{JNIEnv, JavaVM, NativeMethod};
use jni::objects::{JObject, JString, JClass, JValue};
@@ -166,10 +169,11 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
let start_time = std::time::Instant::now();
// load signature cache
if !signature_cache.is_null() {
let sig_cache_str = util::get_jni_string(&mut env, signature_cache).expect("Failed to convert mappings to string");
let sig_cache_str = util::get_jni_string(&mut env, signature_cache)
.expect("Failed to convert mappings to string");
if let Ok(signature_cache) = serde_json::from_str(sig_cache_str.as_str()) {
sig::add_signatures(signature_cache);
} else {
@@ -177,7 +181,11 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
}
}
common::set_native_lib_instance(env.new_global_ref(_class).ok().expect("Failed to create global ref"));
common::set_native_lib_instance(
env.new_global_ref(_class)
.ok()
.expect("Failed to create global ref"),
);
let _ = common::CLIENT_MODULE;

View File

@@ -24,21 +24,24 @@ impl MappedLib {
}
pub fn search(&mut self) -> Result<&Self, Box<dyn Error>> {
procfs::process::Process::myself()?.maps()?.iter().for_each(|map| {
let pathname = &map.pathname;
procfs::process::Process::myself()?
.maps()?
.iter()
.for_each(|map| {
let pathname = &map.pathname;
if let MMapPath::Path(path_buffer) = pathname {
let path = path_buffer.to_string_lossy();
if let MMapPath::Path(path_buffer) = pathname {
let path = path_buffer.to_string_lossy();
if path.contains(&self.name) {
self.regions.push(MappedRegion {
start: map.address.0,
end: map.address.1,
perms: map.perms,
});
if path.contains(&self.name) {
self.regions.push(MappedRegion {
start: map.address.0,
end: map.address.1,
perms: map.perms,
});
}
}
}
});
});
if self.regions.is_empty() {
return Err(format!("No regions found for {}", self.name).into());

View File

@@ -2,33 +2,41 @@ use std::{ffi::CStr, fs};
use nix::libc::{self, c_uint};
use crate::{config, def_hook, dobby_hook_sym};
use crate::{config, def_hook, dobby_hook, modules::util::elf};
def_hook!(
open_hook,
i32,
|path: *const u8, flags: i32, mode: c_uint| {
if let Ok(pathname) = CStr::from_ptr(path).to_str() {
if pathname == "/system/fonts/NotoColorEmoji.ttf" {
if let Some(font_path) = config::native_config().custom_emoji_font_path {
if fs::metadata(&font_path).is_ok() {
return libc::openat(libc::AT_FDCWD, font_path.as_ptr() as *const u8, flags, mode);
} else {
warn!("custom emoji font path does not exist: {}", font_path);
}
def_hook!(open_hook, i32, |path: *const u8,
flags: i32,
mode: c_uint| {
if let Ok(pathname) = CStr::from_ptr(path).to_str() {
if pathname == "/system/fonts/NotoColorEmoji.ttf" {
if let Some(font_path) = config::native_config().custom_emoji_font_path {
if fs::metadata(&font_path).is_ok() {
return libc::openat(
libc::AT_FDCWD,
font_path.as_ptr() as *const u8,
flags,
mode,
);
} else {
warn!("custom emoji font path does not exist: {}", font_path);
}
}
}
open_hook_original.unwrap()(path, flags, mode)
}
);
open_hook_original.unwrap()(path, flags, mode)
});
pub fn init() {
if config::native_config().custom_emoji_font_path.is_none() {
return;
}
dobby_hook_sym!("libc.so", "open", open_hook);
}
let libc = elf::Elf::from_maps("/libc.so").expect("Failed to find libc.so in maps");
if let Some(ptr) = libc.get_symbol_address("open") {
dobby_hook!(ptr as _, open_hook);
} else {
panic!("Failed to find open symbol");
}
}

View File

@@ -4,7 +4,6 @@ use jni::{objects::JObject, sys::jboolean, JNIEnv};
use crate::{common, def_hook, dobby_hook, util::get_jni_string};
def_hook!(
is_same_object,
jboolean,
@@ -20,9 +19,15 @@ def_hook!(
if !env.is_instance_of(&obj1, class).unwrap() {
return is_same_object_original.unwrap()(env, obj1, obj2);
}
let obj1_class_name = env.call_method(&obj1, "getName", "()Ljava/lang/String;", &[]).unwrap().l().unwrap().into();
let class_name = get_jni_string(&mut env, obj1_class_name).expect("Failed to get class name");
let obj1_class_name = env
.call_method(&obj1, "getName", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap()
.into();
let class_name =
get_jni_string(&mut env, obj1_class_name).expect("Failed to get class name");
if class_name.contains("com.snapchat.client.duplex.MessageHandler") {
debug!("is_same_object hook: MessageHandler");
@@ -33,9 +38,11 @@ def_hook!(
}
);
pub fn init() {
common::attach_jni_env(|env| {
dobby_hook!((**env.get_native_interface()).IsSameObject.unwrap() as *mut c_void, is_same_object);
dobby_hook!(
(**env.get_native_interface()).IsSameObject.unwrap() as *mut c_void,
is_same_object
);
});
}
}

View File

@@ -1,44 +1,48 @@
use crate::{
config::{self, native_config},
def_hook, dobby_hook,
modules::util::elf,
};
use nix::libc;
use std::fs;
use nix::libc;
def_hook!(fstat_hook, i32, |fd: i32, statbuf: *mut libc::stat| {
if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) {
let link_str = link.to_string_lossy();
let config = native_config();
if config.disable_metrics && link_str.contains("files/blizzardv2/queues") {
if libc::unlink((link_str.to_string() + "\0").as_ptr()) == -1 {
warn!("Failed to unlink {}", link_str);
}
return -1;
}
use crate::{config::{self, native_config}, def_hook, dobby_hook_sym};
def_hook!(
fstat_hook,
i32,
|fd: i32, statbuf: *mut libc::stat| {
if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) {
let link_str = link.to_string_lossy();
let config = native_config();
if config.disable_metrics && link_str.contains("files/blizzardv2/queues") {
if libc::unlink((link_str.to_string() + "\0").as_ptr()) == -1 {
warn!("Failed to unlink {}", link_str);
}
if config.disable_bitmoji {
if link_str.contains("com.snap.file_manager_4_SCContent") {
return -1;
}
if config.disable_bitmoji {
if link_str.contains("com.snap.file_manager_4_SCContent") {
if link_str.contains("/files/file_manager/") {
let lower = link_str.to_lowercase();
if lower.contains("bitmoji") {
return -1;
}
if link_str.contains("/files/file_manager/") {
let lower = link_str.to_lowercase();
if lower.contains("bitmoji") {
return -1;
}
}
}
}
fstat_hook_original.unwrap()(fd, statbuf)
}
);
fstat_hook_original.unwrap()(fd, statbuf)
});
pub fn init() {
let config = config::native_config();
if config.disable_metrics || config.disable_bitmoji {
dobby_hook_sym!("libc.so", "fstat", fstat_hook);
let libc = elf::Elf::from_maps("/libc.so").expect("Failed to find libc.so in maps");
if let Some(ptr) = libc.get_symbol_address("fstat") {
dobby_hook!(ptr as _, fstat_hook);
} else {
panic!("Failed to find fstat symbol");
}
}
}
}

View File

@@ -1,12 +1,20 @@
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex};
use std::{
collections::HashMap,
ffi::{c_void, CStr},
sync::Mutex,
};
use jni::{objects::{JByteArray, JString}, JNIEnv};
use jni::{
objects::{JByteArray, JString},
JNIEnv,
};
use nix::libc;
use once_cell::sync::Lazy;
use crate::{def_hook, dobby_hook_sym};
use crate::{def_hook, dobby_hook, modules::util::elf};
static SHARED_LIBRARIES: Lazy<Mutex<HashMap<String, Box<Vec<i8>>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static SHARED_LIBRARIES: Lazy<Mutex<HashMap<String, Box<Vec<i8>>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
def_hook!(
linker_openat,
@@ -17,8 +25,13 @@ def_hook!(
if let Some(content) = SHARED_LIBRARIES.lock().unwrap().remove(&pathname_str) {
let memfd = libc::syscall(libc::SYS_memfd_create, "jit-cache\0".as_ptr(), 0) as i32;
let content = content.into_boxed_slice();
if libc::write(memfd, content.as_ptr() as *const c_void, content.len() as libc::size_t) == -1 {
if libc::write(
memfd,
content.as_ptr() as *const c_void,
content.len() as libc::size_t,
) == -1
{
panic!("failed to write to memfd");
}
@@ -36,25 +49,44 @@ def_hook!(
}
);
pub fn add_linker_shared_library(mut env: JNIEnv, _: *mut c_void, path: JString, content: JByteArray) {
pub fn add_linker_shared_library(
mut env: JNIEnv,
_: *mut c_void,
path: JString,
content: JByteArray,
) {
let path = env.get_string(&path).unwrap().to_str().unwrap().to_string();
let content_length = env.get_array_length(&content).expect("Failed to get array length");
let content_length = env
.get_array_length(&content)
.expect("Failed to get array length");
let mut content_buffer = Box::new(vec![0i8; content_length as usize]);
env.get_byte_array_region(content, 0, content_buffer.as_mut_slice()).expect("Failed to get byte array region");
env.get_byte_array_region(content, 0, content_buffer.as_mut_slice())
.expect("Failed to get byte array region");
debug!("added shared library: {}", path);
SHARED_LIBRARIES.lock().unwrap().insert(path, content_buffer);
SHARED_LIBRARIES
.lock()
.unwrap()
.insert(path, content_buffer);
}
pub fn init() {
#[cfg(target_arch = "aarch64")]
{
dobby_hook_sym!("linker64", "__dl___openat", linker_openat);
}
#[cfg(target_arch = "arm")]
{
dobby_hook_sym!("linker", "__dl___openat", linker_openat);
let linker = {
#[cfg(target_arch = "aarch64")]
{
elf::Elf::from_maps("/linker64").expect("Failed to find linker64 in maps")
}
#[cfg(target_arch = "arm")]
{
elf::Elf::from_maps("/linker").expect("Failed to find linker in maps")
}
};
if let Some(ptr) = linker.get_symbol_address("__dl___openat") {
dobby_hook!(ptr as _, linker_openat);
} else {
panic!("Failed to find open symbol");
}
}

View File

@@ -1,8 +1,8 @@
pub mod util;
pub mod linker_hook;
pub mod duplex_hook;
pub mod sqlite_hook;
pub mod fstat_hook;
pub mod unary_call_hook;
pub mod valdi_hook;
pub mod custom_font_hook;
pub mod duplex_hook;
pub mod fstat_hook;
pub mod linker_hook;
pub mod sqlite_hook;
pub mod unary_call_hook;
pub mod util;
pub mod valdi_hook;

View File

@@ -1,25 +1,34 @@
use std::{collections::HashMap, ffi::{c_void, CStr}, mem::size_of, ptr::addr_of_mut, sync::Mutex};
use std::{
collections::HashMap,
ffi::{c_void, CStr},
mem::size_of,
ptr::addr_of_mut,
sync::Mutex,
};
use jni::{objects::{JObject, JString}, JNIEnv};
use jni::{
objects::{JObject, JString},
JNIEnv,
};
use nix::libc::{self, pthread_mutex_t};
use once_cell::sync::Lazy;
use crate::{common, def_hook, dobby_hook, sig, util::get_jni_string};
#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct Sqlite3Mutex {
mutex: pthread_mutex_t
mutex: pthread_mutex_t,
}
#[repr(C)]
struct Sqlite3 {
pad: [u8; 3 * size_of::<usize>()],
mutex: *mut Sqlite3Mutex
mutex: *mut Sqlite3Mutex,
}
static SQLITE3_MUTEX_MAP: Lazy<Mutex<HashMap<String, pthread_mutex_t>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static SQLITE3_MUTEX_MAP: Lazy<Mutex<HashMap<String, pthread_mutex_t>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
def_hook!(
sqlite3_open,
@@ -31,27 +40,38 @@ def_hook!(
let sqlite3_mutex = (**pp_db).mutex;
if sqlite3_mutex != std::ptr::null_mut() {
let filename = CStr::from_ptr(filename).to_string_lossy().to_string().split("/").last().expect("Failed to get filename").to_string();
let filename = CStr::from_ptr(filename)
.to_string_lossy()
.to_string()
.split("/")
.last()
.expect("Failed to get filename")
.to_string();
debug!("sqlite3_open hook {:?}", filename);
SQLITE3_MUTEX_MAP.lock().unwrap().insert(
filename,
(*sqlite3_mutex).mutex
);
SQLITE3_MUTEX_MAP
.lock()
.unwrap()
.insert(filename, (*sqlite3_mutex).mutex);
}
}
result
}
}
);
pub fn lock_database(mut env: JNIEnv, _: *mut c_void, filename: JString, runnable: JObject) {
let database_filename = get_jni_string(&mut env, filename).expect("Failed to get database filename");
let mutex = SQLITE3_MUTEX_MAP.lock().unwrap().get(&database_filename).map(|mutex| *mutex);
let database_filename =
get_jni_string(&mut env, filename).expect("Failed to get database filename");
let mutex = SQLITE3_MUTEX_MAP
.lock()
.unwrap()
.get(&database_filename)
.map(|mutex| *mutex);
let call_runnable = || {
env.call_method(runnable, "run", "()V", &[]).expect("Failed to call run method");
env.call_method(runnable, "run", "()V", &[])
.expect("Failed to call run method");
};
if let Some(mut mutex) = mutex {
@@ -71,16 +91,17 @@ pub fn lock_database(mut env: JNIEnv, _: *mut c_void, filename: JString, runnabl
}
}
pub fn init() {
if let Some(signature) = sig::find_signature(
&common::CLIENT_MODULE,
"FF FF 00 A9 3F 00 00 F9", -0x3C,
"9A 46 90 46 78 44 89 46 05 68",-0xd
&common::CLIENT_MODULE,
"FF FF 00 A9 3F 00 00 F9",
-0x3C,
"9A 46 90 46 78 44 89 46 05 68",
-0xd,
) {
debug!("Found sqlite3_open signature: {:#x}", signature);
dobby_hook!(signature as *mut c_void, sqlite3_open);
} else {
warn!("Failed to find sqlite3_open signature");
panic!("Failed to find sqlite3_open signature");
}
}
}

View File

@@ -1,17 +1,23 @@
use std::ffi::{c_void, CStr};
use jni::{objects::{JByteArray, JMethodID, JValue}, signature::ReturnType};
use jni::{
objects::{JByteArray, JMethodID, JValue},
signature::ReturnType,
};
use nix::libc;
use once_cell::sync::OnceCell;
use crate::{common::{self}, def_hook, dobby_hook, sig};
use crate::{
common::{self},
def_hook, dobby_hook, sig,
};
#[repr(C)]
#[derive(Copy, Clone)]
struct RefCountedSliceByteBuffer {
struct SliceByteBuffer {
ref_counter: *mut c_void,
length: usize,
data: *mut u8
data: *mut u8,
}
#[repr(C)]
@@ -19,7 +25,7 @@ struct GrpcByteBuffer {
reserved: *mut c_void,
type_: *mut c_void,
compression: *mut c_void,
slice_buffer: *mut RefCountedSliceByteBuffer
slice_buffer: *mut SliceByteBuffer,
}
static NATIVE_LIB_ON_UNARY_CALL_METHOD: OnceCell<JMethodID> = OnceCell::new();
@@ -27,7 +33,12 @@ static NATIVE_LIB_ON_UNARY_CALL_METHOD: OnceCell<JMethodID> = OnceCell::new();
def_hook!(
unary_call,
*mut c_void,
|unk1: *mut c_void, uri: *const u8, grpc_byte_buffer: *mut *mut GrpcByteBuffer, unk4: *mut c_void, unk5: *mut c_void, unk6: *mut c_void| {
|unk1: *mut c_void,
uri: *const u8,
grpc_byte_buffer: *mut *mut GrpcByteBuffer,
unk4: *mut c_void,
unk5: *mut c_void,
unk6: *mut c_void| {
macro_rules! call_original {
() => {
unary_call_original.unwrap()(unk1, uri, grpc_byte_buffer, unk4, unk5, unk6)
@@ -45,45 +56,73 @@ def_hook!(
let mut env = java_vm.get_env().expect("Failed to get JNIEnv");
let slice_buffer_length = slice_buffer.length as usize;
let jni_buffer = env.new_byte_array(slice_buffer_length as i32).expect("Failed to create new byte array");
env.set_byte_array_region(&jni_buffer, 0, std::slice::from_raw_parts(slice_buffer.data as *const i8, slice_buffer_length)).expect("Failed to set byte array region");
let jni_buffer = env
.new_byte_array(slice_buffer_length as i32)
.expect("Failed to create new byte array");
env.set_byte_array_region(
&jni_buffer,
0,
std::slice::from_raw_parts(slice_buffer.data as *const i8, slice_buffer_length),
)
.expect("Failed to set byte array region");
let uri_str = CStr::from_ptr(uri).to_str().unwrap();
let native_request_data_object = env.call_method_unchecked(
common::native_lib_instance(),
NATIVE_LIB_ON_UNARY_CALL_METHOD.get().unwrap(),
ReturnType::Object,
&[
JValue::from(&env.new_string(uri_str).unwrap()).as_jni(),
JValue::from(&jni_buffer).as_jni()
]
).expect("Failed to call onNativeUnaryCall method").l().unwrap();
let native_request_data_object = env
.call_method_unchecked(
common::native_lib_instance(),
NATIVE_LIB_ON_UNARY_CALL_METHOD.get().unwrap(),
ReturnType::Object,
&[
JValue::from(&env.new_string(uri_str).unwrap()).as_jni(),
JValue::from(&jni_buffer).as_jni(),
],
)
.expect("Failed to call onNativeUnaryCall method")
.l()
.unwrap();
if native_request_data_object.is_null() {
return call_original!();
}
let is_canceled = env.get_field(&native_request_data_object, "canceled", "Z").expect("Failed to get canceled field").z().unwrap();
let is_canceled = env
.get_field(&native_request_data_object, "canceled", "Z")
.expect("Failed to get canceled field")
.z()
.unwrap();
if is_canceled {
info!("canceled request for {}", uri_str);
return std::ptr::null_mut();
}
let new_buffer: JByteArray = env.get_field(&native_request_data_object, "buffer", "[B").expect("Failed to get buffer field").l().unwrap().into();
let new_buffer_length = env.get_array_length(&new_buffer).expect("Failed to get array length") as usize;
let new_buffer: JByteArray = env
.get_field(&native_request_data_object, "buffer", "[B")
.expect("Failed to get buffer field")
.l()
.unwrap()
.into();
let new_buffer_length = env
.get_array_length(&new_buffer)
.expect("Failed to get array length") as usize;
let mut new_buffer_data = Box::new(vec![0i8; new_buffer_length]);
env.get_byte_array_region(&new_buffer, 0, new_buffer_data.as_mut_slice()).expect("Failed to get byte array region");
env.get_byte_array_region(&new_buffer, 0, new_buffer_data.as_mut_slice())
.expect("Failed to get byte array region");
let ref_counter_struct_size = (slice_buffer.data as usize) - (slice_buffer.ref_counter as usize);
let ref_counter_struct_size =
(slice_buffer.data as usize) - (slice_buffer.ref_counter as usize);
//we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it
let new_ref = {
let new_ref = libc::malloc(ref_counter_struct_size + new_buffer_length) as *mut c_void;
libc::memcpy(new_ref, slice_buffer.ref_counter, ref_counter_struct_size);
libc::memcpy(new_ref.offset(ref_counter_struct_size as isize), new_buffer_data.as_ptr() as *const c_void, new_buffer_length);
libc::memcpy(
new_ref.offset(ref_counter_struct_size as isize),
new_buffer_data.as_ptr() as *const c_void,
new_buffer_length,
);
libc::free(slice_buffer.ref_counter);
new_ref
};
@@ -104,20 +143,25 @@ def_hook!(
pub fn init() {
if let Some(signature) = sig::find_signature(
&common::CLIENT_MODULE,
"A8 03 1F F8 ?? ?? 00 94 ?? ?? ?? 91 ?? ?? ?? A9", -0x48,
"0A 90 00 F0 3F F9", -0x37
"AA A8 03 1F F8 ?? ?? 00 94 ?? ?? 05 91",
-0x47,
"0A 90 00 F0 3F F9",
-0x37,
) {
dobby_hook!(signature as *mut c_void, unary_call);
common::attach_jni_env(|env| {
NATIVE_LIB_ON_UNARY_CALL_METHOD.set(
env.get_method_id(
env.get_object_class(common::native_lib_instance()).unwrap(),
"onNativeUnaryCall",
"(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;"
).expect("Failed to get onNativeUnaryCall method id")
).expect("unary call method already set");
NATIVE_LIB_ON_UNARY_CALL_METHOD
.set(
env.get_method_id(
env.get_object_class(common::native_lib_instance()).unwrap(),
"onNativeUnaryCall",
"(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;",
)
.expect("Failed to get onNativeUnaryCall method id"),
)
.expect("unary call method already set");
});
} else {
error!("Can't find unaryCall signature");
panic!("Can't find unaryCall signature");
}
}

View File

@@ -0,0 +1,61 @@
use procfs::process::MMapPath;
pub struct Elf<'elf> {
base_address: usize,
elf: goblin::elf::Elf<'elf>,
_buffer: &'elf [u8],
}
impl<'elf> Elf<'elf> {
pub fn from_maps(lib: &str) -> Option<Self> {
let maps = procfs::process::Process::myself().ok()?.maps().ok()?;
for memory_map in maps.iter() {
if let MMapPath::Path(path) = &memory_map.pathname {
let path = path.to_string_lossy();
if !path.contains(lib) {
continue;
}
let file_data = std::fs::read(path.to_string()).ok()?;
let file_buffer = Box::leak(file_data.into_boxed_slice());
if let Ok(elf) = goblin::elf::Elf::parse(file_buffer) {
return Some(Elf {
base_address: memory_map.address.0 as usize,
elf,
_buffer: file_buffer,
});
} else {
warn!(
"Failed to parse ELF for library {} at address {:x}",
lib, memory_map.address.0
);
}
}
}
None
}
pub fn get_symbol_address(&self, symbol: &str) -> Option<usize> {
for sym in &self.elf.dynsyms {
if let Some(name) = self.elf.dynstrtab.get_at(sym.st_name) {
if name == symbol {
return Some(self.base_address + sym.st_value as usize);
}
}
}
for sym in &self.elf.syms {
if let Some(name) = self.elf.strtab.get_at(sym.st_name) {
if name == symbol {
return Some(self.base_address + sym.st_value as usize);
}
}
}
None
}
}

View File

@@ -1 +1,2 @@
pub mod elf;
pub mod valdi_utils;

View File

@@ -43,7 +43,12 @@ pub struct ValdiModule {
impl ValdiModule {
pub fn parse(buffer: Vec<u8>) -> Result<ValdiModule, Error> {
let mut offset = 0;
let magic = u32::from_be_bytes([buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3]]);
let magic = u32::from_be_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]);
offset += 4;
@@ -85,13 +90,12 @@ impl ValdiModule {
tags.push(ModuleTag::new(has_padding, tag_buffer));
}
let tags = tags.chunks(2).map(|chunk| {
(chunk[0].clone(), chunk[1].clone())
}).collect();
let tags = tags
.chunks(2)
.map(|chunk| (chunk[0].clone(), chunk[1].clone()))
.collect();
Ok(ValdiModule {
tags,
})
Ok(ValdiModule { tags })
}
pub fn to_bytes(&self) -> Vec<u8> {

View File

@@ -1,35 +1,29 @@
#![allow(dead_code, unused_imports)]
use super::util::valdi_utils::{ValdiModule, ModuleTag};
use std::{collections::HashMap, ffi::c_void, sync::Mutex};
use super::util::valdi_utils::{ModuleTag, ValdiModule};
use crate::{config, def_hook, dobby_hook, modules::util::elf, util::get_jni_string};
use jni::{objects::JString, JNIEnv};
use once_cell::sync::Lazy;
use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, util::get_jni_string};
use std::{
collections::HashMap,
ffi::c_void,
sync::Mutex,
};
static AASSET_MAP: Lazy<Mutex<HashMap<usize, Vec<u8>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static LOADER_DATA: Mutex<Option<String>> = Mutex::new(None);
def_hook!(
aasset_get_length,
i32,
|arg0: *mut c_void| {
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.len() as i32;
}
aasset_get_length_original.unwrap()(arg0)
def_hook!(aasset_get_length, i32, |arg0: *mut c_void| {
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.len() as i32;
}
);
aasset_get_length_original.unwrap()(arg0)
});
def_hook!(
aasset_get_buffer,
*const c_void,
|arg0: *mut c_void| {
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.as_ptr() as *const c_void;
}
aasset_get_buffer_original.unwrap()(arg0)
def_hook!(aasset_get_buffer, *const c_void, |arg0: *mut c_void| {
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.as_ptr() as *const c_void;
}
);
aasset_get_buffer_original.unwrap()(arg0)
});
def_hook!(
aasset_manager_open,
@@ -45,9 +39,13 @@ def_hook!(
let loader_data = LOADER_DATA.lock().unwrap().clone().expect("No loader data");
let archive_buffer: Vec<u8> = std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize).to_vec();
let decompressed = zstd::stream::decode_all(&archive_buffer[..]).expect("Failed to decompress valdi archive");
let mut valdi_module = ValdiModule::parse(decompressed).expect("Failed to parse valdi module");
let archive_buffer: Vec<u8> =
std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize)
.to_vec();
let decompressed = zstd::stream::decode_all(&archive_buffer[..])
.expect("Failed to decompress valdi archive");
let mut valdi_module =
ValdiModule::parse(decompressed).expect("Failed to parse valdi module");
let mut tags = valdi_module.get_tags();
let mut new_tags = Vec::new();
@@ -58,19 +56,22 @@ def_hook!(
continue;
}
let old_file_name = name.split_once(".").unwrap().0.to_owned() + rand::random::<u32>().to_string().as_str();
let old_file_name = name.split_once(".").unwrap().0.to_owned()
+ rand::random::<u32>().to_string().as_str();
tag1.set_buffer((old_file_name.to_owned() + ".js").as_bytes().to_vec());
let original_module_path = path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name;
let original_module_path =
path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name;
let hooked_module = format!("{};module.exports = require(\"{}\");", loader_data, original_module_path);
new_tags.push(
(
ModuleTag::new(true, name.as_bytes().to_vec()),
ModuleTag::new(true, hooked_module.as_bytes().to_vec())
)
let hooked_module = format!(
"{};module.exports = require(\"{}\");",
loader_data, original_module_path
);
new_tags.push((
ModuleTag::new(true, name.as_bytes().to_vec()),
ModuleTag::new(true, hooked_module.as_bytes().to_vec()),
));
debug!("Valdi loader injected in {}", name);
break;
}
@@ -79,22 +80,22 @@ def_hook!(
valdi_module.set_tags(tags);
let compressed = valdi_module.to_bytes();
let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress");
let compressed =
zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress");
AASSET_MAP.lock().unwrap().insert(handle as usize, compressed);
AASSET_MAP
.lock()
.unwrap()
.insert(handle as usize, compressed);
}
handle
}
);
def_hook!(
aasset_close,
c_void,
|handle: *mut c_void| {
AASSET_MAP.lock().unwrap().remove(&(handle as usize));
aasset_close_original.unwrap()(handle)
}
);
def_hook!(aasset_close, c_void, |handle: *mut c_void| {
AASSET_MAP.lock().unwrap().remove(&(handle as usize));
aasset_close_original.unwrap()(handle)
});
pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) {
let new_code = get_jni_string(&mut env, code).expect("Failed to get loader code");
@@ -103,11 +104,33 @@ pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) {
pub fn init() {
if !config::native_config().valdi_hooks {
return
return;
}
dobby_hook_sym!("libandroid.so", "AAsset_getBuffer", aasset_get_buffer);
dobby_hook_sym!("libandroid.so", "AAsset_getLength", aasset_get_length);
dobby_hook_sym!("libandroid.so", "AAsset_close", aasset_close);
dobby_hook_sym!("libandroid.so", "AAssetManager_open", aasset_manager_open);
let lib_android =
elf::Elf::from_maps("/libandroid.so").expect("Failed to find libandroid.so in maps");
if let Some(ptr) = lib_android.get_symbol_address("AAsset_getBuffer") {
dobby_hook!(ptr as _, aasset_get_buffer);
} else {
panic!("Failed to find AAsset_getBuffer symbol");
}
if let Some(ptr) = lib_android.get_symbol_address("AAsset_getLength") {
dobby_hook!(ptr as _, aasset_get_length);
} else {
panic!("Failed to find AAsset_getLength symbol");
}
if let Some(ptr) = lib_android.get_symbol_address("AAsset_close") {
dobby_hook!(ptr as _, aasset_close);
} else {
panic!("Failed to find AAsset_close symbol");
}
if let Some(ptr) = lib_android.get_symbol_address("AAssetManager_open") {
dobby_hook!(ptr as _, aasset_manager_open);
} else {
panic!("Failed to find AAssetManager_open symbol");
}
}

View File

@@ -61,8 +61,18 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o
let mut mask = Vec::new();
let mut i = 0;
if let Some(cache) = SIGNATURE_CACHE.lock().unwrap().iter().find(|(sig, _)| sig == pattern) {
return cache.1.clone().into_iter().map(|offset| module_base + offset).collect();
if let Some(cache) = SIGNATURE_CACHE
.lock()
.unwrap()
.iter()
.find(|(sig, _)| sig == pattern)
{
return cache
.1
.clone()
.into_iter()
.map(|offset| module_base + offset)
.collect();
}
while i < pattern.len() {
@@ -70,7 +80,7 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o
bytes.push(0);
mask.push('?');
} else {
bytes.push(u8::from_str_radix(&pattern[i..i+2], 16).unwrap());
bytes.push(u8::from_str_radix(&pattern[i..i + 2], 16).unwrap());
mask.push('x');
}
i += 3;
@@ -92,7 +102,10 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o
}
if found {
if once {
SIGNATURE_CACHE.lock().unwrap().push((pattern.to_string(), vec![i]));
SIGNATURE_CACHE
.lock()
.unwrap()
.push((pattern.to_string(), vec![i]));
return vec![module_base + i];
}
results.push(module_base + i);
@@ -100,14 +113,22 @@ pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, o
i += 1;
}
SIGNATURE_CACHE.lock().unwrap().push((pattern.to_string(), results.clone()));
SIGNATURE_CACHE
.lock()
.unwrap()
.push((pattern.to_string(), results.clone()));
results
}
pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Option<usize> {
let executable_regions = mapped_lib.regions.iter().filter(|region| {
region.perms.contains(MMPermissions::EXECUTE)
}).collect::<Vec<_>>();
let executable_regions = mapped_lib
.regions
.iter()
.filter(|region| {
region.perms.contains(MMPermissions::EXECUTE)
&& region.perms.contains(MMPermissions::READ)
})
.collect::<Vec<_>>();
for region in executable_regions {
let size = (region.end - region.start) as usize;
@@ -117,16 +138,27 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio
let bytes_buffer = match read_region_bytes(module_base, size) {
Some(buffer) => buffer,
None => {
warn!("Unable to read executable region: {:#x} - {:#x}", region.start, region.end);
warn!(
"Unable to read executable region: {:#x} - {:#x}",
region.start, region.end
);
continue;
}
};
let results = find_signatures(module_base, &bytes_buffer, pattern, true);
if results.is_empty() {
warn!("Signature not found in region: {:#x} - {:#x}", region.start, region.end);
warn!(
"Signature not found in region: {:#x} - {:#x}",
region.start, region.end
);
} else {
debug!("Found {} results in region: {:#x} - {:#x}", results.len(), region.start, region.end);
debug!(
"Found {} results in region: {:#x} - {:#x}",
results.len(),
region.start,
region.end
);
return Some(results[0]);
}
}
@@ -135,13 +167,21 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio
None
}
pub fn find_signature(mapped_lib: &MappedLib, _arm64_pattern: &str, _arm64_offset: i64, _arm32_pattern: &str, _arm32_offset: i64) -> Option<usize> {
pub fn find_signature(
mapped_lib: &MappedLib,
_arm64_pattern: &str,
_arm64_offset: i64,
_arm32_pattern: &str,
_arm32_offset: i64,
) -> Option<usize> {
#[cfg(target_arch = "aarch64")]
{
return find_signature_executable(mapped_lib, _arm64_pattern).map(|address| (address as i64 + _arm64_offset) as usize);
return find_signature_executable(mapped_lib, _arm64_pattern)
.map(|address| (address as i64 + _arm64_offset) as usize);
}
#[cfg(target_arch = "arm")]
{
return find_signature_executable(mapped_lib, _arm32_pattern).map(|address| (address as i64 + _arm32_offset) as usize);
return find_signature_executable(mapped_lib, _arm32_pattern)
.map(|address| (address as i64 + _arm32_offset) as usize);
}
}