Jailbreak detection with jail-monkey on React Native app

Mobile device operating systems often impose certain restrictions to what capabilities the user have on the device like which apps can be installed on the device and what access to information and data apps and user have on the device. The limitations can be bypassed with jailbreaking or rooting the device which might introduce security risks to your app running on the device so you might want to detect if your app is run on jailbroken device.

Jailbreak detection

Detecting and possible acting regarding the device status can be beneficial depending on your app features. The reason for detection is to either disable some, or most of the app's functionalities due to security concerns that come from a jailbroken system. You might want to e.g. prevent jailbroken devices from authenticating to protected applications. There are different methods to use and one of the easiest is to use libraries like jail-monkey.

Overall the jailbreak detection is based on several different aspects on the device which: file existence checks, URI scheme registration checks, sandbox behavior checks, abnormal services and dynamic linker inspection.

jail-monkey is a React Native library for detection if a phone has been jail-broken or rooted for iOS and Android. (note: the library is not actively maintained)

Are users claiming they are crossing the globe in seconds and collecting all the Pokeballs? Some apps need to protect themselves in order to protect data integrity. JailMonkey allows you to:

  • Identify if a phone has been jail-broken or rooted for iOS/Android.
  • Detect mocked locations for phones set in "developer mode".
jail-monkey

Using jail-monkey is straightforward and after adding the library to your app the only unanswered question is what you want to do with the information. Do you block the app from running, limit some features or just log the information to e.g. Sentry or some analytics platform.

import JailMonkey from 'jail-monkey'

if (JailMonkey.isJailBroken()) {
  // Alternative behaviour for jail-broken/rooted devices.
}

jail-monkey answers to following questions:

  • can the location be mocked?
  • is the device in debug mode?
  • is the device jailbroken?
  • on iOS the reason for jailbroken
  • on Android: i.a. adb enabled, development settings mode

You show the jailbreaking status for example as a banner in the app:

Jailbreak detected!

Is it worth it?

Implementing jailbreak detection is easy but you might want to consider if it brings anything of value. Jailbreaking an iOS device does not, on its own, make it less secure but it makes harder to reason about the security properties of the device and more concerning issue is that users of jailbroken devices frequently hold off on updating their devices, as jailbreak development usually lags behind official software releases.

Jailbreak / root detection is never perfect and more or less hinders only the less technology savvy users and these kinds of checks don't contribute too much to the security side. Also it can result in false positives as jail-monkey's issue tracker shows. Hackers are usually one step ahead than detection mechanisms so relying on the jailbreaking status should be taking with a grain of salt.

Bypassing different jailbreak detection secure mechanisms is just a matter of effort as the guide for bypassing jail-monkey and jailbreak detection bypass articles show. There's also ready made tools for iOS like iHide. So if you seriously plan to detect and secure your app against jailbreaking you should implement your own tools or use some custom made library.

If you want to read more about jailbreak detection Duo has written a good analysis of jailbreak detection methods and the tools used to evade them.

Automated End-to-End testing React Native apps with Detox

Everyone knows the importance of testing in software development projects so lets jump directly to the topic of how to use Detox for end-to-end testing React Native applications. It's similar to how you would do end-to-end testing React applications with Cypress like I wrote previously. Testing React Native applications needs a bit more setup especially when running on CI/CD but the benefits of having comprehensive tests is great. This article focuses on using Detox and leaves the setup stuff for the official documentation.

Detox for end-to-end testing

End-to-end testing is widely performed in the web applications using frameworks like Cypress but with mobile applications it's not as common. There are essentially couple of frameworks for end-to-end testing mobile applications: Detox and Appium.

This article covers how to use Detox which uses grey-box-testing by monitoring the state of the app to tackle the problem of flaky tests. The main difference between Appium and Detox is that Appium uses black-box-testing, meaning that it doesn't monitor the internal state of the app. There's a good article of comparing Detox and Appium with example projects.

I'll use my personal Hailakka project as an example of setting up Detox for end-to-end testing and for visual regression testing. The app is based on my native Highkara news reader but done in React Native and is still missing features like localization.

