From 8e5e82c3d8f391ac71fe6d1be6f1b060cddb4e41 Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Wed, 14 Jan 2026 01:22:28 +0700 Subject: [PATCH] Mengatasi Manipulasi waktu dan Deteksi Fake GPS --- .../ubharajaya/sistemakademik/MainActivity.kt | 75 ++++++++++++---- backend/app.py | 88 +++++++++---------- 2 files changed, 101 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index 0614684..4113755 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -1874,26 +1874,71 @@ fun AbsensiScreenWithJadwal( }) } - // ... (Kode LocationPermissionLauncher & CameraLauncher SAMA SEPERTI SEBELUMNYA, SALIN DI SINI) ... - // ... Agar kode tidak kepanjangan, saya asumsikan Anda menyalin launcher location/camera dari kode lama ... - // ... PASTIKAN variable 'locationPermissionLauncher' dan 'cameraLauncher' ada di sini ... - val locationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - fusedLocationClient.lastLocation.addOnSuccessListener { location -> - if (location != null) { - latitude = location.latitude - longitude = location.longitude - val jarak = hitungJarak(location.latitude, location.longitude, AppConstants.KAMPUS_LATITUDE, AppConstants.KAMPUS_LONGITUDE) - jarakKeKampus = jarak - val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) "āœ… DI DALAM AREA" else "āŒ DI LUAR AREA" - lokasi = "šŸ“ Lat: ${String.format("%.6f", location.latitude)}\nšŸ“ Lon: ${String.format("%.6f", location.longitude)}\nšŸ“ Jarak: ${String.format("%.0f", jarak)} m\n$statusLokasi" - } else { lokasi = "āŒ Lokasi tidak tersedia" } - } + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + fusedLocationClient.lastLocation + .addOnSuccessListener { location -> + if (location != null) { + // ======================================================== + // šŸ›”ļø SECURITY FIX: DETEKSI FAKE GPS + // ======================================================== + val isFakeGps = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + location.isMock // Untuk Android 12 ke atas + } else { + location.isFromMockProvider // Untuk Android lama + } + + if (isFakeGps) { + // JIKA TERDETEKSI PALSU + latitude = null // Null-kan agar tombol kirim mati + longitude = null + jarakKeKampus = null + lokasi = "ā›” FAKE GPS TERDETEKSI!\nSistem menolak lokasi palsu.\nMatikan aplikasi Fake GPS Anda." + + // Tampilkan dialog error + errorMessage = "āš ļø Keamanan: Terdeteksi menggunakan Fake GPS/Lokasi Palsu. Mohon matikan aplikasi tersebut dan coba lagi." + } else { + // JIKA LOKASI ASLI (Logika Normal) + latitude = location.latitude + longitude = location.longitude + + val jarak = hitungJarak( + location.latitude, + location.longitude, + AppConstants.KAMPUS_LATITUDE, + AppConstants.KAMPUS_LONGITUDE + ) + jarakKeKampus = jarak + + val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) { + "āœ… DI DALAM AREA" + } else { + "āŒ DI LUAR AREA" + } + + lokasi = "šŸ“ Lat: ${String.format("%.6f", location.latitude)}\n" + + "šŸ“ Lon: ${String.format("%.6f", location.longitude)}\n" + + "šŸ“ Jarak: ${String.format("%.0f", jarak)} m\n" + + "$statusLokasi" + } + } else { + lokasi = "āŒ Lokasi tidak tersedia (Aktifkan GPS)" + } + } + .addOnFailureListener { + lokasi = "āŒ Gagal mengambil lokasi" + errorMessage = "Gagal mengambil lokasi: ${it.message}" + } } + } else { + errorMessage = "āš ļø Izin lokasi ditolak. Aplikasi tidak dapat digunakan." } } diff --git a/backend/app.py b/backend/app.py index b41be45..5759ed9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -424,25 +424,16 @@ def get_profile(): @token_required def submit_absensi(): """ - Endpoint untuk submit absensi (UPDATE: dengan validasi jadwal) - - Request Body: - { - "id_jadwal": 1, - "latitude": -6.223276, - "longitude": 107.009273, - "timestamp": "2026-01-13 14:30:00", - "foto_base64": "base64_string", - "status": "HADIR" - } + Endpoint untuk submit absensi + UPDATE KEAMANAN: Menggunakan Waktu Server untuk validasi dan penyimpanan """ try: data = request.get_json() id_mahasiswa = request.user_data['id_mahasiswa'] npm = request.user_data['npm'] - # Validasi input - required_fields = ['id_jadwal', 'latitude', 'longitude', 'timestamp', 'foto_base64', 'status'] + # Validasi input (timestamp dari client kita abaikan untuk logic, tapi tetap dicek keberadaannya gapapa) + required_fields = ['id_jadwal', 'latitude', 'longitude', 'foto_base64', 'status'] for field in required_fields: if field not in data: return jsonify({'error': f'Field {field} wajib diisi'}), 400 @@ -453,27 +444,21 @@ def submit_absensi(): cursor = connection.cursor(dictionary=True) - # Ambil nama mahasiswa + # 1. Ambil Data Mahasiswa cursor.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa = %s", (id_mahasiswa,)) mahasiswa = cursor.fetchone() - if not mahasiswa: cursor.close() connection.close() return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404 - # Ambil info jadwal & mata kuliah + # 2. Ambil Jadwal cursor.execute(""" - SELECT - j.id_jadwal, - j.jam_mulai, - j.jam_selesai, - m.nama_matkul + SELECT j.id_jadwal, j.jam_mulai, j.jam_selesai, m.nama_matkul FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.id_jadwal = %s """, (data['id_jadwal'],)) - jadwal = cursor.fetchone() if not jadwal: @@ -481,22 +466,29 @@ def submit_absensi(): connection.close() return jsonify({'error': 'Jadwal tidak ditemukan'}), 404 - # Validasi waktu absensi - from datetime import datetime, timedelta + # ========================================================================= + # šŸ›”ļø SECURITY FIX: TIME MANIPULATION + # Menggunakan Waktu Server saat ini, BUKAN waktu dari client Android + # ========================================================================= - timestamp_absensi = datetime.strptime(data['timestamp'], '%Y-%m-%d %H:%M:%S') - waktu_absensi = timestamp_absensi.time() + waktu_server_sekarang = datetime.now() + # Opsi: Jika server Anda UTC, konversi ke WIB (UTC+7) + # waktu_server_sekarang = datetime.utcnow() + timedelta(hours=7) + + jam_sekarang = waktu_server_sekarang.time() + tanggal_sekarang_str = waktu_server_sekarang.strftime('%Y-%m-%d %H:%M:%S') + + # Normalisasi jam mulai & selesai dari database jam_mulai = jadwal['jam_mulai'] jam_selesai = jadwal['jam_selesai'] - # CONVERT timedelta ke time jika perlu + # Helper convert timedelta ke time (jika perlu) if isinstance(jam_mulai, timedelta): total_seconds = int(jam_mulai.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - jam_mulai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time() + jam_mulai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time() elif isinstance(jam_mulai, str): jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time() @@ -504,38 +496,38 @@ def submit_absensi(): total_seconds = int(jam_selesai.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - jam_selesai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time() + jam_selesai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time() elif isinstance(jam_selesai, str): jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time() - if not (jam_mulai <= waktu_absensi <= jam_selesai): + # 3. Validasi Waktu (Pakai Jam Server) + if not (jam_mulai <= jam_sekarang <= jam_selesai): cursor.close() connection.close() return jsonify({ - 'error': 'Absensi di luar jam kelas', + 'error': 'Absensi gagal! Diluar jam kelas (Server Time)', 'detail': { 'jam_mulai': str(jam_mulai), 'jam_selesai': str(jam_selesai), - 'waktu_absensi': str(waktu_absensi) + 'waktu_server': str(jam_sekarang) } }), 400 - # Cek apakah sudah absen hari ini untuk jadwal ini + # 4. Cek Double Absen Hari Ini cursor.execute(""" SELECT COUNT(*) as count FROM absensi WHERE id_mahasiswa = %s AND id_jadwal = %s AND DATE(timestamp) = DATE(%s) - """, (id_mahasiswa, data['id_jadwal'], data['timestamp'])) + """, (id_mahasiswa, data['id_jadwal'], tanggal_sekarang_str)) if cursor.fetchone()['count'] > 0: cursor.close() connection.close() return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400 - # Insert absensi ke MySQL + # 5. Insert ke Database (Pakai Waktu Server) insert_query = """ INSERT INTO absensi ( id_mahasiswa, npm, nama, id_jadwal, mata_kuliah, @@ -543,6 +535,10 @@ def submit_absensi(): ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ + + # Ambil foto (prioritaskan field foto_base64) + foto = data.get('foto_base64') or data.get('photo') + cursor.execute(insert_query, ( id_mahasiswa, npm, @@ -551,9 +547,9 @@ def submit_absensi(): jadwal['nama_matkul'], data['latitude'], data['longitude'], - data['timestamp'], - data.get('photo', data['foto_base64']), - data['foto_base64'], + tanggal_sekarang_str, # <--- PENTING: Simpan waktu server + foto, + foto, data['status'] )) @@ -563,9 +559,8 @@ def submit_absensi(): cursor.close() connection.close() - # KIRIM KE WEBHOOK N8N + # 6. Kirim ke Webhook N8N (Opsional) try: - import requests webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254" webhook_payload = { "npm": npm, @@ -573,13 +568,11 @@ def submit_absensi(): "mata_kuliah": jadwal['nama_matkul'], "latitude": data['latitude'], "longitude": data['longitude'], - "timestamp": data['timestamp'], - "photo": data['foto_base64'], - "foto_base64": data['foto_base64'], + "timestamp": tanggal_sekarang_str, # Kirim waktu server "status": data['status'] } - webhook_response = requests.post(webhook_url, json=webhook_payload, timeout=10) - print(f"āœ… Webhook n8n: {webhook_response.status_code}") + # Gunakan try-except timeout agar tidak memblokir response + requests.post(webhook_url, json=webhook_payload, timeout=3) except Exception as e: print(f"āš ļø Webhook error: {e}") @@ -588,6 +581,7 @@ def submit_absensi(): 'data': { 'id_absensi': id_absensi, 'mata_kuliah': jadwal['nama_matkul'], + 'timestamp': tanggal_sekarang_str, 'status': data['status'] } }), 201