fayeah / blogs

方法论、问题驱动、总结
6 stars 0 forks source link

【Stop Snapshot】手机app在后台运行时,防止自动截屏存到手机上 #3

Open fayeah opened 4 years ago

fayeah commented 4 years ago

对于iOS和Android应用,默认情况下,当app被put into background的时候,手机会显示app内容,并且会截屏存到手机上。这个会有安全隐患,当黑客连接手机设备扫描缓存文件,那些敏感的信息可能会被盗走。我们现在项目时native和RN结合使用,所以想法是在包在外面的container,也就是native去实现。我这里分别说一下纯RN和纯Native的实现。

实现的思路,一是在看recent app的时候显示white screen,这样截图就不会有敏感信息;二是甄别出敏感信息,在被放到background的时候将该信息不可见。我们的app难以实现第二个方法,因为所有的logic都在RN里面,native只是一个容器。所以,选择第一种。

纯Native

iOS

对于iOS来说,在AppDelegate.m文件里面override相应的方法。google上面有两种实现,applicationWillResignActiveapplicationDidEnterBackground

而我们的需求主要是解决手机设备上截图可能敏感信息的问题。所以我们选择了applicationDidEnterBackground

- (void)applicationDidEnterBackground:(UIApplication *)application {
    [application ignoreSnapshotOnNextApplicationLaunch];
    NSPredicate* filter =
        [NSPredicate predicateWithBlock:^BOOL(UIWindow* window, NSDictionary* bindings) {
            return [window isKeyWindow];
    }];
    NSArray* windows = [[UIApplication sharedApplication] windows];
    self.keyWindow = [[windows filteredArrayUsingPredicate:filter] firstObject];
    [self.keyWindow endEditing:YES];

    self.imageView = [[UIImageView alloc] initWithFrame: [self.window frame]];
    self.imageView.tag = 101;
    self.imageView.backgroundColor = [UIColor whiteColor];
    [self.imageView setImage:[UIImage imageNamed:@"SplashIcon.png"]];
    [self.imageView setContentMode:UIViewContentModeCenter];

    [self.keyWindow.subviews.lastObject addSubview:self.imageView];

    // 最开始使用以下方法添加subview,后来发现当keyboard打开的时候,app在后台运行,去查看suspend应用,是不work的。
    // UIWindow *mainWindow = [[[UIApplication sharedApplication] windows] lastObject];
    // [mainWindow addSubview: self.imageView];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    UIImageView *imageView = (UIImageView *)[self.keyWindow.subviews.lastObject viewWithTag:101];
    if(imageView != nil) {
        [imageView removeFromSuperview];
    }
}

Android

Android本身自己提供了secure的flag,可以直接使用,但是需求希望能够加一个icon在splash screen上,由于我本人不是native dev,同时Stackoverflow有人说OnPause and similar lifecycle methods are called too late (the screenshot is taken already)(链接)。所以基本上Android没办法给splash screen自定义页面。 这样的话就很简单,两种方式,一是在 MainActivity的onCreate里面实现,另外一个是在app.java这个文件里实现。由于我们的架构设计,在container的MainActivity里面做不work,因为我们是创建了一个新的Activity,真要放在activity里面,需要放到另外一个repo,这不是一个好的实践。于是我选择了第二种。能够实现所有的activity在create的时候应用secure这个flag。 App.java

    @Override
    public void onCreate() {
        super.onCreate();
        setupActivityListener();
    }
private void setupActivityListener() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
                @Override
                public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                    activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
                }

                @Override
                public void onActivityStarted(@NonNull Activity activity) {

                }

                @Override
                public void onActivityResumed(@NonNull Activity activity) {

                }

                @Override
                public void onActivityPaused(@NonNull Activity activity) {

                }

                @Override
                public void onActivityStopped(@NonNull Activity activity) {

                }

                @Override
                public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {

                }

                @Override
                public void onActivityDestroyed(@NonNull Activity activity) {

                }

            });
        }
    }

Android snapshot 的路径我始终没有找到,但是根据官方文档,理论上是可以work的。“Secure surfaces are used to prevent content rendered into those surfaces by applications from appearing in screenshots or from being viewed on non-secure displays”。

纯RN

Android

由于RN的所有UI都是在一个Activity里面的,所以只需要在MainActivity里面override onCreate方法即可。

    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            getWindow().setFlags(
                WindowManager.LayoutParams.FLAG_SECURE,
                WindowManager.LayoutParams.FLAG_SECURE
            );
        }
     }

iOS

使用RN自带的AppState,监听background或inactive的变化,创建一个high order component。

import React from 'react'
import { AppState, Platform, View } from 'react-native'

const SecurityScreen = () => <View />

const showSecurityScreenFromAppState = appState =>
  ['background', 'inactive'].includes(appState)

const withSecurityScreenIOS = Wrapped => {
  return class WithSecurityScreen extends React.Component {
    state = {
      showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState)
    }

    componentDidMount () {
      AppState.addEventListener('change', this.onChangeAppState)
    }

    componentWillUnmount () {
      AppState.removeEventListener('change', this.onChangeAppState)
    }

    onChangeAppState = nextAppState => {
      const showSecurityScreen = showSecurityScreenFromAppState(nextAppState)

      this.setState({ showSecurityScreen })
    }  

    render() {
      return this.state.showSecurityScreen
        ? <SecurityScreen />
        : <Wrapped {...this.props} />
    }
  }
}

const withSecurityScreenAndroid = Wrapped => Wrapped

export const withSecurityScreen = Platform.OS === 'ios'
  ? withSecurityScreenIOS
  : withSecurityScreenAndroid
// app.js
import { withSecurityScreen } from './withSecurityScreen'
...
export default withSecurityScreen(App);

Note:

  1. check iOS simulators: instruments -s devices;
  2. iOS 有个ignoreSnapshotOnNextApplicationLaunch可以避免snapshot,但只有在 state restoration期间才会被call,我这里不work;
  3. iOS图片添加:到Assets.xcassets里面,需要2X和3X的图片。
  4. iOS snapshot的location:/Users/${userName}/Library/Developer/CoreSimulator/Devices/${emulatorId}/data/Containers/Data/Application/${appId}/Library/SplashBoard/Snapshots/${sceneId}目录里ktx结尾的文件。