Skip to content

Security: mirute02/onebotan

Security

docs/security.md

セキュリティとプライバシー

記録するものは「薬を飲んだ時刻」のような健康情報になりうるデータなので、 「端末の外に出さない」「他のアプリから触らせない」を基本方針にしている。

1. データは端末の外に出ない

項目 対策
ネットワーク INTERNET 権限を宣言していない。通信するコード自体がなく、APK のマニフェストで誰でも確認できる
権限 宣言しているのは VIBRATE(ウィジェットを押したときの振動用。インストール時に自動許可される通常権限で、個人情報には触れない)だけ
解析・広告・クラッシュ収集 入れていない。依存は AndroidX / Material / Kotlin coroutines のみ
クラウドバックアップ allowBackup="false" に加え、dataExtractionRules / fullBackupContent で全ドメインを除外
端末間移行 同上(allowBackup だけでは一部メーカー端末の移行を止められないため明示的に除外)
絵文字フォント androidx の EmojiCompat 初期化を外している(Play 開発者サービスへのフォント要求を避けるため)
ログ 記録内容・ボタン名を Log に出していない

データはアプリ専用領域の SQLite(databases/onebotan.db)にだけ保存される。 Android 10 以降はファイルベース暗号化が必須のため、端末ロック中は OS によって暗号化されている。 アプリ独自の追加暗号化はしていない(ウィジェットがロック解除直後から動く必要があり、 鍵を端末内に置く以上、得られる保護が小さいため)。

2. 他のアプリから記録を追加・閲覧させない

経路 対策
ブロードキャストで記録を偽装 ButtonWidget / BoardWidget / CalendarWidget は exported="false"。他アプリからは届かない(CalendarWidget は描くだけで記録しない)
PendingIntent の横取り・書き換え すべて明示的 Intent + FLAG_IMMUTABLE
別ウィジェットのボタンを押させる ウィジェットごとに requestCode と data URI(onebotan://widget/<id>/...)を分けている。extras だけで区別すると PendingIntent が共有されてしまうため
Intent に tracker id を載せて任意のボタンを押させる 押下時の記録先は Intent ではなくDB のウィジェット割り当て表 / ボード配置表から引く。ボードは (widgetId, 枠番号) ごとに別の PendingIntent
履歴・編集画面を開かせる HistoryActivity / EditActivity / CalendarActivity は exported="false"。ウィジェットの「設定」から開くのはアプリ自身
設定画面を任意の widgetId で開かせる WidgetSetupActivity / BoardEditActivity は APPWIDGET_CONFIGURE のため exported 必須。起動時に getAppWidgetIds() でそれぞれ自分のウィジェットか検証し、違えば即終了
日付更新のアラームを悪用 深夜 0 時の再描画は自分の非公開レシーバー宛ての明示的・FLAG_IMMUTABLE な PendingIntent。届いても全ウィジェットを描き直すだけで、記録には触れない。AlarmManager.set()(非正確・非ウェイクアップ)なので権限も不要
SQL インジェクション 全クエリがバインド引数。ユーザー入力を SQL 文字列に連結していない

ライブラリが追加するコンポーネント:

  • ProfileInstallReceiver(androidx.profileinstaller): exported だが android.permission.DUMP 必須のため、adb/シェル以外からは呼べない
  • DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION(androidx.core): signature 保護の内部用権限

3. エクスポート

  • CSV は Storage Access Framework(ファイル保存ダイアログ)で書き出す。ストレージ権限は不要
  • ファイル名は onebotan-<日付>.csv 固定。ボタン名(例: 薬の名前)をファイル名に含めない
  • 表計算ソフトで開かれる前提で、= + - @ などで始まるセルは先頭に ' を付けて数式として実行されないようにしている(CSV インジェクション対策)
  • 読み込みも Storage Access Framework(ファイル選択)で、利用者が選んだファイルだけを読む。20MB を超えるファイルは読まない
  • 読み込みは内容を数えて確認を出してから、1 つのトランザクションで追加する。失敗すれば何も変わらない。 記録を消したり上書きしたりはせず、同じ記録(ボタン・時刻・種類が同じ)は二重に追加しない
  • 名前は保存時と同じ Limits.cleanName を通し、SQL にはバインド引数で渡す

4. 記録の完全性

  • タップで記録が消えることはない。判定は純粋関数 PressRules.decide にまとめ、タップ列を再生する 単体テストで「記録は増えるか変わらないだけ」を確認している
  • 例外は 1 秒以内の 2 回目のタップ(指のぶれ)だけで、記録せず弱い振動で知らせる
  • 取り消しはアプリの履歴画面でのみ行う(ウィジェットは記録するだけ)

5. 画面に見えるもの(仕組み上の限界)

  • ホーム画面のウィジェットは、端末を見た人には見える。 既定ではボタンの下に最終記録時刻だけが出る。 メニュー「ウィジェットの文字」で「表示しない」にすれば、マーク(ピクトグラム)以外の情報は出ない。 名前を表示する設定にする場合は、知られたくない内容を名前にしない(例: 「朝のアレ」)
  • 端末のロックを破られた場合、アプリを開けば記録は見える

6. 公開・配布

  • 署名鍵はリポジトリに入れない(.gitignore で *.jks / *.keystore / keystore.properties を除外)。 パスワードもどこにも保存しない。tools/release.sh は apksigner にその場で入力させる
  • tools/release.sh はデバッグ鍵で署名された APK を公開用として出さない(証明書が CN=Android Debug なら停止)
  • 各リリースには、署名証明書の SHA-256 と APK の SHA-256 を載せる。利用者は apksigner verify --print-certs で照合できる
  • ビルド: Gradle Wrapper の配布物は distributionSha256Sum で固定、gradle-wrapper.jar は公式の チェックサムと一致を確認済み。リリースは R8 で縮小
  • CSV もリポジトリに入れない(*.csv を除外。実データの誤コミット防止)
  • コミットの作者メールは GitHub の noreply アドレスを使う
  • ツールが生成する Python のキャッシュ(実行環境の絶対パスを含む)は .gitignore で除外し、 過去の履歴からも取り除いた

There aren't any published security advisories