Skip to content

Commit

Permalink
Fixes #4: Support for installing split APKs
Browse files Browse the repository at this point in the history
APKs not supported on the device's architecture are filtered out.

This commit also adds a few additional checks to make installing apps
on Android more robust.
  • Loading branch information
baltpeter authored and zner0L committed Mar 29, 2023
1 parent 2e951be commit 8cab6c2
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Appstraction provides an abstraction layer for common instrumentation functions
With appstraction, you can perform the following actions programmatically on Android and iOS (for a full list with additional details, see the API reference for the [`PlatformApi` type](docs/README.md#platformapi)):

* Reset an emulator to a snapshot.
* Install and uninstall apps.
* Install and uninstall apps (including split APKs on Android).
* Check whether an app is installed.
* Set an app's permissions, either granting everything or granularly specifying which permissions to grant or deny.
* Configure an app's battery optimization settings.
Expand Down
22 changes: 11 additions & 11 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ An ID of a known permission on Android.

#### Defined in

[android.ts:730](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L730)
[android.ts:769](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L769)

___

Expand All @@ -58,7 +58,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the

#### Defined in

[index.ts:330](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L330)
[index.ts:331](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L331)

___

Expand All @@ -77,7 +77,7 @@ The options for each attribute available through the `getDeviceAttribute()` func

#### Defined in

[index.ts:336](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L336)
[index.ts:337](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L337)

___

Expand Down Expand Up @@ -118,7 +118,7 @@ Functions that are available for the platforms.
| `getForegroundAppId` | () => `Promise`<`string` \| `undefined`\> | Get the app ID of the running app that is currently in the foreground. Requires the `frida` capability on iOS. |
| `getPidForAppId` | (`appId`: `string`) => `Promise`<`number` \| `undefined`\> | Get the PID of the app with the given app ID if it is currently running. Requires the `frida` capability on iOS. |
| `getPrefs` | (`appId`: `string`) => `Promise`<`Record`<`string`, `unknown`\> \| `undefined`\> | Get the preferences (`SharedPreferences` on Android, `NSUserDefaults` on iOS) of the app with the given app ID. Requires the `frida` capability on Android and iOS. |
| `installApp` | (`appPath`: `string`) => `Promise`<`void`\> | Install the app at the given path. **`Todo`** How to handle split APKs on Android (#4)? |
| `installApp` | `Platform` extends ``"android"`` ? (`apkPath`: `string` \| `string`[]) => `Promise`<`void`\> : (`ipaPath`: `string`) => `Promise`<`void`\> | Install the app at the given path. **`Param`** Path to the app file (`.ipa` on iOS, `.apk` on Android) to install. On Android, this can also be an array of the paths of the split APKs of a single app. |
| `installCertificateAuthority` | (`path`: `string`) => `Promise`<`void`\> | Install the certificate authority with the given path as a trusted CA on the device. This allows you to intercept and modify traffic from apps on the device. On Android, this installs the CA as a system CA. As this is normally not possible on Android 10 and above, it overlays the `/system/etc/security/cacerts` directory with a tmpfs and installs the CA there. This means that the changes are not persistent across reboots. On iOS, the CA is installed permanently as a root certificate in the Certificate Trust Store. It persists across reboots.\ **Currently, you need to manually trust any CA at least once on the device, CAs can be added but not automatically marked as trusted (see: https://github.com/tweaselORG/appstraction/issues/44#issuecomment-1466151197).** Requires the `root` capability on Android, and the `ssh` capability on iOS. |
| `isAppInstalled` | (`appId`: `string`) => `Promise`<`boolean`\> | Check whether the app with the given app ID is installed. |
| `removeCertificateAuthority` | (`path`: `string`) => `Promise`<`void`\> | Remove the certificate authority with the given path from the trusted CAs on the device. On Android, this works for system CAs, including those pre-installed with the OS. As this is normally not possible on Android 10 and above, it overlays the `/system/etc/security/cacerts` directory with a tmpfs and removes the CA there. This means that the changes are not persistent across reboots. On iOS, this only works for CAs in the Certificate Trust Store. It does not work for pre-installed OS CAs. The changes are persistent across reboots. Requires the `root` capability on Android, and the `ssh` capability on iOS. |
Expand Down Expand Up @@ -153,7 +153,7 @@ The options for the `platformApi()` function.

#### Defined in

[index.ts:268](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L268)
[index.ts:269](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L269)

___

Expand All @@ -172,7 +172,7 @@ Connection details for a proxy.

#### Defined in

[index.ts:344](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L344)
[index.ts:345](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L345)

___

Expand Down Expand Up @@ -202,7 +202,7 @@ The options for a specific platform/run target combination.

#### Defined in

[index.ts:295](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L295)
[index.ts:296](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L296)

___

Expand All @@ -220,7 +220,7 @@ A capability for the `platformApi()` function.

#### Defined in

[index.ts:323](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L323)
[index.ts:324](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L324)

___

Expand Down Expand Up @@ -262,7 +262,7 @@ Configuration string for WireGuard.

#### Defined in

[index.ts:351](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L351)
[index.ts:352](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L352)

## Variables

Expand All @@ -274,7 +274,7 @@ The IDs of known permissions on Android.

#### Defined in

[android.ts:599](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L599)
[android.ts:638](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L638)

___

Expand Down Expand Up @@ -373,4 +373,4 @@ The API object for the given platform and run target.

#### Defined in

[index.ts:360](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L360)
[index.ts:361](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L361)
43 changes: 41 additions & 2 deletions src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
WireGuardConfig,
} from '.';
import { dependencies } from '../package.json';
import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './util';
import { asyncUnimplemented, getObjFromFridaScript, isRecord, parseAppMeta, retryCondition } from './util';

const fridaScripts = {
getPrefs: `var app_ctx = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
Expand Down Expand Up @@ -321,7 +321,46 @@ export const androidApi = <RunTarget extends SupportedRunTarget<'android'>>(
return stdout.includes(`package:${appId}`);
},
installApp: async (apkPath) => {
await execa('adb', ['install-multiple', apkPath], { shell: true });
const apks = typeof apkPath === 'string' ? [apkPath] : apkPath;

const apkMeta = await Promise.all(
apks.map((path) =>
parseAppMeta(path).then((m) => {
if (!m) throw new Error(`Failed to install app: "${path}" is not a valid APK.`);
return { path, ...m };
})
)
);

const appIds = new Set(apkMeta.map((m) => m.id));
if (appIds.size > 1) throw new Error('Failed to install app: Split APKs for different apps provided.');

const androidArches = await execa('adb', ['shell', 'getprop', 'ro.product.cpu.abilist']).then((r) =>
r.stdout.split(',')
);
const androidArchMap = {
'armeabi-v7a': 'arm',
armeabi: 'arm',
'arm64-v8a': 'arm64',
x86: 'x86',
// eslint-disable-next-line camelcase
x86_64: 'x86_64',
mips: 'mips',
mips64: 'mips64',
} as const;
const arches = androidArches.map((a) => androidArchMap[a as keyof typeof androidArchMap]);

const apksForArches = apkMeta
.filter(
(m) =>
!m.architectures || m.architectures.length === 0 || m.architectures.some((a) => arches.includes(a))
)
.map((m) => m.path);

if (apksForArches.length === 0)
throw new Error(`Failed to install app: App doesn't support device's architectures (${androidArches}).`);

await execa('adb', ['install-multiple', ...apksForArches]);
},
uninstallApp: async (appId) => {
await execa('adb', ['shell', 'pm', 'uninstall', '--user', '0', appId]).catch((err) => {
Expand Down
9 changes: 5 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ export type PlatformApi<
/**
* Install the app at the given path.
*
* @param appPath Path to the app file (`.ipa` on iOS, `.apk` on Android) to install. Currently, this can also be a
* glob for split APKs on Android, but this may change in the future.
* @todo How to handle split APKs on Android (#4)?
* @param appPath Path to the app file (`.ipa` on iOS, `.apk` on Android) to install. On Android, this can also be
* an array of the paths of the split APKs of a single app.
*/
installApp: (appPath: string) => Promise<void>;
installApp: Platform extends 'android'
? (apkPath: string | string[]) => Promise<void>
: (ipaPath: string) => Promise<void>;
/**
* Uninstall the app with the given app ID. Will not fail if the app is not installed.
*
Expand Down

0 comments on commit 8cab6c2

Please sign in to comment.