Skip to content

Commit 2754984

Browse files
author
Matthieu Gicquel
committed
SSL pinning
1 parent fa14166 commit 2754984

19 files changed

Lines changed: 386 additions & 11 deletions

File tree

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<h1 align="center">react-native-app-security 🔐</h1>
2+
3+
<p align="center">Easily implement usual security measures in React Native Expo apps</p>
4+
5+
- [SSL public key pinning](#ssl-pinning)
6+
- [🚧 Certificate transparency](#certificate-transparency)
7+
- [🚧 "Recent screenshots" prevention](#recent-screenshots-prevention)
8+
9+
> **⚠️ Disclaimer**<br/>
10+
> This package is intended to help implement a few basic security features but does not in itself guarantee that an app is secure.<br/>
11+
> Refer to [OWASP's resources](https://mas.owasp.org) for more information on mobile app security.<br/>
12+
> You can also [contact us](#👉-about-bam) if you need help with securing your app.
13+
14+
# Installation and setup
15+
16+
This packages is designed for use in expo apps with [development builds](https://docs.expo.dev/develop/development-builds/introduction/).
17+
18+
```sh
19+
yarn add @bam.tech/react-native-app-security
20+
```
21+
22+
Add the config plugin to `app.config.ts` / `app.config.js` / `app.json`:
23+
24+
```json
25+
{
26+
"plugins": [
27+
[
28+
"@bam.tech/react-native-app-security",
29+
{
30+
"sslPinning": {
31+
"yahoo.com": [
32+
"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=",
33+
"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE="
34+
]
35+
}
36+
}
37+
]
38+
]
39+
}
40+
```
41+
42+
Anytime you change the config, don't forget to run:
43+
44+
```sh
45+
yarn expo prebuild
46+
```
47+
48+
# Features
49+
50+
## SSL Pinning
51+
52+
> **🥷 What's the threat?** Attackers intercepting your app's network requests and accessing private data or sending malicious responses. [More details](https://mas.owasp.org/MASTG/General/0x04f-Testing-Network-Communication/#restricting-trust-identity-pinning)
53+
54+
This package implements [public key pinning](https://cheatsheetseries.owasp.org/cheatsheets/Pinning_Cheat_Sheet.html#public-key) using [TrustKit](https://github.com/datatheorem/TrustKit) on iOS and the certificate pinner included in OkHttp on Android.
55+
56+
### Configuration
57+
58+
```jsonc
59+
[
60+
"@bam.tech/react-native-app-security",
61+
{
62+
"sslPinning": {
63+
// The hostname you want to pin, without `https://`
64+
"yahoo.com": [
65+
// The public key hashes for the pinned certificates, without a `sha256/` prefix
66+
"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=",
67+
"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE="
68+
]
69+
}
70+
}
71+
]
72+
```
73+
74+
### Generating the public key hashes
75+
76+
TODO
77+
78+
## Certificate transparency
79+
80+
TODO
81+
82+
## "Recent screenshots" prevention
83+
84+
TODO
85+
86+
# Contributing
87+
88+
TODO
89+
90+
# 👉 About BAM
91+
92+
We are a 100 people company developing and designing multi-platform applications with [React Native](https://www.bam.tech/expertise/react-native) using the Lean & Agile methodology. To get more information on the solutions that would suit your needs, feel free to get in touch by [email](mailto:contact@bam.tech) or through our [contact form](https://www.bam.tech/en/contact)!
93+
94+
We will always answer you with pleasure 😁

android/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ android {
6868
targetSdkVersion safeExtGet("targetSdkVersion", 33)
6969
versionCode 1
7070
versionName "0.1.0"
71+
72+
// package-specific config fields
73+
buildConfigField("String", "RNAS_PINNING_CONFIG", "\"${safeExtGet("RNAS_PINNING_CONFIG", "{}")}\"")
7174
}
7275
lintOptions {
7376
abortOnError false
@@ -86,4 +89,8 @@ repositories {
8689
dependencies {
8790
implementation project(':expo-modules-core')
8891
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
92+
93+
// package-specific dependencies
94+
implementation("com.squareup.okhttp3:okhttp:+")
95+
implementation("com.facebook.react:react-native:+")
8996
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tech.bam.rnas
2+
3+
import android.content.Context
4+
import expo.modules.core.interfaces.Package
5+
import expo.modules.core.interfaces.ReactActivityLifecycleListener
6+
7+
class AndroidListenerPackage : Package {
8+
override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> {
9+
return listOf(AndroidReactActivityLifecycleListener())
10+
}
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package tech.bam.rnas
2+
3+
import android.app.Activity
4+
import android.os.Bundle
5+
import expo.modules.core.interfaces.ReactActivityLifecycleListener
6+
7+
import com.facebook.react.modules.network.OkHttpClientProvider;
8+
9+
class AndroidReactActivityLifecycleListener : ReactActivityLifecycleListener {
10+
override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
11+
super.onCreate(activity, savedInstanceState)
12+
OkHttpClientProvider.setOkHttpClientFactory(SSLPinning())
13+
}
14+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package tech.bam.rnas
2+
3+
import com.facebook.react.modules.network.OkHttpClientFactory;
4+
import com.facebook.react.modules.network.OkHttpClientProvider;
5+
6+
import okhttp3.CertificatePinner;
7+
import okhttp3.OkHttpClient;
8+
9+
import org.json.JSONObject
10+
11+
public class SSLPinning : OkHttpClientFactory {
12+
override fun createNewNetworkModuleClient(): OkHttpClient {
13+
val config = parseConfig()
14+
15+
val certificatePinnerBuilder = CertificatePinner.Builder()
16+
17+
for((hostName, certificates) in config) {
18+
for (certificate in certificates) {
19+
//Certificates must start with 'sha256/' for android
20+
certificatePinnerBuilder.add(hostName, "sha256/$certificate")
21+
}
22+
}
23+
24+
val certificatePinner = certificatePinnerBuilder.build()
25+
26+
val clientBuilder = OkHttpClientProvider.createClientBuilder()
27+
28+
return clientBuilder
29+
.certificatePinner(certificatePinner)
30+
.build()
31+
}
32+
33+
}
34+
35+
36+
fun parseConfig() : Map<String, List<String>> {
37+
val jsonString: String = BuildConfig.RNAS_PINNING_CONFIG
38+
39+
val jsonObject = JSONObject(jsonString)
40+
val resultMap = mutableMapOf<String, List<String>>()
41+
42+
jsonObject.keys().forEach { key ->
43+
val jsonArray = jsonObject.getJSONArray(key)
44+
val valuesList = mutableListOf<String>()
45+
for (i in 0 until jsonArray.length()) {
46+
valuesList.add(jsonArray.getString(i))
47+
}
48+
resultMap[key] = valuesList
49+
}
50+
51+
return resultMap // Map<String, List<String>>
52+
}

app.plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./plugin/build");

example/App.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { StyleSheet, Text, View } from "react-native";
1+
import { Button, StyleSheet, View } from "react-native";
22

33
export default function App() {
44
return (
55
<View style={styles.container}>
6-
<Text>{"hello"}</Text>
6+
<Button title="fetch - valid certificates" onPress={fetchValid} />
7+
<Button title="fetch - invalid certificates" onPress={fetchInvalid} />
78
</View>
89
);
910
}
@@ -14,5 +15,28 @@ const styles = StyleSheet.create({
1415
backgroundColor: "#fff",
1516
alignItems: "center",
1617
justifyContent: "center",
18+
gap: 16,
1719
},
1820
});
21+
22+
const fetchValid = async () => {
23+
try {
24+
const response = await fetch("https://google.com");
25+
console.warn("✅ valid certificate and fetch succeeded", {
26+
status: response.status,
27+
});
28+
} catch (error) {
29+
console.warn("❌ valid certificate but fetch failed", error);
30+
}
31+
};
32+
33+
const fetchInvalid = async () => {
34+
try {
35+
const response = await fetch("https://yahoo.com");
36+
console.warn("❌ invalid certificated but fetch succeeded", {
37+
status: response.status,
38+
});
39+
} catch (error) {
40+
console.warn("✅ invalid certificate and fetch failed", error);
41+
}
42+
};

example/android/gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,5 @@ expo.webp.animated=false
5454

5555
# Enable network inspector
5656
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
57+
58+
RNAS_PINNING_CONFIG={\\"yahoo.com\\":[\\"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=\\",\\"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE=\\"],\\"google.com\\":[\\"We74o5ME3USRtL6+B2UhXnwY9FR91QPJMYDtUNk6tEc=\\",\\"zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=\\"]}

example/app.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@
2727
},
2828
"web": {
2929
"favicon": "./assets/favicon.png"
30-
}
30+
},
31+
"plugins": [
32+
[
33+
"../app.plugin.js",
34+
{
35+
"sslPinning": {
36+
"yahoo.com": [
37+
"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=",
38+
"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE="
39+
],
40+
"google.com": [
41+
"We74o5ME3USRtL6+B2UhXnwY9FR91QPJMYDtUNk6tEc=",
42+
"zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w="
43+
]
44+
}
45+
}
46+
]
47+
]
3148
}
3249
}

example/ios/Podfile.lock

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ PODS:
1919
- React-NativeModulesApple
2020
- React-RCTAppDelegate
2121
- ReactCommon/turbomodule/core
22+
- EXSplashScreen (0.20.5):
23+
- ExpoModulesCore
24+
- RCT-Folly (= 2021.07.22.00)
25+
- React-Core
2226
- FBLazyVector (0.72.6)
2327
- FBReactNativeSpec (0.72.6):
2428
- RCT-Folly (= 2021.07.22.00)
@@ -445,7 +449,9 @@ PODS:
445449
- React-perflogger (= 0.72.6)
446450
- RNAS (0.1.0):
447451
- ExpoModulesCore
452+
- TrustKit (~> 3.0.3)
448453
- SocketRocket (0.6.1)
454+
- TrustKit (3.0.3)
449455
- Yoga (1.14.0)
450456

451457
DEPENDENCIES:
@@ -458,6 +464,7 @@ DEPENDENCIES:
458464
- Expo (from `../node_modules/expo`)
459465
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
460466
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
467+
- EXSplashScreen (from `../node_modules/expo-splash-screen/ios`)
461468
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
462469
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
463470
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
@@ -504,6 +511,7 @@ SPEC REPOS:
504511
- fmt
505512
- libevent
506513
- SocketRocket
514+
- TrustKit
507515

508516
EXTERNAL SOURCES:
509517
boost:
@@ -524,6 +532,8 @@ EXTERNAL SOURCES:
524532
:path: "../node_modules/expo-keep-awake/ios"
525533
ExpoModulesCore:
526534
:path: "../node_modules/expo-modules-core"
535+
EXSplashScreen:
536+
:path: "../node_modules/expo-splash-screen/ios"
527537
FBLazyVector:
528538
:path: "../node_modules/react-native/Libraries/FBLazyVector"
529539
FBReactNativeSpec:
@@ -612,6 +622,7 @@ SPEC CHECKSUMS:
612622
Expo: 61a8e1aa94311557c137c0a4dfd4fe78281cfbb4
613623
ExpoKeepAwake: be4cbd52d9b177cde0fd66daa1913afa3161fc1d
614624
ExpoModulesCore: c480fd4e3c7c8e81f0a6ba3a7c56869f25fe016d
625+
EXSplashScreen: c0e7f2d4a640f3b875808ed0b88575538daf6d82
615626
FBLazyVector: 748c0ef74f2bf4b36cfcccf37916806940a64c32
616627
FBReactNativeSpec: 966f29e4e697de53a3b366355e8f57375c856ad9
617628
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
@@ -650,8 +661,9 @@ SPEC CHECKSUMS:
650661
React-runtimescheduler: f23e337008403341177fc52ee4ca94e442c17ede
651662
React-utils: fa59c9a3375fb6f4aeb66714fd3f7f76b43a9f16
652663
ReactCommon: dd03c17275c200496f346af93a7b94c53f3093a4
653-
RNAS: bfca86744b14635017be943825c262741649afe4
664+
RNAS: bf17eaa3a56a38ecf2834a34444b253add707439
654665
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
666+
TrustKit: 7858ea59d0e226b1457b2e9cc8b95d77ab21d471
655667
Yoga: b76f1acfda8212aa16b7e26bcce3983230c82603
656668

657669
PODFILE CHECKSUM: 6daa8bab0f91b52da2001d6b9f17878279fbf1a9

0 commit comments

Comments
 (0)