Mobile app hardening — security + Android 14 compat
capacitor.config.json:
- webContentsDebuggingEnabled: true → false
(was leaving Chrome DevTools able to attach to released builds)
- allowMixedContent: true → false
(API is HTTPS-only; no need to permit cleartext loads)
- server.allowNavigation: ["*"] → restricted to pedshub.com /
peds.danvics.com origins
(prevents WebView following an attacker-controlled redirect)
AndroidManifest.xml:
- android:allowBackup="false" + data_extraction_rules.xml
(Android system backup would otherwise copy EncryptedSharedPreferences
containing the auth token into Google Cloud backups)
- Removed USE_BIOMETRIC permission (feature removed earlier)
AudioRecordingService.java:
- startForeground(id, notif, TYPE_MICROPHONE) on Android 14+
(without the explicit type Android 14 kills the service with
MissingForegroundServiceTypeException)
- WakeLock cap: 1h → 8h (still bounded, onDestroy releases early)
MainActivity.java:
- Removed dead biometric code path and androidx.biometric imports
mobile/package.json:
- Dropped @aparajita/capacitor-biometric-auth — orphan dependency
This commit is contained in:
parent
fa16cb13cb
commit
cdf178b1c3
6 changed files with 48 additions and 64 deletions
|
|
@ -2,7 +2,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
|
@ -71,7 +73,6 @@
|
|||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.app.NotificationManager;
|
|||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
|
|
@ -40,11 +41,13 @@ public class AudioRecordingService extends Service {
|
|||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Acquire wake lock to keep CPU active during recording
|
||||
// Acquire wake lock to keep CPU active during recording.
|
||||
// 8h cap is a safety net — onDestroy() releases early when recording
|
||||
// stops. The cap prevents a runaway lock if the service leaks.
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (pm != null) {
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
|
||||
wakeLock.acquire(60 * 60 * 1000L); // 1 hour max
|
||||
wakeLock.acquire(8 * 60 * 60 * 1000L);
|
||||
}
|
||||
|
||||
// Stop action in notification
|
||||
|
|
@ -65,7 +68,15 @@ public class AudioRecordingService extends Service {
|
|||
.addAction(android.R.drawable.ic_media_pause, "Stop Recording", stopPending)
|
||||
.build();
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
// Android 14 (SDK 34) requires the 3-arg form with an explicit
|
||||
// foregroundServiceType matching the manifest declaration, else
|
||||
// the service is killed with MissingForegroundServiceTypeException.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(NOTIFICATION_ID, notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.pedshub.scribe;
|
|||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.PermissionRequest;
|
||||
|
|
@ -10,20 +9,15 @@ import android.webkit.WebChromeClient;
|
|||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
private static final int MIC_PERMISSION_CODE = 1001;
|
||||
private PermissionRequest pendingPermissionRequest;
|
||||
private boolean biometricDone = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -79,55 +73,6 @@ public class MainActivity extends BridgeActivity {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Biometric Authentication ──────────────────────────────
|
||||
|
||||
private void showBiometricPrompt() {
|
||||
BiometricManager biometricManager = BiometricManager.from(this);
|
||||
int canAuth = biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK |
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL);
|
||||
|
||||
if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
// No biometric or device credential available — skip
|
||||
biometricDone = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Executor executor = ContextCompat.getMainExecutor(this);
|
||||
|
||||
BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor,
|
||||
new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
biometricDone = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
// Allow access even on error (user can cancel)
|
||||
biometricDone = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
// Don't block — just let them retry or cancel
|
||||
}
|
||||
});
|
||||
|
||||
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle("PedScribe")
|
||||
.setSubtitle("Verify your identity to continue")
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK |
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL)
|
||||
.build();
|
||||
|
||||
biometricPrompt.authenticate(promptInfo);
|
||||
}
|
||||
|
||||
// ── Background Recording Service Bridge ───────────────────
|
||||
|
||||
private void setupRecordingBridge() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android 12+ backup rules. Exclude everything from both cloud backup
|
||||
and device-to-device transfer. The app handles PHI-adjacent data
|
||||
(Keychain/Keystore-backed auth token, cached web content) that must
|
||||
not leave the device via Google's automatic backup system.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
|
|
@ -3,7 +3,12 @@
|
|||
"appName": "PedScribe",
|
||||
"webDir": "src",
|
||||
"server": {
|
||||
"allowNavigation": ["*"]
|
||||
"allowNavigation": [
|
||||
"app.pedshub.com",
|
||||
"*.pedshub.com",
|
||||
"pedshub.com",
|
||||
"peds.danvics.com"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
|
|
@ -21,9 +26,9 @@
|
|||
}
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"allowMixedContent": false,
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": true,
|
||||
"webContentsDebuggingEnabled": false,
|
||||
"appendUserAgent": "PedScribe-Android",
|
||||
"androidScheme": "https"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"@capacitor/cli": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/ios": "^6.0.0",
|
||||
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/push-notifications": "^6.0.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue