v1.1.0: Stable!

This commit is contained in:
particle-box
2026-01-13 23:27:32 +05:30
parent dd27ed9450
commit 1bf6335ca2
305 changed files with 13061 additions and 5218 deletions

View File

@@ -101,8 +101,16 @@ ensure_omvll_bundle() {
fi
if [ ! -f "$OMVLL_PLUGIN_PATH" ]; then
echo "Failed to locate O-MVLL plugin under $OMVLL_PLUGIN_PATH" >&2
exit 1
local found_plugin
found_plugin=$(find "$OMVLL_CACHE_DIR" -type f -name 'omvll-ndk.so' | head -n1 || true)
if [ -n "$found_plugin" ] && [ -f "$found_plugin" ]; then
OMVLL_PLUGIN_PATH="$found_plugin"
else
echo "Failed to locate O-MVLL plugin under $OMVLL_PLUGIN_PATH" >&2
echo "Contents:" >&2
ls -la "$OMVLL_CACHE_DIR" >&2 || true
exit 1
fi
fi
if [ -z "${OMVLL_PYTHONPATH:-}" ]; then

View File

@@ -245,23 +245,25 @@ val syncTasks = cargoTargets.mapIndexed { index, target ->
dependsOn(cargoTask)
val outputLibName = nativeLibFileName
inputs.property("outputLibName", outputLibName)
val builtLibProvider = providers.provider {
val wslCandidate = File(wslStagingDir, "native/rust/target/${target.triple}/release/libpurrfectsnap.so")
val localCandidate = layout.projectDirectory.file("rust/target/${target.triple}/release/libpurrfectsnap.so").asFile
when {
wslCandidate.exists() -> wslCandidate
localCandidate.exists() -> localCandidate
else -> error("Native library not found for ${target.abi}. Tried $wslCandidate and $localCandidate")
}
val wslCandidate = File(wslStagingDir, "native/rust/target/${target.triple}/release/libpurrfectsnap.so")
val localCandidate = layout.projectDirectory.file("rust/target/${target.triple}/release/libpurrfectsnap.so").asFile
val sourceLibProvider = providers.provider {
if (wslCandidate.exists()) wslCandidate else localCandidate
}
from(builtLibProvider) {
from(sourceLibProvider) {
rename { outputLibName }
}
from(sourceLibProvider) {
rename { "libpurrfectsnap.so" }
}
into(layout.buildDirectory.dir("rustJniLibs/android/${target.abi}"))
inputs.files(builtLibProvider)
inputs.files(sourceLibProvider)
val checksumsDir = layout.buildDirectory.dir("checksums")
doLast {
val file = File(destinationDir, outputLibName)
if (!file.exists()) {
error("Native library not found for ${target.abi}. Expected ${file.absolutePath}")
}
val crc = CRC32()
crc.update(file.readBytes())
val checksumsDirFile = checksumsDir.get().asFile
@@ -334,5 +336,6 @@ tasks.matching { it.name.startsWith("pre") && it.name.endsWith("Build") }.config
}
dependencies {
implementation("androidx.annotation:annotation:1.7.1")
implementation("com.google.code.gson:gson:2.10.1")
}

View File

@@ -6,62 +6,154 @@
static const uint8_t KEY = 0x5Au;
static const uint8_t DET_LENS[] = {
5, 11, 6, 9, 15, 16, 7, 12, 9, 14, 23, 10, 21, 24, 10
5, 11, 6, 9, 15, 16, 7, 11, 25, 9, 14, 13, 16, 23, 21, 10, 21, 24, 21,
17, 10, 19, 18, 18, 19, 18, 17, 27, 20, 18, 16, 23, 13, 27, 22, 28,
19, 14
};
static const uint8_t DET_ENC[] = {
// "argos"
'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,
// "attestation"
'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,
// "attest"
'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,
// "integrity"
'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "clientintegrity"
'c'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "client_integrity"
'c'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'_'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "scargos"
's'^KEY,'c'^KEY,'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,
// "argosservice"
'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,'s'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,
// "safetynet"
's'^KEY,'a'^KEY,'f'^KEY,'e'^KEY,'t'^KEY,'y'^KEY,'n'^KEY,'e'^KEY,'t'^KEY,
// "play_integrity"
'p'^KEY,'l'^KEY,'a'^KEY,'y'^KEY,'_'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "android_key_attestation"
'a'^KEY,'n'^KEY,'d'^KEY,'r'^KEY,'o'^KEY,'i'^KEY,'d'^KEY,'_'^KEY,'k'^KEY,'e'^KEY,'y'^KEY,'_'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,
// "app_attest"
'a'^KEY,'p'^KEY,'p'^KEY,'_'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,
// "getattestationheaders"
'g'^KEY,'e'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'h'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "computeattestationheader"
'c'^KEY,'o'^KEY,'m'^KEY,'p'^KEY,'u'^KEY,'t'^KEY,'e'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'h'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,
// "hermod_dup"
'h'^KEY,'e'^KEY,'r'^KEY,'m'^KEY,'o'^KEY,'d'^KEY,'_'^KEY,'d'^KEY,'u'^KEY,'p'^KEY,
0x3B, 0x28, 0x3D, 0x35, 0x29, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B,
0x2E, 0x33, 0x35, 0x34, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x33,
0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x39, 0x36, 0x33,
0x3F, 0x34, 0x2E, 0x33, 0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33, 0x2E,
0x23, 0x39, 0x36, 0x33, 0x3F, 0x34, 0x2E, 0x05, 0x33, 0x34, 0x2E,
0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x29, 0x39, 0x3B, 0x28, 0x3D,
0x35, 0x29, 0x3B, 0x28, 0x3D, 0x35, 0x29, 0x39, 0x36, 0x33, 0x3F,
0x34, 0x2E, 0x2A, 0x36, 0x3B, 0x2E, 0x3C, 0x35, 0x28, 0x37, 0x39,
0x36, 0x33, 0x3F, 0x34, 0x2E, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E,
0x3B, 0x2E, 0x33, 0x35, 0x34, 0x29, 0x3B, 0x3C, 0x3F, 0x2E, 0x23,
0x34, 0x3F, 0x2E, 0x2A, 0x36, 0x3B, 0x23, 0x05, 0x33, 0x34, 0x2E,
0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x2A, 0x36, 0x3B, 0x23, 0x33,
0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x3F, 0x22, 0x2A,
0x28, 0x3F, 0x29, 0x29, 0x33, 0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33,
0x2E, 0x23, 0x3B, 0x34, 0x3E, 0x28, 0x35, 0x33, 0x3E, 0x05, 0x31,
0x3F, 0x23, 0x05, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x3B, 0x34, 0x3E, 0x28, 0x35, 0x33, 0x3E, 0x31,
0x3F, 0x23, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33,
0x35, 0x34, 0x3B, 0x2A, 0x2A, 0x05, 0x3B, 0x2E, 0x2E, 0x3F, 0x29,
0x2E, 0x3D, 0x3F, 0x2E, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B,
0x2E, 0x33, 0x35, 0x34, 0x32, 0x3F, 0x3B, 0x3E, 0x3F, 0x28, 0x29,
0x39, 0x35, 0x37, 0x2A, 0x2F, 0x2E, 0x3F, 0x3B, 0x2E, 0x2E, 0x3F,
0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x32, 0x3F, 0x3B, 0x3E,
0x3F, 0x28, 0x3D, 0x3F, 0x2E, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E,
0x3B, 0x2E, 0x33, 0x35, 0x34, 0x2A, 0x3B, 0x23, 0x36, 0x35, 0x3B,
0x3E, 0x3D, 0x3F, 0x34, 0x3F, 0x28, 0x3B, 0x2E, 0x3F, 0x29, 0x33,
0x3D, 0x34, 0x3B, 0x2E, 0x2F, 0x28, 0x3F, 0x32, 0x3F, 0x28, 0x37,
0x35, 0x3E, 0x05, 0x3E, 0x2F, 0x2A, 0x3E, 0x3F, 0x2C, 0x33, 0x39,
0x3F, 0x05, 0x2C, 0x3F, 0x28, 0x33, 0x3C, 0x33, 0x39, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x3E, 0x3F, 0x2C, 0x33, 0x39, 0x3F, 0x05, 0x3C,
0x33, 0x34, 0x3D, 0x3F, 0x28, 0x2A, 0x28, 0x33, 0x34, 0x2E, 0x3E,
0x3F, 0x2C, 0x33, 0x39, 0x3F, 0x05, 0x3B, 0x2E, 0x2E, 0x3F, 0x29,
0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x39, 0x36, 0x33, 0x3F, 0x34,
0x2E, 0x05, 0x2C, 0x3F, 0x28, 0x33, 0x3C, 0x33, 0x39, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x39, 0x36, 0x33, 0x3F, 0x34, 0x2E, 0x05, 0x3B,
0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x2C,
0x3F, 0x34, 0x3E, 0x35, 0x28, 0x3B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E,
0x3B, 0x2E, 0x33, 0x35, 0x34, 0x3D, 0x35, 0x35, 0x3D, 0x36, 0x3F,
0x3B, 0x34, 0x3E, 0x28, 0x35, 0x33, 0x3E, 0x31, 0x3F, 0x23, 0x3B,
0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x3D,
0x3F, 0x2E, 0x29, 0x33, 0x3D, 0x34, 0x3F, 0x3E, 0x3B, 0x2E, 0x2E,
0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x3B, 0x2E, 0x2E,
0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x37, 0x3B, 0x34,
0x3B, 0x3D, 0x3F, 0x28, 0x2E, 0x32, 0x33, 0x28, 0x3E, 0x2A, 0x3B,
0x28, 0x2E, 0x23, 0x3B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x2E, 0x32,
0x33, 0x28, 0x3E, 0x2A, 0x3B, 0x28, 0x2E, 0x23, 0x3B, 0x39, 0x39,
0x3F, 0x29, 0x29, 0x29, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x2E,
0x32, 0x33, 0x28, 0x3E, 0x2A, 0x3B, 0x28, 0x2E, 0x23, 0x3B, 0x2A,
0x2A, 0x3D, 0x28, 0x2A, 0x39, 0x2E, 0x32, 0x33, 0x28, 0x3E, 0x2A,
0x3B, 0x28, 0x2E, 0x23, 0x3B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x29,
0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x28, 0x3F, 0x39, 0x35, 0x28,
0x3E, 0x2E, 0x32, 0x33, 0x28, 0x3E, 0x2A, 0x3B, 0x28, 0x2E, 0x23,
0x3B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x28, 0x3F, 0x39, 0x35, 0x28,
0x3E, 0x2E, 0x32, 0x33, 0x28, 0x3E, 0x2A, 0x3B, 0x28, 0x2E, 0x23,
0x3B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x3B, 0x39, 0x2E, 0x33, 0x35,
0x34, 0x3B, 0x2F, 0x2E, 0x32, 0x39, 0x35, 0x34, 0x2E, 0x3F, 0x22,
0x2E, 0x3E, 0x3F, 0x36, 0x3F, 0x3D, 0x3B, 0x2E, 0x3F, 0x3D, 0x3F,
0x2E, 0x3B, 0x2F, 0x2E, 0x32, 0x39, 0x35, 0x34, 0x2E, 0x3F, 0x22,
0x2E
};
static char DET_DEC[207];
static char DET_DEC[678];
static size_t DET_LEN = 0;
static int DET_READY = 0;
static const uint8_t RISK_LENS[] = { 22, 24, 22, 19, 22, 22, 19 };
static const uint8_t RISK_ENC[] = {
// "ClientIntegrityService"
'C'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'I'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,'S'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,
// "/ClientIntegrityService/"
'/'^KEY,'C'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'I'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,'S'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,'/'^KEY,
// "/GetAttestationHeaders"
'/'^KEY,'G'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/AttestationHeaders"
'/'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/getAttestationHeaders"
'/'^KEY,'g'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/GetAttestationPayload"
'/'^KEY,'G'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'P'^KEY,'a'^KEY,'y'^KEY,'l'^KEY,'o'^KEY,'a'^KEY,'d'^KEY,
// "/AttestationPayload"
'/'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'P'^KEY,'a'^KEY,'y'^KEY,'l'^KEY,'o'^KEY,'a'^KEY,'d'^KEY,
static const uint8_t RISK_LENS[] = {
22, 24, 16, 18, 22, 19, 22, 19, 25, 21, 27, 21, 30, 18, 24, 26, 28,
34, 36, 14, 17, 18, 20, 14, 16, 12, 26, 10, 27, 29, 23, 53, 29, 30
};
static char RISK_DEC[157];
static const uint8_t RISK_ENC[] = {
0x19, 0x36, 0x33, 0x3F, 0x34, 0x2E, 0x13, 0x34, 0x2E, 0x3F, 0x3D, 0x28,
0x33, 0x2E, 0x23, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75,
0x19, 0x36, 0x33, 0x3F, 0x34, 0x2E, 0x13, 0x34, 0x2E, 0x3F, 0x3D,
0x28, 0x33, 0x2E, 0x23, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F,
0x75, 0x13, 0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x09,
0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75, 0x13, 0x34, 0x2E, 0x3F,
0x3D, 0x28, 0x33, 0x2E, 0x23, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39,
0x3F, 0x75, 0x75, 0x1D, 0x3F, 0x2E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29,
0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x12, 0x3F, 0x3B, 0x3E, 0x3F,
0x28, 0x29, 0x75, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x12, 0x3F, 0x3B, 0x3E, 0x3F, 0x28, 0x29, 0x75,
0x1D, 0x3F, 0x2E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x0A, 0x3B, 0x23, 0x36, 0x35, 0x3B, 0x3E, 0x75,
0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34,
0x0A, 0x3B, 0x23, 0x36, 0x35, 0x3B, 0x3E, 0x75, 0x19, 0x35, 0x37,
0x2A, 0x2F, 0x2E, 0x3F, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B,
0x2E, 0x33, 0x35, 0x34, 0x12, 0x3F, 0x3B, 0x3E, 0x3F, 0x28, 0x75,
0x1D, 0x3F, 0x2E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x12, 0x3F, 0x3B, 0x3E, 0x3F, 0x28, 0x75, 0x1D,
0x3F, 0x34, 0x3F, 0x28, 0x3B, 0x2E, 0x3F, 0x1B, 0x2E, 0x2E, 0x3F,
0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x0A, 0x3B, 0x23, 0x36,
0x35, 0x3B, 0x3E, 0x75, 0x1D, 0x3F, 0x2E, 0x09, 0x33, 0x3D, 0x34,
0x3F, 0x3E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33,
0x35, 0x34, 0x75, 0x1D, 0x3F, 0x2E, 0x09, 0x33, 0x3D, 0x34, 0x3F,
0x3E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35,
0x34, 0x0D, 0x33, 0x2E, 0x32, 0x14, 0x35, 0x34, 0x39, 0x3F, 0x75,
0x0C, 0x3F, 0x34, 0x3E, 0x35, 0x28, 0x1B, 0x2E, 0x2E, 0x3F, 0x29,
0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x0C, 0x3F, 0x34, 0x3E, 0x35,
0x28, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35,
0x34, 0x17, 0x3B, 0x34, 0x3B, 0x3D, 0x3F, 0x28, 0x75, 0x0C, 0x3F,
0x34, 0x3E, 0x35, 0x28, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B,
0x2E, 0x33, 0x35, 0x34, 0x17, 0x3B, 0x34, 0x3B, 0x3D, 0x3F, 0x28,
0x75, 0x75, 0x1D, 0x35, 0x35, 0x3D, 0x36, 0x3F, 0x1B, 0x34, 0x3E,
0x28, 0x35, 0x33, 0x3E, 0x11, 0x3F, 0x23, 0x1B, 0x2E, 0x2E, 0x3F,
0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35, 0x34, 0x1D, 0x35, 0x35, 0x3D,
0x36, 0x3F, 0x1B, 0x34, 0x3E, 0x28, 0x35, 0x33, 0x3E, 0x11, 0x3F,
0x23, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E, 0x33, 0x35,
0x34, 0x17, 0x3B, 0x34, 0x3B, 0x3D, 0x3F, 0x28, 0x75, 0x1D, 0x35,
0x35, 0x3D, 0x36, 0x3F, 0x1B, 0x34, 0x3E, 0x28, 0x35, 0x33, 0x3E,
0x11, 0x3F, 0x23, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E, 0x3B, 0x2E,
0x33, 0x35, 0x34, 0x17, 0x3B, 0x34, 0x3B, 0x3D, 0x3F, 0x28, 0x75,
0x75, 0x0A, 0x36, 0x3B, 0x23, 0x13, 0x34, 0x2E, 0x3F, 0x3D, 0x28,
0x33, 0x2E, 0x23, 0x75, 0x1F, 0x22, 0x2A, 0x28, 0x3F, 0x29, 0x29,
0x13, 0x34, 0x2E, 0x3F, 0x3D, 0x28, 0x33, 0x2E, 0x23, 0x09, 0x19,
0x1B, 0x28, 0x3D, 0x35, 0x29, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39,
0x3F, 0x13, 0x37, 0x2A, 0x36, 0x75, 0x09, 0x19, 0x1B, 0x28, 0x3D,
0x35, 0x29, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x13, 0x37,
0x2A, 0x36, 0x75, 0x09, 0x19, 0x1B, 0x28, 0x3D, 0x35, 0x29, 0x09,
0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75, 0x09, 0x19, 0x1B, 0x28,
0x3D, 0x35, 0x29, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75,
0x75, 0x1B, 0x28, 0x3D, 0x35, 0x29, 0x19, 0x36, 0x33, 0x3F, 0x34,
0x2E, 0x75, 0x0A, 0x36, 0x3B, 0x2E, 0x3C, 0x35, 0x28, 0x37, 0x19,
0x36, 0x33, 0x3F, 0x34, 0x2E, 0x1B, 0x2E, 0x2E, 0x3F, 0x29, 0x2E,
0x3B, 0x2E, 0x33, 0x35, 0x34, 0x32, 0x3F, 0x28, 0x37, 0x35, 0x3E,
0x05, 0x3E, 0x2F, 0x2A, 0x1D, 0x28, 0x2A, 0x39, 0x0E, 0x32, 0x33,
0x28, 0x3E, 0x0A, 0x3B, 0x28, 0x2E, 0x23, 0x1B, 0x39, 0x39, 0x3F,
0x29, 0x29, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75, 0x1D,
0x28, 0x2A, 0x39, 0x0E, 0x32, 0x33, 0x28, 0x3E, 0x0A, 0x3B, 0x28,
0x2E, 0x23, 0x1B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x09, 0x3F, 0x28,
0x2C, 0x33, 0x39, 0x3F, 0x75, 0x0E, 0x32, 0x33, 0x28, 0x3E, 0x0A,
0x3B, 0x28, 0x2E, 0x23, 0x1B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x09,
0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F, 0x75, 0x39, 0x35, 0x37, 0x74,
0x29, 0x34, 0x3B, 0x2A, 0x39, 0x32, 0x3B, 0x2E, 0x74, 0x3B, 0x2F,
0x2E, 0x32, 0x74, 0x2A, 0x28, 0x35, 0x2E, 0x35, 0x74, 0x2E, 0x2A,
0x3B, 0x74, 0x0E, 0x32, 0x33, 0x28, 0x3E, 0x0A, 0x3B, 0x28, 0x2E,
0x23, 0x1B, 0x39, 0x39, 0x3F, 0x29, 0x29, 0x09, 0x3F, 0x28, 0x2C,
0x33, 0x39, 0x3F, 0x75, 0x75, 0x08, 0x3F, 0x39, 0x35, 0x28, 0x3E,
0x0E, 0x32, 0x33, 0x28, 0x3E, 0x0A, 0x3B, 0x28, 0x2E, 0x23, 0x1B,
0x39, 0x39, 0x3F, 0x29, 0x29, 0x1B, 0x39, 0x2E, 0x33, 0x35, 0x34,
0x75, 0x08, 0x3F, 0x39, 0x35, 0x28, 0x3E, 0x0E, 0x32, 0x33, 0x28,
0x3E, 0x0A, 0x3B, 0x28, 0x2E, 0x23, 0x1B, 0x39, 0x39, 0x3F, 0x29,
0x29, 0x09, 0x3F, 0x28, 0x2C, 0x33, 0x39, 0x3F
};
static char RISK_DEC[824];
static size_t RISK_LEN = 0;
static int RISK_READY = 0;

View File

@@ -12,7 +12,7 @@ pub fn native_config() -> NativeConfig {
pub(crate) struct NativeConfig {
pub disable_bitmoji: bool,
pub disable_metrics: bool,
pub composer_hooks: bool,
pub valdi_hooks: bool,
pub custom_emoji_font_path: Option<String>,
}
@@ -39,7 +39,7 @@ impl NativeConfig {
Ok(Self {
disable_bitmoji: get_boolean!("disableBitmoji"),
disable_metrics: get_boolean!("disableMetrics"),
composer_hooks: get_boolean!("composerHooks"),
valdi_hooks: get_boolean!("valdiHooks"),
custom_emoji_font_path: get_string!("customEmojiFontPath"),
})
}
@@ -79,4 +79,4 @@ pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
);
info!("Config loaded {:?}", native_config());
}
}

View File

@@ -15,19 +15,17 @@ mod secstrings;
use android_logger::Config;
use log::LevelFilter;
use modules::{composer_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
use modules::{valdi_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
use jni::{JNIEnv, JavaVM, NativeMethod};
use jni::objects::{JObject, JString, JClass, JValue};
use jni::sys::{jint, jstring, JNI_VERSION_1_6, jboolean, JNI_FALSE, JNI_TRUE};
use sha2::{Digest, Sha256};
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
static IS_VERIFIED: AtomicBool = AtomicBool::new(false);
static TEST_MODE: AtomicBool = AtomicBool::new(false);
static IN_LOGIN_SIGNUP: AtomicBool = AtomicBool::new(false);
static CHECKSUMS: Lazy<Mutex<HashMap<String, u32>>> = Lazy::new(|| Mutex::new(HashMap::new()));
@@ -37,6 +35,8 @@ struct BlockerDecision {
reason: &'static str,
keyword: Option<String>,
keyword_context: Option<&'static str>,
match_type: Option<&'static str>,
match_value: Option<String>,
}
#[allow(non_snake_case)]
@@ -65,11 +65,6 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
env.register_native_methods(
native_lib_class,
&[
NativeMethod {
name: "verifyKey".into(),
sig: "(Ljava/lang/String;)Z".into(),
fn_ptr: verifyKey as *mut c_void,
},
NativeMethod {
name: "preInit".into(),
sig: "()V".into(),
@@ -96,20 +91,20 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
fn_ptr: sqlite_hook::lock_database as *mut c_void,
},
NativeMethod {
name: "setComposerLoader".into(),
name: "setValdiLoader".into(),
sig: "(Ljava/lang/String;)V".into(),
fn_ptr: composer_hook::set_composer_loader as *mut c_void,
},
NativeMethod {
name: "composerEval".into(),
sig: "(Ljava/lang/String;)Ljava/lang/String;".into(),
fn_ptr: composer_hook::composer_eval as *mut c_void,
fn_ptr: valdi_hook::set_valdi_loader as *mut c_void,
},
NativeMethod {
name: "evaluateEndpointNative".into(),
sig: "(Ljava/lang/String;Ljava/lang/String;ZLme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
fn_ptr: evaluateEndpoint as *mut c_void,
},
NativeMethod {
name: "evaluateNetworkRequestNative".into(),
sig: "(Ljava/lang/String;Lme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
fn_ptr: evaluateNetworkRequest as *mut c_void,
},
NativeMethod {
name: "shouldBlockDuplexClient".into(),
sig: "(Ljava/lang/String;)Z".into(),
@@ -203,7 +198,7 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
async_init!(
duplex_hook::init(),
unary_call_hook::init(),
composer_hook::init(),
valdi_hook::init(),
sqlite_hook::init()
);
@@ -219,114 +214,6 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
}
}
#[allow(non_snake_case)]
fn verifyKey(mut env: JNIEnv, _class: JClass, key: JString) -> jboolean {
fn bytes_to_hex(bytes: &[u8]) -> String {
const LUT: &[u8; 16] = b"0123456789abcdef";
let mut out = Vec::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(LUT[(b >> 4) as usize]);
out.push(LUT[(b & 0x0f) as usize]);
}
String::from_utf8_lossy(&out).into_owned()
}
fn normalize_hex(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_hexdigit())
.map(|c| c.to_ascii_lowercase())
.collect()
}
fn get_pkg_cert_sha256_hex(env: &mut JNIEnv, pkg: &str) -> Option<String> {
let at = env.find_class("android/app/ActivityThread").ok()?;
let app_obj = env
.call_static_method(at, "currentApplication", "()Landroid/app/Application;", &[])
.ok()?
.l()
.ok()?;
if app_obj.is_null() {
return None;
}
let pm = env
.call_method(&app_obj, "getPackageManager", "()Landroid/content/pm/PackageManager;", &[])
.ok()?
.l()
.ok()?;
let pkg_j = env.new_string(pkg).ok()?.into();
let flags = 0x08000000i32; // PackageManager.GET_SIGNING_CERTIFICATES (API 28+)
let info = env
.call_method(
&pm,
"getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;",
&[JValue::Object(&pkg_j), JValue::Int(flags)],
)
.ok()?
.l()
.ok()?;
let signing_info = env
.get_field(&info, "signingInfo", "Landroid/content/pm/SigningInfo;")
.ok()?
.l()
.ok()?;
if signing_info.is_null() {
return None;
}
let signers = env
.call_method(
&signing_info,
"getApkContentsSigners",
"()[Landroid/content/pm/Signature;",
&[],
)
.ok()?
.l()
.ok()?;
let signers_arr = jni::objects::JObjectArray::from(signers);
let first = env.get_object_array_element(&signers_arr, 0).ok()?;
let sig_bytes_obj = env
.call_method(&first, "toByteArray", "()[B", &[])
.ok()?
.l()
.ok()?;
let sig_bytes = env
.convert_byte_array(jni::objects::JByteArray::from(sig_bytes_obj))
.ok()?;
let digest = Sha256::digest(&sig_bytes);
Some(bytes_to_hex(&digest))
}
// Harden the check: the provided value must match the module APK signing cert SHA-256.
// This prevents trivial re-signing/repacking from passing verification without patching native code.
let expected_cert = normalize_hex(
&env.get_string(&key)
.ok()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(),
);
if expected_cert.is_empty() {
return JNI_FALSE;
}
let actual_cert = get_pkg_cert_sha256_hex(&mut env, "me.eternal.purrfectsnap")
.map(|s| normalize_hex(&s))
.unwrap_or_default();
if actual_cert.is_empty() {
return JNI_FALSE;
}
if expected_cert == actual_cert {
IS_VERIFIED.store(true, Ordering::Relaxed);
JNI_TRUE
} else {
JNI_FALSE
}
}
fn find_keyword(paths: &[&str], keywords: &[String]) -> Option<String> {
for path in paths {
@@ -347,76 +234,76 @@ fn evaluate_endpoint_logic(
has_attestation: bool,
) -> BlockerDecision {
let targets = [uri, arg0];
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
let snap_security_block = targets.iter().any(|t| {
let lower = t.to_lowercase();
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
});
let block_convo_safety_prompt = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
});
let block_convo_safety_service = targets.iter().any(|t| {
t.to_lowercase()
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
});
let block_device_state_report = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
});
if snap_security_block {
return BlockerDecision {
blocked: true,
reason: "snap_security_block",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_prompt {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_prompt",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_service {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_service",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_device_state_report {
return BlockerDecision {
blocked: true,
reason: "notif_report_device_state",
keyword: detection_keyword,
keyword_context: None,
};
}
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
if let Some(matched) = find_keyword(&targets, &config.allowed_eps_active) {
return BlockerDecision {
blocked: false,
reason: "allowed_whitelist",
keyword: None,
keyword_context: None,
match_type: Some("allowed_whitelist"),
match_value: Some(matched),
};
}
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
if let Some(matched) = find_keyword(&targets, &config.risk_block_list) {
return BlockerDecision {
blocked: true,
reason: "risk_blocklist",
keyword: detection_keyword,
keyword_context: None,
match_type: Some("risk_blocklist"),
match_value: Some(matched),
};
}
let reason = if detection_keyword.is_some() && has_attestation {
"allowed_attestation_keyword"
} else if detection_keyword.is_some() {
"allowed_detection_keyword"
} else {
"allowed"
};
if let Some(keyword) = detection_keyword {
let reason = if has_attestation {
"attestation+keyword"
} else {
"detection_keyword"
};
return BlockerDecision {
blocked: true,
reason,
keyword: Some(keyword.clone()),
keyword_context: None,
match_type: Some("detection_keyword"),
match_value: Some(keyword),
};
}
let blocked = false;
BlockerDecision {
blocked,
reason,
keyword: detection_keyword,
blocked: false,
reason: "allowed",
keyword: None,
keyword_context: None,
match_type: None,
match_value: None,
}
}
fn evaluate_network_request_logic(
config: &config::BlockerConfig,
url: &str,
) -> BlockerDecision {
if let Some(keyword) = find_keyword(&[url], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword.clone()),
keyword_context: None,
match_type: Some("detection_keyword"),
match_value: Some(keyword),
};
}
BlockerDecision {
blocked: false,
reason: "allowed",
keyword: None,
keyword_context: None,
match_type: None,
match_value: None,
}
}
@@ -426,114 +313,41 @@ fn evaluate_auth_context_logic(
attestation_required: bool,
) -> BlockerDecision {
let targets = [request_path];
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
let snap_security_block = targets.iter().any(|t| {
let lower = t.to_lowercase();
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
});
let block_convo_safety_prompt = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
});
let block_convo_safety_service = targets.iter().any(|t| {
t.to_lowercase()
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
});
let block_device_state_report = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
});
if snap_security_block {
return BlockerDecision {
blocked: true,
reason: "snap_security_block",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_prompt {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_prompt",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_service {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_service",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_device_state_report {
return BlockerDecision {
blocked: true,
reason: "notif_report_device_state",
keyword: detection_keyword,
keyword_context: None,
};
}
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
if let Some(matched) = find_keyword(&targets, &config.allowed_eps_active) {
return BlockerDecision {
blocked: false,
reason: "allowed_whitelist",
keyword: detection_keyword,
keyword_context: None,
};
}
let reason = if detection_keyword.is_some() && attestation_required {
"allowed_attestation_keyword"
} else if detection_keyword.is_some() {
"allowed_detection_keyword"
} else {
"allowed"
};
let blocked = false;
BlockerDecision {
blocked,
reason,
keyword: detection_keyword,
keyword_context: None,
}
}
fn evaluate_api_invocation_logic(
config: &config::BlockerConfig,
method_id: &str,
annotations: &str,
) -> BlockerDecision {
// Hard allow specific API calls regardless of keyword matches
const API_ALLOWLIST: &[&str] = &[
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchhighavailablesuggestedfriend",
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchlegacysuggestedfriend",
];
let method_lower = method_id.to_lowercase();
if API_ALLOWLIST.iter().any(|m| method_lower == *m) {
return BlockerDecision {
blocked: false,
reason: "allowed_api_whitelist",
keyword: None,
keyword_context: None,
match_type: Some("allowed_whitelist"),
match_value: Some(matched),
};
}
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
if let Some(matched) = find_keyword(&targets, &config.risk_block_list) {
return BlockerDecision {
blocked: true,
reason: "risk_blocklist",
keyword: detection_keyword,
keyword_context: None,
match_type: Some("risk_blocklist"),
match_value: Some(matched),
};
}
if let Some(keyword) = find_keyword(&[method_id], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword),
keyword_context: Some("method"),
if let Some(keyword) = detection_keyword {
let reason = if attestation_required {
"attestation+keyword"
} else {
"detection_keyword"
};
}
if let Some(keyword) = find_keyword(&[annotations], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword),
keyword_context: Some("annotation"),
reason,
keyword: Some(keyword.clone()),
keyword_context: None,
match_type: Some("detection_keyword"),
match_value: Some(keyword),
};
}
@@ -542,6 +356,56 @@ fn evaluate_api_invocation_logic(
reason: "allowed",
keyword: None,
keyword_context: None,
match_type: None,
match_value: None,
}
}
fn evaluate_api_invocation_logic(
config: &config::BlockerConfig,
method_id: &str,
annotations: &str,
) -> BlockerDecision {
if let Some(matched) = find_keyword(&[method_id], &config.allowed_eps_active) {
return BlockerDecision {
blocked: false,
reason: "allowed_whitelist",
keyword: None,
keyword_context: None,
match_type: Some("allowed_whitelist"),
match_value: Some(matched),
};
}
if let Some(keyword) = find_keyword(&[method_id], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword.clone()),
keyword_context: Some("method"),
match_type: Some("detection_keyword"),
match_value: Some(keyword),
};
}
if let Some(keyword) = find_keyword(&[annotations], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword.clone()),
keyword_context: Some("annotation"),
match_type: Some("detection_keyword"),
match_value: Some(keyword),
};
}
BlockerDecision {
blocked: false,
reason: "allowed",
keyword: None,
keyword_context: None,
match_type: None,
match_value: None,
}
}
@@ -553,27 +417,54 @@ fn write_blocker_decision(env: &mut JNIEnv, decision_obj: JObject, decision: &Bl
decision.reason,
decision.keyword.as_deref(),
decision.keyword_context,
decision.match_type,
decision.match_value.as_deref(),
);
}
fn write_decision(env: &mut JNIEnv, decision: JObject, blocked: bool, reason: &str, keyword: Option<&str>, keyword_context: Option<&str>) {
fn write_decision(
env: &mut JNIEnv,
decision: JObject,
blocked: bool,
reason: &str,
keyword: Option<&str>,
keyword_context: Option<&str>,
match_type: Option<&str>,
match_value: Option<&str>,
) {
let blocked_value = if blocked { JNI_TRUE } else { JNI_FALSE };
env.set_field(&decision, "blocked", "Z", JValue::Bool(blocked_value)).expect("failed to set blocked");
if let Err(err) = env.set_field(&decision, "blocked", "Z", JValue::Bool(blocked_value)) {
error!("failed to set blocked: {:?}", err);
}
set_string_field(env, &decision, "reason", Some(reason));
set_string_field(env, &decision, "keyword", keyword);
set_string_field(env, &decision, "keywordContext", keyword_context);
set_string_field(env, &decision, "matchType", match_type);
set_string_field(env, &decision, "matchValue", match_value);
}
fn set_string_field(env: &mut JNIEnv, obj: &JObject, field: &str, value: Option<&str>) {
if let Some(text) = value {
let jstring = env.new_string(text).expect("failed to alloc string");
let jstring = match env.new_string(text) {
Ok(value) => value,
Err(err) => {
error!("failed to alloc string for {}: {:?}", field, err);
return;
}
};
let j_obj = JObject::from(jstring);
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&j_obj)).expect("failed to set string field");
env.delete_local_ref(j_obj).expect("failed to delete local ref");
if let Err(err) = env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&j_obj)) {
error!("failed to set string field {}: {:?}", field, err);
}
if let Err(err) = env.delete_local_ref(j_obj) {
error!("failed to delete local ref for {}: {:?}", field, err);
}
} else {
let null_obj = JObject::null();
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)).expect("failed to clear string field");
if let Err(err) = env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)) {
error!("failed to clear string field {}: {:?}", field, err);
}
}
}
@@ -587,13 +478,7 @@ fn evaluateEndpoint(
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
return;
}
let uri_str: String = env.get_string(&uri).unwrap().into();
@@ -604,6 +489,24 @@ fn evaluateEndpoint(
write_blocker_decision(&mut env, decision, &blocker_decision);
}
#[allow(non_snake_case)]
fn evaluateNetworkRequest(
mut env: JNIEnv,
_class: JClass,
url: JString,
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
return;
}
let url_str: String = env.get_string(&url).unwrap().into();
let config = config::get_blocker_config();
let blocker_decision = evaluate_network_request_logic(&config, &url_str);
write_blocker_decision(&mut env, decision, &blocker_decision);
}
#[allow(non_snake_case)]
fn shouldBlockDuplexClient(
mut env: JNIEnv,
@@ -613,11 +516,6 @@ fn shouldBlockDuplexClient(
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
return JNI_FALSE;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
return JNI_FALSE;
}
let path_str: String = env.get_string(&path).unwrap().into();
@@ -638,13 +536,7 @@ fn evaluateAuthContext(
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
return;
}
let request_path_str: String = env.get_string(&request_path).unwrap().into();
@@ -663,13 +555,7 @@ fn evaluateApiInvocation(
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
return;
}
let method_id_str: String = env.get_string(&method_id).unwrap().into();
@@ -681,9 +567,7 @@ fn evaluateApiInvocation(
}
fn run_blocker_self_test(allow_unverified: bool) -> bool {
if !IS_VERIFIED.load(Ordering::Relaxed) && !allow_unverified {
return false;
}
let _ = allow_unverified;
let config = config::get_blocker_config();
if config.allowed_eps_active.is_empty()
@@ -704,22 +588,22 @@ fn run_blocker_self_test(allow_unverified: bool) -> bool {
let detection_path = format!("/self_test/{}", detection_sample);
let detection_decision = evaluate_endpoint_logic(&config, &detection_path, "", false);
if detection_decision.blocked {
if !detection_decision.blocked {
return false;
}
let attestation_decision = evaluate_endpoint_logic(&config, &detection_path, "", true);
if attestation_decision.blocked {
if !attestation_decision.blocked {
return false;
}
let risk_decision = evaluate_endpoint_logic(&config, &risk_sample, "", false);
if risk_decision.blocked {
if !risk_decision.blocked {
return false;
}
let auth_detection = evaluate_auth_context_logic(&config, &detection_path, false);
if auth_detection.blocked {
if !auth_detection.blocked {
return false;
}

View File

@@ -10,17 +10,24 @@ def_hook!(
i32,
|fd: i32, statbuf: *mut libc::stat| {
if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) {
if let Some(filename) = link.file_name().map(|t| t.to_string_lossy()) {
let config = native_config();
if config.disable_metrics && filename.contains("files/blizzardv2/queues") {
if libc::unlink((filename.to_owned() + "\0").as_ptr()) == -1 {
warn!("Failed to unlink {}", filename);
}
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;
}
if config.disable_bitmoji {
if link_str.contains("com.snap.file_manager_4_SCContent") {
return -1;
}
if config.disable_bitmoji && filename.contains("com.snap.file_manager_4_SCContent") {
return -1;
if link_str.contains("/files/file_manager/") {
let lower = link_str.to_lowercase();
if lower.contains("bitmoji") {
return -1;
}
}
}
}

View File

@@ -4,5 +4,5 @@ pub mod duplex_hook;
pub mod sqlite_hook;
pub mod fstat_hook;
pub mod unary_call_hook;
pub mod composer_hook;
pub mod custom_font_hook;
pub mod valdi_hook;
pub mod custom_font_hook;

View File

@@ -104,7 +104,7 @@ def_hook!(
pub fn init() {
if let Some(signature) = sig::find_signature(
&common::CLIENT_MODULE,
"A8 03 1F F8 ?? 00 00 94 ?? ?? ?? 91", -0x48,
"A8 03 1F F8 ?? ?? 00 94 ?? ?? ?? 91 ?? ?? ?? A9", -0x48,
"0A 90 00 F0 3F F9", -0x37
) {
dobby_hook!(signature as *mut c_void, unary_call);

View File

@@ -1 +1 @@
pub mod composer_utils;
pub mod valdi_utils;

View File

@@ -0,0 +1,141 @@
use std::{io::Error, string::FromUtf8Error};
#[derive(Debug, Clone)]
pub struct ModuleTag {
has_padding: bool,
buffer: Vec<u8>,
}
impl ModuleTag {
pub fn new(has_padding: bool, buffer: Vec<u8>) -> ModuleTag {
ModuleTag {
has_padding,
buffer,
}
}
pub fn to_string(&self) -> Result<String, FromUtf8Error> {
Ok(String::from_utf8(self.buffer.clone())?)
}
pub fn get_has_padding(&self) -> bool {
self.has_padding
}
pub fn get_size(&self) -> usize {
self.buffer.len()
}
pub fn get_buffer(&self) -> &Vec<u8> {
&self.buffer
}
pub fn set_buffer(&mut self, buffer: Vec<u8>) {
self.buffer = buffer;
}
}
#[derive(Debug, Clone)]
pub struct ValdiModule {
tags: Vec<(ModuleTag, ModuleTag)>, // file name => file content
}
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]]);
offset += 4;
if magic != 0x33c60001 {
return Err(Error::new(std::io::ErrorKind::InvalidData, "Invalid magic"));
}
// skip content length
offset += 4;
let mut tags = Vec::new();
loop {
if offset >= buffer.len() {
break;
}
fn read_u32(buffer: &Vec<u8>, offset: &mut usize) -> Result<(u32, bool), Error> {
let b1 = buffer[*offset] as u32;
let b2 = buffer[*offset + 1] as u32;
let b3 = buffer[*offset + 2] as u32;
let b4 = (buffer[*offset + 3] & 0x7f) as u32;
let has_padding = (buffer[*offset + 3] & 0x80) != 0;
*offset += 4;
Ok((b1 | (b2 << 8) | (b3 << 16) | (b4 << 24), has_padding))
}
let (tag_size, has_padding) = read_u32(&buffer, &mut offset)?;
let tag_buffer = buffer[offset..offset + tag_size as usize].to_vec();
offset += tag_size as usize;
let padding = 4 - (tag_size % 4);
if padding != 4 {
offset += padding as usize;
}
tags.push(ModuleTag::new(has_padding, tag_buffer));
}
let tags = tags.chunks(2).map(|chunk| {
(chunk[0].clone(), chunk[1].clone())
}).collect();
Ok(ValdiModule {
tags,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut tag_buffer = Vec::new();
fn write_u32(buffer: &mut Vec<u8>, value: u32, has_padding: bool) {
buffer.push(value as u8);
buffer.push(((value >> 8) & 0xff) as u8);
buffer.push(((value >> 16) & 0xff) as u8);
buffer.push(((value >> 24) & 0x7f) as u8 | if has_padding { 0x80 } else { 0x00 });
}
fn write_tag(buffer: &mut Vec<u8>, tag: ModuleTag) {
write_u32(buffer, tag.get_size() as u32, tag.get_has_padding());
buffer.extend(tag.get_buffer());
let padding = 4 - (tag.get_size() % 4);
if padding != 4 {
for _ in 0..padding {
buffer.push(0);
}
}
}
for (tag1, tag2) in &self.tags {
write_tag(&mut tag_buffer, tag1.clone());
write_tag(&mut tag_buffer, tag2.clone());
}
let mut buffer = Vec::new();
buffer.extend_from_slice(&[0x33, 0xc6, 0, 1]);
buffer.extend_from_slice(&(tag_buffer.len() as u32).to_le_bytes());
buffer.extend(tag_buffer);
buffer
}
pub fn get_tags(&self) -> Vec<(ModuleTag, ModuleTag)> {
self.tags.clone()
}
pub fn set_tags(&mut self, tags: Vec<(ModuleTag, ModuleTag)>) {
self.tags = tags;
}
}

View File

@@ -0,0 +1,113 @@
#![allow(dead_code, unused_imports)]
use super::util::valdi_utils::{ValdiModule, ModuleTag};
use std::{collections::HashMap, ffi::c_void, sync::Mutex};
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};
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_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,
*mut c_void,
|arg0: *mut c_void, arg1: *const u8, arg2: i32| {
let handle = aasset_manager_open_original.unwrap()(arg0, arg1, arg2);
let path = std::ffi::CStr::from_ptr(arg1).to_str().unwrap_or_default();
if !handle.is_null() && path.starts_with("bridge_observables") {
let asset_buffer = aasset_get_buffer_original.unwrap()(handle);
let asset_length = aasset_get_length_original.unwrap()(handle);
debug!("asset buffer: {:p}, length: {}", asset_buffer, asset_length);
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 mut tags = valdi_module.get_tags();
let mut new_tags = Vec::new();
for (tag1, _) in tags.iter_mut() {
let name = tag1.to_string().unwrap_or_default();
if !name.ends_with("src/utils/converter.js") {
continue;
}
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 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;
}
tags.extend(new_tags);
valdi_module.set_tags(tags);
let compressed = valdi_module.to_bytes();
let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress");
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)
}
);
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");
LOADER_DATA.lock().unwrap().replace(new_code);
}
pub fn init() {
if !config::native_config().valdi_hooks {
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);
}

View File

@@ -4,6 +4,7 @@ extern crate libc;
use jni::sys::{jboolean, JNI_FALSE, JNI_TRUE};
use goldberg::goldberg_string;
use std::{thread, time};
use log::warn;
use std::fs;
use std::process;
use crate::CHECKSUMS;
@@ -14,12 +15,22 @@ fn check_for_debugger() {
loop {
let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
if !status.contains("TracerPid:\t0") {
process::abort();
if should_abort_on_tamper() {
process::abort();
} else {
warn!("TracerPid detected; skipping abort under hooked environment");
return;
}
}
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
if maps.contains("frida") || maps.contains("gumjs") || maps.contains("gdb") {
process::abort();
if should_abort_on_tamper() {
process::abort();
} else {
warn!("Debugger markers detected; skipping abort under hooked environment");
return;
}
}
thread::sleep(time::Duration::from_secs(5));
@@ -56,7 +67,12 @@ fn verify_library_checksum() {
if let Some(abi) = get_abi() {
if let Some(expected_checksum) = CHECKSUMS.lock().unwrap().get(&abi) {
if checksum != *expected_checksum as u32 {
process::abort();
if should_abort_on_tamper() {
process::abort();
} else {
warn!("Checksum mismatch; skipping abort under hooked environment");
return;
}
}
}
}
@@ -66,6 +82,7 @@ fn verify_library_checksum() {
}
}
#[allow(dead_code)]
pub fn verify_key(key: &str) -> bool {
// Transformed key verification
let transformed_key = key.chars().rev().collect::<String>();
@@ -75,6 +92,7 @@ pub fn verify_key(key: &str) -> bool {
// this function will be called from the JNI, it will verify the key and if it's correct, it will return true
// otherwise it will return false
#[allow(dead_code)]
pub fn jni_verify_key(key: &str) -> jboolean {
if verify_key(key) {
JNI_TRUE
@@ -94,3 +112,9 @@ fn get_library_path() -> Option<String> {
}
None
}
fn should_abort_on_tamper() -> bool {
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
let markers = ["lspatch", "lsposed", "xposed", "zygisk", "riru"];
!markers.iter().any(|marker| maps.contains(marker))
}

View File

@@ -1,5 +1,6 @@
use std::sync::Mutex;
use std::{fs::File, os::unix::io::AsRawFd, sync::Mutex};
use nix::libc;
use procfs::process::MMPermissions;
use crate::mapped_lib::MappedLib;
@@ -15,7 +16,46 @@ pub fn get_signatures() -> Vec<(String, Vec<usize>)> {
SIGNATURE_CACHE.lock().unwrap().clone()
}
pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: bool) -> Vec<usize> {
fn read_region_bytes(start: usize, size: usize) -> Option<Vec<u8>> {
let file = File::open("/proc/self/mem").ok();
if let Some(file) = file {
let fd = file.as_raw_fd();
let mut buffer = vec![0u8; size];
let mut offset = 0usize;
while offset < size {
let read = unsafe {
libc::pread(
fd,
buffer[offset..].as_mut_ptr() as *mut libc::c_void,
(size - offset) as libc::size_t,
(start + offset) as libc::off_t,
)
};
if read < 0 {
warn!(
"Failed to read /proc/self/mem at {:#x}: {}",
start,
std::io::Error::last_os_error()
);
return None;
}
if read == 0 {
break;
}
offset += read as usize;
}
if offset == size {
return Some(buffer);
}
warn!("Short read from /proc/self/mem at {:#x}: {} < {}", start, offset, size);
}
None
}
pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, once: bool) -> Vec<usize> {
let mut results = Vec::new();
let mut bytes = Vec::new();
let mut mask = Vec::new();
@@ -37,13 +77,13 @@ pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: boo
}
let mut i = 0;
let size = size - bytes.len();
let size = bytes_buffer.len().saturating_sub(bytes.len());
while i < size {
let mut found = true;
let mut j = 0;
while j < bytes.len() {
if mask[j] == '?' || bytes[j] == unsafe { *(module_base as *const u8).offset(i as isize + j as isize) } {
if mask[j] == '?' || bytes[j] == bytes_buffer[i + j] {
j += 1;
continue;
}
@@ -66,7 +106,7 @@ pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: boo
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) && region.perms.contains(MMPermissions::READ)
region.perms.contains(MMPermissions::EXECUTE)
}).collect::<Vec<_>>();
for region in executable_regions {
@@ -74,7 +114,14 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio
let module_base = region.start as usize;
if size > 0 {
let results = find_signatures(module_base, size, pattern, true);
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);
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);
@@ -97,4 +144,4 @@ pub fn find_signature(mapped_lib: &MappedLib, _arm64_pattern: &str, _arm64_offse
{
return find_signature_executable(mapped_lib, _arm32_pattern).map(|address| (address as i64 + _arm32_offset) as usize);
}
}
}

View File

@@ -5,4 +5,4 @@ use jni::{objects::JString, JNIEnv};
pub fn get_jni_string(env: &mut JNIEnv, obj: JString) -> Result<String, Box<dyn Error>> {
let string = env.get_string(&obj)?;
Ok(string.to_str()?.to_string())
}
}

View File

@@ -6,7 +6,7 @@ data class NativeConfig(
@JvmField
val disableMetrics: Boolean = false,
@JvmField
val composerHooks: Boolean = false,
val valdiHooks: Boolean = false,
@JvmField
val customEmojiFontPath: String? = null,
)
)

View File

@@ -1,8 +1,13 @@
package me.eternal.purrfectsnap.nativelib
import androidx.annotation.Keep
@Keep
class NativeDecision {
var blocked: Boolean = false
var reason: String = "allowed"
var keyword: String? = null
var keywordContext: String? = null
@JvmField var blocked: Boolean = false
@JvmField var reason: String = "allowed"
@JvmField var keyword: String? = null
@JvmField var keywordContext: String? = null
@JvmField var matchType: String? = null
@JvmField var matchValue: String? = null
}

View File

@@ -2,6 +2,7 @@ package me.eternal.purrfectsnap.nativelib
import android.annotation.SuppressLint
import android.util.Log
import java.io.File
import kotlin.math.absoluteValue
import kotlin.random.Random
@@ -14,6 +15,44 @@ class NativeLib {
private set
private var libraryLoaded = false
private fun findNativeLibraryDir(): File? {
val app = runCatching {
val cls = Class.forName("android.app.ActivityThread")
val method = cls.getMethod("currentApplication")
method.invoke(null) as? android.app.Application
}.getOrNull() ?: return null
val dir = app.applicationInfo.nativeLibraryDir ?: return null
return File(dir)
}
private fun tryLoadFromNativeDir(): Boolean {
val dir = findNativeLibraryDir() ?: return false
if (!dir.isDirectory) return false
val candidates = listOf(
"lib${BuildConfig.NATIVE_NAME}.so",
"libpurrfectsnap.so"
).map { File(dir, it) }
val fallback = dir.listFiles()?.firstOrNull { it.name.startsWith("lib") && it.name.endsWith(".so") && it.name.contains("purrfectsnap") }
val ordered = buildList<File> {
addAll(candidates)
fallback?.let { if (!contains(it)) add(it) }
}
for (file in ordered) {
if (!file.exists()) continue
val ok = runCatching {
System.load(file.absolutePath)
libraryLoaded = true
true
}.getOrDefault(false)
if (ok) return true
}
return false
}
fun tryEnsureLibraryLoaded(): Boolean {
if (libraryLoaded) return true
@@ -33,6 +72,8 @@ class NativeLib {
if (ok) return true
}
if (tryLoadFromNativeDir()) return true
Log.e(
"PurrfectSnap",
"Failed to load native library (tried: ${candidates.joinToString()})",
@@ -104,12 +145,11 @@ class NativeLib {
private external fun preInit()
private external fun init(signatureCache: String?): String?
private external fun loadConfig(config: NativeConfig)
external fun verifyKey(key: String): Boolean
private external fun lockDatabase(name: String, callback: Runnable)
external fun setComposerLoader(code: String)
external fun composerEval(code: String): String?
external fun setValdiLoader(code: String)
private external fun addLinkerSharedLibrary(path: String, content: ByteArray)
private external fun evaluateEndpointNative(uri: String, arg0: String, hasAttestation: Boolean, outDecision: NativeDecision)
private external fun evaluateNetworkRequestNative(url: String, outDecision: NativeDecision)
external fun shouldBlockDuplexClient(path: String): Boolean
private external fun evaluateAuthContextNative(requestPath: String, attestationRequired: Boolean, outDecision: NativeDecision)
private external fun evaluateApiInvocationNative(methodId: String, annotations: String, outDecision: NativeDecision)
@@ -123,6 +163,10 @@ class NativeLib {
return NativeDecision().also { evaluateEndpointNative(uri, arg0, hasAttestation, it) }
}
fun evaluateNetworkRequest(url: String): NativeDecision {
return NativeDecision().also { evaluateNetworkRequestNative(url, it) }
}
fun evaluateAuthContext(requestPath: String, attestationRequired: Boolean): NativeDecision {
return NativeDecision().also { evaluateAuthContextNative(requestPath, attestationRequired, it) }
}