当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

COVID-19接触追跡アプリWiqaytnaのモバイルアプリケーションセキュリティレビュー

COVID-19接触追跡アプリケーションWiqaytnaのモバイルセキュリティテスト

概要

この連載記事は、RandoriSecと共同で行った接触追跡アプリケーションのレビューです。第1回は、iOS版とAndroid版の両方で提供されているモロッコのモバイルアプリケーションWiqaytnaのレビューです。このレビューでは、アプリケーションの内部の仕組みと接触追跡プロトコルを取り上げます。プロセスの透明性、セキュリティ、プライバシー、追跡機能、アクセシビリティの観点からアプリケーションをレビューします。

背景

新型コロナウイルス感染症(COVID-19)のパンデミックは、現代を象徴する世界的な健康危機であり、おそらくこの10年で最大の課題です。昨年末にアジアで出現して以来、ウイルスはほぼすべての大陸に広がりました。アフリカ、南北アメリカ、ヨーロッパでは、感染者数が日々増加しています。

このパンデミックは、残念ながら数十万人の命を奪い、多くの国を完全なロックダウンに追い込み、企業を休業や倒産にまで追いやりました。また、著者の一人を含む多くの人々が、自宅を離れた外国で足止めされています。

有効な治療法やワクチンがない中、各国は、患者の検査と治療、移動の制限、市民の隔離、スポーツイベント、コンサート、学校などの大規模な集まりの中止によって、ウイルスの拡散を遅らせようと競い合っています。

テクノロジーもまた、45億台の携帯電話を活用することでCOVID-19との戦いに使われています。携帯電話を使って人の移動を追跡し、その人が感染させた可能性のある人々を見つけ出すという考え方は大きな支持を集め、現在ではほとんどの国が接触追跡を実装したモバイルアプリケーションを公開しています。

では、接触追跡とは何でしょうか。簡単に言えば、自分が接触した可能性のあるすべての人を記録することです。ある人の感染が判明した場合、ほかの人々に検査や隔離を受けるよう通知が届きます。

ゴールドスタンダード

接触追跡アプリケーションをレビューする前に、まず、これらのアプリケーションが満たすべき期待事項、つまり基準について合意しておく必要があります。

当社の知る限り、そうした基準はどこにも定められていません。プライバシーについて語る組織もあれば、追跡プロトコルの効率性やそれが提供する保証について論じる組織もありますが、当社が頼れる仕様やガイドは存在しません。

そのため当社は、収集したさまざまな情報と、当社としての最善の常識に基づいて、期待事項を策定する必要がありました。

これらの要件は高レベルのものであり、意図的に実装に依存しないものにしています。

  • 透明性
    • 追跡プロトコルが文書化されている
    • アプリケーションのアーキテクチャと設計が文書化されている
    • アプリケーションに、プライバシーとセキュリティの問題に対処するプログラム(バグバウンティ)がある
    • レビューとコミュニティによる検証を容易にするため、アプリケーションがオープンソースである
  • プライバシー
    • アプリケーションが個人を特定できる情報(PII)を収集しない
    • アプリケーションがユーザーを追跡したり、位置情報を収集したりしない
    • アプリケーションが端末上で使用する権限は限られており、主にBluetoothによる検出とインターネットアクセスに制限されている
    • アプリケーションが、データの消去と共有についての管理権限をユーザーに提供している
  • セキュリティ
    • アプリケーションがアタックサーフェスを縮小しており、広告、決済、不要な機能がない
    • アプリケーションが、検証済みで最新のサードパーティの依存関係を使用している
    • アプリケーションが、安全な暗号化されたチャネルで通信する
    • アプリケーションが安全であり、いかなる関係者にも追跡情報を漏えいしない
  • 追跡の有効性
    • 追跡プロトコルによって個人を追跡できない
    • 接触情報の共有はオプトイン方式である
    • 追跡によって位置情報やPIIが共有・収集されない
    • 追跡によって感染者の行動をたどることができない
    • 追跡のチェックが中央集権的ではなく、ユーザーが管理できる
    • アプリケーションがバックグラウンドにある場合や画面がオフの場合でも、AndroidとiOSで追跡が機能する
  • アクセシビリティ
    • アプリケーションが、現地以外のユーザーも含め、複数の言語で利用できる
    • アプリケーションを非居住者(渡航禁止措置により国内で足止めされている人)も利用できる
    • 障がいのある人(色覚異常、視覚障がいなど)がアプリケーションを利用できる
    • 低価格帯の端末でアプリケーションを利用できる(アプリケーションのサイズ、バッテリー消費、低帯域幅など)

テスト手法

当社のテスト手法では、まず接触追跡アプリケーションの大規模なリストを作成しました。政府のアプリケーションを中心とし、ダウンロード数によってアプリの優先順位を付けました。

一部のアプリケーションはパスポート番号や現地の電話番号などの居住者情報を必要とするため、当社がテストできるアプリケーションに絞りました。

各アプリケーションは、まずOstorlab Security Scannerでセキュリティ上の脆弱性をスキャンし、その後AndroidとiOSの両方で手動でレビューしました。各アプリについて、5つのテーマ(透明性、プライバシー、セキュリティ、追跡の有効性、アクセシビリティ)をレビューしました。

アプリケーションの数が多いため、完全なレビューは現在も進行中です。

要約

全体的な検出結果は満足できるものではありませんでした。いくつかのアプリケーションは独自の追跡プロトコルを実装しており、Bluetoothと位置情報のデータを継続的に共有し、PIIを要求し、脆弱性や悪用につながる広いアタックサーフェスを抱えていました。平文で通信するものもあり、ほとんどのアプリケーションには透明性のためのプロセスや文書がありませんでした。

次のセクションでは、OpenTraceプロトコルを使用する一つのアプリケーションを詳しく掘り下げます。目的は特定のアプリケーションを名指しで批判することではなく、当社の技術的なアプローチの例を共有することです。

Wiqaytnaアプリケーションについて:

全体として、このアプリケーションにはクリティカルな脆弱性はなく、プライバシーに配慮しており、チームはアプリケーションをモロッコ市場の特性に適合させるために、しっかりと準備をしています。

  • プライバシーに配慮した追跡プロトコルの選択
  • 個人を特定できる情報(PII)や位置情報を収集しない
  • 妥当な実装と技術スタックの選択
  • プロセスを透明化し、文書化するための真摯な取り組み

