cocoa-mhlw / cocoa

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Android] 接触情報が正常に表示されない場合がある

keiji opened this issue · comments

不具合の内容 / Describe the bug

接触情報が正常に表示されない場合がある。

ホーム画面から「陽性登録者との接触結果を確認」をタップすると、過去14日間の接触画面には「陽性登録者との接触確認」と表示されるが、その下に表示されるべき「○日間に合計○分間の接触」の表示がない。

また、「陽性登録者との接触一覧」で遷移した先にも接触情報が表示される部分が空欄になっている。

動作ログを確認したところ、SecureStorageからデータを取り出すときにJava.Lang.Exception: Signature/MAC verification failedが発生している。

Failed to get from secure storage., 
Exception: Java.Lang.Exception: Exception of type 'Java.Lang.Exception' was thrown. ---> Java.Lang.Exception: Signature/MAC verification failed   --- End of inner exception stack trace ---  at Java.Interop.JniEnvironment+InstanceMethods.CallNonvirtualObjectMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x0008e] in <9ac1d9177dc3426fa90571433f8574be>:0   at Java.Interop.JniPeerMembers+JniInstanceMethods.InvokeNonvirtualObjectMethod (System.String encodedMember, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x0001f] in <9ac1d9177dc3426fa90571433f8574be>:0   at Javax.Crypto.Cipher.DoFinal (System.Byte[] input) [0x00029] in <f9aa911b5cd84ab48476e02741e032ff>:0   at Covid19Radar.Droid.Services.SecureStorageService.GetBytes (System.String key) [0x000bd] in <9ae07e8924504272817d360be0536ad6>:0   at Covid19Radar.Services.SecureStorageService.GetValue[T] (System.String key, T defaultValue) [0x0005d] in <f3fd8a68d9cb4d8494931c2b66dcea82>:0   --- End of managed Java.Lang.Exception stack trace ---javax.crypto.AEADBadTagException at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:517) at javax.crypto.Cipher.doFinal(Cipher.java:1736) at mono.java.lang.RunnableImplementor.n_run(Native Method) at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:30) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:251) at android.app.ActivityThread.main(ActivityThread.java:6589) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)Caused by: android.security.KeyStoreException: Signature/MAC verification failed at android.security.KeyStore.getKeyStoreException(KeyStore.java:695) at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224) at android.security.keystore.AndroidKeyStoreAuthenticatedAESCipherSpi$BufferAllOutputUntilDoFinalStreamer.doFinal(AndroidKeyStoreAuthenticatedAESCipherSpi.java:373) at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:506) ... 10 more

再現手順 / Steps to reproduce

不明。

開発チームよりバックアップ・リストアが悪さしているかもという情報有り。
https://stackoverflow.com/a/69345932

期待される挙動 / Expected behavior

陽性登録者との接触結果が正常に表示される。

スクリーンショット

FaGzgxiVQAIQwRp

FaGzjH8UIAAmv2B

