👀

第三者がリリースしたReact Native Expo製アプリの設定ファイルを読み取る方法

に公開

はじめに

自分のAndroid端末にインストールしてある第三者が開発&リリースしたReact Native Expo製のアプリの設定ファイルの中身を読み取る方法を書きます。

ここでいう設定ファイルの中身というのは、app.json / app.config.jsexpoの部分のことです。

https://docs.expo.dev/versions/latest/config/app/

なぜこの記事を書くのか

本記事では、React Native Expo製のAndroidアプリの設定ファイルが外部から参照可能であることを示し、その中の秘匿情報が漏洩する可能性があることを示すためです。

サンプルデモ

今回試しに作ったアプリのデモ動画を載せます。

https://youtube.com/shorts/3x3mtLxYVUY?feature=share

動画の通り他人が作ったReact Native Expo製アプリの設定ファイルの中身が参照できることがわかると思います。

※デモ動画では秘匿情報を含まないアプリを動画にしています。

やり方

事前にReact Native Expoの空プロジェクトを作成しておきます。

1. Expo Modules API を使ってNative Moduleを作る

このドキュメントを参考にしてNative Moduleを作ります。今回はExpoConfigExtractorModuleとしました。

ExpoConfigExtractorModule.ts
import { requireNativeModule } from "expo";

export default requireNativeModule<ExpoConfigExtractorModule>('ExpoConfigExtractor');

export type ExpoConfigExtractorModule = {
  getExpoConfig: () => Promise<string[]>;
};

2. QUERY_ALL_PACKAGES Permission を付与する

https://developer.android.com/training/package-visibility/declaring#all-apps

これはデバイス上にインストールされているアプリと連携できる権限になります。

今回はCNGを使って、AndroidManifest.xmlにこの権限を追加することにします。作った初期プロジェクトのapp.jsonandroidpermissionsにこの権限を追加します。

app.json
{
  // ... 
  "android": {
    "permissions": [
      "android.permission.QUERY_ALL_PACKAGES"
    ]
  }
  // ... 
}

3. ExpoConfigExtractorModule.kt の実装

作ったNative ModuleのModuleクラスを継承したクラスに設定ファイルを読み取るコードを実装していきます。(最低限の実装なのでもっといい書き方があるかもしれません🫠)

ExpoConfigExtractorModule.kt
class ExpoConfigExtractorModule : Module() {

  private val context
    get() = requireNotNull(appContext.reactContext)

  override fun definition() = ModuleDefinition {

    Name("ExpoConfigExtractor")

    AsyncFunction("getExpoConfig") Coroutine { ->
      val configs = mutableListOf<String>()

      @SuppressLint("QueryPermissionsNeeded")
      val apps = context.packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)

      apps?.forEach { pkg ->
        val apkPath = pkg.applicationInfo?.sourceDir ?: return@forEach
        val result = extractAppConfig(apkPath)
        configs.add(result)
      }

      val filteredConfigs = configs.filter { it != "N/A" }

      return@Coroutine filteredConfigs
    }
  }

  private suspend fun extractAppConfig(apkPath: String): String = withContext(Dispatchers.IO) {
    runCatching {
      ZipFile(apkPath).use { zip ->
        val entry = zip.entries().asSequence().find { it.name == "assets/app.config" } ?: return@withContext "N/A"

        val raw = zip.getInputStream(entry).bufferedReader(Charsets.UTF_8).use { it.readText() }

        JSONObject(raw).toString(2)
      }
    }.getOrElse { "N/A" }
  }
}

あとはこのgetExpoConfig関数を使って、UIをいい感じに作ればサンプルデモみたいなものが作れると思います。

設定ファイルの中身を読み取る実装は以上です。

🚨 expo-constantsを使った値の読み取りの注意点

このドキュメントには以下のような案内があります。

EXPO_PUBLIC_ 変数には秘密鍵などの機密情報を保存しないでください。これらの変数は、コンパイル後のアプリケーション内で平文として表示されます。


公式ドキュメントの該当箇所

また、expo-constantsと設定ファイルのextraプロパティを利用すると以下のようにEXPO_PUBLIC_prefixのついていない環境変数を設定ファイル外で読み込むことができます。

.env.local
API_KEY=hoge_api_key
app.config.ts
import { ExpoConfig } from "expo/config";

module.exports = (): ExpoConfig => {
  return {
    // ... 
    extra: {
      API_KEY: process.env.API_KEY,
    },
  };
};
import Constants from "expo-constants";

const apiKey = Constants.expoConfig?.extra?.API_KEY

// 下だと読み取れず、undefined となる。
// const apiKey = process.env.API_KEY

この方法だとサンプルデモで示したように設定ファイルの中身が見れてしまうので、本当に見られてはいけない情報を取り扱う時は注意が必要です。


誰か曰く、私物Android端末にインストール済みの第三者が作ったアプリのExpoConfigを読み取ったら、見えて良いのかダメなのか分からない情報が見れたらしい。


ちなみにQUERY_ALL_PACKAGES権限を持つアプリのリリースはGoogleから制限されていますが、もう既に世の中にリリースされてるアプリの中でインストール済みのReact Native Expo製アプリの設定ファイルを読み込んでいるアプリがありました。(解釈によっては、リバースエンジニアリングで不正アクセス禁止法などに違反するかもしれません。)

🍬 おまけ

RevenueCatというサービスがありますが、これをReact Native Expoを使ったアプリに導入する際以下のように書くと思います。

SDK初期化コード

import { Platform, useEffect } from 'react-native';
import { useEffect } from 'react';
import Purchases from 'react-native-purchases';

export default function App() {

  useEffect(() => {
    if (Platform.OS === 'ios') {
       Purchases.configure({apiKey: <revenuecat_project_apple_api_key>});
    } else if (Platform.OS === 'android') {
       Purchases.configure({apiKey: <revenuecat_project_google_api_key>});
    }
  }, []);
}

この時使うAPI Keyは公式ドキュメントを読むと公開キーなので第三者に見られたとしても問題ないように思えました。[公式doc]

Public API keys (also known as SDK API keys in the dashboard)


改善点など気づいたことがあればフィードバックいただけると嬉しいです。

Discussion