In the initial segment of this blog series, we explored the concepts of application and module sandboxing, identified scenarios that necessitate a module sandboxing solution, and highlighted its advantages.
Moving on to the second installment of the Module Sandboxing blog series, we will delve further into a comprehensive strategy for implementing module sandboxing. Additionally, we will briefly overview some of the implementation details.
As emphasized in the preceding article of this series, building a complete sandboxing solution requires a multi-faceted approach. A singular solution cannot fully address the complexities involved. To establish a comprehensive end-to-end sandboxing solution, it is imperative to tackle the problem from various perspectives.
The solutions have been organised into three primary categories below:
- Development phase isolation
When dealing with highly regulated entities, it is crucial to implement stringent access controls on the codebase of the sandbox module. This ensures a complete separation from the host app’s codebase, employing distinct access control protocols. Additionally, it is advisable to enforce a clear separation of concerns between the host app and the sandbox module throughout the active development phase.
The following components outline the means through which isolation is achieved during the development phase:
To ensure robust access control mechanisms within both the host app and the sandbox module, a viable approach is to house the sandbox module codebase in a distinct Git Repository. This repository should have considerably more stringent access control privileges compared to the repository of the host app codebase.
In addition to reinforcing stringent access controls, maintaining a separate repository presents the opportunity to establish finer control over the Continuous Integration/Continuous Deployment (CI/CD) pipeline for the sandbox module. This involves implementing more meticulous hygiene guardrails for the sandbox module, such as:
- Rigorous code guardrails, including lint checks and test coverage, for the sandbox codebase in comparison to the host app.
- Enforcing security protocols to prevent the inclusion of hardcoded cryptographic secrets in the codebase.
Integration of Sandbox Module into Host App
An essential aspect of creating the sandbox module involves determining how it will be incorporated into the host app. There are various possible approaches, and some of them are outlined below:
- Application Module: In this approach, the sandbox module’s codebase is introduced as a new application module within the existing codebase of the host app. This can be achieved using “Android app module” (in Android) or “Development Cocoapod” (in iOS). While this approach offers a degree of separation of concerns between the sandbox module and the host app, it poses challenges in maintaining exclusive access control over the sandbox module’s codebase. Effectively, the codebase is shared between the host app and the sandbox module using this method.
- Git Submodule: Another approach involves incorporating the sandbox module’s codebase into the host app as a new Git submodule. This method offers improved separation of concerns compared to the previous approach and grants more granular control over access control privileges for the sandbox module’s codebase. However, it is important to note that this approach does not entirely prevent overlap between the host app and the sandbox module during active development. For developers working on the host app codebase, even if they lack edit access to the sandbox module codebase, the sandbox module’s codebase remains visible and editable as part of the host app itself.
- Compiled Binary SDK: Integrating the sandbox module into the host app involves using compiled AAR (Android) or Framework (iOS) files, which are hosted in a dedicated repository. The host app can establish a dependency on the sandbox module, akin to integrating a third-party SDK hosted in a different repository. When combined with the earlier strategy of employing a distinct Git repository for the sandbox codebase, this approach achieves the utmost separation of concerns, meticulous access control, and exclusive development isolation. Furthermore, it paves the way for implementing advanced security protocols, such as separate obfuscation for the sandbox SDK compared to the host app.
As evident from the above-mentioned approaches, integrating the sandbox module as a Compiled Binary SDK into the Host app offers the highest degree of separation between the host app and the sandbox module.
The use of a Compiled Binary SDK introduces an additional challenge for Android, particularly in the realm of obfuscation. Specific details regarding obfuscation are explained below:
Code obfuscation is a process that transforms the source code into a more intricate and less readable form, significantly raising the difficulty level for reverse engineering or tampering with the app. This robust practice serves to protect sensitive data, preserve intellectual property, and fortify overall app security. Obfuscation tools achieve this by renaming variables, methods, and classes, thereby obscuring the app’s underlying logic, and by removing debugging information. This deterrent not only thwarts reverse engineering attempts but also acts as a safeguard against malicious activities, ensuring the confidentiality and integrity of apps. Various tools, such as Dexguard and Proguard, are available for obfuscation.
In our specific use case, we opted for Proguard/R8-based obfuscation, a choice that will be elucidated in the following section.
Proguard/R8 Based Obfuscation
In the realm of Android App Development, R8, an integral part of the Android Gradle Plugin, stands out as a powerful code shrinker and obfuscator. R8’s obfuscation process involves renaming classes, methods, and variables, alongside eliminating unused code and resources, thereby fortifying security and optimizing the app’s size.
A pivotal aspect of R8-based Proguarding (obfuscation) involves the generation of a Mapping File. This file acts as a translation key, connecting the obfuscated code to its original human-readable form. It plays a crucial role in managing the obfuscated code by facilitating the interpretation of stack traces, thereby easing the debugging process for obfuscated production code.
As emphasized in the preceding section, the mapping file, while indispensable, is a sensitive component that must be shielded from leaks to prevent the reverse engineering of the app. Without manual intervention, the Android Gradle Plugin automatically obfuscates both the host app codebase and any included SDK’s codebase using the same rules specified in the proguard-rules.pro file. Consequently, a common mapping file is generated for both the host app and SDK. In our scenario, this poses a potential security risk, as the leakage of the host app’s mapping file could expose the sandbox module’s codebase to reverse engineering vulnerabilities.
For additional reference, you can explore further details at this link:
Separate Obfuscation for Host App and Sandbox Module SDK
To address the aforementioned security concern, an SDK (Android Library) can mitigate potential risks by specifying custom Proguard rules in a dedicated file, termed the consumer-rules.pro file, housed within the SDK. By employing a distinct set of obfuscation rules in this file, we guarantee the generation of a separate Mapping File exclusively for the SDK. This allows for the implementation of more rigorous security measures, specifically designed to prevent inadvertent leaks.
2. Backend Application Isolation
Given that the sandbox module typically doesn’t operate in isolation and often communicates with a corresponding backend application, it becomes crucial to incorporate isolation as a fundamental construct when designing the associated backend application. This ensures the creation of a genuinely sandboxed environment.
3. Data Isolation from Host App
Achieving isolation for the sandbox module within a host app demands careful consideration to ensure compliance with data protection and security standards. Two critical types of data requiring isolation are “Persisted Data” and “In-memory data.”
For Persisted Data, stored in databases and UserDefaults (iOS)/SharedPreferences (Android), strict data isolation mandates the maintenance of separate instances of these data storage units between the sandbox module and the host app. Moreover, if data at rest requires encryption within these instances, it is crucial to manage the cryptographic keys used for encryption separately.
In addressing In-memory data isolation, we adopt a dual approach. Firstly, a unidirectional communication flow is established from the host app to the sandbox module through a dedicated interface layer exposed from the compiled SDK. Secondly, for shared dependencies, such as Networking Library or Analytics Library, between the sandbox module and the host app, we implement the concept of tenantization.
In the upcoming part of our series, we will delve into how we applied the concept of tenantization to SaaSify shared dependencies. This strategy played a pivotal role in maintaining the required isolation and security for module sandboxing. Stay tuned for more details!