Dating-Safety · 4 Kern-Module
Tech-Architektur · v1.0Senior-Entwickler Referenz · iOS + Android · Offline-first · Privacy-by-design
< 500ms
Latenz Trigger → SOS
~25mW
Akku Normal-Modus
85%
Konfidenz-Schwelle
Cosine 0.82
Speaker-Verifikation
// ═══════════════════════════════════════════════════════════════
// MODULE 1 · VOICE-TRIGGER · Offline Safe-Word Detection
// Stack: TFLite (Android) / CoreML (iOS) · No cloud required
// ═══════════════════════════════════════════════════════════════
// ── ARCHITECTURE OVERVIEW ────────────────────────────────────────
//
// Microphone Input (16kHz mono)
// │
// ▼
// [MFCC Feature Extractor] ← on-device, < 2ms
// │ 40 coefficients, 1s window, 50ms hop
// ▼
// [TFLite / CoreML Inference] ← NNAPI / ANE accelerated
// │ keyword_model_v3.tflite ← user-enrolled safe-word
// │ threshold: 0.85 confidence
// ▼
// [Enrollment Engine] ← Speaker Verification
// │ cosine similarity ≥ 0.82 (same voice, not a recording)
// ▼
// SOS Dispatcher.trigger()
// ── ANDROID: KeywordService (Kotlin) ────────────────────────────
class KeywordListenerService : Service() {
companion object {
const val SAMPLE_RATE = 16_000
const val FRAME_MS = 30 // 30ms frames
const val KEYWORD_THRESH = 0.85f
const val SPEAKER_THRESH = 0.82f // Anti-Playback
const val MODEL_FILE = "safe_word_model.tflite"
const val SPEAKER_MODEL = "speaker_embed_model.tflite"
}
private lateinit var keywordInterp: Interpreter
private lateinit var speakerInterp: Interpreter
private lateinit var audioCapture: BatteryAwareCapture
private lateinit var enrolledEmbed: FloatArray // stored in EncryptedSharedPrefs
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIF_ID, stealth_notification()) // invisible to user
initModels()
startListenLoop()
return START_STICKY
}
private fun startListenLoop() {
CoroutineScope(Dispatchers.Default).launch {
audioCapture.stream { pcmFrame ->
val mfcc = MFCCExtractor.compute(pcmFrame, SAMPLE_RATE)
val kwScore = runTFLite(keywordInterp, mfcc)
if (kwScore >= KEYWORD_THRESH) {
val speakerEmbed = runTFLite(speakerInterp, mfcc)
val similarity = cosineSim(speakerEmbed, enrolledEmbed)
if (similarity >= SPEAKER_THRESH) {
// ✅ Correct keyword + correct speaker
SOSDispatcher.trigger(source = "voice_keyword")
}
}
}
}
}
// BATTERY: Duty-cycle audio capture
// Normal (>30% battery): continuous 16kHz
// Eco (≤30% battery): 200ms listen / 300ms pause @ 8kHz
// Critical(≤15% battery): 150ms listen / 850ms pause @ 8kHz
private fun stealth_notification() =
NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_transparent)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setShowWhen(false)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.build()
}
// ── iOS: KeywordEngine (Swift) ────────────────────────────────────
class KeywordEngine: NSObject, SNResultsObserving {
private let audioEngine = AVAudioEngine()
private let mlModel = try! SecSIKeyword_v3(configuration: {
let c = MLModelConfiguration()
c.computeUnits = .cpuAndNeuralEngine // ANE: max efficiency
return c
}()).model
func startListening(onTrigger: @escaping () -> Void) throws {
let session = AVAudioSession.sharedInstance()
// .mixWithOthers: doesn't interrupt Spotify/podcasts
try session.setCategory(.playAndRecord, options: [.mixWithOthers, .allowBluetooth])
try session.setActive(true)
let request = try SNClassifySoundRequest(mlModel: mlModel)
request.windowDuration = CMTimeMakeWithSeconds(1.0, preferredTimescale: 44100)
request.overlapFactor = 0.5
let analyzer = SNAudioStreamAnalyzer(format: audioEngine.inputNode.outputFormat(forBus: 0))
try analyzer.add(request, withObserver: self)
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 8192, format: nil) { buf, time in
analyzer.analyze(buf, atAudioFramePosition: time.sampleTime)
}
try audioEngine.start()
}
func request(_ req: SNRequest, didProduce result: SNResult) {
guard let r = result as? SNClassificationResult,
let top = r.classifications.first,
top.identifier == "safe_word",
top.confidence >= 0.85 else { return }
// Verified → dispatch SOS
SOSDispatcher.shared.trigger(source: .voiceKeyword)
}
}
// ── ENROLLMENT FLOW (one-time setup) ────────────────────────────
// User records safe-word 5× → average speaker embedding stored
// in device EncryptedSharedPreferences / iOS Keychain (never leaves device)
EnrollmentManager.enroll(samples: 5, keyword: userChosenWord) { embedding in
Keychain.store("speaker_embedding", AES256.encrypt(embedding))
}Opus 24kbps
Codec
4kbps GPRS
Fallback Bitrate
30 Sekunden
Pre-Trigger Buffer
AES-256-GCM
Verschlüsselung
// ═══════════════════════════════════════════════════════════════
// MODULE 2 · SILENT LIVE-AUDIO + STEALTH MODE
// Stack: WebRTC (one-way) · AES-256-GCM · Display Stealth
// ═══════════════════════════════════════════════════════════════
// ── SIGNAL FLOW ──────────────────────────────────────────────────
//
// SOS Trigger
// │
// ▼
// [30s RAM Ring-Buffer flush] ← pre-trigger audio saved
// │
// ▼
// [WebRTC Peer Connection init]
// │ STUN/TURN: fra.turn.sec-si.de
// │ Offer SDP → Signal Server → Guardian
// ▼
// [AudioTrack: encrypted one-way stream]
// │ Codec: Opus 24kbps (low bandwidth mode available)
// │ E2E: ECDH P-384 key exchange → AES-256-GCM per chunk
// ▼
// Guardian receives audio (read-only, no mic opened on their side)
// ── ANDROID: SilentAudioService (Kotlin) ─────────────────────────
class SilentAudioService : Service() {
private lateinit var peerConnection: PeerConnection
private lateinit var ringBuffer: CircularAudioBuffer // 30s RAM
fun startStream(guardianSignalId: String) {
// 1. Create peer connection (send-only)
val rtcConfig = PeerConnection.RTCConfiguration(listOf(
PeerConnection.IceServer.builder("turn:fra.turn.sec-si.de:3478")
.setUsername(SESSION.turnUser).setPassword(SESSION.turnPass).createIceServer()
))
peerConnection = factory.createPeerConnection(rtcConfig, peerObserver)!!
// 2. Audio source: microphone (system-level, bypasses mute)
val audioConstraints = MediaConstraints()
val audioSource = factory.createAudioSource(audioConstraints)
val localAudioTrack = factory.createAudioTrack("audio0", audioSource)
peerConnection.addTrack(localAudioTrack, listOf("stream0"))
// 3. SDP offer
peerConnection.createOffer(object : SdpObserver {
override fun onCreateSuccess(sdp: SessionDescription) {
peerConnection.setLocalDescription(this, sdp)
SignalServer.send(guardianSignalId, sdp)
}
}, MediaConstraints())
}
// ── STEALTH DISPLAY CONTROL ───────────────────────────────────
fun activateStealthMode(mode: StealthMode) = when (mode) {
StealthMode.BLACK_SCREEN -> {
// Screen off via DeviceAdmin, service continues
(getSystemService(POWER_SERVICE) as PowerManager)
devicePolicyManager.lockNow()
}
StealthMode.DECOY_APP -> {
// Launch decoy activity that looks like Instagram/Maps
startActivity(Intent(this, DecoyActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP)
})
// Audio stream runs in background service — invisible
}
StealthMode.FAKE_SHUTDOWN -> {
// See Module 4
startActivity(Intent(this, FakeShutdownActivity::class.java))
}
}
}
// ── iOS: SilentAudioManager (Swift) ──────────────────────────────
class SilentAudioManager {
var peerConnection: RTCPeerConnection?
let factory = RTCPeerConnectionFactory()
func startStream(guardianId: String) {
let config = RTCConfiguration()
config.iceServers = [RTCIceServer(urlStrings: ["turn:fra.turn.sec-si.de:3478"],
username: session.turnUser, credential: session.turnPass)]
config.sdpSemantics = .unifiedPlan
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
peerConnection = factory.peerConnection(with: config, constraints: constraints, delegate: self)
// Send-only audio track
let audioSource = factory.audioSource(with: constraints)
let audioTrack = factory.audioTrack(with: audioSource, trackId: "audio0")
peerConnection?.add(audioTrack, streamIds: ["stream0"])
// AVAudioSession: background audio + screen dark
try? AVAudioSession.sharedInstance().setCategory(
.playAndRecord,
mode: .voiceChat,
options: [.mixWithOthers, .allowBluetooth]
)
// UIScreen brightness → 0 without turning off audio
UIScreen.main.brightness = 0
}
// Adaptive bitrate: poor connectivity fallback
func setBitrate(kbps: Int) {
let params = peerConnection?.senders.first?.parameters
params?.encodings.first?.maxBitrateBps = (kbps * 1000) as NSNumber
peerConnection?.senders.first?.parameters = params!
}
// 24kbps normal → 8kbps (GPRS) → 4kbps (edge-network fallback)
}
// ── PRE-TRIGGER RING BUFFER ───────────────────────────────────────
// Always running → on SOS trigger: flush last 30s to encrypted file
// then upload as evidence alongside live stream
class CircularAudioBuffer(val durationSeconds: Int = 30, sampleRate: Int = 16000) {
private val capacity = durationSeconds * sampleRate
private val buffer = ShortArray(capacity)
private var writePos = 0
fun write(samples: ShortArray) {
samples.forEach { buffer[writePos++ % capacity] = it }
}
fun flush(): ShortArray {
// Returns ordered 30s snapshot (oldest → newest)
val result = ShortArray(capacity)
for (i in 0 until capacity) result[i] = buffer[(writePos + i) % capacity]
return result
}
}< 400ms
End-to-End Latenz
5 Audio-Assets
Szenarien
CallKit CXProvider
iOS API
ÖPNV Maps
Smart Exit
// ═══════════════════════════════════════════════════════════════
// MODULE 3 · REMOTE FAKE-CALL
// Stack: WebSocket relay · CallKit (iOS) · Android Overlay
// Latency: Guardian click → call appears on victim < 400ms
// ═══════════════════════════════════════════════════════════════
// ── SIGNAL FLOW ──────────────────────────────────────────────────
//
// Guardian Dashboard
// │ "trigger_fake_call" + scenario + victim_session_id
// ▼
// WSS Hub (sec-si.de) ← JWT validated relay
// │ routes to victim socket only
// ▼
// Victim device receives FAKE_CALL event
// │
// ├─ iOS → CallKit CXCallUpdate (native call UI, locked screen)
// └─ Android → FLAG_SHOW_WHEN_LOCKED overlay + RingtoneManager
// │
// ▼
// User answers → scenario audio plays (local .m4a asset)
// │
// ▼
// Call ends → Smart Exit: Maps open with transit route home
// ── SCENARIOS (local audio assets, no streaming) ─────────────────
const SCENARIOS = [
{ id: "worried_mom", caller: "Mama", audio: "alibi_worried_mom.m4a", urgency: "high" },
{ id: "crying_child", caller: "Kita Sonnenschein", audio: "alibi_crying_child.m4a", urgency: "high" },
{ id: "boss_urgent", caller: "Dr. Hoffmann", audio: "alibi_boss_urgent.m4a", urgency: "high" },
{ id: "friend_pickup", caller: "Lisa", audio: "alibi_friend_pickup.m4a", urgency: "low" },
{ id: "doctor", caller: "Praxis Dr. Meier", audio: "alibi_doctor.m4a", urgency: "high" },
]
// ── ANDROID: IncomingCallActivity ────────────────────────────────
class IncomingFakeCallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val payload = intent.extras!!
// Override lockscreen + keep screen on
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
setContentView(R.layout.incoming_call_ui)
// Populate UI with scenario data
findViewById<TextView>(R.id.caller_name).text = payload.getString("caller_name")
findViewById<TextView>(R.id.caller_number).text = payload.getString("caller_number")
// Ringtone: override silent mode via STREAM_RING
val ringtone = RingtoneManager.getRingtone(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE))
ringtone.streamType = AudioManager.STREAM_RING
ringtone.play()
// Buttons
findViewById<Button>(R.id.btn_accept).setOnClickListener { onAccepted(payload) }
findViewById<Button>(R.id.btn_decline).setOnClickListener { finish() }
}
private fun onAccepted(payload: Bundle) {
// Play local audio asset (no network needed)
val mp = MediaPlayer.create(this, assets.openFd(payload.getString("audio_file")!!))
mp.start()
mp.setOnCompletionListener { onCallEnded() }
}
private fun onCallEnded() {
// Smart Exit: open Maps with transit route home
val homeAddr = EncryptedPrefs.get("home_address")
val mapsIntent = Intent(Intent.ACTION_VIEW,
Uri.parse("google.navigation:q=${Uri.encode(homeAddr)}&mode=transit"))
startActivity(mapsIntent)
// Update session: status = "navigating_home"
SessionRepository.updateStatus("navigating_home")
finish()
}
}
// ── CROSS-PLATFORM SECURE LINK GENERATION ────────────────────────
// JavaScript / Node.js (Dashboard / Backend):
const homeAddr = "Notfall-Standort"; // Variable für die Adresse
const secureMapUrl = `https://sec.si/nav/${encodeURIComponent(homeAddr)}`;
console.log("Gesicherte Notfall-URL generiert:", secureMapUrl);
// Flutter / Dart (App):
final secureUri = Uri.parse('https://sec.si/nav/${Uri.encodeComponent(homeAddr)}');
// ── SEC.SI DNS - Security Layer: Location Encryption ─────────────
const crypto = require('crypto'); // Standard-Verschlüsselung
function generateSecureSOS(gpsData, secretKey) {
try {
// Wir verschlüsseln die GPS-Daten (Breitengrad, Längengrad)
const cipher = crypto.createCipher('aes-256-cbc', secretKey);
let encrypted = cipher.update(gpsData, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Der fertige, sichere Link für das Guardian-Dashboard
return `https://sec.si/sos/${encrypted}`;
} catch (e) {
console.error("Verschlüsselungsfehler im SEC.SI Kernmodul:", e);
// Fallback: Nur verschlüsselte Koordinaten senden
return "ERROR_SECURE_LAYER_FAILED";
}
}
// ── iOS: CallKit Integration (Swift) ─────────────────────────────
class FakeCallProvider: NSObject, CXProviderDelegate {
private let provider: CXProvider
private var activeCallUUID = UUID()
private var player: AVAudioPlayer?
override init() {
let config = CXProviderConfiguration()
config.ringtoneSound = nil // system default ringtone
config.supportsVideo = false
config.maximumCallsPerCallGroup = 1
provider = CXProvider(configuration: config)
super.init()
provider.setDelegate(self, queue: nil)
}
func incomingCall(callerName: String, callerNumber: String, audioFile: String) {
activeCallUUID = UUID()
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: callerNumber)
update.localizedCallerName = callerName
update.hasVideo = false
update.supportsHolding = false
// Reports incoming call → native iOS call UI appears (even on lock screen)
provider.reportNewIncomingCall(with: activeCallUUID, update: update) { [weak self] error in
if error == nil { self?.pendingAudioFile = audioFile }
}
}
func providerDidActivateAudioSession(_ provider: CXProvider) {
// User answered → play scenario audio
if let file = pendingAudioFile,
let url = Bundle.main.url(forResource: file, withExtension: nil) {
player = try? AVAudioPlayer(contentsOf: url)
player?.play()
}
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
player?.stop()
action.fulfill()
SmartExit.navigate() // Open Maps transit route home
}
private var pendingAudioFile: String?
}800ms
Fade-Dauer
Normal-Deaktiv.
PIN A
🚔 Silent Police
PIN B (Duress)
Bleiben aktiv
Services
// ═══════════════════════════════════════════════════════════════
// MODULE 4 · ANTI-SHUTDOWN · Fake Power-Off Overlay
// Stack: Android FLAG_SHOW_WHEN_LOCKED + DeviceAdmin · iOS UIKit
// GPS + Audio remain active while screen appears off
// ═══════════════════════════════════════════════════════════════
// ── TRIGGER ──────────────────────────────────────────────────────
// Activated by:
// A) User holds power button → app intercepts volume+power combo
// B) Duress PIN entered → automatic fake-shutdown
// C) Guardian triggers remotely via WebSocket
// ── ANDROID: FakeShutdownActivity ────────────────────────────────
class FakeShutdownActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContentView(R.layout.fake_shutdown) // pure black layout
// Imitate real power-off: fade to black over 800ms
val overlay = findViewById<View>(R.id.overlay)
overlay.alpha = 0f
overlay.animate().alpha(1f).setDuration(800).withEndAction {
// Drop KEEP_SCREEN_ON → display goes dark naturally
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}.start()
// Haptic: single 200ms buzz (matches real power-off)
(getSystemService(VIBRATOR_SERVICE) as Vibrator)
.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
// CRITICAL: SafetyMonitoringService keeps running (START_STICKY)
// GPS interval: unchanged
// Audio ring-buffer: unchanged
// WebSocket heartbeat: unchanged
}
// Intercept back/home — nothing should exit fake-shutdown
override fun onBackPressed() {}
override fun onUserLeaveHint() {}
}
// ── iOS: FakeShutdownViewController ──────────────────────────────
class FakeShutdownViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
view.alpha = 0
// Fade to black over 0.8s
UIView.animate(withDuration: 0.8) { self.view.alpha = 1 } completion: { _ in
// Keep AVAudioSession active (Background Audio capability)
// CLLocationManager continues (Always Authorization)
// UIScreen brightness → 0 (display dark, CPU/GPS running)
UIScreen.main.brightness = 0
UIApplication.shared.isIdleTimerDisabled = false // allow sleep
}
// Vibration
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
}
// Swipe-up / home gesture → swallow
override var prefersHomeIndicatorAutoHidden: Bool { true }
}
// ── DURESS PIN LOGIC ──────────────────────────────────────────────
// PIN A (normal) → deactivates alarm visibly
// PIN B (duress) → looks identical but silently:
// 1. Sends Priority_RED to server → police dispatch
// 2. Activates FakeShutdown overlay
// 3. Continues GPS + audio recording
// Attacker sees "alarm off" — actual escalation invisible
fun validatePIN(input: String): PINResult {
val normalPIN = EncryptedPrefs.get("pin_normal")
val dressPIN = EncryptedPrefs.get("pin_duress")
return when (input) {
normalPIN -> PINResult.DEACTIVATE_NORMAL
dressPIN -> { triggerSilentPoliceDispatch(); PINResult.DEACTIVATE_DURESS }
else -> { failCount++; if (failCount >= 3) triggerSilentPoliceDispatch(); PINResult.WRONG }
}
}Was das für dich bedeutet · Einfache Zusammenfassung
Dein persönliches Zauberwort
Du legst ein geheimes Wort fest (z.B. "Ananas"). Sagst du es laut – auch wenn dein Handy in der Tasche ist – löst die App sofort einen stillen Notruf aus. Kein Antippen, kein Entsperren. Nur dein Wort.
Dein Notfallkontakt hört mit – unsichtbar
Sobald der Alarm ausgelöst wird, hört dein Notfallkontakt live alles, was um dich herum passiert. Dein Display bleibt dabei dunkel oder zeigt eine harmlose App – niemand sieht, dass du Hilfe gerufen hast.
Dein Rettungsanruf auf Knopfdruck
Dein Notfallkontakt kann dir mit einem Klick einen täuschend echten Anruf schicken – von "Mama", dem Chef oder der Kita. Du hebst ab, hörst ein vorgefertigtes Gespräch und hast den perfekten Grund zu gehen.
Selbst Ausschalten schützt dich
Zwingt dich jemand, dein Handy auszuschalten? Die App zeigt den Ausschalt-Bildschirm – aber GPS und Audio laufen still weiter. Dein Standort wird weiter übermittelt, obwohl das Handy "aus" aussieht.
Dein Datenschutz ist garantiert
Alles läuft auf deinem Gerät – kein Stichwort verlässt dein Handy. Nur im Notfall wird ein verschlüsseltes Signal gesendet. Wir als Betreiber können deinen Audio-Stream nicht mithören.