問題の概要:

項目 カテゴリ 重大度
iOSでは、アプリがフォアグラウンドにないときに一時IDを収集しなかった 接触追跡 高
有効期限を過ぎても同じ一時IDが使用されている 接触追跡/プライバシー 高
ユニットテストの欠如 コード品質/セキュリティ 高
iOSの脆弱な依存関係 セキュリティ 中
PINの検証がブルートフォース可能 セキュリティ 中
Bluetoothサービスは一意である必要があるが、シンガポールのTraceTogtherのテスト用UUIDを使用している 接触追跡 低
オープンソースのサーバー実装の欠如 透明性 低
対応言語が2言語に限られている。ほかの公用語や外国語にも拡大すべき アクセシビリティ 低
非推奨のIPCメカニズムの使用 コード品質/セキュリティ 低


代替テキスト
モバイルアプリケーションセキュリティテスト

詳細分析:Wiqaytnaアプリ

透明性

このモロッコのアプリケーションは、当社がリストアップした中で数少ないオープンソースのアプリケーションの一つです。ソースコードの大部分はGithubリポジトリで公開されていますが、バックエンドのソースだけはOpenTraceのリポジトリを指しているにすぎません。 コンパイル済みのアプリケーションを分析したところ、コンパイル済みのバージョンはオープンソースのバージョンと一致していました。

代替テキスト
モバイルアプリケーションセキュリティテスト

このアプリケーションは、接触追跡にOpenTraceのプロトコルと実装を使用しています。このプロトコルは十分に文書化されており、シンガポールやオーストリアのアプリなど、ほかのいくつかのアプリケーションでも使用されています。

ただし、このプロトコルにはいくつかの問題があり、iOSでは追跡にいくつかの制限があります。

代替テキスト
モバイルアプリケーションセキュリティテスト

ただし、OpenTraceの実装はライブラリとしてモロッコのアプリケーションに組み込まれているのではなく、コードが直接取り込まれています。これにより、標準の実装から乖離するリスクが生じ、修正やアップデートを追加することが難しくなります。Wiqaytnaのチームは、関連する変更をすべて確実に取り込めるよう、リポジトリの変更と更新を注視し続ける必要があります。

OpenTraceは公開されている複数の追跡プロトコルの一つであり、TCN、Robert、AppleとGoogleの追跡は、それ以外の実装です。一部のアプリケーションは独自の実装を使用する道を選び、深刻なプライバシー上の問題を招いています。

代替テキスト
COVIDモバイルアプリケーションのプロトコル
出典:https://en.wikipedia.org/wiki/COVID-19_apps

これらのプロトコルはいずれも追跡から保護するために一時IDを使用していますが、主な違いは追跡の解決プロセス、つまり、ある人が別の感染者と接触していたかどうかを判定するプロセスにあります。

OpenTraceは中央集権型の追跡実装を使用しているのに対し、たとえばTCN、Apple、Googleの実装はローカル型です。

後者のローカル型は、より強力なプライバシーの保証を提供しますが、帯域幅とバッテリー消費の面で実装上の課題があります。

Wiqaytnaのチームがこのプロトコルを選択した理由は、ホワイトペーパーで公開されています。

代替テキスト
モバイルアプリケーションセキュリティテスト

このホワイトペーパーでは、テストプロセスを含むアプリケーションのいくつかの側面について説明しています。しかし当社は、リポジトリ内にユニットテストも結合テストも見つけられませんでした。チームは、セキュリティ上の問題を報告するためのセキュリティポリシーも追加しています。

代替テキスト
モバイルアプリケーションセキュリティテスト

セキュリティ、プライバシー、接触追跡

モバイルアプリケーションのセキュリティ評価は大きなテーマです。本記事では、最も興味深い部分だけに焦点を当てます。アプリケーションのいくつかの側面は、Ostorlabの脆弱性分析を使って自動的にレビューしました。

このモバイルアプリケーションは、アタックサーフェスが縮小されています。これに対し、当社がレビューしたほかの接触追跡アプリのいくつかは、QRコードなどの機能を追加したり、決済ライブラリや広告ライブラリを含んでいたり、URLスキーム、コンテンツプロバイダー、JavaScriptインターフェースを公開したりしています。

サードパーティの依存関係

アプリケーションに追加されている依存関係はごくわずかです。しかし監査の時点では、iOSアプリケーションの依存関係の一つに既知の脆弱性がありました。この依存関係はFrameworksの一覧からフィンガープリントを取得したもので、Podfile.lockのソースコードでも確認できます。

Payload/Wiqaytna.app/Frameworks/nanopb.framework/Info.plist:

{
    "BuildMachineOSBuild": "19E287",
    "CFBundleDevelopmentRegion": "en",
    "CFBundleExecutable": "nanopb",
    "CFBundleIdentifier": "org.cocoapods.nanopb",
    "CFBundleInfoDictionaryVersion": "6.0",
    "CFBundleName": "nanopb",
    "CFBundlePackageType": "FMWK",
    "CFBundleShortVersionString": "0.3.9011",
    "CFBundleSignature": "????",
    "CFBundleSupportedPlatforms": [
        "iPhoneOS"
    ],
    "CFBundleVersion": "1",
    "DTCompiler": "com.apple.compilers.llvm.clang.1_0",
    "DTPlatformBuild": "17F65",
    "DTPlatformName": "iphoneos",
    "DTPlatformVersion": "13.5",
    "DTSDKBuild": "17F65",
    "DTSDKName": "iphoneos13.5",
    "DTXcode": "1150",
    "DTXcodeBuild": "11E608c",
    "MinimumOSVersion": "8.0",
    "UIDeviceFamily": [
        1,
        2
    ]
}

この脆弱な依存関係は、Firebaseライブラリによる推移的な依存関係です。ただし、悪用される可能性は低いと考えられます。この脆弱性はnanopbのバージョン0.3.9011に影響します。CVE-2020-5235の説明は次のとおりです。