Hailakka on Android and iOS

Setup Detox for React Native project

Starting with Detox e2e and React Native gets you through setting Detox up in your project, one step at a time both for Android and iOS.

Detox provides an example project for React Native which shows a starting point for your own e2e tests. It's good to note that support for Mocha is dropped on upcoming Detox 20.

Now you just need to write simple e2e tests for starters and build your app and run Detox tests. Detox has good documentation to get you through the steps and has some troubleshooting tips you might come across especially with Android.

Detox with Expo

Using Detox with Expo projects should work quite nicely without any helper libraries (although some older blog posts suggests so). You can even run e2e tests on EAS builds. For practice I added example of using Detox to my personal Hailakka project with managed workflow.

The changes compared to non-Expo project I followed the Detox on EAS build documentation and added the @config-plugins/detox for a managed project. Also to get the native projects for Detox I ran the npx expo prebuild script which practically configured both iOS and especially Android projects to work with Detox!. You can see my detox.config.js from my GitHub.

Running Detox end-to-end tests

With iOS related tests on simulator I got everything working pretty smoothly but on Android I had some problems to troubleshoot with the help of Detox documentation. Using the FORCE_BUNDLING=true environment variable helped to get rid of the Metro bundler when running Detox as Android emulator had problems connecting to it. Before that I got "unable to load script from assets index.android.bundle" error and I had to use adb reverse tcp:8081 tcp:8081 for proxying the connection.

For running the Detox tests I created the following npm scripts.

"e2e:ios": "npm run e2e:ios:build && npm run e2e:ios:run",
"e2e:ios:build": "detox build --configuration ios.development",
"e2e:ios:run": "detox test --configuration ios.development",
"e2e:android": "npm run e2e:android:build && npm run e2e:android:run",
"e2e:android:build": "detox build --configuration android.development",
"e2e:android:run": "detox test --configuration android.development",

Note on running on Android: Use system images without Google services (AOSP emulators). Detox "strongly recommend to strictly use this flavor of emulators for running automation/Detox tests.".

Also remember to change the Java SDK to 11. You can do it with e.g. asdf.

brew reinstall asdf
asdf plugin add java
asdf list-all java
asdf install java zulu-11.60.19
asdf global java zulu-11.60.19

If you need to debug your Detox tests on Android and run Metro bundler on the side you might need to start the metro builder (react-native start) manually. Also when using concurrently the arguments need to be passed through to desired script, e.g. "e2e:android:run": "concurrently --passthrough-arguments 'npx react-native start' 'npm run e2e:android:test -- {@}' --",.

Running same tests on iOS and Android have some differences and it helps to use "--loglevel=verbose" parameter to debug component hierarchy. For example opening the sidebar navigation from icon button failed on Android but worked on iOS. The issue was that I was using Android system image with Google's services and not the AOSP version.

Setting locale for Detox tests

Detox provides launchApp command which allows you to accept permissions and set language and locale.

For example in beforeAll you can set the locale for and accept the permissions for notifications as we can't accept it when the app runs and the modal is shown.

beforeAll(async () => {
    await device.launchApp({
        languageAndLocale: { language: 'en', locale: 'en-US' },
        newInstance: true,
        permissions: { notifications: 'YES' }, // This will cause the app to terminate before permissions are applied.
    });
});

But unfortunately setting the languageAndLocale works only on iOS. For Android you need a lot more steps to achieve that. Fortunately I'm not alone and "Changing Android 10 or higher device Locale programatically" covers just what I needed. You can use the Appium Settings application and get it to automatically installed to the emulator by using "utilBinaryPaths" as shown in Detox documentation

You can run the needed adb shell commands with execSync when setting up the tests:

import { execSync } from 'child_process';

