Android self-bind protection auf nahezu MDM-Niveau ohne Device-Owner: - Device-Admin (RebreakDeviceAdminReceiver) blockt Uninstall OS-seitig, aktiv ab Boot ohne Prozess/a11y. Deaktivierung nur via 24h-Cooldown (removeDeviceAdmin in forceDisable). a11y blockt die DeviceAdminAdd-Settings-Seite (Class-Match, auf Samsung One UI per Logcat verifiziert). - Boot-Receiver (RebreakVpnBootReceiver) startet VPN+a11y nach Reboot, damit der Tamper-Lock ohne manuellen App-Start hochkommt. - Manifest-Wiring (Device-Admin-Receiver, Boot-Receiver, RECEIVE_BOOT_COMPLETED, device_admin.xml) ins with-rebreak-protection-android Config-Plugin verlagert → ueberlebt 'expo prebuild' (android/ ist gitignored). - a11y-Detection zurueck auf die funktionierende Version: zu breites 'loeschen'- Uninstall-Keyword raus (blockte halbe Settings); a11y-Label jetzt 'ReBreak Schutz'. - a11y-Deeplink behaelt den Samsung-Step-Guide (openAccessibilitySettings). Session-Frontend in diesem Batch: - Avatar-Placeholder: neutrales clarity-avatar-line SVG statt dominantem Blau. - DiGA-Milestone folgt kumulativen protectedDays (erreicht rueckfall-anfaellige User). - Dev-Build crasht nicht mehr ohne CallKit-Native-Modul. - VPN-Permission-Dialog nur noch im Bypass-Fall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
254 lines
9.7 KiB
JavaScript
254 lines
9.7 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
/**
|
|
* VoIP-PushKit + CallKit Bridge im iOS-AppDelegate.
|
|
*
|
|
* Patcht beim `expo prebuild`:
|
|
* 1) ReBreak-Bridging-Header.h
|
|
* → exposed RNVoipPushNotificationManager + RNCallKeep an Swift.
|
|
* 2) AppDelegate.swift
|
|
* → `import PushKit`
|
|
* → PKPushRegistry init in didFinishLaunchingWithOptions
|
|
* → PKPushRegistryDelegate-Extension: didUpdatePushCredentials forwarded an
|
|
* RNVoipPushNotificationManager; didReceiveIncomingPushWithPayload meldet
|
|
* sofort eingehenden Anruf an RNCallKeep (iOS 13+ Anforderung: MUSS in
|
|
* derselben run-loop CXProvider.reportNewIncomingCall aufrufen, sonst
|
|
* killt iOS die App).
|
|
*
|
|
* react-native-voip-push-notification hat KEIN config-plugin → ohne diesen
|
|
* Patch greift `pnpm expo prebuild --clean` jedes Mal die Edits ab.
|
|
*
|
|
* Idempotent via Marker. Läuft als withDangerousMod('ios'), nach prebuild,
|
|
* vor pod install.
|
|
*/
|
|
const { withDangerousMod } = require('@expo/config-plugins');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const MARKER = '// REBREAK_VOIP_PUSHKIT';
|
|
|
|
// ───────────────────────────────────────────────────── Bridging-Header
|
|
|
|
const BRIDGING_PATCH = `
|
|
${MARKER}
|
|
#import "RNVoipPushNotificationManager.h"
|
|
#import "RNCallKeep.h"
|
|
${MARKER}
|
|
`;
|
|
|
|
// ───────────────────────────────────────────────────── AppDelegate.swift
|
|
|
|
const IMPORT_PATCH = `${MARKER}
|
|
import PushKit
|
|
${MARKER}
|
|
`;
|
|
|
|
// In didFinishLaunchingWithOptions — direkt vor `return super.application(...)`.
|
|
const REGISTRY_INIT = `
|
|
${MARKER}
|
|
// PushKit-Registry für VoIP-Push (CallKit). MUSS in didFinishLaunching
|
|
// initialisiert werden, sonst kommt der erste Push nach App-Cold-Start nicht
|
|
// an. Als Property gehalten (self.voipRegistry, siehe Property-Deklaration
|
|
// unter dem class-Header) damit sie nicht out-of-scope dealloziert wird —
|
|
// sonst kommt die Token-Registration noch durch, aber didReceiveIncomingPush
|
|
// feuert NIE → iPhone wacht im Background nicht auf.
|
|
let registry = PKPushRegistry(queue: .main)
|
|
registry.delegate = self
|
|
registry.desiredPushTypes = [.voIP]
|
|
self.voipRegistry = registry
|
|
${MARKER}
|
|
`;
|
|
|
|
// Class-Property für den Registry-Halter — muss innerhalb der AppDelegate-Klasse
|
|
// stehen. Wird direkt nach `var reactNativeFactory:` eingefügt.
|
|
const CLASS_PROPERTY = `
|
|
${MARKER}
|
|
var voipRegistry: PKPushRegistry?
|
|
${MARKER}
|
|
`;
|
|
|
|
// PKPushRegistryDelegate-Extension am Ende der Datei.
|
|
const DELEGATE_EXTENSION = `
|
|
|
|
${MARKER}
|
|
// MARK: - VoIP PushKit + CallKit Bridge
|
|
extension AppDelegate: PKPushRegistryDelegate {
|
|
|
|
// Device-Token vom System nach voIP-Registrierung.
|
|
public func pushRegistry(
|
|
_ registry: PKPushRegistry,
|
|
didUpdate pushCredentials: PKPushCredentials,
|
|
for type: PKPushType
|
|
) {
|
|
let hex = pushCredentials.token.map { String(format: "%02hhx", $0) }.joined()
|
|
NSLog("[VoIP] didUpdate token (len=%d) prefix=%@", pushCredentials.token.count, String(hex.prefix(16)))
|
|
RNVoipPushNotificationManager.didUpdate(pushCredentials, forType: type.rawValue)
|
|
}
|
|
|
|
// Token-Invalidierung — aktuell no-op; Client refresht beim nächsten Boot.
|
|
public func pushRegistry(
|
|
_ registry: PKPushRegistry,
|
|
didInvalidatePushTokenFor type: PKPushType
|
|
) {
|
|
NSLog("[VoIP] didInvalidatePushTokenFor type=%@", type.rawValue)
|
|
}
|
|
|
|
// Eingehender VoIP-Push. MUSS auf iOS 13+ in derselben run-loop
|
|
// CXProvider.reportNewIncomingCall aufrufen, sonst killt iOS die App
|
|
// ("VoIP push received but app didn't post incoming call").
|
|
public func pushRegistry(
|
|
_ registry: PKPushRegistry,
|
|
didReceiveIncomingPushWith payload: PKPushPayload,
|
|
for type: PKPushType,
|
|
completion: @escaping () -> Void
|
|
) {
|
|
let dict = payload.dictionaryPayload
|
|
let callId = (dict["callId"] as? String) ?? UUID().uuidString
|
|
let callerName = (dict["callerName"] as? String) ?? "ReBreak"
|
|
let handle = (dict["handle"] as? String) ?? callerName
|
|
// callId (z.B. "1780632453911-8jeln4xg") ist KEIN valides UUID. CXProvider.
|
|
// reportNewIncomingCall parst den String via NSUUID → nil → NSInvalidArgument
|
|
// Exception → App-Crash → iOS-VoIP-Drossel. Deterministisch konvertieren wie
|
|
// der JS-Layer (lib/callkit.ts:callIdToUuid), damit CallKit + JS dieselbe UUID
|
|
// nutzen (accept/end matchen sonst nicht).
|
|
let callUUID = AppDelegate.callIdToUuid(callId)
|
|
NSLog("[VoIP] didReceiveIncomingPush callId=%@ uuid=%@ callerName=%@", callId, callUUID, callerName)
|
|
|
|
// 1) Sofort an CallKit melden (iOS-Pflicht). uuid MUSS deterministisch sein,
|
|
// damit der JS-Layer den Call mit demselben UUID antworten/auflegen kann.
|
|
RNCallKeep.reportNewIncomingCall(
|
|
callUUID,
|
|
handle: handle,
|
|
handleType: "generic",
|
|
hasVideo: false,
|
|
localizedCallerName: callerName,
|
|
supportsHolding: false,
|
|
supportsDTMF: false,
|
|
supportsGrouping: false,
|
|
supportsUngrouping: false,
|
|
fromPushKit: true,
|
|
payload: dict,
|
|
withCompletionHandler: completion
|
|
)
|
|
|
|
// 2) Forward an JS — wenn die App schon im Vordergrund war, kann sie auf
|
|
// didLoadWithEvents reagieren (Auto-Navigation /call).
|
|
RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue)
|
|
}
|
|
|
|
// Deterministische callId→UUID-Abbildung. MUSS exakt lib/callkit.ts:callIdToUuid
|
|
// entsprechen, sonst nutzen CallKit (nativ) und der JS-Layer verschiedene UUIDs
|
|
// und accept/end greifen ins Leere.
|
|
static func callIdToUuid(_ callId: String) -> String {
|
|
var hex = ""
|
|
for scalar in callId.unicodeScalars {
|
|
if hex.count >= 32 { break }
|
|
let code = Int(scalar.value)
|
|
hex.append(String((code >> 4) & 0xf, radix: 16))
|
|
if hex.count < 32 { hex.append(String(code & 0xf, radix: 16)) }
|
|
}
|
|
while hex.count < 32 { hex.append("0") }
|
|
let c = Array(hex.prefix(32))
|
|
func sub(_ a: Int, _ b: Int) -> String { String(c[a..<b]) }
|
|
// UUID v4 Layout: xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx (Index 12 + 16 werden
|
|
// durch die Literale '4'/'8' ersetzt — exakt wie im JS-Original).
|
|
return "\\(sub(0, 8))-\\(sub(8, 12))-4\\(sub(13, 16))-8\\(sub(17, 20))-\\(sub(20, 32))"
|
|
}
|
|
}
|
|
${MARKER}
|
|
`;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
function patchBridgingHeader(iosRoot) {
|
|
// Bridging-Header heißt analog zum Target — bei uns "ReBreak-Bridging-Header.h".
|
|
const candidates = fs
|
|
.readdirSync(iosRoot, { withFileTypes: true })
|
|
.filter((d) => d.isDirectory())
|
|
.flatMap((d) => {
|
|
const p = path.join(iosRoot, d.name);
|
|
return fs.existsSync(p)
|
|
? fs
|
|
.readdirSync(p)
|
|
.filter((f) => f.endsWith('-Bridging-Header.h'))
|
|
.map((f) => path.join(p, f))
|
|
: [];
|
|
});
|
|
|
|
if (candidates.length === 0) {
|
|
console.warn('[with-voip-pushkit-ios] no *-Bridging-Header.h found — skipping bridging patch');
|
|
return;
|
|
}
|
|
|
|
for (const headerPath of candidates) {
|
|
let header = fs.readFileSync(headerPath, 'utf-8');
|
|
if (header.includes(MARKER)) continue;
|
|
header += BRIDGING_PATCH;
|
|
fs.writeFileSync(headerPath, header);
|
|
console.log('[with-voip-pushkit-ios] patched bridging header:', path.basename(headerPath));
|
|
}
|
|
}
|
|
|
|
function patchAppDelegate(iosRoot) {
|
|
const candidates = fs
|
|
.readdirSync(iosRoot, { withFileTypes: true })
|
|
.filter((d) => d.isDirectory())
|
|
.flatMap((d) => {
|
|
const p = path.join(iosRoot, d.name);
|
|
const file = path.join(p, 'AppDelegate.swift');
|
|
return fs.existsSync(file) ? [file] : [];
|
|
});
|
|
|
|
if (candidates.length === 0) {
|
|
console.warn('[with-voip-pushkit-ios] AppDelegate.swift not found — skipping');
|
|
return;
|
|
}
|
|
|
|
for (const appDelegatePath of candidates) {
|
|
let src = fs.readFileSync(appDelegatePath, 'utf-8');
|
|
if (src.includes(MARKER)) continue;
|
|
|
|
// 1) Top-of-file: import PushKit (nach `import React` einfügen).
|
|
if (!src.includes('import PushKit')) {
|
|
src = src.replace(/(import React\n)/, `$1${IMPORT_PATCH}`);
|
|
}
|
|
|
|
// 2a) Class-Property `var voipRegistry: PKPushRegistry?` einfügen \u2014 muss
|
|
// gehalten werden, sonst dealloziert iOS die Registry nach
|
|
// didFinishLaunching und Pushes erreichen den Delegate nie.
|
|
const classPropRe = /(\n\s+var reactNativeFactory:[^\n]+\n)/;
|
|
if (classPropRe.test(src) && !src.includes('var voipRegistry')) {
|
|
src = src.replace(classPropRe, `$1${CLASS_PROPERTY}`);
|
|
}
|
|
|
|
// 2b) didFinishLaunching: PKPushRegistry direkt vor dem return-super injizieren.
|
|
const returnSuperRe = /(\n\s+return super\.application\(application, didFinishLaunchingWithOptions: launchOptions\))/;
|
|
if (returnSuperRe.test(src)) {
|
|
src = src.replace(returnSuperRe, `${REGISTRY_INIT}$1`);
|
|
} else {
|
|
console.warn('[with-voip-pushkit-ios] kein didFinishLaunching-return gefunden — Registry-Init NICHT eingefügt');
|
|
}
|
|
|
|
// 3) Delegate-Extension am Ende der Datei anhängen.
|
|
src += DELEGATE_EXTENSION;
|
|
|
|
fs.writeFileSync(appDelegatePath, src);
|
|
console.log('[with-voip-pushkit-ios] patched AppDelegate.swift');
|
|
}
|
|
}
|
|
|
|
module.exports = function withVoipPushKitIOS(config) {
|
|
return withDangerousMod(config, [
|
|
'ios',
|
|
async (cfg) => {
|
|
const iosRoot = cfg.modRequest.platformProjectRoot;
|
|
try {
|
|
patchBridgingHeader(iosRoot);
|
|
patchAppDelegate(iosRoot);
|
|
} catch (e) {
|
|
console.error('[with-voip-pushkit-ios] patch failed:', e);
|
|
}
|
|
return cfg;
|
|
},
|
|
]);
|
|
};
|