Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Breaking

- `allChecksFinished` RASP execution state action renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed
- `TalsecIosConfig.appBundleId` (string) replaced by `appBundleIds` (string[]), matching the iOS SDK
- Methods that resolved with a status string or a constant now resolve to `void`: `talsecStart`, `addToWhitelist`, `blockScreenCapture`, `blockScreenControl`, `startOngoingCallDetection`, `stopOngoingCallDetection`, `storeExternalId`, `removeExternalId`
- All methods now reject with `TalsecError` (`code`: `talsec-failure`, `unsupported-platform`, `configuration-exception`, `external-id-failure`, `malware-failure`) instead of native errors or plain strings
- `onInvalidCallback` and `removeListenerForEvent` are no longer exported; use `abortApp` to terminate the app

### React Native

#### Added

- `useFreeRasp` returns `{ started, error }`; `error` is the `TalsecError` that prevented the listeners from being registered or freeRASP from starting, which was previously only logged to the console
- `ongoingCall` threat action with the new `CallState` type (`'STARTED' | 'ONGOING' | 'STOPPED'`), driven by `startOngoingCallDetection()` and `stopOngoingCallDetection()` (Android only)
- `blockScreenControl()` and `isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only)
- `blockOverlayWindows()` and `isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission)
Expand All @@ -27,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Android package (namespace) of the plugin renamed from `com.freeraspreactnative` to `app.talsec.rasp.reactnative`, following the Android SDK's `app.talsec.rasp` (update the import if you register `FreeraspReactNativePackage` manually)
- On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity
- Threat and RASP execution state actions are typed as returning `void` instead of `any`

### Android

Expand Down
22 changes: 16 additions & 6 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const App = () => {
},
},
iosConfig: {
appBundleId: 'org.reactjs.native.example.FreeraspReactNativeExample',
appBundleIds: ['org.reactjs.native.example.FreeraspReactNativeExample'],
appTeamId: 'your_team_ID',
},
watcherMail: 'your_email_address@example.com',
Expand Down Expand Up @@ -351,18 +351,28 @@ const App = () => {
await Promise.all(
appsToWhitelist.map(async (app) => {
try {
const whitelistResponse = await addToWhitelist(app);
console.info(
`${app} stored to Malware Whitelist: ${whitelistResponse}`
);
await addToWhitelist(app);
console.info(`${app} stored to Malware Whitelist`);
} catch (error) {
console.info('Malware whitelist failed: ', error);
}
})
);
};

useFreeRasp(config, actions, raspExecutionStateActions);
const { error: raspError } = useFreeRasp(
config,
actions,
raspExecutionStateActions
);

useEffect(() => {
if (raspError) {
console.error(
`freeRASP failed to start: ${raspError.code}: ${raspError.message}`
);
}
}, [raspError]);