// Set permissions for Settings app for changing locale
execSync('adb shell pm grant io.appium.settings android.permission.CHANGE_CONFIGURATION');
// Set lang and country
execSync(`adb shell am broadcast -a io.appium.settings.locale -n io.appium.settings/.receivers.LocaleSettingReceiver --es lang fi --es country FI`

Tips for writing Detox tests

People at Siili have written about Detox testing native mobile apps which provides good practices for testing.

  • Start each scenario from a predictable application state. Using beforeAll, beforeEach, await device.reloadReactNative() methods can help with that
  • Add testIDs to the elements you are going to use in the tests,
  • Keep all locators in one place (changing them in the future will not cost too much time)
  • Create helpers method to make your code more legible and easily maintainable

Visual regression testing React Native apps with Detox

Now we have achieved end-to-end tests with Detox but we can do more. By harnessing Detox for visual regression testing we can "verify the proper visual structure and layout of elements appearing on the device's screen". Detox supports taking screenshots, which can be used for visual regression testing purposes as they describe in their documentation.

  • Taking a screenshot, once, and manually verifying it, visually.
  • Storing it as an e2e-test asset (i.e. the snapshot).
  • Using it as the point-of-reference for comparison against screenshots taken in consequent tests, from that point on.

They write that "more practical way of doing this, is by utilizing more advanced 3rd-party image snapshotting & comparison tools such as Applitools." And here is were we come into the good article of how to do Visual regression testing with Detox. In short, we use Jest Image Snapshot and recommendations using SSIM Comparison.

jest-image-snapshot generates an image which shows thee baseline and how it differs with current state with red color.

jest-image-snapshot baseline diff with Detox

To aid writing tests, use some convenience methods by extending Jest expect and automatically taking a screenshot if the method is invoked; consider the platform and device name when doing the comparisons.

Helper methods in setup.ts extending Jest expect, adapted to TypeScript from Visual regression testing React Native apps with Detox and Jest:

import { device } from 'detox';
import fs from 'fs';
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
import path from 'path';
const jestExpect = (global as any).expect;

const kebabCase = (str: string) =>
    str.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)!.join('-').toLowerCase();

const toMatchImage = configureToMatchImageSnapshot({
    comparisonMethod: 'ssim',
    failureThreshold: 0.01, // fail if there is more than a 1% difference
    failureThresholdType: 'percent',
});

jestExpect.extend({ toMatchImage });

jestExpect.extend({
    async toMatchImageSnapshot(
        // a matcher is a method, it has access to Jest context on `this`
        this: jest.MatcherContext,
        screenName: string
    ) {
        const { name } = device;
        const deviceName = name.split(' ').slice(1).join('').replace('(','').replace(')', '');
        const language = languageAndLocale();
        const SNAPSHOTS_DIR = `__image_snapshots__/${language.code}/${deviceName}`;
        const { testPath } = this;
        const customSnapshotsDir = path.join(path.dirname(testPath || ''),
SNAPSHOTS_DIR);
        const customSnapshotIdentifier = kebabCase(`${screenName}`);
        const tempPath = await device.takeScreenshot(screenName);
        const image = fs.readFileSync(tempPath);
        jestExpect(image).toMatchImage({ customSnapshotIdentifier, customSnapshotsDir });
        return { message: () => 'screenshot matches', pass: true };
    },
});

Writing an expectation for an image comparison then becomes as simple as (HomeScreen.e2e.ts):

import { by, element, expect } from 'detox';

const jestExpect = (global as any).expect;

describe('Home Screen', () => {
  it('should show section header', async () => {
    await expect(element(by.id('navigation-container-view'))).toBeVisible();

    await jestExpect('Home Screen').toMatchImageSnapshot();
  });
});

You should also put the device into demo mode by freezing the irrelevant, volatile elements (like time, network information etc.). The helper.ts might look like:

import { execSync } from 'child_process';
import { device } from 'detox';