0.4.1、0.3.9.5、0.2.9.4より前のNanopbには、悪用される可能性のあるメモリ不足の状態が存在します。nanopbがPB_ENABLE_MALLOCを有効にしてコンパイルされ、デコード対象のメッセージに繰り返しのstring、bytes、またはmessageフィールドが含まれ、配列の拡張時にrealloc()がメモリ不足になると、nanopbは初期化されていないメモリに由来するポインター値に対してfree()を呼び出してしまう可能性があります。プラットフォームによっては、これがクラッシュやさらなるメモリ破壊を引き起こし、場合によっては悪用可能となるおそれがあります。この問題は、nanopb-0.4.1、nanopb-0.3.9.5、nanopb-0.2.9.4で修正されています。

バックエンドとネットワーク通信

このアプリケーションは、バックエンドの実装のホスティング、認証の実行、追跡ファイルのアップロードにFirebaseを使用しています。

通信はTLS上で行われており、適切に検証されています。サーバーの実装は主にOpenTraceのオープンソースの実装に基づいていますが、統計情報やプロファイルの更新(性別、県など)といったいくつかのエンドポイントはオープンソースになっていません。

このアプリケーションは、すべてのAPI呼び出しにFirebase Functionsを、追跡ファイルのアップロードにFirebase Storageを、構成管理にFirebase configを使用しています。

  • Firebase Functionsのエンドポイントの例
POST /getTempIDs HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 13
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1

{"data":null}
  • Firebase Storageのエンドポイントの例
POST /v0/b/opentrace-a065c.appspot.com/o?uploadType=resumable&name=streetPassRecords%2F20200608%2FStreetPassRecord_LGE_Nexus%205_2020-06-08_23-36-01.json HTTP/1.1
Authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY3MDIxMiwiZXhwIjoxNTkxNjczODEyLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.BFa9yg_l03TeeFuBL8Obo1NYSN80xSpmtcRlA-lFCXlYuS6kX8azYMfrik6G1S0x9PuxmftBxxd7Yn6rsueJtdoTchrF67XDMbqlnCiqtlNJuPItPKFyaeuB3IgY219yZxO4KWlQYmxy5IOL71jKj_RNS7iQpi0G3fG_lGa01CD6xaafBuSC3MmZyu-yeaeY7kNsdZRF7_YHPQerINR6C9sQH0ZZepcwv5onzh5j3sALfvaQVVTL1VyA6gZEAeZzPYPR_c2n5Izkk0VsNcgqKejHRte-Qh5heJ8MZAzRZep6_rUjMhCxv7DdbmBC_gTBvYs8ZXDdhgdUESPAMfDlvA
X-Firebase-Storage-Version: Android/20.15.15 (040308-306758586)
X-Goog-Upload-Header-Content-Type: application/octet-stream
X-Goog-Upload-Protocol: resumable
x-firebase-gmpid: 1:319155529705:android:f102bb0f7be8a8bac2cd78
X-Goog-Upload-Command: start
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; Nexus 5 Build/NJH47F)
Host: firebasestorage.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate
ファイルのアップロード

バックエンドの実装で特定された最も重要な問題の一つは、追跡データのアップロード機能に影響するものです。アップロードには、保健当局が発行するPINが必要で、このPINはアップロードトークンの取得に使われます。

代替テキスト
モバイルアプリケーションセキュリティテスト

PINの検証は、getUploadTokenメソッドを通じて行われます。

/**
 * Get upload token by passing in a secret string as `data`
 */
const getUploadToken = async (uid: string, data: any, context: functions.https.CallableContext) => {
  console.log("getUploadToken:", "uid", uid, "data", data, "ip", context.rawRequest.ip);

  let valid = false;
  if (data) {
    const uploadCodes = await retrieveUploadCodes();
    console.log("getUploadToken:", "obtained ${uploadCodes.length} upload codes");
    valid = uploadCodes.find(x => x === data) !== undefined;
    console.log("getUploadToken:", "data is ${valid ? "valid" : "not valid"} code");
  }
  valid = uploadCodes.find(x => x === data) !== undefined;
    console.log("getUploadToken:", "data is ${valid ? "valid" : "not valid"} code");
  }

  if (valid) {
    const payload = Buffer.from(JSON.stringify(
      {
        uid,
        createdAt: Date.now() / 1000,
        upload: data
      }
    ));
    console.log("getUploadToken:", "uid:", "${uid.substring(0, 8)}***", "createdAt:", formatTimestamp(Date.now() / 1000));

    // Prepare encrypter
    const encryptionKey = await getEncryptionKey();
    const customEncrypter = new CustomEncrypter(encryptionKey);

    // Encode payload
    const payloadData = customEncrypter.encryptAndEncode(payload);
    console.log("getUploadToken: Completed. Payload byte size: ${payloadData.length}");

    return {
      status: "SUCCESS",
      token: payloadData.toString("base64")
    };
  } else {
    console.log("getUploadToken:", "Invalid data: ${data}");
    throw new functions.https.HttpsError("invalid-argument", "Invalid data: ${data}");
  }

しかし、トークンの検証はユーザーごとに行われているわけではありません。Wiqaytnaアプリケーションの場合、PINは6桁の数字です。Firebaseのスケーラビリティのおかげで、6桁の空間全体をわずか数分でブルートフォースすることができました。

実際には、追跡ファイルのアップロードにPINは必要ありません。認証さえ済めば、任意の名前の任意のファイルを/oリポジトリにアップロードできます。以下のリクエスト例を参照してください。

POST /v0/b/opentrace-a065c.appspot.com/o?uploadType=resumable&name=streetPassRecords%2F20200608%2FStreetPassRecord_LGE_Nexus%205_2020-06-08_23-36-01.json&upload_id=AAANsUkiafDezgJ4QQJa9JsncG1RoKvvi08qh40ZhmOrbOU5_tare9a_3kQABvsYS9m1pVmRDCiFS9WxVkq2Aq_NZrM&upload_protocol=resumable HTTP/1.1
Authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY3MDIxMiwiZXhwIjoxNTkxNjczODEyLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.BFa9yg_l03TeeFuBL8Obo1NYSN80xSpmtcRlA-lFCXlYuS6kX8azYMfrik6G1S0x9PuxmftBxxd7Yn6rsueJtdoTchrF67XDMbqlnCiqtlNJuPItPKFyaeuB3IgY219yZxO4KWlQYmxy5IOL71jKj_RNS7iQpi0G3fG_lGa01CD6xaafBuSC3MmZyu-yeaeY7kNsdZRF7_YHPQerINR6C9sQH0ZZepcwv5onzh5j3sALfvaQVVTL1VyA6gZEAeZzPYPR_c2n5Izkk0VsNcgqKejHRte-Qh5heJ8MZAzRZep6_rUjMhCxv7DdbmBC_gTBvYs8ZXDdhgdUESPAMfDlvA
X-Firebase-Storage-Version: Android/20.15.15 (040308-306758586)
X-Goog-Upload-Offset: 0
X-Goog-Upload-Protocol: resumable
x-firebase-gmpid: 1:319155529705:android:f102bb0f7be8a8bac2cd78
X-Goog-Upload-Command: upload, finalize
Content-Length: 32739
Content-Type: application/x-www-form-urlencoded
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; Nexus 5 Build/NJH47F)
Host: firebasestorage.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate

