A few words about automatically generated screenshots
One of the most common elements of mobile applications are data forms. We use them to log in to our account, or to enter other important data about us, sometimes very sensitive and confidential.
One such is the form for entering payment card details in a banking application. In this case, we are very keen that such data should not be leaked in any way.
What possible attack vectors do we have? One of them could be an attempt to steal an automatically generated screenshot of our form when the application goes into the background.
How does it work? When you press the home key on your iPhone, a screenshot of the current app is immediately taken. This is done to generate an aesthetically pleasing effect – an animation of the app, which appears to “shrink” on the screen. The image is also stored for use as a thumbnail of the running app. This feature can pose a security risk because screenshots may display sensitive information, which may unknowingly be stored unencrypted on the device. These data can be recovered by a rogue application with a sandbox bypass exploit or someone who steals the device.
The following is an example of the location of images in the cache:
/var/mobile/Containers/Data/Application/$APP_ID/Library/Caches/Snapshots/
How to protect applications against this? One solution is to overlay an image as the application enters the background state. The overlaid image will “mask” the current screen, thus covering any sensitive information that may be on the screen.
Here is a complete solution:
import UIKit extension ScreenshotHelper { enum CoveringStyle { case blur case image(UIImage) } } /// A helper class that adds and removes overlay view on top of the current window /// in the case of switching of the app between background/foreground modes class ScreenshotHelper { private let topView: UIView private var coveringView: UIView? private var coveringStyle: CoveringStyle = .blur init(topView: UIView) { self.topView = topView } func register(coveringStyle: CoveringStyle = .blur) { self.coveringStyle = coveringStyle NotificationCenter.default.addObserver(self, selector: #selector(ScreenshotHelper.willDisappear(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(ScreenshotHelper.willAppear(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(ScreenshotHelper.willAppear(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) } func unregister() { NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) } @objc private func willDisappear(_ notification: NSNotification) { // Add overlay switch coveringStyle { case .blur: let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light) coveringView = UIVisualEffectView(effect: blurEffect) case .image(let image): coveringView = UIImageView(image: image) coveringView?.contentMode = .bottom coveringView?.translatesAutoresizingMaskIntoConstraints = false } // just double checking guard coveringView != nil else { return } topView.addSubview(coveringView!) coveringView!.addConstraints([ equal(topView, \.topAnchor, \.topAnchor, constant: 0), equal(topView, \.bottomAnchor, \.bottomAnchor, constant: 0), equal(topView, \.leadingAnchor, \.leadingAnchor, constant: 0), equal(topView, \.trailingAnchor, \.trailingAnchor, constant: 0) ]) } @objc private func willAppear(_ notification: NSNotification) { // Remove overlay //swiftlint: disable multiple_closures_with_trailing_closure UIView.animate(withDuration: 0.3, animations: { self.coveringView?.alpha = 0.0 }, completion: { (finished) in if finished { self.coveringView?.removeFromSuperview() } }) //swiftlint: enable multiple_closures_with_trailing_closure } }
final class SomeViewController: UIViewController { lazy var screenshot: ScreenshotHelper = ScreenshotHelper(topView: self.view) // MARK: Lifecycle override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) screenshot.register(coveringStyle: .image(view.asHierarchyImage())) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) screenshot.unregister() } }
extension UIView { func asHierarchyImage() -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: bounds) return renderer.image { _ in drawHierarchy(in: bounds, afterScreenUpdates: true) } } }
We have two options to choose from: using the blur effect and overlaying any image. In this case it will be an overlay as a screenshot of the blank form before entering data, created after the view did appear.
And that’s probably all for today 😉