export const setDemoMode = async () => {
    if (device.getPlatform() === 'ios') {
        await (device as any).setStatusBar({
            batteryLevel: '100',
            batteryState: 'charged',
            cellularBars: '4',
            cellularMode: 'active',
            dataNetwork: 'wifi',
            time: '9:42',
            wifiBars: '3',
        });
    } else {
        // enter demo mode
        execSync('adb shell settings put global sysui_demo_allowed 1');
        // display time 12:00
        execSync('adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200');
        // Display full mobile data with 4g type and no wifi
        execSync(
            'adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 4 -e datatype 4g -e wifi true'
        );
        // Hide notifications
        execSync('adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false');
        // Show full battery but not in charging state
        execSync(
            'adb shell am broadcast -a com.android.systemui.demo -e command battery -e plugged false -e level 100'
        );
    }
};

And as your application changes you may want to add some script to update the snapshots to you package.json

"e2e:ios:refresh": "npm run e2e:ios:build && npm run e2e:ios:run -- --updateSnapshot",
"e2e:android:refresh": "npm run e2e:android:build && npm run e2e:android:run -- --updateSnapshot",

Now you can just enjoy your end-to-end tests taking care that the application looks and works like you want it to after changes. And wait for React Native Owl to mature for getting out-of-the box visual regression testing.

The visual regression testing part of using Detox was for my case more of a theoretical practice than a real use case. As the visual regression testing compares current state to baseline images but with my personal is news reader with changing news it isn't feasible at it's current state.

Notes from React Native EU 2022

React Native EU 2022 was held couple of weeks ago and it's a conference which focuses exclusively on React Native but consists also on general topics which are universal in software development while applied to RN context. This year the online event provided great talks and especially there were many presentations about apps performance improvements, achieving better code and identifying bugs. Here are my notes from the talks I found interesting. All of the talks are available in conference stream on Youtube.

Better performance and quality code from RN EU 2022

This year the React Native EU talks had among other presentations two common topics: performance and code quality. Important aspects of software development which are often dismissed until problems arise so it was refreshing to see it talked so much about.

Here are my notes from the talks I found most interesting. Also the "Can't touch this" talk about accessibility was a good reminder that not everyone use their mobile devices by hand.

How we made our app 80% faster, a data structure story

Marin Godechot gave an interesting talk of React Native apps performance improvements and one of the crucial tools for achieving that.

Validate your assumption before investing in solutions

The learnings of the talk which went through trying different approaches to fix the performance problem was that:

  • Validate your assumption before investing in solutions
  • Performance improvements without tooling is a guessing game
  • Slow Redux selectors are bad
  • Data structures and complexity matters

The breakthrough for finding the problem was after actually measuring the performance with Datadog Real User Monitoring (RUM) and better understanding of the bottlenecks. What you can't measure, you can't improve.

The issue wasn't with useless rerenders, lists and navigation stack but with the datamodels but not the one you would guess. Although the persisted JSON stringified state with Redux was transferred between the JS side and the Native (JS <-> json <-> Bridge <-> json <-> Native) it wasn't the issue.

The big reveal was that when they instrumented Redux's selectors, reducers and sagas to monitoring tool they found that as the user permissions were handled by using the Attribute Based Access Control model the data in JSON format had grown over the years from 15 permissions per user to 10000 permissions per user which caused problems with Redux selectors. The fix was relatively simple: change array to object -> {agengy: [agency:caregivers:manage]}

What you can't measure, you can't improve

You can go EVERYWHERE but I can go FAST - holistic case study on performance

Jakub Binda made a clever comparison in his talk that apps are like cars and same modifications apply to fast cars and fast applications.

The talk starts slow but gets to speed nicely with good overview how to create faster apps. Some of the points were:

  • Reduce weight:
    • Remove dead / legacy code
    • Avoid bundling heavy JSON files (e.g. translations)
    • Keep node_modules clean
  • Keep component structure "simple"
    • More complex structure leads to longer render time and more resources consumption and more re-renders
    • Shimmers are complex and heavy (improve UX by making an impression that content appers faster than it actually does)
  • Using hooks:
    • Might lead to unexpected re-renders when: their value change, dependency array is not set correctly
    • Lead to increased resources comsumption if: their logic is complex, the consumer of the results is hidden behind the flag
  • Tooling:
    • Flipper debug tool: flame charts & performance monitoring tooling

Reducing bugs in a React codebase