{"token":"klklklk","records":[],"events":[{"id":1,"msg":"Scanning Started","timestamp":1591635248},{"id":2,"msg":"Scanning Stopped","timestamp":1591635256},{"id":3,"msg":"Scanning Started","timestamp":1591635293},{"id":4,"msg":"Scanning Stopped","timestamp":1591635301},

ただし、PINはユーザーのUIDからトークンを生成するために使用されます。このトークンはアップロードされたファイルに追加され、一時IDを収集して解析する前に検証されます。

export async function _processUploadedData(filePath: string, validateTokenTimestamp: boolean = true): Promise<{ status: string; message?: string; filePath?: string }> {
  const fileName = path.basename(filePath, ".json");
  let uid = "", uploadCode = "", step = "";

  try {
    //
    // Step 1: load file content into memory
    //
    step = "1 - load file";
    const { token, records, events } = JSON.parse(await getStorageData(config.upload.bucketForArchive, filePath));
    console.log("processUploadedData:", "step ${step}", "File is loaded, record count:", records.length);

    //
    // Step 2: Validate upload token to get uid
    //
    step = "2 - validate upload token";
    ({ uid, uploadCode } = await validateToken(token, validateTokenTimestamp));
    console.log("processUploadedData:", "step ${step}", "Upload token is valid, id:", uid);

ほかのユーザーのファイルを上書きしたり、アップロードされたファイルを単に読み取ったりするリスクは、Firebaseのセキュリティポリシーによって緩和されています。また、ファイルはアップロードされるとアーカイブされて削除されるため、同様の攻撃が成立する可能性は限られています。

rules_version = "2";
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow create: if request.auth != null; // Only allow write, Cloud Functions have read/write access by default.
    }
  }
}

Firebaseのデフォルトのポリシーは、認証済みのすべてのユーザーに読み取りと書き込みのアクセスを許可するため、安全ではなかったはずです。

認証

認証は、バックエンドと通信するための一時トークンを取得するFirebaseのサービスを使って行われます。トークンはバックグラウンドで継続的に更新されますが、認証のハンドシェイクを完了するには複数のリクエストが必要です。以下のリクエスト例を参照してください。

POST /c2dm/register3 HTTP/1.1
Authorization: AidLogin 3751703094547129981:5572975265165158895
app: covid.trace.morocco
gcm_ver: 201515018
User-Agent: Android-GCM/1.5 (hammerhead NJH47F)
Content-Length: 1153
content-type: application/x-www-form-urlencoded
Host: android.clients.google.com
Connection: close
Accept-Encoding: gzip, deflate

X-subtype=319155529705&sender=319155529705&X-app_ver=67&X-osv=25&X-cliv=fiid-20.1.6&X-gmsv=201515018&X-appid=davxJEzSQ-KU19cQzA_uoT&X-scope=*&X-Goog-Firebase-Installations-Auth=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWQiOiJkYXZ4SkV6U1EtS1UxOWNRekFfdW9UIiwicHJvamVjdE51bWJlciI6MzE5MTU1NTI5NzA1LCJleHAiOjE1OTIyNTQ4MjAsImFwcElkIjoiMTozMTkxNTU1Mjk3MDU6YW5kcm9pZDpmMTAyYmIwZjdiZThhOGJhYzJjZDc4In0.AB2LPV8wRAIgMgJx98gRA6_SU6N8nW6wgTZVuddlyyBevprKhZo6dHoCIA4Iy3e5EJyGHeoFc2Qe_iYGdaiJsx_6HifTuSC3v46n&X-gmp_app_id=1%3A319155529705%3Aandroid%3Af102bb0f7be8a8bac2cd78&X-Firebase-Client=fire-auth%2F19.3.1+fire-iid%2F20.1.6+fire-analytics%2F17.4.0+fire-abt%2F19.0.1+fire-android%2F+fire-rc%2F19.1.4+fire-cls%2F17.0.0+fire-core-ktx%2F19.3.0+fire-fcm%2F20.1.6+kotlin%2F1.3.61+fire-installations%2F16.2.2+fire-fn%2F19.0.2+fire-perf%2F19.0.7+fire-gcs%2F19.1.1+fire-core%2F19.3.0&X-firebase-app-name-hash=R1dAH9Ui7M-ynoznwBdw01tLxhI&X-Firebase-Client-Log-Type=1&X-app_ver_name=1.0.67&app=covid.trace.morocco&device=3751703094547129981&app_ver=67&info=k9JSinrwmBIcQKri541rkWV16eIluRY&gcm_ver=201515018&plat=0&cert=ad695288cfbac51f5d7a1c01b70dc9d17bfdee75&target_ver=28
POST /v1/token?alt=proto&key=AIzaSyAoR1Pr0U0xhT52a3FlnLK2OeGEjtUNJic HTTP/1.1
Content-Type: application/x-protobuf
X-Firebase-Locale: 
X-Client-Version: Android/GmsCore/X19003001/FirebaseCore-Android
Accept-Language: en-US
X-Android-Package: covid.trace.morocco
X-Android-Cert: AD695288CFBAC51F5D7A1C01B70DC9D17BFDEE75
X-Goog-Spatula: CjMKE2NvdmlkLnRyYWNlLm1vcm9jY28aHHJXbFNpTSs2eFI5ZGVod0J0dzNKMFh2OTduVT0SIHJZIRStW1F0J8MBBVCOImgRlh8OUv/zaX9g98To5sncGP3M3Onkwq6INCCI4u/0m5zAmDQqSQBeaoCcvd7j4G9W0tHHPxFfDghEUwL14YXByaqT5AoHKQjFCAIyjLFGBRfafsYYmjO71FlNW7YtO+W7X97HZvZmUw67FR2vvn8=
User-Agent: Mozilla 5.0 (Linux; U; Android 7.1.2; en_US; Nexus 5; Build/NJH47F); com.google.android.gms/201515018; FastParser/1.1; ApiaryHttpClient/1.0; (gzip) (hammerhead NJH47F); gzip
Content-Length: 222
Host: securetoken.googleapis.com
Connection: close
Accept-Encoding: gzip, deflate



refresh_tokenÌAE0u-Nepqs-8lFOdGMvgfgNcJA9fyrDqMrSXfbVgtY811m5NNcHv5XsmssQdkv3csqJr1LCSQ4m_aSCvPa2KjKNGJsHMAGs-jmbN_GYS64PFOMzq8bY-78vf9u6QMUHBgwDziJlrHQioVk843vPorA3LIyrDPDOCAcHbU8fndIzA5ySj-X7kMA_RK0t5evIOy2cGxrXbhU4M
HTTP/1.1 200 OK
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Date: Tue, 09 Jun 2020 01:33:34 GMT
Content-Type: application/x-protobuf
Content-Disposition: attachment
Vary: Origin
Vary: X-Origin
Vary: Referer
Server: ESF
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
Content-Length: 1957


ÐeyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY2NjQxNCwiZXhwIjoxNTkxNjcwMDE0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.DiTkt4EC3IUM8Qx4BKkX_Wu-DSt_59wX_bS0-8SVHA94M4vmcVQ7Bn4ixk-QkaT-AW6qxzcJyffc726TNaoG6KIYO4AHsdb-1ZO6ipBtetOx53vLxeF7L-5fwqerSzGqXvL4UDbetAWFxQylbcV0YbWzgXBrvBNoawG8osiBgZ-qC-b6VyElMwcm5s7Gt8mv_cdS8c4MB6xBppUYFBBdaUXzfAJqse6w0KR1CwUqhI_SojcuIWF9e0O7du0zngnThoixluqHM9bmYzoWcOD8QKfhLYgRDzet6r_sJCMlc48tzb8r3XeWodktMnIgj__P7N4tglLofjDX65BzISwwZgBearer"ÌAE0u-Nepqs-8lFOdGMvgfgNcJA9fyrDqMrSXfbVgtY811m5NNcHv5XsmssQdkv3csqJr1LCSQ4m_aSCvPa2KjKNGJsHMAGs-jmbN_GYS64PFOMzq8bY-78vf9u6QMUHBgwDziJlrHQioVk843vPorA3LIyrDPDOCAcHbU8fndIzA5ySj-X7kMA_RK0t5evIOy2cGxrXbhU4M*ÐeyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY2NjQxNCwiZXhwIjoxNTkxNjcwMDE0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.DiTkt4EC3IUM8Qx4BKkX_Wu-DSt_59wX_bS0-8SVHA94M4vmcVQ7Bn4ixk-QkaT-AW6qxzcJyffc726TNaoG6KIYO4AHsdb-1ZO6ipBtetOx53vLxeF7L-5fwqerSzGqXvL4UDbetAWFxQylbcV0YbWzgXBrvBNoawG8osiBgZ-qC-b6VyElMwcm5s7Gt8mv_cdS8c4MB6xBppUYFBBdaUXzfAJqse6w0KR1CwUqhI_SojcuIWF9e0O7du0zngnThoixluqHM9bmYzoWcOD8QKfhLYgRDzet6r_sJCMlc48tzb8r3XeWodktMnIgj__P7N4tglLofjDX65BzISwwZg2lXh3IIMEZCgUdC1wAOayVAxrRAm28éϛù¤ 
POST /getOTPCode HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 229
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1

{"data":{"token":"eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T","os":"ANDROID","phoneNumber":"+212xxxxxxxxx"}}

トラフィックの分析中に、WiqaytnaがPIIを一切収集していないことを確認しました。ただし、OTPを受け取るにはモロッコの電話番号が必要です。

POST /getOTPCode HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 229
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1

{"data":{"token":"eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T","os":"ANDROID","phoneNumber":"+212612345678"}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"1a-XRg53+foeSHn5xZ1DF/JVqySS30"
Function-Execution-Id: uohs4he3srne
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: f52f429cd5b8842c69de05793722650b
Date: Mon, 08 Jun 2020 21:42:38 GMT
Server: Google Frontend
Content-Length: 26
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close

{"result":{"result":true}}
POST /verifyOTP HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc0Mzg3ZGUyMDUxMWNkNDgzYTIwZDIyOGQ5OTI4ZTU0YjNlZTBlMDgiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9vcGVudHJhY2UtYTA2NWMiLCJhdWQiOiJvcGVudHJhY2UtYTA2NWMiLCJhdXRoX3RpbWUiOjE1OTE2NTIyOTUsInVzZXJfaWQiOiJsWGgzSUlNRVpDZ1VkQzF3QU9heVZBeHJSQW0yIiwic3ViIjoibFhoM0lJTUVaQ2dVZEMxd0FPYXlWQXhyUkFtMiIsImlhdCI6MTU5MTY1MjI5NSwiZXhwIjoxNTkxNjU1ODk1LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.IVuITUlZH3ch254qhThumeIKNYPaIvnFsj3Bdnp_51J4eKp7HYhy8l5TZAHm9pW51VwC5mEbKrLXbrnmHhSgDjeXnNnAWfUHGRl_5BJUSvsrXIiVhUNT6uWFwq85oRzM95fhUQzP3zA1V-40Rt91-pUTBbq548mrabdFwxHgbW7WlbT2kM75V2al0ZwRTcci5TpOACf_zlNVUcia4L2K1R28vE_EGTPYPYvDPuEa4Bv5zz5GX0gL4PgQJGgStqp-Q4r1o9MuXBdyaxBr8cMNNXXlOT-Doin2Xavhzxy1A73sDbGeA9TkwntwOUwacL6bkQzoeg_SaAU5pQa4SsyOhg
Firebase-Instance-ID-Token: eVusymDJTJWMmm8tG4nQUZ:APA91bHLa36gjJeGnE-ZiTTEnXKwwLE6AOEZa7gps0BKiJuujVDXVG9_onEp6z2QUIsdco8QWQ3jpsxKW2Tbe8-4pI82qGWmXLheRZ1_vChXqJmjt9kCCQUSS9XGaOQwPVnxCfXDyu9T
Content-Type: application/json; charset=utf-8
Content-Length: 25
Host: europe-west1-opentrace-a065c.cloudfunctions.net
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.12.1

{"data":{"OTP":"415322"}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"f-ayLlCL3PuzXSThdu78iReSEjl6Y"
Function-Execution-Id: wee1qz85dpid
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: cdc7b708c0b133eeb7de7cd8171a498a
Date: Mon, 08 Jun 2020 21:54:55 GMT
Server: Google Frontend
Content-Length: 15
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close

{"result":true}
権限

アプリケーションが要求する権限は、すべて正当なものです。主にBluetoothとインターネットアクセスに関するものです。一部のユーザーから位置情報の権限(ACCESS_FINE_LOCATION)について懸念の声が上がりましたが、これはAndroidでBluetoothにアクセスするために必要なものです。詳細はAndroidのドキュメントを参照してください。

当社の分析では、アプリケーションが位置座標を使用または収集していないことが示されました。

Androidの場合:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>

iOSの場合:

<key>NSBluetoothAlwaysUsageDescription</key>
    <string>Le Bluetooth est utilisé pour échanger des codes anonymes avec les autres utilisateurs de Wiqaytna et ainsi déterminer si ils ont été à proximité de vous</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>Le Bluetooth est utilisé pour échanger des codes anonymes avec les autres utilisateurs de Wiqaytna et ainsi déterminer si ils ont été à proximité de vous</string>
    <key>TRACER_SVC_ID</key>
...
<key>UIBackgroundModes</key>
    <array>
        <string>bluetooth-central</string>
        <string>bluetooth-peripheral</string>
        <string>fetch</string>
        <string>processing</string>
        <string>remote-notification</string>
    </array>
プロセス間通信(IPC)

このAndroidアプリケーションは、内部での通信に複数のIPC方式を使用しています。これらの方式の一部は、ローカルのBroadcast Managerを使って動的に登録され、処理後に登録解除されます。

private fun prepare() {
    val deviceAvailableFilter = IntentFilter(ACTION_DEVICE_SCANNED)
    localBroadcastManager.registerReceiver(scannedDeviceReceiver, deviceAvailableFilter)

    val deviceProcessedFilter = IntentFilter(ACTION_DEVICE_PROCESSED)
    localBroadcastManager.registerReceiver(blacklistReceiver, deviceProcessedFilter)

    timeoutHandler = Handler()
    queueHandler = Handler()
    blacklistHandler = Handler()
}

ただし、Local Broadcast managerは非推奨であり、LiveDataやリアクティブストリームに置き換えるべきです。

接触追跡

前述のとおり、接触追跡にはOpenTraceプロトコルが使用されています。このプロトコルは、15分間のみ有効な一時トークンを作成することで、追跡とリプレイ攻撃を防ぎます。ただし、COVIDSafeなどのほかの実装ではプロトコルを調整して有効期間を2時間にしており、リプレイ攻撃のリスクを高めています。

このプロトコルは、一時IDがサーバーに一切保存されないという意味でステートレスです。その代わりに、一時IDには、バックエンドの秘密鍵で暗号化されたユーザーのUIDが含まれています。

登録時には、一時IDのバッチが生成されてモバイルアプリケーションに送信されます。以下のレスポンス例を参照してください。

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Etag: W/"11b78-pjrDe0QcfmRJJ5LFAbO0gM2nD5Q"
Function-Execution-Id: c8wiwl18rlh3
Vary: Origin
X-Powered-By: Express
X-Cloud-Trace-Context: b98efbdc458ce408e0c6de05fe37421c
Date: Tue, 09 Jun 2020 01:33:49 GMT
Server: Google Frontend
Content-Length: 72568
Alt-Svc: h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close

{"result":{"status":"SUCCESS","tempIDs":[{"tempID":"RkL8XSgATVNi4BtqVnD5PEAQ/W4XbRowpSQI8GKuO929Q7Es0DwZRrsst2n2iYNDBupPFXBpeF8SzN7Xxg==","startTime":1591659229,"expiryTime":1591660129},{"tempID":"XMTgQ10Qcawdnkk6pvTnmRY7Lupp9iQZVvi8PQ2Rs34t0lLLnQphIyc0h/QuGMWytD7UqY3wShHT3h741Q==","startTime":1591660129,"expiryTime":1591661029},{"tempID":"4DWjqu4N6R1iJzFI15F1AhHqX3ZPYPwi+7vifSHJa3d7qYSrBgDS1dQWb+yb5PAcha23BqvxGKJWzeK+XA==","startTime":1591661029,"expiryTime":1591661929},{"tempID":"4IzfigFLAmvPjJgnJs74dAJIMhEWQGy1e86XgkcUlTWD4EL1pNKY0r1R8sHVjfIPIvgbbznCW358cVYISw==","startTime":1591661929,"expiryTime":1591662829},{"tempID":"JJ9QS14beseLakhTb6m8D15Ju7fZnz0T9nA5SiiNG1DobcyHVsceZWN/uaQ8gbzhNQScLYg+A63lNfzktQ==","startTime":1591662829,"expiryTime":1591663729},{"tempID":"daMVpZZWqR+SI8mYhQbw07VgdM4KCMcSobQDKzZUn9nq/Y8w5p5BSxeDox7ZTPQufa5zJ35Jf0E7pkzxJw==","startTime":1591663729,"expiryTime":1591664629},{"tempID":"xO+yM4u2AzNVSVWbiLw7xrSOyOVTy0PLW++VTtW7olkZ6cG4G4AFGRj5eDyIpqh4+GPKP9KQzAO8Q4ptSw==","startTime":1591664629,"expiryTime":1591665529},{"tempID":"55wlRGy2k2dpJQtL37dozYOU6Pz/W06LHYoeQ4z7Dkohg4veCAKYwe9LSKDC+zwQAqQXaq14/jud3SVBGA==","startTime":1591665529,"expiryTime":1591666429},{"tempID":"a5N6awZjMKzANIZyZUTFOzl2UnJsFH26Przt0PMwg/YGOBh9oBW7OMART5fZPMnd8fC/sKu3bq8VQzZtBA==","startTime":1591666429,"expiryTime":1591667329},{"tempID":"2MBbQrT3/9bqsHHKnmqkoops/xw64wmButkg0NQMrmw/mYlyaCtknRSW6TOowM07UsUQpLLQDVcUgMvEug==","startTime":1591667329,"expiryTime":1591668229},

一時IDはその後、Bluetooth Low Energy(BLE)を使ってビーコンとして発信されます。以下は、AndroidのBluetoothサービスから収集したスタックトレースの例です。本番ビルドは難読化されているため、メソッド名は意味の読み取れない1文字になっています。

at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at long covid.trace.morocco.services.BluetoothMonitoringService.a()

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at void covid.trace.morocco.services.b.a()
at void covid.trace.morocco.services.b.b()
at int covid.trace.morocco.services.BluetoothMonitoringService$b.a()

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.O()
at void covid.trace.morocco.services.b.a()
at void covid.trace.morocco.services.b.a()
at int covid.trace.morocco.services.BluetoothMonitoringService$b.a()

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at boolean covid.trace.morocco.idmanager.TempIDManager.needToUpdate()
at void covid.trace.morocco.c.a$a.c()

 With parameters:
1: : java.lang.String = "TempIDManager"
2: : java.lang.String = "Need to update and fetch TemporaryIDs? 1591864491000 vs 1591816764979: false"

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at boolean covid.trace.morocco.idmanager.TempIDManager.needToUpdate()
at void covid.trace.morocco.c.a$a.c()
at boolean covid.trace.morocco.c.a$a.b()

このサービスはUUID 17E033D3-490E-4BC9-9FE8-2F567643F4D3とキャラクタリスティックUUID 117BDD58-57CE-4E7A-8E87-7CCCDDA2A804を使用しています。これらの値は、バイナリから抽出したものと、Bluetoothサービスをスキャンして得たものです。

代替テキスト
モバイルアプリケーションセキュリティテスト

しかし、このUUIDはTraceTogetherアプリケーションが使用しているステージング用のUUIDと同じです。Wiqytnaのチームは、独自のUUIDを生成して使用すべきでした。

Androidでは検出可能性の確保は簡単ですが、iOSでは非常に厄介になります。Appleは、アプリケーションがバックグラウンドにある間に実行できる処理に制限を課しており、追跡サービスをアドバタイズできる可能性が限られています。Appleには、メーカー値のオーバーフロー領域を使ってサービスをハッシュ化した形式でアドバタイズするという、規格を独自に利用(あるいは悪用)する方法があります。

代替テキスト
モバイルアプリケーションセキュリティテスト

代替テキスト
モバイルアプリケーションセキュリティテスト

Open Traceのリポジトリには、AppleのBluetoothの制限を回避する方法について議論している長いissueがあります。この理由から、一部の国では、こうした厄介な詳細をOSが処理するGoogleとAppleの追跡プロトコルを採用しています。AppleはBluetoothスタックを完全に制御しているからです。Covid Watchは、より高度な工夫を用いてクロスプラットフォームでの検出をうまく機能させた、別のアプリケーションの例です。

キャラクタリスティックは、距離の計算に役立つ一時IDやその他の測定情報を含む、JSONでエンコードされたメッセージを公開します。

代替テキスト
モバイルアプリケーションセキュリティテスト

モロッコのアプリケーションについて、当社の実験では2つの異なる問題が明らかになりました。

1- iOS端末では、画面がロックされているときやアプリケーションがバックグラウンドにあるときに、一時IDがほかの端末と交換されませんでした。これは、次のスキーマを持つiOSのデータベースを調べることで確認しました。

sqlite> .schema ZENCOUNTER

CREATE TABLE ZENCOUNTER ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZV INTEGER, ZRSSI FLOAT, ZTIMESTAMP TIMESTAMP, ZTXPOWER FLOAT, ZMODELC VARCHAR, ZMODELP VARCHAR, ZMSG VARCHAR, ZORG VARCHAR );

Select * from ZENCOUNTER;

2- AndroidとiOSの両方の端末で、アプリケーションは有効期限が切れているはずの一時IDをビーコンとして発信し続けていました。

1|1591809886424|2|not_found|MAR|Nexus 5|iPhone 7|-84|
2|1591810481878|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-90|
3|1591810486993|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-85|
...
143|1591823491233|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-53|
144|1591823554837|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-59|
145|1591823611469|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-54|
146|1591823672303|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-52|
147|1591823730987|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-58|
148|1591823790687|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-58|
149|1591823851166|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-59|
150|1591823911950|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-57|
151|1591823973664|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-62|
152|1591824032336|2|0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==|MAR|Nexus 5|iPhone 7|-56|

Androidのデータベースから抽出した次のデータでは、有効期限のタイムスタンプを過ぎても一時IDが一度も変わっていません。iOS端末を確認すると、一時IDは1591821662(エポックタイムスタンプ)に有効期限が切れているはずですが、実際にはその値を過ぎてもビーコンとして発信されています。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>
        <key>ADVT_DATA</key>
        <data>
       eyJtcCI6ImlQaG9uZSA3IiwiaWQiOiIwakhpWWkwb05Gek5VZ1lFV21iREd1Mk1YZHord1MxN0ZwUllwZzNxR3l1OHZKemNYdzJCUm5qMjV2UWMrTmhBMWhQK2FwaldDNlc2N056MVZ3PT0iLCJvIjoiTUFSIiwidiI6Mn0=
        </data>
        <key>ADVT_EXPIRY</key>
        <date>2020-06-11T10:26:02Z</date>
        <key>BROADCAST_MSG</key>
   <string>0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==</string>
        <key>BROAD_MSG_ARRAY</key>
        <array>
                <dict>
                        <key>expiryTime</key>
                        <integer>1591821662</integer>
                        <key>startTime</key>
                        <integer>1591820762</integer>
                        <key>tempID</key>
                       <string>0jHiYi0oNFzNUgYEWmbDGu2MXdz+wS17FpRYpg3qGyu8vJzcXw2BRnj25vQc+NhA1hP+apjWC6W67Nz1Vw==</string>
                </dict>
暗号

このアプリケーションは、一時IDとアップロードトークンの生成と検証に暗号処理を使用しています。

アプリケーションはすべての処理に単一のアルゴリズムと同じ鍵を使用していますが、複数の鍵にも対応しています。オープンソースの実装では、AESをGCM(Galois Counter Mode)またはCBC(Cipher Block Chaining)のいずれかで使用することに言及していますが、デフォルトではどちらも設定されていません。Wiqaytnaがどちらの実装を使用しているかは、当社には分かりません。

代替テキスト
モバイルアプリケーションセキュリティテスト

GCMは、暗号化と完全性の両方を提供する認証付き暗号モードであるため、より強力なセキュリティの保証を提供します。一方、CBCには、ビット反転攻撃やパディングオラクル攻撃など、いくつかの既知の攻撃が存在します。

暗号化と復号の両方を必要とする複数の処理に同じ鍵が使用されていることは、攻撃の余地を生みます。たとえば、攻撃者が制御する入力をあるAPIで暗号化させ、それを復号を行うAPIに注入するといった攻撃です。

ソースコードを分析した結果、6桁のPINを除き、暗号化メソッドへの入力はいずれもユーザーが制御できるものではないため、こうした攻撃が存在することは確認されませんでした。

import crypto from "crypto";
import config from "../../config";

class CustomEncrypter {

  algorithm: string;
  key: Buffer;

  constructor(key: Buffer, algorithm: string = config.encryption.defaultAlgorithm) {
    this.algorithm = algorithm;
    this.key = key;
  }

  decode(encodedData: Buffer, lengths: number[]): string[] {
    const [cipherData, ivData, authTagData] = lengths.map((e, i) => Buffer.alloc(lengths[i]));
    encodedData.copy(cipherData, 0, 0, lengths[0]);
    encodedData.copy(ivData, 0, lengths[0], lengths[0] + lengths[1]);
    encodedData.copy(authTagData, 0, lengths[0] + lengths[1], lengths[0] + lengths[1] + lengths[2]);

    return [
      cipherData.toString("base64"),
      ivData.toString("base64"),
      authTagData.toString("base64")
    ];
  }

  decrypt(cipherText: string, iv: string, authTag: string) {
    const decipher = crypto.createDecipheriv(this.algorithm, this.key, Buffer.from(iv, "base64"));
    // Add ts-ignore due to build error TS2339: Property 'setAuthTag' does not exist on type 'Decipher'.
    // @ts-ignore
    decipher.setAuthTag(Buffer.from(authTag, "base64"));

    // @ts-ignore
    let plainText = decipher.update(cipherText, "base64", "base64");
    plainText += decipher.final("base64");
    return plainText;
  }

  encode(cipherText: string, iv: string, authTag: string): Buffer {
    const [cipherData, ivData, authTagData] = [cipherText, iv, authTag].map(e => Buffer.from(e, "base64"));
    const buffer = Buffer.alloc(cipherData.length + ivData.length + authTagData.length);
    cipherData.copy(buffer, 0);
    ivData.copy(buffer, cipherData.length);
    authTagData.copy(buffer, cipherData.length + ivData.length);

    return buffer;
  }

  encrypt(data: Buffer, ivLength = 16, authTagLength = 16): string[] {
    const dataB64 = data.toString("base64");
    const iv = crypto.randomBytes(ivLength);
    // @ts-ignore
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv, {authTagLength});

    let cipherText = cipher.update(dataB64, "base64", "base64");
    cipherText += cipher.final("base64");
    return [
      cipherText,
      iv.toString("base64"),
      cipher.getAuthTag().toString("base64")
    ];
  }

  encryptAndEncode(data: Buffer, ivLength = 16, authTagLength = 16): Buffer {
    const [cipherTextB64, ivB64, authTagB64] = this.encrypt(data, ivLength, authTagLength);
    return this.encode(cipherTextB64, ivB64, authTagB64);
  }

  decodeAndDecrypt(encodedData: Buffer, lengths: number[]): string {
    const [decodedCipherTextB64, decodedIvB64, decodedAuthTagB64] = this.decode(encodedData, lengths);
    return this.decrypt(decodedCipherTextB64, decodedIvB64, decodedAuthTagB64);
  }
}

export default CustomEncrypter;
その他

Androidアプリケーションのログ出力は一元化されており、本番ビルドでは無効になっています。動的解析では、ログAPIをフックしてアプリケーションのログを収集し、追跡プロセス中に何が起きているかを把握しました。

at void com.android.internal.os.ZygoteInit.main()
at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run()
at java.lang.Object java.lang.reflect.Method.invoke()
at void android.app.ActivityThread.main()
at void android.os.Looper.loop()
at void android.os.Handler.dispatchMessage()
at void covid.trace.morocco.services.b.handleMessage()
at void covid.trace.morocco.services.BluetoothMonitoringService.a()
at void covid.trace.morocco.services.BluetoothMonitoringService.F()
at void covid.trace.morocco.c.a$a.c()

 With parameters:
1: : java.lang.String = "BTMService"
2: : java.lang.String = "[TempID] Dont need to update Temp ID in actionScan"

ファイルシステムへのアクセスも、SDカードへのログ出力に限定されており、これも本番バージョンでは無効になっています。

アクセシビリティ

アプリケーションはフランス語とアラビア語で利用できます。これは良い出発点ですが、この国のすべての公用語(アラビア語とタマジグト語)をカバーしているわけではなく、国内で足止めされている多くの観光客や居住者は、フランス語もアラビア語も話しません。英語やスペイン語などの言語を追加すれば、普及に役立つでしょう。

アプリケーションの登録にはモロッコの電話番号が必要です。悪用を防ぐという意図は理解できますが、観光客がアプリケーションを使用するには電話番号を購入する必要があります。

技術スタック(ネイティブのAndroidとiOS)のおかげでアプリケーションのサイズは小さく、低価格帯の端末に適しています。また、追跡プロトコルをこの国のネットワーク環境に合わせて調整しています。

このアプリケーションには、障がいのある人が利用しやすくなるためのアクセシビリティ機能がほかにありません。

タグ:

covid-19, privacy, MAST