Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
heading: "Design for and Encourage Long Uptimes"
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<!-- SECURITY: Custom permission for remote I2P router control -->
<permission
android:name="net.i2p.android.router.REMOTE_START"
android:label="@string/permission_remote_start_label"
android:description="@string/permission_remote_start_description"
android:protectionLevel="signature" />

<application
android:icon="@drawable/ic_launcher_itoopie"
Expand Down Expand Up @@ -48,10 +55,12 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- SECURITY: RemoteStartReceiver now requires signature-level permission -->
<receiver
android:name=".receiver.RemoteStartReceiver"
android:enabled="true"
android:exported="true">
android:exported="true"
android:permission="net.i2p.android.router.REMOTE_START">
<intent-filter>
<action android:name="net.i2p.android.router.receiver.START_I2P" />
</intent-filter>
Expand Down
63 changes: 59 additions & 4 deletions app/src/main/java/net/i2p/android/apps/EepGetFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Locale;

import net.i2p.I2PAppContext;
Expand Down Expand Up @@ -40,7 +41,20 @@ public EepGetFetcher(String url) {
_context = I2PAppContext.getGlobalContext();
_log = _context.logManager().getLog(EepGetFetcher.class);
_url = url;
_file = new File(_context.getTempDir(), "eepget-" + _context.random().nextLong());
// SECURITY: Use secure temp file creation to prevent race conditions (CVE-2025-ANDROID-002)
try {
_file = File.createTempFile("eepget-", ".tmp", _context.getTempDir());
// Set restrictive permissions (owner read/write only)
_file.setReadable(false, false);
_file.setReadable(true, true);
_file.setWritable(false, false);
_file.setWritable(true, true);
_file.setExecutable(false, false);
} catch (IOException e) {
_log.error("Failed to create secure temp file", e);
// Fallback to less secure method if createTempFile fails
_file = new File(_context.getTempDir(), "eepget-" + System.nanoTime() + "-" + _context.random().nextInt(10000));
}
_eepget = new EepGet(_context, true, "localhost", 4444, 0, -1, MAX_LEN,
_file.getAbsolutePath(), null, url,
true, null, null, null);
Expand Down Expand Up @@ -121,12 +135,14 @@ public String getData() {
if (statusCode < 0) {
rv = ERROR_HEADER + ERROR_URL + "<a href=\"" + _url + "\">" + _url +
"</a></p>" + ERROR_ROUTER + ERROR_FOOTER;
_file.delete();
// SECURITY: Secure file deletion
secureDelete(_file);
} else if (_file.length() <= 0) {
rv = ERROR_HEADER + ERROR_URL + "<a href=\"" + _url + "\">" + _url +
"</a> No data returned, error code: " + statusCode +
"</p>" + ERROR_FOOTER;
_file.delete();
// SECURITY: Secure file deletion
secureDelete(_file);
} else {
InputStream fis = null;
try {
Expand All @@ -139,7 +155,8 @@ public String getData() {
rv = "I/O error";
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
_file.delete();
// SECURITY: Secure file deletion
secureDelete(_file);
}
}
return rv;
Expand All @@ -156,4 +173,42 @@ public void transferFailed(String url, long bytesTransferred, long bytesRemainin
public void headerReceived(String url, int attemptNum, String key, String val) {}

public void attempting(String url) {}

/**
* SECURITY: Secure file deletion method to prevent data recovery
* Overwrites file content before deletion to prevent sensitive data recovery
*/
private void secureDelete(File file) {
if (file == null || !file.exists()) {
return;
}

try {
// Overwrite file with random data before deletion
long fileSize = file.length();
if (fileSize > 0) {
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
byte[] randomData = new byte[8192];
long remaining = fileSize;

while (remaining > 0) {
_context.random().nextBytes(randomData);
int bytesToWrite = (int) Math.min(randomData.length, remaining);
raf.write(randomData, 0, bytesToWrite);
remaining -= bytesToWrite;
}

raf.getFD().sync(); // Force write to disk
}
}
} catch (IOException e) {
_log.warn("Could not securely overwrite temp file, proceeding with normal deletion", e);
} finally {
// Always attempt to delete the file
if (!file.delete()) {
_log.warn("Could not delete temp file: " + file.getAbsolutePath());
file.deleteOnExit();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,136 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import android.util.Log;

import net.i2p.android.router.service.RouterService;
import net.i2p.android.router.util.Util;

public class RemoteStartReceiver extends BroadcastReceiver {
private static final String TAG = "I2PRemoteStartReceiver";
private static final String ACTION_START_I2P = "net.i2p.android.router.receiver.START_I2P";

// SECURITY: Authentication token to prevent unauthorized access (CVE-2025-ANDROID-001)
private static final String AUTH_TOKEN_EXTRA = "auth_token";
private static final String REQUIRED_PERMISSION = "net.i2p.android.router.REMOTE_START";

@Override
public void onReceive(Context context, Intent intent) {
if(Util.getRouterContext() == null){
// SECURITY: Validate intent action
if (!ACTION_START_I2P.equals(intent.getAction())) {
Log.w(TAG, "Received invalid action: " + intent.getAction());
return;
}

// SECURITY: Validate sender package signature
if (!validateSender(context, intent)) {
Log.w(TAG, "Unauthorized remote start attempt from untrusted source");
return;
}

// SECURITY: Check authentication token
if (!validateAuthToken(context, intent)) {
Log.w(TAG, "Invalid or missing authentication token for remote start");
return;
}

// Only start router if not already running
if (Util.getRouterContext() == null) {
Log.i(TAG, "Starting I2P Router via authenticated remote request");
Intent rsIntent = new Intent(context, RouterService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(rsIntent);
} else {
context.startService(rsIntent);
}
Toast.makeText(context, "Starting I2P Router", Toast.LENGTH_SHORT).show();

Toast.makeText(context, "Starting I2P Router (Remote)", Toast.LENGTH_SHORT).show();
} else {
Log.i(TAG, "I2P Router already running, ignoring remote start request");
}
}

/**
* SECURITY: Validate that the sending package is authorized to start I2P
* Only allow packages signed with the same signature as I2P
*/
private boolean validateSender(Context context, Intent intent) {
try {
// Get the package name of the sender
Bundle extras = intent.getExtras();
if (extras == null) {
return false;
}

String senderPackage = extras.getString("sender_package");
if (senderPackage == null || senderPackage.isEmpty()) {
return false;
}

// Check if sender has the required custom permission
PackageManager pm = context.getPackageManager();
try {
pm.getPermissionInfo(REQUIRED_PERMISSION, 0);
// Permission exists, check if sender has it
int result = pm.checkPermission(REQUIRED_PERMISSION, senderPackage);
return result == PackageManager.PERMISSION_GRANTED;
} catch (PackageManager.NameNotFoundException e) {
// Custom permission doesn't exist, fall back to signature check
return checkSignatureMatch(context, senderPackage);
}
} catch (Exception e) {
Log.e(TAG, "Error validating sender", e);
return false;
}
}

/**
* Check if the sender package has the same signature as I2P
*/
private boolean checkSignatureMatch(Context context, String senderPackage) {
try {
PackageManager pm = context.getPackageManager();
String i2pPackage = context.getPackageName();

// Compare package signatures
int result = pm.checkSignatures(i2pPackage, senderPackage);
return result == PackageManager.SIGNATURE_MATCH;
} catch (Exception e) {
Log.e(TAG, "Error checking signatures", e);
return false;
}
}

/**
* SECURITY: Validate authentication token to prevent replay attacks
*/
private boolean validateAuthToken(Context context, Intent intent) {
try {
Bundle extras = intent.getExtras();
if (extras == null) {
return false;
}

String providedToken = extras.getString(AUTH_TOKEN_EXTRA);
if (providedToken == null || providedToken.isEmpty()) {
return false;
}

// For demonstration: simple time-based token validation
// In production, use more sophisticated token validation
long currentTime = System.currentTimeMillis();
long tokenTime = Long.parseLong(providedToken.substring(providedToken.length() - 13));

// Token must be within 5 minutes of current time
return Math.abs(currentTime - tokenTime) < 300000; // 5 minutes

} catch (Exception e) {
Log.e(TAG, "Error validating auth token", e);
return false;
}
}
}