Darshita Chaturvedi's hands-on and thoroughly explained talk for identifying bugs in React codebase was insightful.

Reducing bugs in a React codebase

Getting Better All the Time: How to Escape Bad Code

Josh Justice had a great presentation with hands-on example of an application with needed fixes and features and how to apply them with refactoring. And what is TDD by recreating the application.

The slides are available with rest of the TDD sequence and pointers to more resources on TDD in RN.

Refactoring

The key point of the talk was about what do you do about bad code? Do you work around bad code or rewrite bad code? The answer is refactoring: small changes that improve the arrangements of the code without changing its functionality. And comprehensive tests will save you with refactoring. The value is that you're making the improvements that pay off right away and code is shippable after each refactoring. But how do you get there? By using Test-Driven Development (TDD)

"TDD is too much work"
Living with bad code forever is also a lot of work

Getting Better All the Time: How to Escape Bad Code

"Make code better all the time (with refactoring and better test coverage)"

Visual Regression Testing in React Native

Rob Walker talked about visual regression testing and about using React Native OWL. Slides available.

React Native OWL:

  • Visual regression testing for React Native
  • CLI to build and run
  • Jest matcher
  • Interaction API
  • Report generator
  • Inspired by Detox
Baseline, latest, diff
Easy to approach (in theory)

Different test types:

  • Manual testing pros: great for exploratory testing, very flexible, can be outsourced
  • Manual testing cons: easy to miss things, time consuming, hard to catch small changes
  • Jest snapshot tests pros: fast to implement, fast to run, runs on CI
  • Jest snapshot tests cons: only tests in isolation, does not test flow, only comparing JSX
  • Visual regression tests pros: tests entire UI, checks multi-step flows, runs on CI
  • Visual regression tests cons: can be difficult to setup, slower to run, potentially flaky
Use case for visual regression tests

Can't touch this - different ways users interact with their mobile devices

Eevis talked about accessibility and what does this mean for a developer. The slides give a good overview to the topic.

  • Methods talked about
    • Screen reader: trouble seeing or understanding screen, speech or braille feedback, voiceover, talkback
    • Physical keyboard: i.a. tremors
    • Switch devices: movement limiting disabilites
    • Voice recognition
    • Zooming / screen magnifying: low vision
  • 4 easy to start with tips
    • Use visible focus styles
    • Annotate headings (accessibilityRole)
    • Add name, role and state for custom elements
    • Respect reduced motion
  • Key takeaways
    • Users use mobile devices differently
    • Test with different input methods
    • Educate yourself
4 easy to start with tips

Performance issues - the usual suspects

Alexande Moureaux hands-on presentation of how to use Flipper React DevTools profiler and Flamegraph and Android performance profiler to debug Android application performance issues. E.g. why the app has below 60 fps? Concentrates on debugging Android but similar approaches work also for iOS.

First make your measures deterministic, average your measures over several iterations, keep the same conditions for every measure and automate the behavior you want to test.

The presentation showed how to use Flipper React DevTools profiler Flamegraph to find which component is slow and causes e.g. rerendering. Then refactoring the view by moving and the updating component which caused rerendering. And using @perf-profiler/web-reporter to visualize the measures from JSON files.

You can record the trace Hermes profiler and open it on Google Chrome:

  • bundleInDebug: true
  • Start profiler in simulator, trace is saved in device
  • npx react-native profile-hermes pulls the trace and converts it for usable format
  • Findings: filtering tweets list by parsing weekday from tweet was slow (high complexity), replace it with Date.getDay from tweet.createdAt

Connect the app to Android Studio for profiling: finding long running animation (background skeleton content "grey boxes")

How to actually improve the performance of a RN App?

Michal Chudziak presented how to use Define, Measure, Analyze, Improve, Control (DMAIC) pattern to improve performance of a React Native application.

  • Define where we want to go, listen to customers
  • Make it measurable
  • Analyze common user paths
  • Choose priorities

Define your goals:

Define

Measure: where are we?

  • React Profiler API
  • react-native-performance
  • Native IDEs
  • Screen capture
  • Flipper & perf monitor
