保护 React Native 应用程序
探索开发安全的反应原生应用程序的方法。
要点 -
- 截屏预防
- 已 Root/已越狱的设备检测
- SSL 固定
- 敏感数据存储 - API EndPoint / FB / Google / Firebase 密钥
- 本地存储
- 深度链接
- Android 特定安全性
- iOS 特定安全性
- 身份验证方法
- 数据加密
1. 截屏防护
iOS 集成 -
直接方式无法进行屏幕截图限制,但您可以执行以下操作,当应用程序处于非活动状态时 - 在窗口上添加模糊层/视图,当处于活动状态时 - 删除模糊层/视图。
在 AppDelegate.m 中添加以下代码行
// AppDelegate.m
- (void)applicationWillResignActive:(UIApplication *)application {
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
[blurEffectView setFrame:self.window.bounds];
blurEffectView.tag = 1234;
blurEffectView.alpha = 0;
[self.window addSubview:blurEffectView];
[self.window bringSubviewToFront:blurEffectView];
[UIView animateWithDuration:0.5 animations:^{
blurEffectView.alpha = 1;
}];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
UIVisualEffectView *blurEffectView = [self.window viewWithTag:1234];
[UIView animateWithDuration:0.5 animations:^{
blurEffectView.alpha = 0;
} completion:^(BOOL finished) {
[blurEffectView removeFromSuperview];
}];
}
Android 集成 -
在 Android 中,限制用户以防止截屏非常简单 - 转到 MainActivity.java
// MainActivity.java
// Import Following
+ import android.view.WindowManager;
+ import android.os.Bundle
Add following lines of Code
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ );
+ }
防止截图的另一种方法 - 转到 MainActivity.java,在 onCreate 方法中添加标记为 + 的行。
// MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
2. 已 Root/已越狱的设备检测
iOS 集成 -
要检测 iOS 设备是否已越狱,请使用以下代码。您需要在 iOS 项目中创建 React Native 原生模块并集成 iOS 代码。
我们将检查以下内容来检测已越狱的设备 -
- 检查 Cydia 是否安装
- 检查应用程序是否可以编辑系统文件
- 检查系统是否包含可疑文件
- 检查是否安装了其他可疑应用程序(FakeCarrier、Icy 等)
- 检查 Cydia 是否使用其他名称安装(使用 URIScheme)
import Foundation
import UIKit
extension UIDevice {
var isSimulator: Bool {
return TARGET_OS_SIMULATOR != 0
}
var isJailBroken: Bool {
get {
if UIDevice.current.isSimulator { return false }
if JailBrokenHelper.hasCydiaInstalled() { return true }
if JailBrokenHelper.isContainsSuspiciousApps() { return true }
if JailBrokenHelper.isSuspiciousSystemPathsExists() { return true }
return JailBrokenHelper.canEditSystemFiles()
}
}
}
private struct JailBrokenHelper {
//check if cydia is installed (using URI Scheme)
static func hasCydiaInstalled() -> Bool {
return UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
}
//Check if suspicious apps (Cydia, FakeCarrier, Icy etc.) is installed
static func isContainsSuspiciousApps() -> Bool {
for path in suspiciousAppsPathToCheck {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
return false
}
//Check if system contains suspicious files
static func isSuspiciousSystemPathsExists() -> Bool {
for path in suspiciousSystemPathsToCheck {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
return false
}
//Check if app can edit system files
static func canEditSystemFiles() -> Bool {
let jailBreakText = "Developer Insider"
do {
try jailBreakText.write(toFile: jailBreakText, atomically: true, encoding: .utf8)
return true
} catch {
return false
}
}
//suspicious apps path to check
static var suspiciousAppsPathToCheck: [String] {
return ["/Applications/Cydia.app",
"/Applications/blackra1n.app",
"/Applications/FakeCarrier.app",
"/Applications/Icy.app",
"/Applications/IntelliScreen.app",
"/Applications/MxTube.app",
"/Applications/RockApp.app",
"/Applications/SBSettings.app",
"/Applications/WinterBoard.app"
]
}
//suspicious system paths to check
static var suspiciousSystemPathsToCheck: [String] {
return ["/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
"/private/var/lib/apt",
"/private/var/lib/apt/",
"/private/var/lib/cydia",
"/private/var/mobile/Library/SBSettings/Themes",
"/private/var/stash",
"/private/var/tmp/cydia.log",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
"/usr/bin/sshd",
"/usr/libexec/sftp-server",
"/usr/sbin/sshd",
"/etc/apt",
"/bin/bash",
"/Library/MobileSubstrate/MobileSubstrate.dylib"
]
}
}
另外,别忘了在 info.plist 的 LSApplicationQueriesSchemes 键中添加“Cydia”。否则 canOpenURL 将始终返回 false。
<key>LSApplicationQueriesSchemes</key>
<array>
<string>cydia</string>
</array>
Android 集成 -
Rootbear库有助于检测已 root 的设备。只需按照安装步骤并使用 React Native 原生模块,即可访问 Rootbear 功能来检测已 root 的设备。
库将检查已 Root 的设备
- 检查Root管理应用程序
- 检查潜在危险应用程序
- 检查RootCloakingApps
- 检查测试键
- 检查危险道具
- 检查BusyBox二进制
- 检查二进制文件
- 检查SuExists
- 检查RW系统
集成到代码中很简单 -
RootBeer rootBeer = new RootBeer(context);
if (rootBeer.isRooted()) {
//we found indication of root
} else {
//we didn't find indication of root
}
SafetyNet专为 Android 打造的 API,可帮助检测已 root 的设备并解锁引导加载程序。它还能有效防御安全威胁、设备篡改、恶意应用和虚假用户。
react -native-google-safetynet是 SafetyNet API 的包装插件,也可用于验证用户的设备。react-native-device-info 插件可用于查看应用是否正在模拟器上运行。
3. SSL 固定
SLL 固定可以通过 3 种不同的方式完成
- 公钥固定
- 证书锁定
- 主题公钥信息 (SPKI) 固定
iOS 集成 -将 .cer 文件放入 iOS 项目中。别忘了在 Xcode 的 Build Phases - Copy Bundle Resources 中添加它们。
Android 集成 -将 .cer 文件放在 src/main/assets/ 下
使用 react-native-ssl-pinning 进行证书锁定 -
iOS - 将 .cer 拖到 Xcode 项目,标记您的目标并“如果需要,复制项目”
fetch(url, {
method: "POST" ,
timeoutInterval: communication_timeout, // milliseconds
body: body,
// your certificates array (needed only in android) ios will pick it automatically
sslPinning: {
certs: ["cert1","cert2"] // your certificates name (without extension), for example cert1.cer, cert2.cer
},
headers: {
Accept: "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", "e_platform": "mobile",
}
})
.then(response => {
console.log("response received:", response);
})
.catch(err => {
console.log("error:", err);
})
使用 react-native-ssl-pinning(iOS)进行公钥锁定
-将 .cer 拖到 Xcode 项目,标记目标并“根据需要复制项目”。无需额外步骤即可完成公钥锁定。AFNetworking 将直接从证书中提取公钥。
Android -应通过以下命令提取公钥,只需将 google 替换为您的域名即可。
openssl s_client -servername google.com -connect google.com:443 | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
import {fetch} from 'react-native-ssl-pinning';
fetch("https://publicobject.com", {
method: "GET" ,
timeoutInterval: 10000, // milliseconds
// your certificates array (needed only in android) ios will pick it automatically
pkPinning: true,
sslPinning: {
certs: [
"sha256//r8udi/Mxd6pLOS73Djkex2EP4nFnIWXCqeHsTDRqy8=",
]
},
headers: {
Accept: "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", "e_platform": "mobile",
}
})
使用 react-native-pinch 进行证书锁定 -
import pinch from 'react-native-pinch';
pinch.fetch('https://my-api.com/v1/endpoint', {
method: 'post',
headers: { customHeader: 'customValue' },
body: '{"firstName": "Jake", "lastName": "Moxey"}',
timeoutInterval: 10000 // timeout after 10 seconds
sslPinning: {
cert: 'cert-file-name', // cert file name without the .cer
certs: ['cert-file-name-1', 'cert-file-name-2'], // optionally specify multiple certificates
}
})
.then(res => console.log("response -", res);)
.catch(err => console.log("error -",err);)
4. 敏感数据的存储 - API 端点 / FB / Google / Firebase 密钥
切勿将您的 API 端点、AccessKey、Firebase 和 Google/FB 社交密钥直接存储在代码中。您的 bundle 文件可能会被解码为纯文本,并提取所有信息。
建议使用react-native-config和react-native-dot-env来放置您的安全密钥和端点。
注意: react-native-config 模块不会加密打包的秘密,因此请勿将敏感密钥存储在 .env 文件中。
5.本地存储
开发者经常需要在本地存储数据,有时开发者更喜欢使用 AsyncStorage 来存储访问密钥/访问令牌/用户令牌。但 AsyncStorage 是非加密存储,因此信息可以从 AsyncStorage 中提取。
React Native 不提供安全数据存储的解决方案。iOS 和 Android 中已经有现成的解决方案,比如我们熟知的 iOS Keychain 和 Android Keystore。
在 iOS 中,我们使用 Keychain 服务,它允许开发人员安全地存储敏感信息,如证书、令牌、安全密钥、密码以及任何其他敏感信息,如我们在应用程序中使用的开放平台服务的秘密。
Android 密钥库 -
Android 密钥库允许开发者将加密密钥存储在容器中,从而增加从设备中提取密钥的难度。密钥一旦进入密钥库,即可用于加密操作,且密钥材料不可导出。
要从 React Native 使用 iOS Keychain 和 Android Secure Shared Preferences,您可以使用以下 NPM。
- React Native 加密存储
- react-native-keychain
- redux-persist-敏感存储
注意: redux-persist-sensitive-storage 将 react-native-sensitive-info 与 redux-persist 结合使用。react-native-sensitive-info 管理存储在 Android 共享首选项和 iOS 钥匙串中的所有数据。Android 共享首选项并不安全,但 react-native-sensitive-info 有一个分支,它使用 Android 密钥库而不是共享首选项。如果您愿意,可以将该分支与 redux-persist-sensitive-storage 一起使用。
6.深度链接
深度链接是一种从其他来源打开应用程序的方法。深度链接包含文本数据以及链接。例如yourappname://
假设您有一个电子商务应用程序,并且您的深层链接是 yourappname://products/1,这意味着它将打开您的应用程序并显示产品 1 的详细信息。
深层链接并不安全,您不应在深层链接中附加任何敏感信息。
处理深度链接时的安全问题 -
目前没有统一的 URL 方案注册方法。作为开发者,您可以通过在 iOS 版 Xcode 中配置或在 Android 版中添加 Intent 来使用您选择的任何 URL 方案。
恶意应用可能会使用相同的方案劫持您的数据,然后获取您链接中包含的数据。发送类似yourappname://products/1 的内容并无害,但发送令牌则存在安全隐患。
iOS 允许多个应用使用同一个 URL Scheme。例如,sample:// 可以被两个完全独立的应用在其 URL Scheme 实现中使用。某些恶意应用正是利用这个 URL Scheme 来攻击用户。
解决深度链接安全问题的安全解决方案 -
Apple 在 iOS 9 中引入了通用链接,以解决自定义 URI 方案深度链接缺乏优雅回退功能的问题。通用链接是标准的 Web 链接,既指向网页,也指向应用内的内容。
打开通用链接时,iOS 会检查是否有已安装的应用已注册到该域名。如果已注册,则立即启动该应用,无需加载网页。如果没有注册,则 Safari 浏览器会加载该网址(可以简单地重定向到 App Store)。
设置通用链接(HTTP或HTTPS)登录接口,并使用随机标识符在本地验证收到的登录令牌,防止劫持和恶意登录令牌重放。
7. Android 特定安全性
让我们看看如何保护我们的 APK 或应用程序包免受逆向工程攻击。
黑客可以通过对 APK 或 App Bundle 文件进行逆向工程,轻松访问我们的代码库。为了避免这种情况,我们可以添加 Pro Guard 规则。Pro Guard 会混淆您的代码。因此,即使有人对其进行逆向工程,代码也无法读取,从而保护您免受工程攻击。Pro Guard 还可以通过删除未使用的代码和资源来减小 APK 的大小。如果您的项目包含任何第三方库,则可以在规则文件中添加该库的 Pro Guard 规则。
要启用 Pro Guard 规则,我们必须在 app/build.gradle 文件中启用 minifyEnabled 属性。
buildTypes {
release: {
minifyEnabled true
}
}
8. iOS 特定安全性
让我们看看如何在 iOS 中限制不安全域名的使用。这将帮助我们避免传输层攻击。您可以通过在 Info.plist 文件中配置一些属性来限制不安全域名。
现在,让我们看看您应该在 Info.plist 文件中添加什么内容。
从 iOS 9.0 开始,Apple 引入了 NSAppTransportSecurity,您可以在 info.plist 文件中找到它。NSAppTransportSecurity 中有一个键 NSAllowArbitraryLoads,默认情况下设置为 NO,表示您同意安全优势。在某些情况下,如果您使用 localhost 或 HTTP 域(如果需要),则必须将其设置为 YES,否则您将无法通过这些不安全的域发出网络请求。
您的应用在上传到 Apple Store 时,可能会因为您将 NSAllowArbitraryLoads 值设置为 YES 而被拒绝。为了解决这个问题,您可以使用 NSExceptionDomains,并在其中提供一个域名列表。应用程序会认为您已同意所有安全权益,但 NSExceptionDomains 中列出的域名除外(即使您已将 NSAllowArbitraryLoads 值设置为 YES)。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
9. 身份验证方法
如今,OAuth 已成为应用程序之间身份验证的主流。设想一下,你的应用程序与 API 通信,向服务器发送/检索数据。服务器如何知道即将到来的请求已通过身份验证?OAuth 2.0 简化了身份验证流程。OAuth 允许使用 Token 进行身份验证,而无需共享密码。这是一种使用JWT Token进行 API 身份验证的方法。
10.数据加密
Crypto JS是一个流行的 JavaScript 加密标准库。为了存储和发送数据到服务器,需要使用 CrytpJS 进行加密。因此,它不能直接启用。
感谢您阅读博客!
KPITENG | 数字化转型
www.kpiteng.com/blogs | hello@kpiteng.com
连接 | 关注我们 - Linkedin | Facebook | Instagram