// 普通 TextInput 的安全问题
import { TextInput } from 'react-native';
function VulnerableInput() {
const [password, setPassword] = useState('');
return (
<TextInput
value={password}
onChangeText={setPassword}
secureTextEntry={true} // ❌ 只隐藏字符,内存中仍然可见
placeholder="请输入密码"
/>
);
}
原生模块 (Android)
// SecureKeyboardModule.java
package com.securekeyboard;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.view.ReactViewGroup;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class SecureKeyboardModule extends ReactContextBaseJavaModule {
private static final String MODULE_NAME = "SecureKeyboard";
private ReactApplicationContext reactContext;
private SecureKeyboardView keyboardView;
private EditText secureEditText;
private String currentValue = "";
private Callback onInputCallback;
private boolean isCapital = false;
private boolean isSymbol = false;
public SecureKeyboardModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return MODULE_NAME;
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("KEYBOARD_TYPE_NUMBER", "number");
constants.put("KEYBOARD_TYPE_TEXT", "text");
constants.put("KEYBOARD_TYPE_PASSWORD", "password");
return constants;
}
@ReactMethod
public void showKeyboard(final String type, final Callback onInput) {
this.onInputCallback = onInput;
Activity activity = getCurrentActivity();
if (activity == null) return;
activity.runOnUiThread(() -> {
// 禁用截屏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
activity.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
);
}
// 创建安全输入框
createSecureEditText(activity);
// 创建自定义键盘
createKeyboardView(activity, type);
// 显示键盘
keyboardView.setVisibility(View.VISIBLE);
// 隐藏系统键盘
hideSystemKeyboard(activity);
});
}
@ReactMethod
public void hideKeyboard() {
Activity activity = getCurrentActivity();
if (activity == null) return;
activity.runOnUiThread(() -> {
if (keyboardView != null) {
FrameLayout rootView = (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.removeView(keyboardView);
keyboardView = null;
}
if (secureEditText != null) {
FrameLayout rootView = (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.removeView(secureEditText);
secureEditText = null;
}
// 恢复截屏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
});
}
@ReactMethod
public void clear() {
currentValue = "";
if (onInputCallback != null) {
onInputCallback.invoke("");
}
}
private void createSecureEditText(Activity activity) {
secureEditText = new EditText(activity);
secureEditText.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
1
));
secureEditText.setBackgroundColor(Color.TRANSPARENT);
secureEditText.setTextColor(Color.TRANSPARENT);
secureEditText.setCursorVisible(false);
// 防止系统键盘弹出
secureEditText.setShowSoftInputOnFocus(false);
secureEditText.setFocusable(true);
secureEditText.setFocusableInTouchMode(true);
// 添加到根视图
FrameLayout rootView = (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.addView(secureEditText);
// 获取焦点
secureEditText.requestFocus();
}
private void createKeyboardView(Activity activity, String type) {
keyboardView = new SecureKeyboardView(activity, type);
keyboardView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
));
// 设置键盘回调
keyboardView.setOnKeyClickListener(new SecureKeyboardView.OnKeyClickListener() {
@Override
public void onKeyClick(String key) {
handleKeyPress(key);
}
});
// 添加到根视图
FrameLayout rootView = (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.addView(keyboardView);
}
private void handleKeyPress(String key) {
if (key.equals("BACKSPACE")) {
if (currentValue.length() > 0) {
currentValue = currentValue.substring(0, currentValue.length() - 1);
}
} else if (key.equals("CLEAR")) {
currentValue = "";
} else if (key.equals("CAPS")) {
isCapital = !isCapital;
if (keyboardView != null) {
keyboardView.updateCapsState(isCapital);
}
} else if (key.equals("SYMBOL")) {
isSymbol = !isSymbol;
if (keyboardView != null) {
keyboardView.updateSymbolState(isSymbol);
}
} else if (key.equals("DONE")) {
hideKeyboard();
return;
} else {
currentValue += key;
}
// 回调给 JavaScript
if (onInputCallback != null) {
onInputCallback.invoke(currentValue);
}
// 在安全输入框中显示随机字符
if (secureEditText != null) {
secureEditText.setText(generateRandomString(currentValue.length()));
}
}
private String generateRandomString(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
private void hideSystemKeyboard(Activity activity) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View view = activity.getCurrentFocus();
if (view != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
}
// SecureKeyboardView.java - 自定义键盘视图
package com.securekeyboard;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.GridLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class SecureKeyboardView extends GridLayout {
public interface OnKeyClickListener {
void onKeyClick(String key);
}
private OnKeyClickListener onKeyClickListener;
private List<String> currentKeys = new ArrayList<>();
private boolean isCapital = false;
private boolean isSymbol = false;
private String keyboardType;
// 键盘布局
private static final String[][] LETTER_KEYS = {
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"},
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"},
{"a", "s", "d", "f", "g", "h", "j", "k", "l"},
{"CAPS", "z", "x", "c", "v", "b", "n", "m", "BACKSPACE"},
{"SYMBOL", "SPACE", "DONE"}
};
private static final String[][] NUMBER_KEYS = {
{"1", "2", "3"},
{"4", "5", "6"},
{"7", "8", "9"},
{"CLEAR", "0", "BACKSPACE"},
{"DONE"}
};
private static final String[][] SYMBOL_KEYS = {
{"!", "@", "#", "$", "%", "^", "&", "*", "(", ")"},
{"-", "_", "=", "+", "[", "]", "{", "}", "|", "\\"},
{";", ":", "'", "\"", ",", ".", "<", ">", "/", "?"},
{"CAPS", "~", "`", "SYMBOL", "BACKSPACE"},
{"LETTERS", "SPACE", "DONE"}
};
public SecureKeyboardView(Context context) {
super(context);
init();
}
public SecureKeyboardView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SecureKeyboardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public SecureKeyboardView(Context context, String type) {
super(context);
this.keyboardType = type;
init();
}
private void init() {
setBackgroundColor(Color.parseColor("#F5F5F5"));
setPadding(8, 8, 8, 8);
if ("number".equals(keyboardType)) {
createNumberKeyboard();
} else {
createLetterKeyboard();
}
}
private void createLetterKeyboard() {
removeAllViews();
setRowCount(LETTER_KEYS.length);
setColumnCount(10);
for (int row = 0; row < LETTER_KEYS.length; row++) {
String[] rowKeys = LETTER_KEYS[row];
int startColumn = (10 - rowKeys.length) / 2; // 居中
for (int col = 0; col < rowKeys.length; col++) {
String key = rowKeys[col];
TextView keyView = createKeyView(key);
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.rowSpec = GridLayout.spec(row);
params.columnSpec = GridLayout.spec(startColumn + col);
if ("CAPS".equals(key) || "SYMBOL".equals(key) || "BACKSPACE".equals(key) ||
"SPACE".equals(key) || "DONE".equals(key)) {
params.width = 0;
params.height = GridLayout.LayoutParams.WRAP_CONTENT;
params.setGravity(Gravity.FILL);
params.columnSpec = GridLayout.spec(startColumn + col, 2); // 占2列
} else {
params.width = 0;
params.height = GridLayout.LayoutParams.WRAP_CONTENT;
params.setGravity(Gravity.FILL);
}
addView(keyView, params);
}
}
}
private void createNumberKeyboard() {
removeAllViews();
setRowCount(NUMBER_KEYS.length);
setColumnCount(3);
for (int row = 0; row < NUMBER_KEYS.length; row++) {
String[] rowKeys = NUMBER_KEYS[row];
for (int col = 0; col < rowKeys.length; col++) {
String key = rowKeys[col];
TextView keyView = createKeyView(key);
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.rowSpec = GridLayout.spec(row);
params.columnSpec = GridLayout.spec(col);
params.width = 0;
params.height = GridLayout.LayoutParams.WRAP_CONTENT;
params.setGravity(Gravity.FILL);
if ("CLEAR".equals(key) || "BACKSPACE".equals(key) || "DONE".equals(key)) {
params.columnSpec = GridLayout.spec(col, 2); // 占2列
}
addView(keyView, params);
}
}
}
private void createSymbolKeyboard() {
removeAllViews();
setRowCount(SYMBOL_KEYS.length);
setColumnCount(10);
for (int row = 0; row < SYMBOL_KEYS.length; row++) {
String[] rowKeys = SYMBOL_KEYS[row];
int startColumn = (10 - rowKeys.length) / 2;
for (int col = 0; col < rowKeys.length; col++) {
String key = rowKeys[col];
TextView keyView = createKeyView(key);
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.rowSpec = GridLayout.spec(row);
params.columnSpec = GridLayout.spec(startColumn + col);
params.width = 0;
params.height = GridLayout.LayoutParams.WRAP_CONTENT;
params.setGravity(Gravity.FILL);
if ("CAPS".equals(key) || "SYMBOL".equals(key) || "BACKSPACE".equals(key) ||
"LETTERS".equals(key) || "SPACE".equals(key) || "DONE".equals(key)) {
params.columnSpec = GridLayout.spec(startColumn + col, 2);
}
addView(keyView, params);
}
}
}
private TextView createKeyView(final String key) {
TextView keyView = new TextView(getContext());
keyView.setText(getDisplayText(key));
keyView.setTextSize(18);
keyView.setGravity(Gravity.CENTER);
keyView.setPadding(16, 24, 16, 24);
// 设置背景
GradientDrawable background = new GradientDrawable();
background.setColor(Color.WHITE);
background.setCornerRadius(8);
background.setStroke(1, Color.parseColor("#DDDDDD"));
keyView.setBackground(background);
// 特殊键样式
if (isSpecialKey(key)) {
keyView.setTextColor(Color.parseColor("#007AFF"));
keyView.setBackgroundColor(Color.parseColor("#E8F4FF"));
} else {
keyView.setTextColor(Color.BLACK);
}
// 点击效果
keyView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onKeyClickListener != null) {
onKeyClickListener.onKeyClick(key);
}
// 特殊键处理
if ("SYMBOL".equals(key)) {
createSymbolKeyboard();
} else if ("LETTERS".equals(key)) {
createLetterKeyboard();
}
}
});
return keyView;
}
private String getDisplayText(String key) {
if ("BACKSPACE".equals(key)) {
return "⌫";
} else if ("CAPS".equals(key)) {
return isCapital ? "⇧" : "⇪";
} else if ("CLEAR".equals(key)) {
return "清空";
} else if ("SPACE".equals(key)) {
return "空格";
} else if ("DONE".equals(key)) {
return "完成";
} else if ("SYMBOL".equals(key)) {
return "符号";
} else if ("LETTERS".equals(key)) {
return "字母";
} else {
return isCapital ? key.toUpperCase() : key;
}
}
private boolean isSpecialKey(String key) {
return "CAPS".equals(key) || "SYMBOL".equals(key) || "BACKSPACE".equals(key) ||
"CLEAR".equals(key) || "SPACE".equals(key) || "DONE".equals(key) ||
"LETTERS".equals(key);
}
public void setOnKeyClickListener(OnKeyClickListener listener) {
this.onKeyClickListener = listener;
}
public void updateCapsState(boolean isCapital) {
this.isCapital = isCapital;
refreshKeyTexts();
}
public void updateSymbolState(boolean isSymbol) {
this.isSymbol = isSymbol;
refreshKeyTexts();
}
private void refreshKeyTexts() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof TextView) {
TextView textView = (TextView) child;
String key = (String) textView.getTag();
if (key != null) {
textView.setText(getDisplayText(key));
}
}
}
}
}
// SecureKeyboardPackage.java
package com.securekeyboard;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SecureKeyboardPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new SecureKeyboardModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.securekeyboard">
<!-- 禁止截屏权限 -->
<uses-permission android:name="android.permission.PREVENT_POWER_KEY" />
<application>
<!-- 安全配置 -->
<meta-data
android:name="android.app.uses_flags"
android:value="true" />
<!-- 禁用备份 -->
android:allowBackup="false"
android:fullBackupContent="false"
<!-- 禁用调试 -->
android:debuggable="false"
<!-- 清除任务栈 -->
android:clearTaskOnLaunch="true"
android:excludeFromRecents="true"
</application>
</manifest>
// SecureKeyboardModule.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <UIKit/UIKit.h>
@interface SecureKeyboardModule : RCTEventEmitter <RCTBridgeModule>
@property (nonatomic, strong) UIWindow *keyboardWindow;
@property (nonatomic, strong) UIView *keyboardView;
@property (nonatomic, strong) UITextField *secureTextField;
@property (nonatomic, copy) NSString *currentValue;
@property (nonatomic, assign) BOOL isCapital;
@property (nonatomic, assign) BOOL isSymbol;
@property (nonatomic, copy) NSString *keyboardType;
@end
// SecureKeyboardModule.m
#import "SecureKeyboardModule.h"
@interface SecureKeyboardModule ()
@property (nonatomic, strong) NSArray *letterKeys;
@property (nonatomic, strong) NSArray *numberKeys;
@property (nonatomic, strong) NSArray *symbolKeys;
@end
@implementation SecureKeyboardModule
RCT_EXPORT_MODULE(SecureKeyboard);
- (NSArray<NSString *> *)supportedEvents {
return @[@"onKeyPress", @"onKeyboardShow", @"onKeyboardHide"];
}
- (NSDictionary *)constantsToExport {
return @{
@"KEYBOARD_TYPE_NUMBER": @"number",
@"KEYBOARD_TYPE_TEXT": @"text",
@"KEYBOARD_TYPE_PASSWORD": @"password"
};
}
+ (BOOL)requiresMainQueueSetup {
return YES;
}
RCT_EXPORT_METHOD(showKeyboard:(NSString *)type) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setupSecureEnvironment];
[self createSecureTextField];
[self createKeyboardViewWithType:type];
[self showKeyboardWindow];
[self sendEventWithName:@"onKeyboardShow" body:nil];
});
}
RCT_EXPORT_METHOD(hideKeyboard) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideKeyboardWindow];
[self cleanupSecureEnvironment];
[self sendEventWithName:@"onKeyboardHide" body:nil];
});
}
RCT_EXPORT_METHOD(clear) {
self.currentValue = @"";
[self sendKeyPressEvent:@""];
}
RCT_EXPORT_METHOD(getValue:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
resolve(self.currentValue ?: @"");
}
#pragma mark - 安全环境设置
- (void)setupSecureEnvironment {
// 禁用截屏
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
// 防止录屏检测
if (@available(iOS 11.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenCaptureChanged:)
name:UIScreenCapturedDidChangeNotification
object:nil];
}
// 禁用系统键盘缓存
[[UITextField appearance] setAutocorrectionType:UITextAutocorrectionTypeNo];
[[UITextField appearance] setSpellCheckingType:UITextSpellCheckingTypeNo];
[[UITextField appearance] setAutocapitalizationType:UITextAutocapitalizationTypeNone];
[[UITextField appearance] setSmartDashesType:UITextSmartDashesTypeNo];
[[UITextField appearance] setSmartQuotesType:UITextSmartQuotesTypeNo];
[[UITextField appearance] setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
}
- (void)cleanupSecureEnvironment {
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.keyboardWindow = nil;
self.keyboardView = nil;
self.secureTextField = nil;
}
#pragma mark - 安全输入框
- (void)createSecureTextField {
self.secureTextField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
self.secureTextField.hidden = YES;
self.secureTextField.secureTextEntry = YES;
self.secureTextField.autocorrectionType = UITextAutocorrectionTypeNo;
self.secureTextField.spellCheckingType = UITextSpellCheckingTypeNo;
self.secureTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
// 防止系统键盘弹出
self.secureTextField.inputView = [[UIView alloc] initWithFrame:CGRectZero];
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:self.secureTextField];
[self.secureTextField becomeFirstResponder];
}
#pragma mark - 自定义键盘
- (void)createKeyboardViewWithType:(NSString *)type {
self.keyboardType = type;
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat keyboardHeight = 260;
self.keyboardView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, screenWidth, keyboardHeight)];
self.keyboardView.backgroundColor = [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0];
// 创建键盘布局
if ([type isEqualToString:@"number"]) {
[self createNumberKeyboard];
} else {
[self createLetterKeyboard];
}
}
- (void)createLetterKeyboard {
NSArray *rows = @[
@[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"0"],
@[@"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p"],
@[@"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l"],
@[@"CAPS", @"z", @"x", @"c", @"v", @"b", @"n", @"m", @"BACKSPACE"],
@[@"SYMBOL", @"SPACE", @"DONE"]
];
[self createKeyboardWithRows:rows];
}
- (void)createNumberKeyboard {
NSArray *rows = @[
@[@"1", @"2", @"3"],
@[@"4", @"5", @"6"],
@[@"7", @"8", @"9"],
@[@"CLEAR", @"0", @"BACKSPACE"],
@[@"DONE"]
];
[self createKeyboardWithRows:rows];
}
- (void)createSymbolKeyboard {
NSArray *rows = @[
@[@"!", @"@", @"#", @"$", @"%", @"^", @"&", @"*", @"(", @")"],
@[@"-", @"_", @"=", @"+", @"[", @"]", @"{", @"}", @"|", @"\\"],
@[@";", @":", @"'", @"\"", @",", @".", @"<", @">", @"/", @"?"],
@[@"CAPS", @"~", @"`", @"SYMBOL", @"BACKSPACE"],
@[@"LETTERS", @"SPACE", @"DONE"]
];
[self createKeyboardWithRows:rows];
}
- (void)createKeyboardWithRows:(NSArray *)rows {
// 移除旧的按键
for (UIView *subview in self.keyboardView.subviews) {
[subview removeFromSuperview];
}
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat keyWidth = screenWidth / 10;
CGFloat keyHeight = 50;
CGFloat keySpacing = 4;
CGFloat rowSpacing = 8;
CGFloat y = rowSpacing;
for (int row = 0; row < rows.count; row++) {
NSArray *rowKeys = rows[row];
CGFloat totalWidth = rowKeys.count * keyWidth + (rowKeys.count - 1) * keySpacing;
CGFloat x = (screenWidth - totalWidth) / 2;
for (int col = 0; col < rowKeys.count; col++) {
NSString *key = rowKeys[col];
UIButton *keyButton = [self createKeyButtonWithTitle:key];
CGFloat buttonWidth = keyWidth;
CGFloat buttonHeight = keyHeight;
// 特殊键调整宽度
if ([self isSpecialKey:key]) {
buttonWidth = keyWidth * ([self specialKeyWidth:key] ?: 1);
}
keyButton.frame = CGRectMake(x, y, buttonWidth, buttonHeight);
[self.keyboardView addSubview:keyButton];
x += buttonWidth + keySpacing;
}
y += keyHeight + rowSpacing;
}
}
- (UIButton *)createKeyButtonWithTitle:(NSString *)title {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
NSString *displayTitle = [self displayTitleForKey:title];
[button setTitle:displayTitle forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:18];
// 按钮样式
button.backgroundColor = [UIColor whiteColor];
button.layer.cornerRadius = 8;
button.layer.borderWidth = 1;
button.layer.borderColor = [[UIColor colorWithRed:0.87 green:0.87 blue:0.87 alpha:1.0] CGColor];
// 特殊键样式
if ([self isSpecialKey:title]) {
button.backgroundColor = [UIColor colorWithRed:0.91 green:0.96 blue:1.0 alpha:1.0];
[button setTitleColor:[UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
}
// 点击事件
[button addTarget:self action:@selector(keyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
button.tag = [title hash];
return button;
}
- (NSString *)displayTitleForKey:(NSString *)key {
if ([key isEqualToString:@"BACKSPACE"]) {
return @"⌫";
} else if ([key isEqualToString:@"CAPS"]) {
return self.isCapital ? @"⇧" : @"⇪";
} else if ([key isEqualToString:@"CLEAR"]) {
return @"清空";
} else if ([key isEqualToString:@"SPACE"]) {
return @"空格";
} else if ([key isEqualToString:@"DONE"]) {
return @"完成";
} else if ([key isEqualToString:@"SYMBOL"]) {
return @"符号";
} else if ([key isEqualToString:@"LETTERS"]) {
return @"字母";
} else {
return self.isCapital ? [key uppercaseString] : key;
}
}
- (BOOL)isSpecialKey:(NSString *)key {
NSArray *specialKeys = @[@"CAPS", @"SYMBOL", @"BACKSPACE", @"CLEAR", @"SPACE", @"DONE", @"LETTERS"];
return [specialKeys containsObject:key];
}
- (NSNumber *)specialKeyWidth:(NSString *)key {
NSDictionary *specialWidths = @{
@"CAPS": @1.5,
@"BACKSPACE": @1.5,
@"CLEAR": @1.5,
@"SPACE": @3,
@"DONE": @2,
@"SYMBOL": @1.5,
@"LETTERS": @1.5
};
return specialWidths[key];
}
#pragma mark - 按键处理
- (void)keyButtonTapped:(UIButton *)sender {
NSString *key = @"";
// 通过tag找到对应的key
for (UIView *subview in self.keyboardView.subviews) {
if ([subview isKindOfClass:[UIButton class]] && subview.tag == sender.tag) {
// 这里需要维护一个key到title的映射
key = [sender titleForState:UIControlStateNormal];
break;
}
}
if ([key isEqualToString:@"⌫"]) key = @"BACKSPACE";
if ([key isEqualToString:@"⇧"] || [key isEqualToString:@"⇪"]) key = @"CAPS";
if ([key isEqualToString:@"清空"]) key = @"CLEAR";
if ([key isEqualToString:@"空格"]) key = @"SPACE";
if ([key isEqualToString:@"完成"]) key = @"DONE";
if ([key isEqualToString:@"符号"]) key = @"SYMBOL";
if ([key isEqualToString:@"字母"]) key = @"LETTERS";
[self handleKeyPress:key];
}
- (void)handleKeyPress:(NSString *)key {
if ([key isEqualToString:@"BACKSPACE"]) {
if (self.currentValue.length > 0) {
self.currentValue = [self.currentValue substringToIndex:self.currentValue.length - 1];
}
} else if ([key isEqualToString:@"CLEAR"]) {
self.currentValue = @"";
} else if ([key isEqualToString:@"CAPS"]) {
self.isCapital = !self.isCapital;
[self refreshKeyboard];
} else if ([key isEqualToString:@"SYMBOL"]) {
[self createSymbolKeyboard];
} else if ([key isEqualToString:@"LETTERS"]) {
[self createLetterKeyboard];
} else if ([key isEqualToString:@"SPACE"]) {
self.currentValue = [self.currentValue stringByAppendingString:@" "];
} else if ([key isEqualToString:@"DONE"]) {
[self hideKeyboard];
return;
} else {
NSString *actualKey = self.isCapital ? [key uppercaseString] : key;
self.currentValue = [self.currentValue stringByAppendingString:actualKey];
}
[self sendKeyPressEvent:self.currentValue];
[self updateSecureTextField];
}
- (void)refreshKeyboard {
if ([self.keyboardType isEqualToString:@"number"]) {
[self createNumberKeyboard];
} else {
[self createLetterKeyboard];
}
}
- (void)updateSecureTextField {
// 在安全输入框中显示随机字符
NSString *randomText = [self generateRandomString:self.currentValue.length];
self.secureTextField.text = randomText;
}
- (NSString *)generateRandomString:(NSInteger)length {
NSString *chars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()";
NSMutableString *result = [NSMutableString string];
for (NSInteger i = 0; i < length; i++) {
[result appendFormat:@"%C", [chars characterAtIndex:arc4random_uniform((uint32_t)chars.length)]];
}
return result;
}
#pragma mark - 键盘窗口管理
- (void)showKeyboardWindow {
if (!self.keyboardWindow) {
self.keyboardWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.keyboardWindow.windowLevel = UIWindowLevelAlert + 1;
self.keyboardWindow.backgroundColor = [UIColor clearColor];
self.keyboardWindow.hidden = NO;
self.keyboardWindow.userInteractionEnabled = YES;
}
[self.keyboardWindow addSubview:self.keyboardView];
// 动画显示
CGRect finalFrame = self.keyboardView.frame;
CGRect startFrame = CGRectOffset(finalFrame, 0, finalFrame.size.height);
self.keyboardView.frame = startFrame;
[UIView animateWithDuration:0.3 animations:^{
self.keyboardView.frame = finalFrame;
}];
}
- (void)hideKeyboardWindow {
[UIView animateWithDuration:0.3 animations:^{
CGRect frame = self.keyboardView.frame;
self.keyboardView.frame = CGRectOffset(frame, 0, frame.size.height);
} completion:^(BOOL finished) {
[self.keyboardView removeFromSuperview];
self.keyboardWindow.hidden = YES;
self.keyboardWindow = nil;
}];
}
#pragma mark - 事件发送
- (void)sendKeyPressEvent:(NSString *)value {
[self sendEventWithName:@"onKeyPress" body:@{@"value": value}];
}
#pragma mark - 安全通知处理
- (void)applicationDidEnterBackground:(NSNotification *)notification {
// 应用进入后台时清空输入
self.currentValue = @"";
[self hideKeyboard];
}
- (void)screenCaptureChanged:(NSNotification *)notification {
// 检测到录屏时隐藏键盘
if (@available(iOS 11.0, *)) {
if ([UIScreen mainScreen].isCaptured) {
[self hideKeyboard];
}
}
}
@end
// AppDelegate.m 安全配置
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "SecureKeyboardModule.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 安全配置
[self setupSecurity];
// React Native 初始化
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"SecureKeyboardDemo"
initialProperties:nil];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)setupSecurity {
// 禁用系统键盘缓存
[[UITextField appearance] setAutocorrectionType:UITextAutocorrectionTypeNo];
[[UITextField appearance] setSpellCheckingType:UITextSpellCheckingTypeNo];
[[UITextField appearance] setAutocapitalizationType:UITextAutocapitalizationTypeNone];
// 防止数据备份到 iCloud
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
if ([paths count] > 0) {
NSString *documentsPath = [paths objectAtIndex:0];
[self addSkipBackupAttributeToItemAtPath:documentsPath];
}
// 防止粘贴板数据泄露
[self clearPasteboard];
}
- (BOOL)addSkipBackupAttributeToItemAtPath:(NSString *)filePathString {
NSURL *URL = [NSURL fileURLWithPath:filePathString];
if (![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) {
return NO;
}
NSError *error = nil;
BOOL success = [URL setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
error:&error];
if (!success) {
NSLog(@"Error excluding %@ from backup: %@", [URL lastPathComponent], error);
}
return success;
}
- (void)clearPasteboard {
// 应用启动时清空粘贴板
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = @"";
// 监听粘贴板变化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(pasteboardChanged:)
name:UIPasteboardChangedNotification
object:nil];
}
- (void)pasteboardChanged:(NSNotification *)notification {
// 检测到粘贴板变化时清空敏感信息
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
NSString *content = pasteboard.string;
if ([self containsSensitiveInformation:content]) {
pasteboard.string = @"";
}
}
- (BOOL)containsSensitiveInformation:(NSString *)text {
// 检测敏感信息(银行卡号、密码等)
NSArray *sensitivePatterns = @[
@"\\d{16,19}", // 银行卡号
@"\\d{6}", // 6位密码
@"password", // 密码关键词
@"passwd",
@"pwd"
];
for (NSString *pattern in sensitivePatterns) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:NSRegularExpressionCaseInsensitive
error:nil];
if ([regex numberOfMatchesInString:text options:0 range:NSMakeRange(0, text.length)] > 0) {
return YES;
}
}
return NO;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// 应用失去焦点时隐藏敏感信息
[[NSNotificationCenter defaultCenter] postNotificationName:@"HideSensitiveContent" object:nil];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// 应用获得焦点时检查安全状态
[self checkSecurityStatus];
}
- (void)checkSecurityStatus {
// 检查越狱状态
if ([self isJailbroken]) {
NSLog(@"设备已越狱,可能存在安全风险");
// 可以在这里提示用户或采取其他措施
}
// 检查调试状态
if ([self isDebuggerAttached]) {
NSLog(@"检测到调试器连接");
}
}
- (BOOL)isJailbroken {
// 检查越狱的常见文件路径
NSArray *jailbreakPaths = @[
@"/Applications/Cydia.app",
@"/Library/MobileSubstrate/MobileSubstrate.dylib",
@"/bin/bash",
@"/usr/sbin/sshd",
@"/etc/apt"
];
for (NSString *path in jailbreakPaths) {
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
return YES;
}
}
return NO;
}
- (BOOL)isDebuggerAttached {
// 检查是否附加了调试器
struct kinfo_proc info;
size_t info_size = sizeof(info);
int name[4];
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();
if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) {
return YES;
}
return (info.kp_proc.p_flag & P_TRACED) != 0;
}
@end
// SecureKeyboard.js
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Platform,
Modal,
Dimensions,
Animated,
Easing,
TextInput,
Alert,
} from 'react-native';
import { NativeModules, NativeEventEmitter } from 'react-native';
const { SecureKeyboard } = NativeModules;
const SecureKeyboardEmitter = new NativeEventEmitter(SecureKeyboard);
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
const KEYBOARD_TYPES = {
NUMBER: 'number',
TEXT: 'text',
PASSWORD: 'password',
};
const KEYBOARD_LAYOUTS = {
NUMBER: [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['CLEAR', '0', 'BACKSPACE'],
],
LETTER: [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['CAPS', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'BACKSPACE'],
],
SYMBOL: [
['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'],
['-', '_', '=', '+', '[', ']', '{', '}', '|', '\\'],
[';', ':', "'", '"', ',', '.', '<', '>', '/', '?'],
['CAPS', '~', '`', 'SYMBOL', 'BACKSPACE'],
],
};
const SPECIAL_KEYS = {
BACKSPACE: 'BACKSPACE',
CAPS: 'CAPS',
CLEAR: 'CLEAR',
SYMBOL: 'SYMBOL',
LETTER: 'LETTER',
SPACE: 'SPACE',
DONE: 'DONE',
};
export default function SecureKeyboardComponent({
visible = false,
type = KEYBOARD_TYPES.PASSWORD,
maxLength = 20,
onInputChange,
onComplete,
onCancel,
title = '安全输入',
placeholder = '请输入',
showValue = false,
animation = true,
theme = 'light',
customLayout,
secureEntry = true,
vibration = true,
sound = false,
}) {
const [value, setValue] = useState('');
const [isCaps, setIsCaps] = useState(false);
const [isSymbol, setIsSymbol] = useState(false);
const [layout, setLayout] = useState(KEYBOARD_LAYOUTS.LETTER);
const slideAnim = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
const secureInputRef = useRef(null);
const lastInputTime = useRef(Date.now());
const inputHistory = useRef([]);
// 安全配置
const securityConfig = {
maxRetryCount: 5,
lockDuration: 30000, // 30秒
allowScreenshots: false,
preventScreenRecording: true,
clearClipboard: true,
memoryProtection: true,
};
useEffect(() => {
if (type === KEYBOARD_TYPES.NUMBER) {
setLayout(KEYBOARD_LAYOUTS.NUMBER);
} else {
setLayout(isSymbol ? KEYBOARD_LAYOUTS.SYMBOL : KEYBOARD_LAYOUTS.LETTER);
}
}, [type, isSymbol]);
useEffect(() => {
if (visible) {
showKeyboard();
setupSecurityListeners();
} else {
hideKeyboard();
cleanupSecurityListeners();
}
return () => {
cleanupSecurityListeners();
};
}, [visible]);
useEffect(() => {
// 监听原生键盘事件
const keyPressListener = SecureKeyboardEmitter.addListener(
'onKeyPress',
handleNativeKeyPress
);
const keyboardShowListener = SecureKeyboardEmitter.addListener(
'onKeyboardShow',
handleNativeKeyboardShow
);
const keyboardHideListener = SecureKeyboardEmitter.addListener(
'onKeyboardHide',
handleNativeKeyboardHide
);
return () => {
keyPressListener.remove();
keyboardShowListener.remove();
keyboardHideListener.remove();
};
}, []);
const showKeyboard = useCallback(() => {
if (Platform.OS === 'ios' || Platform.OS === 'android') {
SecureKeyboard.showKeyboard(type);
} else {
// Web 端模拟
if (animation) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
]).start();
}
// 聚焦安全输入框
if (secureInputRef.current) {
secureInputRef.current.focus();
}
}
}, [type, animation, fadeAnim, slideAnim]);
const hideKeyboard = useCallback(() => {
if (Platform.OS === 'ios' || Platform.OS === 'android') {
SecureKeyboard.hideKeyboard();
} else {
if (animation) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: SCREEN_HEIGHT,
duration: 300,
easing: Easing.in(Easing.cubic),
useNativeDriver: true,
}),
]).start(() => {
setValue('');
setIsCaps(false);
setIsSymbol(false);
});
}
}
}, [animation, fadeAnim, slideAnim, SCREEN_HEIGHT]);
const setupSecurityListeners = useCallback(() => {
// 监听截屏/录屏
if (Platform.OS === 'android') {
// Android 截屏监听
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
// 检测到屏幕方向变化可能是截屏
checkScreenshot();
});
return () => subscription?.remove();
} else if (Platform.OS === 'ios') {
// iOS 录屏监听
// 通过原生模块监听
}
}, []);
const cleanupSecurityListeners = useCallback(() => {
// 清理监听器
}, []);
const checkScreenshot = useCallback(() => {
if (!securityConfig.allowScreenshots) {
Alert.alert(
'安全提示',
'检测到屏幕截图,为保护您的信息安全,已清空输入内容。',
[{ text: '确定', onPress: () => clearInput() }]
);
}
}, [securityConfig.allowScreenshots]);
const clearInput = useCallback(() => {
setValue('');
setIsCaps(false);
setIsSymbol(false);
inputHistory.current = [];
if (Platform.OS === 'ios' || Platform.OS === 'android') {
SecureKeyboard.clear();
}
if (onInputChange) {
onInputChange('');
}
}, [onInputChange]);
const handleKeyPress = useCallback(
async (key) => {
// 防抖处理
const now = Date.now();
if (now - lastInputTime.current < 100) {
return;
}
lastInputTime.current = now;
// 振动反馈
if (vibration && Platform.OS !== 'web') {
const { Vibration } = require('react-native');
Vibration.vibrate(10);
}
// 声音反馈
if (sound) {
// 播放按键音
}
// 记录输入历史
inputHistory.current.push({
key,
timestamp: now,
valueBefore: value,
});
// 限制历史记录长度
if (inputHistory.current.length > 100) {
inputHistory.current.shift();
}
let newValue = value;
switch (key) {
case SPECIAL_KEYS.BACKSPACE:
if (newValue.length > 0) {
newValue = newValue.slice(0, -1);
}
break;
case SPECIAL_KEYS.CLEAR:
newValue = '';
break;
case SPECIAL_KEYS.CAPS:
setIsCaps((prev) => !prev);
break;
case SPECIAL_KEYS.SYMBOL:
setIsSymbol((prev) => !prev);
break;
case SPECIAL_KEYS.LETTER:
setIsSymbol(false);
break;
case SPECIAL_KEYS.SPACE:
if (newValue.length < maxLength) {
newValue += ' ';
}
break;
case SPECIAL_KEYS.DONE:
handleComplete();
return;
default:
if (newValue.length < maxLength) {
const char = isCaps ? key.toUpperCase() : key;
newValue += char;
}
break;
}
// 更新值
setValue(newValue);
// 回调
if (onInputChange) {
onInputChange(newValue);
}
// 内存保护
if (securityConfig.memoryProtection) {
protectMemory(newValue);
}
},
[value, isCaps, maxLength, onInputChange, securityConfig.memoryProtection, vibration, sound]
);
const handleComplete = useCallback(() => {
if (onComplete) {
onComplete(value);
}
hideKeyboard();
}, [value, onComplete, hideKeyboard]);
const handleNativeKeyPress = useCallback(
(event) => {
const { value: newValue } = event;
setValue(newValue);
if (onInputChange) {
onInputChange(newValue);
}
},
[onInputChange]
);
const handleNativeKeyboardShow = useCallback(() => {
console.log('原生键盘已显示');
}, []);
const handleNativeKeyboardHide = useCallback(() => {
console.log('原生键盘已隐藏');
if (onCancel) {
onCancel();
}
}, [onCancel]);
const protectMemory = useCallback((data) => {
// 内存保护策略
// 1. 避免在内存中存储明文
// 2. 及时清理临时变量
// 3. 使用 SecureStore 存储敏感数据
// 这里可以添加内存清理逻辑
if (Platform.OS === 'android') {
// 调用原生方法清理内存
SecureKeyboard?.protectMemory?.();
}
}, []);
const renderKey = useCallback(
(key, index) => {
const isSpecial = Object.values(SPECIAL_KEYS).includes(key);
const displayKey = getDisplayKey(key, isCaps);
return (
<TouchableOpacity
key={index}
style={[
styles.key,
isSpecial && styles.specialKey,
key === SPECIAL_KEYS.DONE && styles.doneKey,
theme === 'dark' && styles.darkKey,
]}
onPress={() => handleKeyPress(key)}
activeOpacity={0.7}
delayPressIn={0}
>
<Text
style={[
styles.keyText,
isSpecial && styles.specialKeyText,
key === SPECIAL_KEYS.DONE && styles.doneKeyText,
theme === 'dark' && styles.darkKeyText,
]}
>
{displayKey}
</Text>
</TouchableOpacity>
);
},
[handleKeyPress, isCaps, theme]
);
const getDisplayKey = useCallback((key, caps) => {
switch (key) {
case SPECIAL_KEYS.BACKSPACE:
return '⌫';
case SPECIAL_KEYS.CAPS:
return caps ? '⇧' : '⇪';
case SPECIAL_KEYS.CLEAR:
return '清空';
case SPECIAL_KEYS.SYMBOL:
return '符号';
case SPECIAL_KEYS.LETTER:
return '字母';
case SPECIAL_KEYS.SPACE:
return '空格';
case SPECIAL_KEYS.DONE:
return '完成';
default:
return caps ? key.toUpperCase() : key;
}
}, []);
const renderKeyboard = useCallback(() => {
return (
<View style={[styles.keyboard, theme === 'dark' && styles.darkKeyboard]}>
{layout.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}>
{row.map((key, keyIndex) => renderKey(key, `${rowIndex}-${keyIndex}`))}
</View>
))}
</View>
);
}, [layout, renderKey, theme]);
const renderValueDisplay = useCallback(() => {
if (!showValue) return null;
const displayValue = secureEntry ? '•'.repeat(value.length) : value;
return (
<View style={styles.valueContainer}>
<Text style={[styles.valueText, theme === 'dark' && styles.darkValueText]}>
{displayValue || placeholder}
</Text>
{value.length > 0 && (
<TouchableOpacity onPress={clearInput} style={styles.clearButton}>
<Text style={styles.clearButtonText}>×</Text>
</TouchableOpacity>
)}
</View>
);
}, [value, showValue, secureEntry, placeholder, theme, clearInput]);
if (Platform.OS === 'ios' || Platform.OS === 'android') {
// 使用原生键盘
return (
<Modal
visible={visible}
transparent={true}
animationType="none"
onRequestClose={onCancel}
>
<View style={styles.nativeContainer}>
<View style={styles.nativeHeader}>
<Text style={styles.nativeTitle}>{title}</Text>
<TouchableOpacity onPress={onCancel} style={styles.nativeCloseButton}>
<Text style={styles.nativeCloseButtonText}>×</Text>
</TouchableOpacity>
</View>
{renderValueDisplay()}
</View>
</Modal>
);
}
// Web/模拟键盘
return (
<Modal
visible={visible}
transparent={true}
animationType="none"
onRequestClose={onCancel}
>
<Animated.View
style={[
styles.overlay,
{
opacity: fadeAnim,
},
]}
>
<TouchableOpacity
style={styles.backdrop}
activeOpacity={1}
onPress={onCancel}
/>
<Animated.View
style={[
styles.container,
{
transform: [{ translateY: slideAnim }],
},
]}
>
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
<TouchableOpacity onPress={onCancel} style={styles.closeButton}>
<Text style={styles.closeButtonText}>×</Text>
</TouchableOpacity>
</View>
{renderValueDisplay()}
{renderKeyboard()}
{/* 隐藏的安全输入框 */}
<TextInput
ref={secureInputRef}
style={styles.hiddenInput}
value=""
onChangeText={() => {}}
autoCorrect={false}
autoCapitalize="none"
keyboardType="default"
secureTextEntry={true}
showSoftInputOnFocus={false}
/>
</Animated.View>
</Animated.View>
</Modal>
);
}
const styles = StyleSheet.create({
// 模态框样式
overlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
container: {
backgroundColor: '#F5F5F5',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingBottom: 20,
},
// 头部样式
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
closeButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 20,
color: '#666',
lineHeight: 20,
},
// 值显示样式
valueContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 20,
backgroundColor: '#FFF',
marginHorizontal: 16,
marginVertical: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
},
valueText: {
flex: 1,
fontSize: 24,
color: '#333',
letterSpacing: 2,
},
darkValueText: {
color: '#FFF',
},
clearButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
clearButtonText: {
fontSize: 20,
color: '#666',
lineHeight: 20,
},
// 键盘样式
keyboard: {
backgroundColor: '#F5F5F5',
paddingHorizontal: 8,
paddingTop: 8,
},
darkKeyboard: {
backgroundColor: '#2C2C2C',
},
row: {
flexDirection: 'row',
justifyContent: 'center',
marginBottom: 8,
},
// 按键样式
key: {
width: (SCREEN_WIDTH - 16 - 9 * 8) / 10,
height: 48,
backgroundColor: '#FFF',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 4,
borderWidth: 1,
borderColor: '#E0E0E0',
},
specialKey: {
backgroundColor: '#E8F4FF',
borderColor: '#B8D9FF',
},
doneKey: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
darkKey: {
backgroundColor: '#3A3A3A',
borderColor: '#4A4A4A',
},
keyText: {
fontSize: 18,
color: '#333',
fontWeight: '500',
},
specialKeyText: {
color: '#007AFF',
},
doneKeyText: {
color: '#FFF',
},
darkKeyText: {
color: '#FFF',
},
// 隐藏输入框
hiddenInput: {
position: 'absolute',
width: 1,
height: 1,
opacity: 0,
},
// 原生容器样式
nativeContainer: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
nativeHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#FFF',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
nativeTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
nativeCloseButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
nativeCloseButtonText: {
fontSize: 20,
color: '#666',
lineHeight: 20,
},
});
// SecureInput.js
import React, { useState, useRef, useEffect, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
TextInput,
Platform,
Animated,
Easing,
Alert,
ActivityIndicator,
} from 'react-native';
import SecureKeyboard from './SecureKeyboard';
import Icon from 'react-native-vector-icons/MaterialIcons';
const SECURITY_LEVELS = {
LOW: 'low', // 基本安全
MEDIUM: 'medium', // 中等安全
HIGH: 'high', // 高安全
MAXIMUM: 'maximum', // 最高安全
};
export default function SecureInput({
label,
placeholder = '请输入',
value = '',
onChangeText,
onFocus,
onBlur,
onSubmitEditing,
keyboardType = 'default',
secureTextEntry = true,
maxLength = 20,
required = false,
disabled = false,
loading = false,
error = null,
success = null,
securityLevel = SECURITY_LEVELS.MEDIUM,
showToggle = true,
customValidator,
style,
containerStyle,
inputStyle,
labelStyle,
errorStyle,
successStyle,
}) {
const [isFocused, setIsFocused] = useState(false);
const [showSecureKeyboard, setShowSecureKeyboard] = useState(false);
const [internalValue, setInternalValue] = useState(value);
const [showText, setShowText] = useState(!secureTextEntry);
const [validationState, setValidationState] = useState(null);
const inputRef = useRef(null);
const focusAnim = useRef(new Animated.Value(0)).current;
const shakeAnim = useRef(new Animated.Value(0)).current;
const securityConfig = getSecurityConfig(securityLevel);
useEffect(() => {
setInternalValue(value);
}, [value]);
useEffect(() => {
validateInput(internalValue);
}, [internalValue]);
const getSecurityConfig = (level) => {
const configs = {
[SECURITY_LEVELS.LOW]: {
useSecureKeyboard: false,
preventScreenshot: false,
clearClipboard: false,
memoryProtection: false,
maxRetry: 3,
},
[SECURITY_LEVELS.MEDIUM]: {
useSecureKeyboard: Platform.OS !== 'web',
preventScreenshot: true,
clearClipboard: true,
memoryProtection: true,
maxRetry: 5,
},
[SECURITY_LEVELS.HIGH]: {
useSecureKeyboard: true,
preventScreenshot: true,
clearClipboard: true,
memoryProtection: true,
biometricAuth: true,
maxRetry: 3,
lockDuration: 60000,
},
[SECURITY_LEVELS.MAXIMUM]: {
useSecureKeyboard: true,
preventScreenshot: true,
clearClipboard: true,
memoryProtection: true,
biometricAuth: true,
encryptStorage: true,
obfuscateMemory: true,
maxRetry: 1,
lockDuration: 300000,
},
};
return configs[level] || configs[SECURITY_LEVELS.MEDIUM];
};
const handleFocus = useCallback(() => {
setIsFocused(true);
Animated.timing(focusAnim, {
toValue: 1,
duration: 200,
easing: Easing.out(Easing.quad),
useNativeDriver: false,
}).start();
if (onFocus) {
onFocus();
}
if (securityConfig.useSecureKeyboard) {
setShowSecureKeyboard(true);
// 阻止系统键盘
if (inputRef.current) {
inputRef.current.blur();
}
}
}, [focusAnim, onFocus, securityConfig.useSecureKeyboard]);
const handleBlur = useCallback(() => {
setIsFocused(false);
Animated.timing(focusAnim, {
toValue: 0,
duration: 200,
easing: Easing.out(Easing.quad),
useNativeDriver: false,
}).start();
if (onBlur) {
onBlur();
}
if (showSecureKeyboard) {
setShowSecureKeyboard(false);
}
}, [focusAnim, onBlur, showSecureKeyboard]);
const handleChangeText = useCallback((text) => {
setInternalValue(text);
if (onChangeText) {
onChangeText(text);
}
// 内存保护
if (securityConfig.memoryProtection) {
protectMemory(text);
}
}, [onChangeText, securityConfig.memoryProtection]);
const handleSecureKeyboardInput = useCallback((text) => {
handleChangeText(text);
}, [handleChangeText]);
const handleSecureKeyboardComplete = useCallback(() => {
setShowSecureKeyboard(false);
if (inputRef.current) {
inputRef.current.blur();
}
if (onSubmitEditing) {
onSubmitEditing();
}
}, [onSubmitEditing]);
const handleSecureKeyboardCancel = useCallback(() => {
setShowSecureKeyboard(false);
if (inputRef.current) {
inputRef.current.blur();
}
}, []);
const toggleShowText = useCallback(() => {
if (securityLevel === SECURITY_LEVELS.MAXIMUM) {
Alert.alert(
'安全提示',
'最高安全级别下不能显示明文',
[{ text: '确定' }]
);
return;
}
setShowText(prev => !prev);
}, [securityLevel]);
const validateInput = useCallback((text) => {
if (!text && !required) {
setValidationState(null);
return true;
}
let isValid = true;
let message = '';
// 基本验证
if (required && !text) {
isValid = false;
message = '此项为必填项';
} else if (maxLength && text.length > maxLength) {
isValid = false;
message = `长度不能超过${maxLength}位`;
}
// 自定义验证
if (isValid && customValidator) {
const customResult = customValidator(text);
if (typeof customResult === 'string') {
isValid = false;
message = customResult;
} else if (typeof customResult === 'boolean' && !customResult) {
isValid = false;
message = '输入内容不符合要求';
}
}
// 安全级别验证
if (isValid && securityLevel >= SECURITY_LEVELS.MEDIUM) {
if (text.length < 6) {
isValid = false;
message = '密码长度至少6位';
}
// 检查密码强度
const strength = checkPasswordStrength(text);
if (strength < 2 && securityLevel >= SECURITY_LEVELS.HIGH) {
isValid = false;
message = '密码强度不足,请包含字母、数字和特殊字符';
}
}
setValidationState({
isValid,
message,
});
if (!isValid && text) {
triggerShakeAnimation();
}
return isValid;
}, [required, maxLength, customValidator, securityLevel]);
const checkPasswordStrength = useCallback((password) => {
let strength = 0;
if (password.length >= 8) strength++;
if (/[a-z]/.test(password)) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^a-zA-Z0-9]/.test(password)) strength++;
return strength;
}, []);
const triggerShakeAnimation = useCallback(() => {
Animated.sequence([
Animated.timing(shakeAnim, {
toValue: 10,
duration: 50,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: -10,
duration: 50,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: 10,
duration: 50,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: 0,
duration: 50,
useNativeDriver: true,
}),
]).start();
}, [shakeAnim]);
const protectMemory = useCallback((text) => {
// 内存保护实现
// 1. 避免在闭包中保留引用
// 2. 使用 WeakRef
// 3. 及时清理
if (securityConfig.obfuscateMemory) {
// 混淆内存中的数据
const obfuscated = obfuscateText(text);
// 存储混淆后的数据
}
}, [securityConfig.obfuscateMemory]);
const obfuscateText = useCallback((text) => {
// 简单的混淆算法
if (!text) return '';
const chars = text.split('');
const obfuscated = chars.map(char => {
const code = char.charCodeAt(0);
return String.fromCharCode(code ^ 0x55); // 简单的XOR混淆
});
return obfuscated.join('');
}, []);
const getDisplayValue = useCallback(() => {
if (!internalValue) return '';
if (showText) {
return internalValue;
} else {
return '•'.repeat(internalValue.length);
}
}, [internalValue, showText]);
const getKeyboardType = useCallback(() => {
if (securityConfig.useSecureKeyboard) {
return 'default';
}
return keyboardType;
}, [securityConfig.useSecureKeyboard, keyboardType]);
const renderStatusIcon = useCallback(() => {
if (loading) {
return <ActivityIndicator size="small" color="#007AFF" />;
}
if (error || validationState?.isValid === false) {
return <Icon name="error" size={20} color="#FF3B30" />;
}
if (success || validationState?.isValid === true) {
return <Icon name="check-circle" size={20} color="#34C759" />;
}
return null;
}, [loading, error, success, validationState]);
const renderToggleButton = useCallback(() => {
if (!secureTextEntry || !showToggle) return null;
return (
<TouchableOpacity
onPress={toggleShowText}
style={styles.toggleButton}
disabled={securityLevel === SECURITY_LEVELS.MAXIMUM}
>
<Icon
name={showText ? 'visibility-off' : 'visibility'}
size={20}
color={securityLevel === SECURITY_LEVELS.MAXIMUM ? '#999' : '#666'}
/>
</TouchableOpacity>
);
}, [secureTextEntry, showToggle, toggleShowText, showText, securityLevel]);
const borderColor = focusAnim.interpolate({
inputRange: [0, 1],
outputRange: ['#E0E0E0', '#007AFF'],
});
const labelTop = focusAnim.interpolate({
inputRange: [0, 1],
outputRange: [12, -8],
});
const labelSize = focusAnim.interpolate({
inputRange: [0, 1],
outputRange: [16, 12],
});
const labelColor = focusAnim.interpolate({
inputRange: [0, 1],
outputRange: ['#999', '#007AFF'],
});
return (
<View style={[styles.container, containerStyle]}>
<Animated.View
style={[
styles.inputContainer,
{
borderColor,
transform: [{ translateX: shakeAnim }],
},
isFocused && styles.inputContainerFocused,
error && styles.inputContainerError,
success && styles.inputContainerSuccess,
disabled && styles.inputContainerDisabled,
style,
]}
>
<Animated.Text
style={[
styles.label,
{
top: labelTop,
fontSize: labelSize,
color: labelColor,
},
(error || validationState?.isValid === false) && styles.labelError,
(success || validationState?.isValid === true) && styles.labelSuccess,
labelStyle,
]}
>
{label} {required && '*'}
</Animated.Text>
<View style={styles.inputWrapper}>
<TextInput
ref={inputRef}
style={[styles.input, inputStyle]}
value={getDisplayValue()}
onChangeText={securityConfig.useSecureKeyboard ? undefined : handleChangeText}
onFocus={handleFocus}
onBlur={handleBlur}
onSubmitEditing={onSubmitEditing}
keyboardType={getKeyboardType()}
secureTextEntry={secureTextEntry && !showText}
maxLength={securityConfig.useSecureKeyboard ? undefined : maxLength}
editable={!disabled && !securityConfig.useSecureKeyboard}
selectTextOnFocus={!securityConfig.useSecureKeyboard}
autoCorrect={false}
autoCapitalize="none"
placeholder={isFocused ? undefined : placeholder}
placeholderTextColor="#999"
/>
<View style={styles.rightContainer}>
{renderStatusIcon()}
{renderToggleButton()}
</View>
</View>
</Animated.View>
{(error || validationState?.message) && (
<Text style={[styles.errorText, errorStyle]}>
{error || validationState.message}
</Text>
)}
{success && (
<Text style={[styles.successText, successStyle]}>
{success}
</Text>
)}
<SecureKeyboard
visible={showSecureKeyboard}
type={keyboardType === 'numeric' ? 'number' : 'password'}
maxLength={maxLength}
onInputChange={handleSecureKeyboardInput}
onComplete={handleSecureKeyboardComplete}
onCancel={handleSecureKeyboardCancel}
title={`输入${label}`}
placeholder={placeholder}
showValue={!secureTextEntry}
secureEntry={secureTextEntry}
theme="light"
vibration={true}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginVertical: 8,
},
inputContainer: {
borderWidth: 1,
borderRadius: 8,
backgroundColor: '#FFF',
paddingHorizontal: 12,
paddingTop: 16,
paddingBottom: 8,
},
inputContainerFocused: {
backgroundColor: '#FAFAFA',
},
inputContainerError: {
borderColor: '#FF3B30',
backgroundColor: '#FFF5F5',
},
inputContainerSuccess: {
borderColor: '#34C759',
backgroundColor: '#F5FFF5',
},
inputContainerDisabled: {
backgroundColor: '#F5F5F5',
opacity: 0.6,
},
label: {
position: 'absolute',
left: 12,
backgroundColor: '#FFF',
paddingHorizontal: 4,
fontWeight: '500',
},
labelError: {
color: '#FF3B30',
},
labelSuccess: {
color: '#34C759',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
input: {
flex: 1,
fontSize: 16,
color: '#333',
paddingVertical: 8,
paddingRight: 8,
minHeight: 24,
},
rightContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
toggleButton: {
padding: 4,
},
errorText: {
fontSize: 12,
color: '#FF3B30',
marginTop: 4,
marginLeft: 12,
},
successText: {
fontSize: 12,
color: '#34C759',
marginTop: 4,
marginLeft: 12,
},
});