Measure

Measureme

  • Measurement needs to be accurate and precise

Analyze phase: how do we get there?

  • List potential root causes
  • Cause and effect diagram (fish bone diagram)
  • Narrow the list
Analyze

Improve phase

  • Identify potential solutions
  • Select the best solution
  • Implement and test the solution
Improve

Control phase: are we on track?

Performance will degrade if it's not under control so create a control plan.

Control

Monitor regressions

  • Reassure: performance regression testing (local + CI)
  • Firebase: performance monitoring on production
  • Sentry:  performance monitoring on production

Starting with React Native and Expo

For some time I've wanted to experiment with React Native and mobile development outside native iOS but there has always been something on the way to get really started with it. Recently I had time to watch React Europe 2020 conference talks and "On Expo and React Native Web" by Evan Bacon got me inspired.

All the talks in React Europe 2020 can be found in their playlist on Youtube

Universal React with Expo

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

Expo

"Expo: Universal React" talk showed what Expo can do and after some hassling around with Expo init templates I got a React Native app running on iOS for reading news articles from REST API with theme-support and some navigation written with TypeScript. And it also worked on Android, Web and as a PWA.

Expo is a toolchain built around React Native to help you quickly start an app. It provides a set of tools that simplify the development and testing of React Native app and arms you with the components of users interface and services that are usually available in third-party native React Native components. With Expo you can find all of them in Expo SDK.

Understanding Expo for React Native

You can use the Expo Snack online editor to run you code in iOS, Android and Web platforms. And if you need vector icons, there's and app for searching them. Also Build icon app is crafty.

Expo has good documentation to get you started and following the documentation (and choosing the managed workflow when initing an app) you get things running on your device or on iOS and Android simulators. If you don't have a macOS or iPhone you can use their Snack playground to see how it looks on iOS.

Expo comes with a client which you can use to send the app to your device or to others for review which is very useful when testing as you can see all changes in code in Expo client without creating apk or ipa files.

One great feature of Expo is that you can quickly test and show examples of solutions with the Snack editor and run the code either on the integrated simulator or on your device.

I've previously quickly used Ignite which has a different approach to get you running and compared to that Expo is more of a platform than just tools which has it's good and not so good points. One of the main points of Expo is that it practically binds you to Expo and their platform where as if you use only tools you're "more free".

Drawbacks

The "Understanding Expo for React Native" post lists the following drawbacks in managed workflow. Some of those can be work aroud with bare workflow or with ejecting but then you lose the advantages of Expo workflow:

  1. Can't add native modules written in Objective-C, Swift, Java, Kotlin
  2. Can't use packages with native languages that require linking
  3. App has a big size as it is built with all Expo SDK solutions
  4. Often everything works well in Expo client but problems may occur in a standalone app.

Feelings

This far I've just started with Expo and getting more adjusted to React Native and writing Highlakka news client has been insightful and good experience when comparing the same app, "Highkara", written in Swift for iOS. My plan is to implement some of the features in Highlakka as in Highkara and see how it works as an universal app. Especially the PWA is interesting option and Over the Air updates with Expo as shown in React Europe 2020 talk.

The Hailakka app is now "usable" and iOS app builds nicely with Expo Turtle. The PWA runs on Netlify which is great.

Tools

What about tools to help you with React Native development? Basically you can just use VS Code and go on with it.

Flipper DevTool Platform for React Native talk at React Europe 2020 by Michel Weststrate and "Flipper — A React Native revolution" post shows one option. It's baked into React Native v0.62 but isn't yet available with Expo 41 (there's feature request for it and suggestions to use what Expo offers like React Native debugger and Redux devTools integration).

Build tools

Expo provides tools for building your application and Expo Dashboard shows your builds and their details. You can download the IPA packaged app when the build is ready and then upload it to App Store Connect.

You can build your application to different platforms with Expo cli. For example PWA with expo build:web and iOS with expo build:ios. And you can also do it from CI. Of course you still need Apple account for submitting the app to the App Store.

While your build is running you can check the queue from Turtle.