ReactNativeでFirebase JS SDKのユーザー情報が保存される場所を確認

ニッチな記事なので、メモ的な形式ですみません。

# firebase-js-sdkの実装を確認

この辺りで環境ごとに使用するストレージの種類を定義している
https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/auth/src/storage/factory.js#L79
https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/auth/src/authstorage.js#L181
https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/auth/src/utils.js#L679
環境を特定している処理を遡るとどこまでいくのかわからないけど、ともかくReactNative環境ではAsyncStorageを使用している。

fireauth.storage.AsyncStorage
https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/auth/src/storage/asyncstorage.js

ユーザーの認証情報はこの辺りでJSON化してストレージに保存している。
https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/auth/src/storageusermanager.js#L374

# AsyncStorageの実装を確認

## Android

AsyncStorageModule.java
https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java
ReactDatabaseSupplier.java
https://github.com/facebook/react-native/blob/aee88b6843cea63d6aa0b5879ad6ef9da4701846/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java
SQLiteを使用している。

DB名は`RKStorage`に、テーブル名は`catalystLocalStorage`なっているはず。
https://github.com/facebook/react-native/blob/aee88b6843cea63d6aa0b5879ad6ef9da4701846/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java#L25

## iOS

RCTAsyncLocalStorage.mm
https://github.com/facebook/react-native/blob/652fa1b8d485ef86bc332116b9790ef61b905a0a/React/CoreModules/RCTAsyncLocalStorage.mm
https://github.com/facebook/react-native/blob/652fa1b8d485ef86bc332116b9790ef61b905a0a/React/CoreModules/RCTAsyncLocalStorage.mm#L81
writeToFileでAppのDocumentsディレクトリに保存しているのがわかる。

https://github.com/facebook/react-native/blob/652fa1b8d485ef86bc332116b9790ef61b905a0a/React/CoreModules/RCTAsyncLocalStorage.mm#L22
`/Documents/RCTAsyncLocalStorage_V1`ディレクトリが用意される。

# 実機から内容を読み取ってみる

このような感じで、Firebase Authenticationでメールアドレス・パスワードによるログインをするだけの画面を用意し、確認してみました。
`firebase.initializeApp`は割愛します。

```jsx:App.js
import React, { Component } from 'react';
import { StyleSheet, Text, View, AsyncStorage } from 'react-native';
import firebase from './js/firebase';

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      currentUser: null,
      storageData: null
    };
    firebase.auth().onAuthStateChanged(async (currentUser) => {
      console.log(currentUser);
      this.setState({ currentUser });
      if (currentUser && !this.state.storageData) {
        const keys = await AsyncStorage.getAllKeys();
        const values = await Promise.all(keys.map((key) => (AsyncStorage.getItem(key))));
        this.setState({
          storageData: JSON.stringify({ values, keys })
        });
      }
    });
    firebase.auth().signInWithEmailAndPassword('xxx@xxx.com', 'password');
  }

  render() {
    const { storageData } = this.state;
    return (
      <View style={styles.container}>
        <Text>{storageData}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});
```

## Android

端末をPCと繋いで、`adb`を使ってアプリのデータを確認します。

```console
$ adb shell
[ユーザ名]:/ $
```

アプリのデータは`/data/data/[パッケージ名]`ディレクトリに入っています。

```console
[ユーザ名]:/ $ cd data/data
[ユーザ名]:/data/data $ cd com.expofirebaseauthstoragetest
/system/bin/sh: cd: /data/data/com.expofirebaseauthstoragetest: Permission denied
[ユーザ名]:/data/data $
```