return (
<DemoApp
Expand Down
3 changes: 1 addition & 2 deletions example/src/DemoApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ export const DemoApp: React.FC<{

const toggleScreenCaptureBlock = async () => {
try {
const response = await blockScreenCapture(!hasScreenCaptureBlocked);
console.info('Changing Screen Capture Status:', response);
await blockScreenCapture(!hasScreenCaptureBlocked);
await updateScreenCaptureStatus();
} catch (error: any) {
console.error('Screen capture Error:', error.message);
Expand Down
6 changes: 2 additions & 4 deletions example/src/MalwareItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ export const MalwareItem: React.FC<{ app: SuspiciousAppInfo }> = ({ app }) => {

const whitelistApp = async (packageName: string) => {
try {
const whitelistResponse = await addToWhitelist(packageName);
console.info(
`Malware Whitelist response for ${app}: ${whitelistResponse}`
);
await addToWhitelist(packageName);
console.info(`${app.packageInfo.packageName} added to Malware Whitelist`);
alert('Restart app for whitelist to take effect');
} catch (error: any) {
console.info('Error while adding app to malware whitelist: ', error);
Expand Down
6 changes: 3 additions & 3 deletions ios/FreeraspReactNative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class FreeraspReactNative: RCTEventEmitter {
guard let iosConfig = talsecConfig["iosConfig"] as? NSDictionary else {
throw NSError(domain: "Missing iosConfig parameter in Talsec Native Plugin", code: 1)
}
guard let appBundleIds = iosConfig["appBundleId"] as? String else {
throw NSError(domain: "Missing appBundleId parameter in Talsec Native Plugin", code: 2)
guard let appBundleIds = iosConfig["appBundleIds"] as? [String] else {
throw NSError(domain: "Missing appBundleIds parameter in Talsec Native Plugin", code: 2)
}
guard let appTeamId = iosConfig["appTeamId"] as? String else {
throw NSError(domain: "Missing appTeamId parameter in Talsec Native Plugin", code: 3)
Expand All @@ -62,7 +62,7 @@ class FreeraspReactNative: RCTEventEmitter {
}
let isProd = talsecConfig["isProd"] as? Bool ?? true

let config = TalsecConfig(appBundleIds: [appBundleIds], appTeamId: appTeamId, watcherMailAddress: watcherMailAddress, isProd: isProd)
let config = TalsecConfig(appBundleIds: appBundleIds, appTeamId: appTeamId, watcherMailAddress: watcherMailAddress, isProd: isProd)
Talsec.start(config: config)
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/listeners/raspExecutionState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NativeEventEmitter, type EmitterSubscription } from 'react-native';
import { FreeraspReactNative } from '../nativeModules';
import { onInvalidCallback, removeListenerForEvent } from '../methods/native';
import { onInvalidCallback, removeListenerForEvent } from '../methods/internal';
import type {
NativeEvent,
RaspExecutionStateEventActions,
Expand Down
2 changes: 1 addition & 1 deletion src/api/listeners/threat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NativeEventEmitter, type EmitterSubscription } from 'react-native';
import { FreeraspReactNative } from '../nativeModules';
import { onInvalidCallback, removeListenerForEvent } from '../methods/native';
import { onInvalidCallback, removeListenerForEvent } from '../methods/internal';
import { Threat } from '../../models/threat';
import type {
CallState,
Expand Down
13 changes: 13 additions & 0 deletions src/api/methods/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FreeraspReactNative } from '../nativeModules';

/**
* Terminates the app. Called when the native side sends data that freeRASP
* never sends, which indicates tampering. Exported publicly as `abortApp`.
*/
export const onInvalidCallback = (): void => {
FreeraspReactNative.onInvalidCallback();
};

export const removeListenerForEvent = (channel: string): Promise<string> => {
return FreeraspReactNative.removeListenerForEvent(channel);
};
108 changes: 72 additions & 36 deletions src/api/methods/native.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
import { Platform } from 'react-native';
import { FreeraspReactNative } from '../nativeModules';
import type { TalsecConfig } from '../../types/types';
import { androidOnly, wrapNative } from '../../types/errors';
import { withDefaults } from '../../utils/config';

export const talsecStart = async (options: TalsecConfig): Promise<string> => {
return FreeraspReactNative.talsecStart(withDefaults(options));
/**
* Starts freeRASP. Prefer the `useFreeRasp` hook, which also registers the
* listeners.
*/
export const talsecStart = async (options: TalsecConfig): Promise<void> => {
await wrapNative('talsec-failure', () =>
FreeraspReactNative.talsecStart(withDefaults(options))
);
};

export const addToWhitelist = async (packageName: string): Promise<boolean> => {
/**
* Android only. Adds an app to the malware whitelist so it is not reported.
*/
export const addToWhitelist = async (packageName: string): Promise<void> => {
if (Platform.OS === 'ios') {
return Promise.reject('Malware detection not available on iOS');
return androidOnly('Malware detection');
}
return FreeraspReactNative.addToWhitelist(packageName);
await wrapNative('malware-failure', () =>
FreeraspReactNative.addToWhitelist(packageName)
);
};

export const blockScreenCapture = (enable: boolean): Promise<string> => {
return FreeraspReactNative.blockScreenCapture(enable);
/**
* Blocks or unblocks screenshots and screen recording of the app screen.
*/
export const blockScreenCapture = async (enable: boolean): Promise<void> => {
await wrapNative('talsec-failure', () =>
FreeraspReactNative.blockScreenCapture(enable)
);
};

/**
* Returns whether screen capture is blocked for the current screen.
*/
export const isScreenCaptureBlocked = (): Promise<boolean> => {
return FreeraspReactNative.isScreenCaptureBlocked();
return wrapNative('talsec-failure', () =>
FreeraspReactNative.isScreenCaptureBlocked()
);
};

const androidOnly = (feature: string): Promise<never> =>
Promise.reject(`${feature} not available on iOS`);

/**
* Android only. Prevents accessibility services from reading or controlling
* the app screen.
*/
export const blockScreenControl = (enable: boolean): Promise<string> => {
export const blockScreenControl = async (enable: boolean): Promise<void> => {
if (Platform.OS === 'ios') {
return androidOnly('Screen control blocking');
}
return FreeraspReactNative.blockScreenControl(enable);
await wrapNative('talsec-failure', () =>
FreeraspReactNative.blockScreenControl(enable)
);
};

/**
Expand All @@ -43,7 +64,9 @@ export const isScreenControlBlocked = (): Promise<boolean> => {
if (Platform.OS === 'ios') {
return androidOnly('Screen control blocking');
}
return FreeraspReactNative.isScreenControlBlocked();
return wrapNative('talsec-failure', () =>
FreeraspReactNative.isScreenControlBlocked()
);
};

/**
Expand All @@ -56,7 +79,9 @@ export const blockOverlayWindows = (enable: boolean): Promise<boolean> => {
if (Platform.OS === 'ios') {
return androidOnly('Overlay windows blocking');
}
return FreeraspReactNative.blockOverlayWindows(enable);
return wrapNative('talsec-failure', () =>
FreeraspReactNative.blockOverlayWindows(enable)
);
};

/**
Expand All @@ -66,51 +91,62 @@ export const isOverlayWindowsBlocked = (): Promise<boolean> => {
if (Platform.OS === 'ios') {
return androidOnly('Overlay windows blocking');
}
return FreeraspReactNative.isOverlayWindowsBlocked();
return wrapNative('talsec-failure', () =>
FreeraspReactNative.isOverlayWindowsBlocked()
);
};

/**
* Android only. Starts the ongoing call (cellular and VoIP) detection.
* Call states are reported through the `ongoingCall` threat action.
*/
export const startOngoingCallDetection = (): Promise<string> => {
export const startOngoingCallDetection = async (): Promise<void> => {
if (Platform.OS === 'ios') {
return androidOnly('Ongoing call detection');
}
return FreeraspReactNative.startOngoingCallDetection();
await wrapNative('talsec-failure', () =>
FreeraspReactNative.startOngoingCallDetection()
);
};

/**
* Android only. Stops the ongoing call detection.
*/
export const stopOngoingCallDetection = (): Promise<string> => {
export const stopOngoingCallDetection = async (): Promise<void> => {
if (Platform.OS === 'ios') {
return androidOnly('Ongoing call detection');
}
return FreeraspReactNative.stopOngoingCallDetection();
await wrapNative('talsec-failure', () =>
FreeraspReactNative.stopOngoingCallDetection()
);
};

export const storeExternalId = (data: string): Promise<string> => {
return FreeraspReactNative.storeExternalId(data);
/**
* Stores an external ID sent with freeRASP logs.
*/
export const storeExternalId = async (data: string): Promise<void> => {
await wrapNative('external-id-failure', () =>
FreeraspReactNative.storeExternalId(data)
);
};

export const removeExternalId = (): Promise<string> => {
return FreeraspReactNative.removeExternalId();
/**
* Removes the stored external ID.
*/
export const removeExternalId = async (): Promise<void> => {
await wrapNative('external-id-failure', () =>
FreeraspReactNative.removeExternalId()
);
};

/**
* Android only. Returns the icon of the given app as a base64-encoded PNG.
*/
export const getAppIcon = (packageName: string): Promise<string> => {
if (Platform.OS === 'ios') {
return Promise.reject(
'App icon retrieval for Malware detection not available on iOS'
);
return androidOnly('App icon retrieval for malware detection');
}
return FreeraspReactNative.getAppIcon(packageName);
};

export const onInvalidCallback = (): void => {
FreeraspReactNative.onInvalidCallback();
};

export const removeListenerForEvent = (channel: string): Promise<string> => {
return FreeraspReactNative.removeListenerForEvent(channel);
return wrapNative('malware-failure', () =>
FreeraspReactNative.getAppIcon(packageName)
);
};
Loading