(画像は https://twitter.com/chokuzou44/status/1558725166762377217 より)

動作環境 / Environments

サポートチームに聞いたところ、今回とは別に1件だけ、同事象の問い合わせを受けたことがあるとのこと。

  • デバイス:SONY SO-02J, SHARP SHV40
  • OS: Android 8.0, Android 9 *iOSでの発生は確認されていない
  • バージョン: COCOA v2.0.1

その他 / Additional context

これまでの発生報告が極めて少ない事象であり、現時点で再現方法などの手がかりが乏しいので、「うちでも発生したことがある」など手がかりがあればコメントで教えてください。

SecureStorage周りだと、過去に暗号化を止める(通常のアプリ管理領域に保存する)ことも検討している( #374 )ので、抜本的な解決策がないようであればこちらも考えてもいいかも。。

Internal IDs:

  • 起票中

SecureStorage からのデータ復号に失敗してそうなので、機種変更などが原因で暗号化・復号の為のキーが変わってしまうのかなと(Android 事情はよく知らないですが)

鍵が消えるパターン(P19 〜)

https://www.slideshare.net/ak_shio_555/droid-kaigi2018

指紋認証の登録し直しは個人的にわりとやりますね。

現状推測されるシンプルな再現手段としては。

  1. 接触通知を発生させ、SecureStorage に接触情報を保存。
  2. デバイスのロック方法を変更(具体的な操作はAndroidバージョンに依存)
  3. 接触情報画面を開く

と言う所でしょうか。

例外の発生場所。

var keyStore = KeyStore.GetInstance(KeyStoreType);
keyStore.Load(null);
var storeKey = keyStore.GetKey(Alias, null);
if (storeKey == null)
{
throw new InvalidOperationException("Could not get the KeyStore key.");
}
var cipher = Cipher.GetInstance(CipherTransformation);
cipher.Init(CipherMode.DecryptMode, storeKey, new GCMParameterSpec(GcmTagLength, iv));
result = cipher.DoFinal(encryptedBytes);

KeyStore.getKey()
https://docs.microsoft.com/en-us/dotnet/api/java.security.keystore.getkey?view=xamarin-android-sdk-12#java-security-keystore-getkey(system-string-system-char())

Chipher.doFinal()
https://docs.microsoft.com/en-us/dotnet/api/javax.crypto.cipher.dofinal?view=xamarin-android-sdk-12#javax-crypto-cipher-dofinal(system-byte())

情報提供ありがとうございます。
セキュリティ設定を変更したことがあるか聞いてみますね(機種変更はしていないとの情報をもらっています)。

さて具体的な対処ですが、上がってきている例外が Java.Lang.Exception と言うところが問題を複雑にしているように思います。

これだと共通プロジェクト側で受けられないことに加えて、セキュリティ系の例外であることも判別できなさそうです。AndroidネイティブだとExceptionがどかんと上がってくることはないと思うので、Xamarin固有の挙動な気がします。この点も再現試験をしながら確認します。

原因の究明と合わせて対処方法ですが、これはSecureStorage使わない方向に舵を切ることも検討したいと考えています。
今は接触情報はエクスポートできるようにしてありますし(インポートはできませんが)、アプリの専用領域に保存していれば通常のAndroid端末であれば他のアプリから容易にアクセスできることはないという認識です。

加えて、現時点で発生している人たちへの案内も必要に思います。
暗号化鍵が失われてると既存のデータの復号は不可能なので、そのことを案内するのが主眼になると思います。1つ言えるのは、アプリを起動時にSecureStorageをチェックしてSignature/MAC verification failedを検出して案内を出す。と言うことが考えられます。

接触が発生しているのに見られないという状況をリカバーする方策についてはい、くつか方法が考えられますが、再現の成功を待ってから検討したいと考えています。

接触情報がデバイス内で秘匿されるべき情報化は判断が付きかねますが、所有者本人であっても改竄出来ると困りそうだなと言う印象は有ります。

一応 StackTrace 内には AEADBadTagException とか KeyStoreException の文字は出てくるので Exception#getCause でさかのぼれば判別のは付くんですかね。やった事無いので分からないですが。

本件の原因と関連するかはさておき、 Android Keystore は簡単に鍵が消えてしまう(または新しい鍵に置き換わる)様なのでこれにのみ依存したデータ保存は避けた方が良さそうですね。
iOS の KeyChain は設計**が違うのか、同様の操作では消えないようです。(詳細は追っていません)

状態そのものは再現できた。

Screen Shot 2022-08-20 at 16 00 50
Screen Shot 2022-08-20 at 16 00 55

DailySummaryまたはExposureWindows、その両方の暗号化に用いたキー情報がシステムから失われることで復号が不可能な状態になり「Signature/MAC verification」が発生している。

一方、単純にDailySummaryまたはExposureWindowsのどちらかが復号されない状態であれば、そもそも「接触情報画面」に遷移が発生せず「接触なし」という取り扱いになるはず。

DailySummaryまたはExposureWindowsのどちらか、または両方が復号できない状態で、かつCOCOAv1時代のExposureInformationが1つ以上存在する状態であると考えられる。ExposureInformationはシステムのキーで復号可能で(「Signature/MAC verification」が発生せず)、そのため「接触情報画面」に遷移が発生してしまっている。

ExposureInformationの時期のキーAが存在して、かつDailySummaryまたはExposureWindowsの暗号に使ったキーBが失われている状態がなぜ発生したのかは現時点で不明。

再現手法

端末Aで保存したSecureStorageファイルを端末Bに移動する。
それだけだとホーム画面で「陽性登録者との接触結果を確認」をタップしても、その時点で読み込みに失敗して「接触情報画面」に遷移しないので、ソースコードを書き換えて無理矢理「接触情報画面」に遷移するようにした。

var hasHighRiskExposure = userExposureInformationList.Count() > 0;
foreach (var ew in exposureWindowList.GroupBy(exposureWindow => exposureWindow.GetDateTime()))
{
if (!dailySummaryMap.ContainsKey(ew.Key))
{
loggerService.Warning($"ExposureWindow: {ew.Key} found, but that is not contained the list of dailySummary.");
continue;
}
var dailySummary = dailySummaryMap[ew.Key];
RiskLevel riskLevel = _exposureRiskCalculationService.CalcRiskLevel(
dailySummary,
ew.ToList(),
exposureRiskCalculationConfiguration
);
if (riskLevel >= RiskLevel.High)
{
hasHighRiskExposure = true;
break;
}
}

hasHighRiskExposureの初期値はExposureInformationが1つ以上あるかとなっているので、ここをtrueと設定した。

セキュリティ設定の変更でシステムの鍵が消える挙動については、次の端末とAndroidのバージョンで試してみたが発生を確認できなかった。

  • Essential PH-1 Android 10
  • Nexus 9 Android 7.1.1
  • Pixel 3 Android 12

Issueに挙げられているAndroid 8および9ではテストできていない。いま手元にないので、次に端末のある場所にいったら探してみます。

一方、今回の事象がセキュリティ設定の変更に起因するなら、鍵Aと鍵B、どちらも失われてExposureInformationも復号化できないので「接触情報画面」に遷移しないと考える。また、ExposureWindowが記録されるCOCOA2以降のバージョンになればExposureInformationは記録しないので、仮に推測通り端末内に復号可能なExposureInformationが記録されているとすれば、以前のキーは存在するが、後のキーが失われている状態になっていると考えられるので、事象とマッチしていないように思われる。

失礼。 #1113 (comment) こちらの資料を読み間違えていました。

セキュリティ設定の変更で鍵が消えるのは AndroidKeyStore 以前の旧APIの話で AndroidKeyStore を前提とし Android 6 以降を対象にするCOCOAには関係ありません。
AndroidKeyStore を使用しているケースでも指紋認証の追加で鍵が消えるパターンも有るようですが、鍵生成時の設定に依存しこれもCOCOAでは該当しません。

ユーザー操作により鍵が消失するケースとして想定されるのは以下の2パターンぐらいでしょうか。

  • アプリの再インストール (SecureStrage 自体は復元されるか要検証)
  • 機種変更やバックアップからの復元(想定すべきとは思うけれど確認は取れていない)

@b-wind お気になさらず。リサーチと情報提供に感謝しています!

バックアップからの復元について、キー自体はアプリ内ではなく端末内部のKeyStoreに保存して取り出せないという性質を考えると、どういうケースで起きるかなと言ったところです。

原因は突き詰めていくとして、修復と回避策をどうするかも悩みどころですね……SecureStorageを止めてしまって、SharedPreferenceに保存する。
マイグレーションの過程でSecureStorageが読み出せなければExposureNotification APIからgetDailySummariesとgetExposureWindowsを使って最新情報を取得する。と言うことも考えられますね。

デバッグ版なら、アプリのストレージを1回全消去したとき、これまでの日次キーが速やかに消えたりしない(2,3時間猶予がある)のですが、先ほどぼくの普段使いの端末のCOCOAのストレージを全消去してみたら14日分の日次キー(おそらく受信したRPIも)が消えたので、この手は使えなさそうです。

何が起こっているのか、に着目すると今ハッキリ言えることは以下の 一点のみ 二点なのかなと。

  • SecureStrage からのデータの複合に失敗している
  • 「過去14日間の接触」画面及び「過去14日間の接触一覧」画面への遷移が可能

鍵や暗号化されたデータの破損という可能性も考えましたが、それは別の例外が上がってきそう。
そしておそらくは鍵は失っているのでは無く、想定される物とは別の物が(Android Keystore から)取り出されていそう。

現状からの修復という面で見るとやるべき事は比較的明らかで以下の項目ぐらいしか手は無い物と思います。

  1. 何らかの手順ミス等で本来の複合鍵が入手出来る場合利用する(多分無理)
  2. EN API から最新の接触結果を再取得する(本来期待する情報とは別の物になる可能性は高い&再入手自体難しい場合もあり得る)
  3. 諦める

保存先のマイグレーションを行う場合でも同様で、不整合があるデータをマイグレーションするのは想定外の状況になり得る怖さがありますね。

SecureStrage は暗号化処理を行いますが、実データは SharedPreference に保存していると言う理解です。

var saveText = Convert.ToBase64String(saveBytes);
var edit = sharedPreferences.Edit();
edit.PutString(key, saveText);
edit.Commit();

バックグラウンド等での接触チェックと画面表示が並列で動作したときに不整合が起きたりしないだろうかと考える物の、そう言うことが起こるのかどうか自体分かっておらず……
正直よくわからない状況であるのは本音ですね。

ローカル通知からアプリを起動するとHomePageを経由せずに「過去14日間の接触」(ContactedNotifyPage ?)画面は開けますか?

Intent intent = MainActivity.NewIntent(Platform.AppContext, Destination.ContactedNotifyPage);
intent.AddFlags(ActivityFlags.ClearTask);
PendingIntent pendingIntent = PendingIntent.GetActivity(
Platform.AppContext,
REQUEST_CODE,
intent,
PendingIntentFlags.CancelCurrent | PendingIntentFlags.Immutable
);
var notification = new NotificationCompat
.Builder(Platform.AppContext, NOTIFICATION_CHANNEL_ID)
.SetStyle(new NotificationCompat.BigTextStyle())
.SetSmallIcon(Resource.Drawable.ic_notification)
.SetContentTitle(AppResources.LocalExposureNotificationTitle)
.SetContentText(AppResources.LocalExposureNotificationContent)
.SetVisibility(NotificationCompat.VisibilitySecret)
.SetContentIntent(pendingIntent)
.SetLocalOnly(true)
.SetAutoCancel(true)
.Build();

本件とは恐らく関係無い物の、Android Keystore API は Thread Safe が保証されて居ないので同時実行制御を行った方が良さそうです。

https://weidianhuang.medium.com/how-to-write-thread-safe-code-for-android-keystore-40a0fa17f416

ローカル通知からアプリを起動するとHomePageを経由せずに「過去14日間の接触」(ContactedNotifyPage ?)画面は開けますか?

はい。開くことができます。

Android Keystore API は Thread Safe が保証されて居ないので

たしかに、ここを見る限りセマフォ取ってませんね。

public class SecureStorageService : ISecureStorageDependencyService
{
private readonly string Alias = $"{AppInfo.PackageName}.securestorage";
private readonly int IvLength = 12;
private readonly int GcmTagLength = 128;
private readonly string KeyStoreType = "AndroidKeyStore";
private readonly string CipherTransformation = "AES/GCM/NoPadding";
private readonly ISharedPreferences sharedPreferences;
public SecureStorageService()
{
sharedPreferences = Android.App.Application.Context.GetSharedPreferences(Alias, FileCreationMode.Private);
}

SecureStorageService自体はasync対応に作っていないので、ExposureDataRepositoryのレベルで取ることを検討します。

public class ExposureDataRepository : IExposureDataRepository
{
private const string EMPTY_LIST_JSON = "[]";
private readonly ISecureStorageService _secureStorageService;
private readonly IDateTimeUtility _dateTimeUtility;
private readonly ILoggerService _loggerService;