Skip to content

Commit 737ae7a

Browse files
author
Matthieu Gicquel
committed
preventRecentScreenshots
1 parent 98da5f8 commit 737ae7a

10 files changed

Lines changed: 151 additions & 12 deletions

File tree

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- [SSL public key pinning](#ssl-pinning)
66
- [Certificate transparency](#certificate-transparency)
7-
- [🚧 "Recent screenshots" prevention](#recent-screenshots-prevention)
7+
- [Prevent "recent screenshots"](#prevent-recent-screenshots)
88

99
> **⚠️ Disclaimer**<br/>
1010
> This package is intended to help implement a few basic security features but does not in itself guarantee that an app is secure.<br/>
@@ -32,6 +32,10 @@ Add the config plugin to `app.config.ts` / `app.config.js` / `app.json`:
3232
"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=",
3333
"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE="
3434
]
35+
},
36+
"preventRecentScreenshots": {
37+
"ios": { "enabled": true },
38+
"android": { "enabled": true }
3539
}
3640
}
3741
]
@@ -93,9 +97,29 @@ To test that SSL pinning is working as expected, you can:
9397

9498
None, enabled by default.
9599

96-
## "Recent screenshots" prevention
100+
## Prevent "recent screenshots"
97101

98-
TODO
102+
> **🥷 What's the threat?** When the OS terminates the app, it may take a screenshot and store it on the device to display in the app switcher. This screenshot could leak sensitive data
103+
104+
Mitigating this threat is achieved by:
105+
106+
- Using [`FLAG_SECURE`](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE) on Android < 13
107+
- Using [`Activity.setRecentScreenshotsEnabled`](<https://developer.android.com/reference/android/app/Activity#setRecentsScreenshotEnabled(boolean)>) on Android >= 13
108+
- Covering the app with the splashscreen on iOS (requires [expo-splash-screen](https://docs.expo.dev/versions/latest/sdk/splash-screen/) to be setup)
109+
110+
### Configuration
111+
112+
```jsonc
113+
[
114+
"@bam.tech/react-native-app-security",
115+
{
116+
"preventRecentScreenshots": {
117+
"ios": { "enabled": true },
118+
"android": { "enabled": true }
119+
}
120+
}
121+
]
122+
```
99123

100124
# Contributing
101125

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ android {
7171

7272
// package-specific config fields
7373
buildConfigField("String", "RNAS_PINNING_CONFIG", "\"${safeExtGet("RNAS_PINNING_CONFIG", "{}")}\"")
74+
buildConfigField("boolean", "RNAS_PREVENT_RECENT_SCREENSHOTS", safeExtGet("RNAS_PREVENT_RECENT_SCREENSHOTS", "false"))
7475
}
7576
lintOptions {
7677
abortOnError false
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
package tech.bam.rnas
22

33
import android.app.Activity
4+
import android.os.Build
5+
import android.view.WindowManager
46
import android.os.Bundle
57
import expo.modules.core.interfaces.ReactActivityLifecycleListener
68

79
import com.facebook.react.modules.network.OkHttpClientProvider;
810

911
class AndroidReactActivityLifecycleListener : ReactActivityLifecycleListener {
12+
override fun onResume(activity: Activity) {
13+
super.onResume(activity)
14+
15+
if(BuildConfig.RNAS_PREVENT_RECENT_SCREENSHOTS && Build.VERSION.SDK_INT < 33) {
16+
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
17+
}
18+
}
19+
20+
override fun onPause(activity: Activity) {
21+
super.onPause(activity)
22+
23+
if(BuildConfig.RNAS_PREVENT_RECENT_SCREENSHOTS && Build.VERSION.SDK_INT < 33) {
24+
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
25+
}
26+
}
27+
1028
override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
1129
super.onCreate(activity, savedInstanceState)
1230
OkHttpClientProvider.setOkHttpClientFactory(SSLPinning())
31+
32+
if(BuildConfig.RNAS_PREVENT_RECENT_SCREENSHOTS && Build.VERSION.SDK_INT >= 33) {
33+
activity.setRecentsScreenshotEnabled(false)
34+
}
1335
}
1436
}

example/app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
"We74o5ME3USRtL6+B2UhXnwY9FR91QPJMYDtUNk6tEc=",
3939
"zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w="
4040
]
41+
},
42+
"preventRecentScreenshots": {
43+
"enabled": true
4144
}
4245
}
4346
]

