サウンドマインド
モハニ 子どものスマートフォン利用管理
保護者の制御命令がプッシュで届くサービスです。プッシュは失われ、子どもは権限をオフにします。その2つの状況でも統制が保たれるようにする仕事でした。
- 役割
- サーバー · 子どもアプリ · 保護者アプリ
- 期間
- 2025.07 ~ 現在
- 使用技術
- Spring BootReact NativeAndroidSamsung Knox SDKFCMRedis
改善結果
バージョンカウンター
プッシュ喪失時に端末が自ら追いつく復旧の仕組み
二重化
アクセシビリティ権限が切られてもブロックが維持される構造
01課題
保護者がアプリのブロックや就寝時間を設定すると、その命令がプッシュで子どもの端末に届きます。しかしプッシュには保証がありません。失われると、保護者の画面ではブロックがかかっているのに、端末では何も起きていない状態になります。別の軸の問題もありました。子どもがアクセシビリティ権限をオフにしてしまうと、ブロック自体が無力化されます。そしてプッシュが失われるのは偶然ではありません。画面が消えた端末は省電力モード(Doze)に落ち、プッシュ配送を遅らせたりまとめて届けたりし、バックグラウンドのポーリングも同じ理由で抑え込まれます。命令が届く保証のないチャネルの上で統制を設計する必要がありました。
02判断
プッシュを信頼しない方向へ設計を変えました。設定が変わるたびに子どもごとのバージョン番号をDBでアトミックに増やし、端末は定期的にその番号を比較して、遅れていれば自ら最新の設定を取得します。プッシュはその確認を早める合図にすぎません。権限の問題は別の監視サービスで二重化し、アクセシビリティがオフになると利用履歴を定期的に確認して非許可アプリをブロックし続けます。
- 設定バージョンカウンターをアトミックに増加させ、端末が比較・自己復旧
- Dozeでポーリングが抑えられることを前提に、画面点灯・アプリ切替・起動・プッシュ受信といった目覚めの瞬間ごとにバージョン確認とポリシー再評価を載せ、常時ポーリングなしで追いつくようにした
- アクセシビリティがオフになると監視サービスが利用履歴ベースでブロックを継続
- 旧バージョンのバンドルにはサーバーが応答ヘッダーで世代を識別し、強制アップデートを誘導
- デプロイはコードをプッシュすると自動でトラフィック切り替えまでつながるパイプラインとして構成
03設置初日の関門
この製品の統制はアプリを入れただけでは始まりません。子どもが端末を受け取りオンボーディングを終えて初めてポリシーがかかりますが、その間が最も緩いのです。権限を一つでも飛ばせばブロックは半分になり、メーカーの管理ライセンスが有効化されなければ、ファイアウォールやアプリ無効化といった強い手段がまるごと抜け落ちます。そこでオンボーディングを案内ではなく、通過しなければ端末が使えないゲートにしました。
- 権限要求を段階に分け、各段階で実際に付与されたかをシステムに再確認したうえでのみ次へ進めるようにしました。画面を進めたことと権限があることは別です
- オンボーディングが終わっても管理ライセンスが無効なら、画面全体を覆うオーバーレイで端末を封鎖し再有効化を促します。常時監視ではなく、画面点灯・アプリ切替・起動・ライセンス結果といった目覚めの瞬間にだけ再判定します
- ただしそのカバーが電話とSMSを塞いではいけません。通話アプリが前面にあるときや着信音が鳴っている間はカバーを即座に下げ、緊急連絡を保証します
- メーカーのポリシー上、個別の許可が必要な機能があるため、メーカーへ許可申請を出して承認を得る手続きまで導入プロセスに含めました
04設置後に開く迂回ルート
運用に入ると子どもたちは素早く隙間を見つけます。ブロックを正面から破るのではなく、ブロックが立っている足場を取り除く方向で、その足場はすべてAndroidの権限モデルとメーカーのポリシーの上にあります。そこで迂回が報告されるたびに、それがAndroidのどの挙動を利用したものかをまず確認し、同じ階層で塞ぐ手段を探しました。アプリ層で応急的に覆うと、次の迂回がすぐ出てきます。
迂回の試みと対応する層
迂回が来た層と同じ場所で塞ぎます
アクセシビリティ権限の解除
ブロック判定がアクセシビリティイベントに依存するため、権限を切られるとまるごと無力化されます。別の常駐サービスをデッドマンスイッチとして置き、アクセシビリティが切れたら猶予の後、利用履歴を1秒周期で走査して非許可アプリを止め続けます。画面分割・ポップアップ・PIPのように前面が一つではない場合まで含みます
ブラウザの暗号化DNS
ドメインブロックはDNS参照に依存しますが、Chromium系ブラウザが暗号化経路で参照しブロックを通り抜けることを実測しました。個別ドメインの一覧と分離した全体ブロックリストをサーバーから配信し、ブラウザアプリ自体を統制対象に加えました
アプリ内ブラウザ
ブロック済みのサイトを別アプリのWebViewで開く経路があるため、アプリ単位ではなく画面に表示されている内容を基準にも判定するようにしました
時計の巻き戻し
就寝時間ポリシーが端末時刻を基準にするため、時刻を変えれば外れます。メーカーのポリシーで時刻変更自体をロックしました
設定アプリ経由
権限やアプリ管理画面へ入る経路を、メーカーポリシーの設定封鎖で塞ぎました
アプリの強制終了
落としてから再起動するまでの隙を狙います。ログイントークンをネイティブのストレージに二重で保持し、再起動直後にポリシーを即座に復元します。重要な遠隔命令はアプリの画面コードを経由せずネイティブで直接処理するため、アプリが落ちていても届きます
パッケージの偽装と再インストール
名前だけ違う同じアプリ群を一つの保護グループにまとめ、個別対応ではなくグループ単位で判定します
05結果
プッシュが失われても端末が自ら追いつくため、保護者の画面と端末の状態が食い違ったまま残る問題がなくなりました。権限をオフにしてブロックを逃れる経路も塞がれ、サーバーの状態とログはモニタリングで一箇所から確認します。