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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<a href="https://pub.dev/packages/safe_device"><img src="https://img.shields.io/badge/pub-1.4.1-blue" alt="Safe Device" height="18"></a>
<img src="https://imgur.com/Vw4Z93n.png" alt="Safe Device">
Flutter (Null-Safety) Jailbroken, root, emulator and mock location detection.
Flutter (Null-Safety) Jailbroken, root, emulator, mock location, and VPN detection.

## Getting Started

Expand Down Expand Up @@ -65,12 +65,18 @@ Checks whether device is real or emulator
bool isRealDevice = await SafeDevice.isRealDevice;
```

bool isMockLocation = await SafeDevice.isMockLocation;
Checks whether mock location is active

```
bool isMockLocation = await SafeDevice.isMockLocation;
```

Check whether VPN connection is active on device

```
bool isVpnActive = await SafeDevice.isVpnEnabled();
```

**Android:** If mock location check is disabled via config, the check always returns `false` and no location updates are started. If enabled (default), the check is active.

**iOS:** The config is stored for future use but does not affect current detection logic. Mock location detection on iOS is based on jailbreak/emulator status.
Expand Down
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application>
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.xamdesign.safe_device.MockLocation.LocationAssistant;
import com.xamdesign.safe_device.Rooted.RootedCheck;
import com.xamdesign.safe_device.SafeDeviceConfig;
import com.xamdesign.safe_device.isVpnEnabled.IsVpnEnabled;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
Expand Down Expand Up @@ -85,6 +86,8 @@ public void onMethodCall(MethodCall call, final MethodChannel.Result result) {
}
} else if (call.method.equals("rootDetectionDetails")) {
result.success(RootedCheck.getRootDetectionDetails(context));
} else if (call.method.equals("isVpnEnabled")) {
result.success(IsVpnEnabled.isVpnActive(context));
} else if (call.method.equals("init")) {
@SuppressWarnings("unchecked")
Map<String, Object> configMap = (Map<String, Object>) call.arguments;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.xamdesign.safe_device.isVpnEnabled;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;

public class IsVpnEnabled {
public static boolean isVpnActive(Context context) {
if (context == null) {
return false;
}

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}

ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager == null) {
return false;
}

Network[] networks = connectivityManager.getAllNetworks();
for (Network network : networks) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
return true;
}
}

return false;
}
}
7 changes: 7 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class _MyAppState extends State<MyApp> {
bool isRealDevice = false;
bool isOnExternalStorage = false;
bool isSafeDevice = false;
bool isVpnEnabled = false;
bool isDevelopmentModeEnable = false;
Map<String, dynamic> jailbreakDetails = {};
Map<String, dynamic> rootDetectionDetails = {};
Expand All @@ -40,6 +41,7 @@ class _MyAppState extends State<MyApp> {
isOnExternalStorage = false;
isSafeDevice = false;
isDevelopmentModeEnable = false;
isVpnEnabled = false;
jailbreakDetails = {};
rootDetectionDetails = {};
});
Expand All @@ -50,6 +52,8 @@ class _MyAppState extends State<MyApp> {
isOnExternalStorage = await SafeDevice.isOnExternalStorage;
isSafeDevice = await SafeDevice.isSafeDevice;
isDevelopmentModeEnable = await SafeDevice.isDevelopmentModeEnable;
isVpnEnabled = await SafeDevice.isVpnEnabled;


// iOS-specific enhanced jailbreak detection
if (Platform.isIOS) {
Expand All @@ -64,6 +68,7 @@ class _MyAppState extends State<MyApp> {

setState(() {
this.isJailBroken = isJailBroken;
this.isVpnEnabled = isVpnEnabled;
this.isJailBrokenCustom = isJailBrokenCustom;
this.isMockLocation = isMockLocation;
this.isRealDevice = isRealDevice;
Expand Down Expand Up @@ -110,6 +115,7 @@ class _MyAppState extends State<MyApp> {
),
SizedBox(height: 8),
if (jailbreakDetails.containsKey('isSimulator'))

Card(
color: jailbreakDetails['isSimulator'] == true
? Colors.blue.shade50
Expand Down Expand Up @@ -212,6 +218,7 @@ class _MyAppState extends State<MyApp> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildInfoRow('isJailBroken()', isJailBroken),
buildInfoRow('isVpnEnabled()', isVpnEnabled),
if (Platform.isIOS) ...[
buildInfoRow('isJailBrokenCustom()', isJailBrokenCustom),
],
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.3.10"
version: "1.4.1"
sky_engine:
dependency: transitive
description: flutter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

#import "SafeDeviceJailbreakDetection.h"
#import <UIKit/UIKit.h>

#import <SystemConfiguration/SystemConfiguration.h>
#include <ifaddrs.h>
@implementation SafeDeviceJailbreakDetection

#pragma mark - Jailbreak Detection Paths
Expand Down Expand Up @@ -126,6 +127,39 @@ + (BOOL)isSimulator {
#endif
}

#pragma mark - VPN Detection

+ (BOOL)isVPNConnected {
BOOL isVPN = NO;
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;

// Retrieve the current interfaces - returns 0 on success
if (getifaddrs(&interfaces) == 0) {
// Loop through linked list of interfaces
temp_addr = interfaces;
while (temp_addr != NULL) {
if (temp_addr->ifa_addr != NULL && (temp_addr->ifa_addr->sa_family == AF_INET || temp_addr->ifa_addr->sa_family == AF_INET6)) {
NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name];
if ([interfaceName containsString:@"tap"] ||
[interfaceName containsString:@"tun"] ||
[interfaceName containsString:@"ppp"] ||
[interfaceName containsString:@"ipsec"] ||
[interfaceName containsString:@"utun"]) {
isVPN = YES;
break;
}
}
temp_addr = temp_addr->ifa_next;
}
}

// Free memory
freeifaddrs(interfaces);

return isVPN;
}

#pragma mark - Main Detection Method

+ (BOOL)isJailbroken {
Expand Down
3 changes: 3 additions & 0 deletions ios/safe_device/Sources/safe_device/SafeDevicePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
result([NSNumber numberWithBool:[self isJailBrokenCustom]]);
}else if ([@"jailbreakDetails" isEqualToString:call.method]) {
result([self jailbreakDetails]);
}else if ([@"isVpnEnabled" isEqualToString:call.method]) {
result([NSNumber numberWithBool:[SafeDeviceJailbreakDetection isVPNConnected]]);
}else if ([@"canMockLocation" isEqualToString:call.method]) {
//For now we have returned if device is Jail Broken or if it's not real device. There is no
//strong detection of Mock location in iOS
Expand Down Expand Up @@ -176,6 +178,7 @@ - (NSDictionary*)jailbreakDetails {
@"hasObviousJailbreakSigns": @(obviousJailbreak),
@"dttResult": @([DTTJailbreakDetection isJailbroken]),
@"customResult": @([SafeDeviceJailbreakDetection isJailbroken]),
@"isVpnConnected": @([SafeDeviceJailbreakDetection isVPNConnected]),
@"finalResult": @([self isJailBroken])
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Main jailbreak detection method
+ (BOOL)isJailbroken;

/// Check whether a VPN connection is currently active.
+ (BOOL)isVPNConnected;

/// Get detailed breakdown of jailbreak detection methods
+ (NSDictionary<NSString *, NSNumber *> *)getJailbreakDetails;

Expand Down
10 changes: 10 additions & 0 deletions lib/safe_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,14 @@ class SafeDevice {
}
return {}; // iOS devices return empty map for Android-specific method
}

static Future<bool> get isVpnEnabled async {
ensureInitiated();
try {
return await _channel.invokeMethod<bool>('isVpnEnabled') ?? false;
} catch (e) {
print('Error checking VPN status: $e');
return false;
}
}
}
17 changes: 17 additions & 0 deletions test/safe_device_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:safe_device/safe_device.dart';

void main() {
const MethodChannel channel = MethodChannel('safe_device');
Expand All @@ -24,4 +25,20 @@ void main() {
test('getPlatformVersion', () async {
//expect(await SafeDevice.platformVersion, '42');
});

test('isVpnEnabled forwards the platform call', () async {
final binaryMessenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
binaryMessenger.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'init') {
return null;
}
if (methodCall.method == 'isVpnEnabled') {
return true;
}
return null;
});

expect(await SafeDevice.isVpnEnabled(), isTrue);
});
}