expo-module.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"platforms": ["ios", "android"],
33
"ios": {
4-
"modules": ["RNASModule"]
4+
"modules": ["RNASModule"],
5+
"appDelegateSubscribers": ["RNASAppLifecycleDelegate"]
56
},
67
"android": {
78
"modules": ["tech.bam.rnas.RNASModule"]

ios/RNASAppLifecyleDelegate.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import ExpoModulesCore
2+
import UIKit
3+
4+
public class RNASAppLifecycleDelegate: ExpoAppDelegateSubscriber {
5+
public func applicationDidFinishLaunching(_ application: UIApplication) {
6+
application.ignoreSnapshotOnNextApplicationLaunch()
7+
}
8+
9+
public func applicationWillResignActive(_ application: UIApplication) {
10+
// https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background
11+
12+
// TODO: better error handling in case there's an issue with the splashscreen (instead of !)
13+
let launchScreen = UIStoryboard(name: "SplashScreen", bundle: nil).instantiateInitialViewController()!
14+
launchScreen.modalPresentationStyle = .overFullScreen
15+
16+
UIApplication.shared.windows.first?.rootViewController?.present(launchScreen, animated: false)
17+
}
18+
19+
public func applicationDidBecomeActive(_ application: UIApplication) {
20+
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: false)
21+
}
22+
}

plugin/src/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { ConfigPlugin } from "@expo/config-plugins";
22
import withSSLPinning from "./withSSLPinning";
3+
import withpreventRecentScreenshots from "./withPreventRecentScreenshots";
4+
import { RNASConfig } from "./types";
35

4-
type Props = {
5-
sslPinning?: {
6-
[hostName: string]: string[];
7-
};
8-
};
9-
10-
const withRNAS: ConfigPlugin<Props> = (config, props) => {
6+
const withRNAS: ConfigPlugin<RNASConfig> = (config, props) => {
117
config = withSSLPinning(config, props.sslPinning);
128

9+
config = withpreventRecentScreenshots(config, props.preventRecentScreenshots);
10+
1311
return config;
1412
};
1513

1614
export default withRNAS;
15+
16+
export type { RNASConfig };

plugin/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type RNASConfig = {
2+
sslPinning?: {
3+
[hostName: string]: string[];
4+
};
5+
preventRecentScreenshots?: {
6+
ios?: { enabled: boolean };
7+
android?: { enabled: boolean };
8+
};
9+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
ConfigPlugin,
3+
withInfoPlist,
4+
withGradleProperties,
5+
} from "@expo/config-plugins";
6+
import { RNASConfig } from "./types";
7+
8+
type Props = RNASConfig["preventRecentScreenshots"];
9+
10+
const withpreventRecentScreenshots: ConfigPlugin<Props> = (config, props) => {
11+
config = withInfoPlist(config, (config) => {
12+
const infoPlist = config.modResults;
13+
14+
const isEnabled = props?.ios?.enabled ?? false;
15+
16+
if (!isEnabled) {
17+
delete infoPlist.RNAS_PREVENT_RECENT_SCREENSHOTS;
18+
return config;
19+
}
20+
21+
infoPlist.RNAS_PREVENT_RECENT_SCREENSHOTS = true;
22+
23+
return config;
24+
});
25+
26+
config = withGradleProperties(config, (config) => {
27+
const gradleProperties = config.modResults;
28+
29+
const isEnabled = props?.android?.enabled ?? false;
30+
31+
const existingIndex = gradleProperties.findIndex(
32+
(prop) =>
33+
prop.type === "property" &&
34+
prop.key === "RNAS_PREVENT_RECENT_SCREENSHOTS"
35+
);
36+
if (existingIndex !== -1) {
37+
gradleProperties.splice(existingIndex, 1);
38+
}
39+
40+
if (!isEnabled) {
41+
return config;
42+
}
43+
44+
gradleProperties.push({
45+
type: "property",
46+
key: "RNAS_PREVENT_RECENT_SCREENSHOTS",
47+
value: "true",
48+
});
49+
50+
return config;
51+
});
52+
53+
return config;
54+
};
55+
56+
export default withpreventRecentScreenshots;

plugin/src/withSSLPinning.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {
33
withInfoPlist,
44
withGradleProperties,
55
} from "@expo/config-plugins";
6+
import { RNASConfig } from "./types";
67

7-
type Props = { [hostName: string]: string[] } | undefined;
8+
type Props = RNASConfig["sslPinning"];
89

910
const withSSLPinning: ConfigPlugin<Props> = (config, props) => {
1011
config = withInfoPlist(config, (config) => {

0 commit comments

Comments
 (0)