この記事は 2024 年 2 月時点の記録です(2026 年 7 月 追記)
バックアップを暗号化してファイルに書き出し、共有シートから取り出すという流れは今も有効です。ただし暗号化まわりについて、当時のコードをそのまま使わないでいただきたい点があります。先にそこからお伝えします。
1. 鍵をソースコードに書かないでください(最重要)
本文では説明を簡単にするため、鍵を次のように直接書いています。
CryptoJS.AES.encrypt(JSON.stringify(backupLists), 'secret key 123')この書き方には根本的な問題があります。アプリに埋め込まれた文字列は、配布したバイナリから取り出せます。鍵が誰にでも分かる状態では、暗号化しても中身を守れません。バックアップファイルには利用者のデータが入っているので、そのまま出荷するのは避けてください。
実装するときの考え方は次のいずれかになります。
- 利用者にパスワードを入力してもらう — 復元時に同じパスワードを求める。鍵をアプリに持たせずに済みます
- 端末の安全な保管領域に鍵を置く — iOS のキーチェーン、Android の Keystore に相当する仕組みを使います。ただし機種変更で持ち越せないため、引き継ぎ用途とは相性を確認してください
「機種変更のためのバックアップ」という目的を考えると、前者が素直です。
2. crypto-js は現在メンテナンスされていません
GitHub のセキュリティアドバイザリには、crypto-js について「パッチはない。このパッケージはメンテナンスされていない」と明記されています。
実際に、2023 年 10 月に公開された CVE-2023-46233 では、鍵導出関数 PBKDF2 の既定値が SHA1・繰り返し 1 回という状態であることが指摘されました。1993 年当時の仕様と比べても 1,000 倍弱く、現在の水準からは 130 万倍以上弱いとされています。4.2.0 で既定値が修正されています。
本文の
npm install crypto-jsはバージョンを指定していません。使い続ける場合は最低でも 4.2.0 以上を指定してください。新規に実装するなら、メンテナンスが続いているライブラリを選ぶことをおすすめします。3. expo-file-system の API が再編されました
現在の Expo では、
expo-file-systemの新しい API と、従来の書き方に対応する FileSystem (legacy) が別々に提供されています。本文のFileSystem.documentDirectoryやwriteAsStringAsyncがどちらに属するかは、お使いの SDK バージョンの公式ドキュメントでご確認ください。4. Share と Sharing の使い分けについて
本文で iOS と Android を分岐しているのは、React Native の
Share.share()のurlオプションが iOS 向けであるためです。ファイルを共有する目的なら、両プラットフォームでexpo-sharingに寄せるほうが分岐が減ります。当時の実装をそのまま残していますが、書き直す際の選択肢として書き添えておきます。※ 2 は 2026 年 7 月時点の GitHub Security Advisory の記載に基づきます。1・3・4 は当方の環境で改めて検証したものではありません。実装時は各ライブラリの公式ドキュメントをご確認ください。
「暗号化+バックアップ版」Apollo Client の Reactive variables データと SQLite のデータを暗号化し バックアップ ➡︎ リストア する方法をご紹介しています。
開発環境(Expo Bare)
- “typescript”: “^5.2.2”
- “expo”: “~49.0.13”,
- “react”: “18.2.0”,
- “react-native”: “0.72.6”,
- “expo-router”: “^2.0.0”,
- “react-native-paper”: “^5.10.6”,
- “@apollo/client“: “^3.8.5”,
- “expo-sqlite”: “~11.3.3”,
- “crypto-js“: “^4.2.0”,
- “expo-sharing“: “~11.5.0”,
- “expo-file-system”: “~15.4.5”,
※24/02/13 追記Expo SDK@50 にアップグレードして node_modules/expo-router/_ctx.ios.tsx: Expected fromDir to be of type string, got undefined エラーが発生した場合は使い方がほぼ同じである rn-crypto-js を代わりに使っても良いと思います。
セットアップする
@apollo/client (フロントエンド ONLY)
// _layout.tsx
export default function RootLayout() {
const client = useApolloNoPersistClient();
...
return (
<SafeAreaProvider>
<ApolloProvider client={client}>
<PaperProvider theme={paperTheme}>
<RootLayoutNav />
<StatusBar style={preferredTheme === 'dark' ? 'light' : 'dark'} />
</PaperProvider>
</ApolloProvider>
</SafeAreaProvider>
);
}
// useApolloNoPersistClient.tsx
import {useState, useEffect} from 'react';
import {ApolloClient, InMemoryCache, NormalizedCacheObject} from '@apollo/client';
export const useApolloNoPersistClient = () => {
const [client, setClient] = useState<ApolloClient<NormalizedCacheObject>>(
{} as ApolloClient<NormalizedCacheObject>,
);
useEffect(() => {
async function init() {
setClient(new ApolloClient({cache: new InMemoryCache(), connectToDevTools: true}));
}
init().catch(err => {
console.log(err);
});
}, []);
return client;
};
expo-sqlite
詳細は下記をご参考ください。
crypto-js (暗号化ライブラリ)
inlineRequires: true 設定を追加する
// metro.config.js
...
module.exports = {
...config,
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
react-native-get-random-values を追加する
yarn add react-native-get-random-values
crypto-jsをインポートする前に react-native-get-random-valuesをインポートする
// _layout.tsx
import 'react-native-get-random-values'; // <-- 追加する (Line1)
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {LogBox, AppState, Platform} from 'react-native';
。。。
crypto-jsを追加する
npm install crypto-js
プロジェクトをリビルドする
# For iOS npx pod-install npx react-native run-ios # For Android npx react-native run-android
expo-sharing / expo-file-system
割愛します。(expo-sharing、expo-file-system)
バックアップする
- Step1データを抽出する
// Reactive Variables const backupRVLists = [ {rvCommon: {userId:'123456', preferredTheme: 'light', point: 0, ...} ]; // SQLite const backupDBLists = [{items: items}]; const backupLists = [...backupRVLists, ...backupDBLists];※ items 抽出方法(例)
- Step2暗号化する
const ciphertextOfBackuped = CryptoJS.AES.encrypt( JSON.stringify(backupLists), 'secret key 123', ).toString();
- Step3プラットフォーム別バックアップする
自身の
iCound・Dropbox・Google Driveなど外部の媒体に保存します。if (Platform.OS === 'ios') { jcommonMapper.backupIOS(ciphertextOfBackuped); } else if (Platform.OS === 'android') { jcommonMapper.backupAndroid(ciphertextOfBackuped); } - Step3-1jcommonMapper.backupAndroid()
import {Share} from 'react-native'; import * as FileSystem from 'expo-file-system'; import * as Sharing from 'expo-sharing'; export const jcommonMapper = { ... backupAndroid: async (jsonData: string) => { const fileUrl = FileSystem.documentDirectory + `kotango.xxxxxxxx.json`; FileSystem.writeAsStringAsync(fileUrl, jsonData).then(async () => { if (await Sharing.isAvailableAsync()) { try { Sharing.shareAsync(fileUrl, { UTI: '.json', dialogTitle: 'Kotango | データバックアップ', mimeType: 'application/json', }); } catch (err) { console.log('!@# backup/err:', err); } } }); }, ... } - Step3-2jcommonMapper.backupIOS()
export const jcommonMapper = { ... backupIos: async (jsonData: string) => { const fileUrl = FileSystem.documentDirectory + `kotango.xxxxxxxx.json`; FileSystem.writeAsStringAsync(fileUrl, jsonData).then(async () => { try { const result = await Share.share({url: fileUrl}); if (result.action === Share.sharedAction) { console.log('成功しました!'); } else if (result.action === Share.dismissedAction) { // iOS only console.log('キャンセルされました!!'); } } catch (error: any) { console.log('!@# error:', error); } }); }, ... } - Step4バックアップファイル(最終版)を確認する
復元時に暗号化キーを使って復号化しますので、大事に保管してください。

暗号化済みのバックアップファイル
動作デモ
復元する
リリースする
※上記の詳細は以下のアプリに実装されています!
この機能を実装したアプリ
このデータ引き継ぎ機能は、単語学習アプリ「Kotango(ハングル単語)」に実装しています。実際の動作は、下記からご確認いただけます。



コメント