このようにそのままだと`Permission denied`になってしまうので、アプリの権限でアクセスする必要があります。
参考：[[Android] アプリの内部メモリを覗くとパーミッションでブロックされる](https://akira-watson.com/android/adb_run-as.html)

```console
[ユーザ名]:/data/data $ run-as com.expofirebaseauthstoragetest
[ユーザ名]:/data/data/com.expofirebaseauthstoragetest $
```

ただし、リリースビルドのアプリではrun-asコマンドを使ってもアクセスできませんでした。
デバッグビルドかつ`android:debuggable="true"`の場合のみ内部ファイルにアクセスできるようです。
そのため、Expoを使わず素のReactNativeアプリで確認しています。

```console
[ユーザ名]:/data/data/com.expofirebaseauthstoragetest $ cd databases/
[ユーザ名]:/data/data/com.expofirebaseauthstoragetest/databases $ ls
RKStorage
```

`RKStorage`というデータベースが確認できました。とりあえずこれをコピーして、PCで中身を表示してみます。

```console
[ユーザ名]:/data/data/com.expofirebaseauthstoragetest/databases $ cp RKStorage /mnt/sdcard
[ユーザ名]:/data/data/com.expofirebaseauthstoragetest/databases $ exit
$ adb pull /mnt/sdcard/RKStorage
```

SDカードのディレクトリは機種によって違うかもしれません。
`adb pull`によりRKStorageをPCに移動できたら、DBの中身をDB Browser for SQLite等のソフトで確認します。
![DB_Browser_for_SQLite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/deeed400-5188-3637-3754-ad1d1f42c2dd.png)
`catalystLocalStorage`テーブルがあり、`firebase:authUser:<API Key>:[DEFAULT]`キーにJSON形式でユーザー情報が入っているのがわかります。
ちなみに、この形式はWebでIndexedDBに入っているときの形と同様です。

## iOS

端末とMacを繋げ、Xcodeでアプリを起動してFirebaseへのログインが完了したら、
[iExplorer](https://macroplant.com/iexplorer)を使ってアプリ内のファイルを確認します。
![iExplorer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/9886031a-de82-c203-8014-dafe829efc3e.png)

開発時なので見れていますが、リリースビルドのアプリ内のディレクトリは`info.plist`の`UIFileSharingEnabled`（iTunesでファイルを共有できる設定）が`YES`でない限り表示できないようです。例えばGmailなどはこんな感じ↓
![iExplorer2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/2764bdf7-ad73-99d6-9368-6e2faf6fcd18.png)

先ほどのアプリのディレクトリを見てみると、
![iExplorer3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/1aa56b70-a0bb-6e63-89f4-bf8a3601ed16.png)
`RCTAsyncLocalStorage_V1`ディレクトリに`manifest.json`と、ランダム文字列のように見える名前のファイルがあります。

`manifest.json`の方はこんな感じ。

```json:manifest.json
{"firebase:authUser:<API Key>:[DEFAULT]":null}
```

key/value形式になっていますが、ランダム文字列のような名前のファイルの方に実際のデータが入っていました。
このファイル名は上記のkeyをMD5でハッシュ化したもののようです。

```text:a4f088f2e38522fd210d0ae5995f904c
{"uid":"xxxxxxxxxxx","displayName":null,"photoURL":null,"email":"xxx@xxx.com","emailVerified":false,"phoneNumber":null,"isAnonymous":false,"tenantId":null,"providerData":...
```

この最大文字数（１０２４字）を超えるものが別ファイルになり、それ以下の場合は`manifest.json`にそのまま値を入れるという実装になっています。
https://github.com/facebook/react-native/blob/652fa1b8d485ef86bc332116b9790ef61b905a0a/React/CoreModules/RCTAsyncLocalStorage.mm#L24

# まとめ

### Android

* ストレージの実装：SQLiteを使用  
 * DB名：`RKStorage`  
 * テーブル名：`catalystLocalStorage`
* アクセス制限：デバッグビルドかつ`android:debuggable="true"`のときのみ

### iOS

* ストレージの実装：アプリのDocumentsディレクトリにファイルとして保存
 * `/Documents/RCTAsyncLocalStorage_V1/manifest.json`にキーがあり、値は別ファイル（キーをMD5でハッシュ化したファイル名）に分かれている
* アクセス制限：デバッグビルド、あるいは`UIFileSharingEnabled`:`YES`のときのみ