Skip to content

Commit c292cb8

Browse files
EveJulliardMatthieu Gicquel
authored andcommitted
add Safe keyboard detector for android
1 parent 2e1699c commit c292cb8

File tree

9 files changed

+132
-20
lines changed

9 files changed

+132
-20
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [SSL public key pinning](#ssl-pinning)
66
- [Certificate transparency](#certificate-transparency)
77
- [Prevent "recent screenshots"](#prevent-recent-screenshots)
8+
- [Safe Keyboard Detector](#safe-keyboard-detector)
89

910
> **⚠️ Disclaimer**<br/>
1011
> This package is intended to help implement a few basic security features but does not in itself guarantee that an app is secure.<br/>
@@ -125,10 +126,30 @@ Mitigating this threat is achieved by:
125126
]
126127
```
127128

129+
## Safe Keyboard Detector
130+
131+
> **🥷 What's the threat?** A third-party keyboard might embed a malicious keylogger to record passwords and sensitive data. [More details](https://www.synopsys.com/blogs/software-security/mitigate-third-party-mobile-keyboard-risk.html)
132+
133+
Mitigating this threat is achieved by:
134+
135+
- On Android, comparing the current keyboard id with a list of [keyboard packages that we deem safe](./android/src/main/java/tech/bam/rnas/RNASModule.kt#31).
136+
- On iOS, doing nothing specific since iOS already prevent the use of third-party keyboard on sensitive fields such as passwords.
137+
138+
```tsx
139+
import { SafeKeyboardDetector } from '@bam.tech/react-native-app-security';
140+
141+
const isCurrentKeyboardSafe = SafeKeyboardDetector.isCurrentKeyboardSafe() // will always return true on iOS
142+
143+
// Prompt the user to change the current keyboard
144+
SafeKeyboardDetector.showInputMethodPicker() // can only be called on Android
145+
```
146+
128147
# Contributing
129148

130149
Contributions are welcome. See the [Expo modules docs](https://docs.expo.dev/modules/get-started/) for information on how to build/run/develop on the project.
131150

151+
When making a change to the `plugin` folder, you'll need to run `yarn build` before prebuilding and building the example app.
152+
132153
# 👉 About BAM
133154

134155
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:[email protected]) or through our [contact form](https://www.bam.tech/en/contact)!

android/src/main/java/tech/bam/rnas/RNASModule.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,57 @@ package tech.bam.rnas
22

33
import expo.modules.kotlin.modules.Module
44
import expo.modules.kotlin.modules.ModuleDefinition
5+
import android.content.Context
6+
import android.view.inputmethod.InputMethodManager
7+
import android.provider.Settings
8+
59

610
class RNASModule : Module() {
711
override fun definition() = ModuleDefinition {
812
Name("RNAS")
13+
14+
Function("showInputMethodPicker") {
15+
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
16+
inputMethodManager.showInputMethodPicker()
17+
}
18+
19+
20+
Function("isCurrentKeyboardSafe") { customPackagesList: Array<String>? ->
21+
val currentKeyboardId =
22+
Settings.Secure.getString(context.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
23+
24+
return@Function isKeyboardSafe(currentKeyboardId, customPackagesList)
25+
}
926
}
27+
private val context
28+
get() = requireNotNull(appContext.reactContext)
29+
}
30+
31+
val defaultAllowedKeyboardPackagesList = arrayOf("com.touchtype.swiftkey", "com.samsung.android", "com.google.android")
32+
33+
34+
/**
35+
* Check whether the package name provided as `input` matches any of the allowed packages
36+
37+
* Examples:
38+
* Input: com.myPackage.android.latin/id
39+
* Array: ['com.myPackage.android']
40+
* Output: true
41+
*
42+
* Input: android.com.myPackage.android.latin/id
43+
* Array: ['com.myPackage.android']
44+
* Output: false
45+
*
46+
* Input: com.randomPackage/com.myPackage.android.latin
47+
* Array: ['com.myPackage.android']
48+
* Output: false
49+
*/
50+
fun doesPackageNameMatch(input: String, allowedPackagesList: Array<String>): Boolean {
51+
val packageName = input.substringBefore('/')
52+
return allowedPackagesList.any { packageName.matches("${Regex.escape(it)}.*".toRegex()) }
53+
}
54+
55+
fun isKeyboardSafe(keyboardID: String, customAllowedKeyboardPackagesList: Array<String>? = defaultAllowedKeyboardPackagesList): Boolean {
56+
val allowedPackagesList = customAllowedKeyboardPackagesList ?: defaultAllowedKeyboardPackagesList
57+
return doesPackageNameMatch(keyboardID, allowedPackagesList)
1058
}

example/App.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from "react";
2-
import { Button, Modal, StyleSheet, View } from "react-native";
2+
import { Button, StyleSheet, Text, View } from "react-native";
3+
import { SafeKeyboardDetector } from '@bam.tech/react-native-app-security';
34

45
export default function App() {
56
const [isModalVisible, setIsModalVisible] = useState(false);
@@ -17,6 +18,8 @@ export default function App() {
1718
<Button title="open modal" onPress={() => setIsModalVisible(true)} />
1819
<Button title="fetch - valid certificates" onPress={fetchValid} />
1920
<Button title="fetch - invalid certificates" onPress={fetchInvalid} />
21+
<Button title="Is current keyboard safe?" onPress={checkIsKeyboardSafe} />
22+
<Button title="show keyboard picker" onPress={() => SafeKeyboardDetector.showInputMethodPicker()} />
2023
</View>
2124
);
2225
}
@@ -58,3 +61,8 @@ const fetchInvalid = async () => {
5861
console.warn("✅ invalid certificate and fetch failed", error);
5962
}
6063
};
64+
65+
const checkIsKeyboardSafe = () => {
66+
const isKeyboardSafe = SafeKeyboardDetector.isCurrentKeyboardSafe();
67+
console.warn("is Keyboard safe", isKeyboardSafe);
68+
}

example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ PODS:
447447
- React-jsi (= 0.72.6)
448448
- React-logger (= 0.72.6)
449449
- React-perflogger (= 0.72.6)
450-
- RNAS (0.1.1):
450+
- RNAS (0.1.3):
451451
- ExpoModulesCore
452452
- TrustKit (~> 3.0.3)
453453
- SocketRocket (0.6.1)
@@ -661,11 +661,11 @@ SPEC CHECKSUMS:
661661
React-runtimescheduler: f23e337008403341177fc52ee4ca94e442c17ede
662662
React-utils: fa59c9a3375fb6f4aeb66714fd3f7f76b43a9f16
663663
ReactCommon: dd03c17275c200496f346af93a7b94c53f3093a4
664-
RNAS: 9b977ccebdd5e07aa5286251885d808baaf1eaea
664+
RNAS: 6ee0db1ee999e6cbb05d476b1bfbcb0313f09036
665665
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
666666
TrustKit: 7858ea59d0e226b1457b2e9cc8b95d77ab21d471
667667
Yoga: b76f1acfda8212aa16b7e26bcce3983230c82603
668668

669669
PODFILE CHECKSUM: 6daa8bab0f91b52da2001d6b9f17878279fbf1a9
670670

671-
COCOAPODS: 1.11.3
671+
COCOAPODS: 1.13.0

example/ios/reactnativeappsecurityexample.xcodeproj/project.pbxproj

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
057D9169AD3F49318AA27304 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657B304E93CD465EA8FBA77D /* noop-file.swift */; };
1011
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
1112
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
1213
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
1314
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
1415
96905EF65AED1B983A6B3ABC /* libPods-reactnativeappsecurityexample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-reactnativeappsecurityexample.a */; };
15-
9C312102E4654C58A52F2D9A /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF78828B1CC41A48D3F458E /* noop-file.swift */; };
1616
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
1717
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
1818
/* End PBXBuildFile section */
1919

2020
/* Begin PBXFileReference section */
21-
0EF78828B1CC41A48D3F458E /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "reactnativeappsecurityexample/noop-file.swift"; sourceTree = "<group>"; };
2221
13B07F961A680F5B00A75B9A /* reactnativeappsecurityexample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = reactnativeappsecurityexample.app; sourceTree = BUILT_PRODUCTS_DIR; };
2322
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = reactnativeappsecurityexample/AppDelegate.h; sourceTree = "<group>"; };
2423
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = reactnativeappsecurityexample/AppDelegate.mm; sourceTree = "<group>"; };
2524
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = reactnativeappsecurityexample/Images.xcassets; sourceTree = "<group>"; };
2625
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = reactnativeappsecurityexample/Info.plist; sourceTree = "<group>"; };
2726
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = reactnativeappsecurityexample/main.m; sourceTree = "<group>"; };
27+
2DCC5C34B65B430C9D23848F /* reactnativeappsecurityexample-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "reactnativeappsecurityexample-Bridging-Header.h"; path = "reactnativeappsecurityexample/reactnativeappsecurityexample-Bridging-Header.h"; sourceTree = "<group>"; };
2828
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-reactnativeappsecurityexample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-reactnativeappsecurityexample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
29+
657B304E93CD465EA8FBA77D /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "reactnativeappsecurityexample/noop-file.swift"; sourceTree = "<group>"; };
2930
6C2E3173556A471DD304B334 /* Pods-reactnativeappsecurityexample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-reactnativeappsecurityexample.debug.xcconfig"; path = "Target Support Files/Pods-reactnativeappsecurityexample/Pods-reactnativeappsecurityexample.debug.xcconfig"; sourceTree = "<group>"; };
30-
6FFFA5F664574FBABA0E4F06 /* reactnativeappsecurityexample-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "reactnativeappsecurityexample-Bridging-Header.h"; path = "reactnativeappsecurityexample/reactnativeappsecurityexample-Bridging-Header.h"; sourceTree = "<group>"; };
3131
7A4D352CD337FB3A3BF06240 /* Pods-reactnativeappsecurityexample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-reactnativeappsecurityexample.release.xcconfig"; path = "Target Support Files/Pods-reactnativeappsecurityexample/Pods-reactnativeappsecurityexample.release.xcconfig"; sourceTree = "<group>"; };
3232
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = reactnativeappsecurityexample/SplashScreen.storyboard; sourceTree = "<group>"; };
3333
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
@@ -57,8 +57,8 @@
5757
13B07FB61A68108700A75B9A /* Info.plist */,
5858
13B07FB71A68108700A75B9A /* main.m */,
5959
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
60-
0EF78828B1CC41A48D3F458E /* noop-file.swift */,
61-
6FFFA5F664574FBABA0E4F06 /* reactnativeappsecurityexample-Bridging-Header.h */,
60+
657B304E93CD465EA8FBA77D /* noop-file.swift */,
61+
2DCC5C34B65B430C9D23848F /* reactnativeappsecurityexample-Bridging-Header.h */,
6262
);
6363
name = reactnativeappsecurityexample;
6464
sourceTree = "<group>";
@@ -145,13 +145,13 @@
145145
buildPhases = (
146146
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
147147
FD10A7F022414F080027D42C /* Start Packager */,
148-
A76648976B5E4C1BC1D596C0 /* [Expo] Configure project */,
148+
AA0019DBEFC1BD061A006FDD /* [Expo] Configure project */,
149149
13B07F871A680F5B00A75B9A /* Sources */,
150150
13B07F8C1A680F5B00A75B9A /* Frameworks */,
151151
13B07F8E1A680F5B00A75B9A /* Resources */,
152152
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
153153
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
154-
207FB40296F78CAFC68D7563 /* [CP] Embed Pods Frameworks */,
154+
43091AD653F59D5941B8CE0E /* [CP] Embed Pods Frameworks */,
155155
);
156156
buildRules = (
157157
);
@@ -243,7 +243,7 @@
243243
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
244244
showEnvVarsInLog = 0;
245245
};
246-
207FB40296F78CAFC68D7563 /* [CP] Embed Pods Frameworks */ = {
246+
43091AD653F59D5941B8CE0E /* [CP] Embed Pods Frameworks */ = {
247247
isa = PBXShellScriptBuildPhase;
248248
buildActionMask = 2147483647;
249249
files = (
@@ -281,7 +281,7 @@
281281
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-reactnativeappsecurityexample/Pods-reactnativeappsecurityexample-resources.sh\"\n";
282282
showEnvVarsInLog = 0;
283283
};
284-
A76648976B5E4C1BC1D596C0 /* [Expo] Configure project */ = {
284+
AA0019DBEFC1BD061A006FDD /* [Expo] Configure project */ = {
285285
isa = PBXShellScriptBuildPhase;
286286
alwaysOutOfDate = 1;
287287
buildActionMask = 2147483647;
@@ -329,7 +329,7 @@
329329
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
330330
13B07FC11A68108700A75B9A /* main.m in Sources */,
331331
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
332-
9C312102E4654C58A52F2D9A /* noop-file.swift in Sources */,
332+
057D9169AD3F49318AA27304 /* noop-file.swift in Sources */,
333333
);
334334
runOnlyForDeploymentPostprocessing = 0;
335335
};
@@ -360,7 +360,7 @@
360360
);
361361
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
362362
PRODUCT_BUNDLE_IDENTIFIER = tech.bam.rnas.example;
363-
PRODUCT_NAME = "reactnativeappsecurityexample";
363+
PRODUCT_NAME = reactnativeappsecurityexample;
364364
SWIFT_OBJC_BRIDGING_HEADER = "reactnativeappsecurityexample/reactnativeappsecurityexample-Bridging-Header.h";
365365
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
366366
SWIFT_VERSION = 5.0;
@@ -388,7 +388,7 @@
388388
);
389389
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
390390
PRODUCT_BUNDLE_IDENTIFIER = tech.bam.rnas.example;
391-
PRODUCT_NAME = "reactnativeappsecurityexample";
391+
PRODUCT_NAME = reactnativeappsecurityexample;
392392
SWIFT_OBJC_BRIDGING_HEADER = "reactnativeappsecurityexample/reactnativeappsecurityexample-Bridging-Header.h";
393393
SWIFT_VERSION = 5.0;
394394
TARGETED_DEVICE_FAMILY = "1,2";

ios/RNASModule.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import ExpoModulesCore
33
public class RNASModule: Module {
44
public func definition() -> ModuleDefinition {
55
Name("RNAS")
6-
// There's nothing here because we use TrustKit's "auto-setup" via the config in `Info.plist`
6+
// There's nothing here related to SSL pinning because we use TrustKit's "auto-setup" via the config in `Info.plist`
7+
8+
Function("showInputMethodPicker") {() in
9+
throw RNASModuleError.methodNotImplemented
10+
}
11+
12+
Function("isCurrentKeyboardSafe") {() in
13+
return true
14+
}
715
}
816
}
17+
18+
enum RNASModuleError: Error {
19+
case methodNotImplemented
20+
}

plugin/src/index.ts

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

66
const withRNAS: ConfigPlugin<RNASConfig> = (config, props) => {
77
config = withSSLPinning(config, props.sslPinning);

src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
// No JS API for now, but without this file expo plugin resolution fails
1+
import RNASModule from "./RNASModule";
2+
import { SafeKeyboardDetectorInterface } from "./types";
3+
4+
export const SafeKeyboardDetector: SafeKeyboardDetectorInterface = {
5+
isCurrentKeyboardSafe: RNASModule.isCurrentKeyboardSafe,
6+
showInputMethodPicker: RNASModule.showInputMethodPicker,
7+
};

src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type SafeKeyboardDetectorInterface ={
2+
/**
3+
* Compare the current keyboard package name with a list of safe keyboard package names on Android
4+
* Will always return true on iOS
5+
*
6+
* @param customAllowedKeyboardList a list keyboard package names. If not provided, a default list is used.
7+
*
8+
* @example
9+
* const isSafe = isCurrentKeyboardSafe(["com.touchtype.swiftkey", "com.samsung.android", "com.google.android"])
10+
*/
11+
isCurrentKeyboardSafe: (customAllowedKeyboardList?: string[]) => boolean;
12+
/**
13+
* Prompt the user to change his current keyboard to a safe one.
14+
* Will throw an error if used on iOS
15+
*/
16+
showInputMethodPicker: () => void;
17+
}

0 commit comments

Comments
 (0)