minulo は、Instagram や YouTube のウェブ版を WKWebView で開いてリールやショートを消す iPhone アプリで、利用者の ID・パスワードや見ていたページを開発者が受け取らない形で作っています(消す機能は無料)。ログインは SNS のページで直接させ、Cookie は iOS がアプリごとに分けた場所に置き、ページに入れるスクリプトは何も外へ送らず、エラーログからは URL と名前を消してから送ります。この記事では、この4か所の作り方を書きます。(この記事は minulo の開発者が書いています)
minulo の作者の北林です。同じ形のアプリを作る人向けに、4か所それぞれの注意点を書きます。コードは説明用の例で、minulo のものそのままではありません。リールを隠す側の話は YouTube のショートの記事 に書いたので、ここでは「触らない」ことに絞ります。
1. ログインは SNS のページで、アプリは欄を読まない
ネイティブの欄で ID とパスワードを受け取り、JS でフォームに流し込む作りにはしません。SNS のログインページをそのまま出し、そこで入力してもらいます。値はページから SNS へ直接送られ、アプリのコードは通りません。
気をつけるのは途中で開くページです。Google や Apple のログイン、reCAPTCHA の埋め込みを止めると通りません。decidePolicyFor で開く先を絞るときは、メインフレームと iframe を分けます。
func webView(_ webView: WKWebView,
decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let host = action.request.url?.host else { return decisionHandler(.cancel) }
let isMain = action.targetFrame?.isMainFrame ?? true
if isMain {
// 表示してよいのは対応する SNS とログイン画面だけ
decisionHandler(allowedMainHosts.contains { host == $0 || host.hasSuffix("." + $0) } ? .allow : .cancel)
} else {
// reCAPTCHA など、ログインに要る埋め込みは通す
decisionHandler(.allow)
}
}
新しいウィンドウを開くリンクは WKUIDelegate の createWebViewWith でも同じ判定をします。
2. Cookie は iOS がアプリごとに分けた場所に置く
WKWebsiteDataStore.default() を使うと、Cookie はそのアプリの中だけに保存され、ほかのアプリからは読めません。サーバーに同期しないので、開発者には届きません。
やりがちなのは、httpCookieStore から Cookie を取り出して自前で保存し直すことです。守らなくていい値を自分のコードが持つことになります。ストアに任せて触らないのが安全です。
3. 注入スクリプトは分けた世界で動かし、送り口を作らない
おすすめかどうかを見分けるため、スクリプトは画面の文字を読みます。読むこと自体は避けられないので、外へ出す道を作らないことで守ります。
-
WKUserScriptはWKContentWorld.world(name:)でページと別の世界で動かす。ページ側からこちらの変数に手が届かなくなります。 -
WKScriptMessageHandlerで読んだ文字や URL を渡す口は作らない。要るなら「隠した数」だけにします。 - 注入スクリプトから自分のサーバーへ
fetchしない。
4. エラーログからは URL・@名前・メールを消してから送る
見落としやすいのがエラーログです。例外の文には、開いていたページの URL や @ユーザー名 が混ざります。そのまま送ると閲覧の記録を送るのと同じです。minulo は送る前に URL・@ の名前・メールを伏せ、長さも切っています。
func scrub(_ s: String) -> String {
var t = s
t = t.replacingOccurrences(of: #"\b[a-z][\w+.-]*://\S+"#, with: "<url>", options: [.regularExpression, .caseInsensitive])
t = t.replacingOccurrences(of: #"[\w.+-]+@[\w-]+\.[\w.-]+"#, with: "<email>", options: .regularExpression)
t = t.replacingOccurrences(of: #"@[\w.]{2,}"#, with: "@<name>", options: .regularExpression)
return String(t.prefix(160))
}
メールを先に消すのがコツです。逆にすると @ 以降だけが名前として消え、アドレスの前半が残ります。http 以外のスキームも伏せます。
App Store の「アプリのプライバシー」は開発者の申告なので、申告と実際に送るものが合っているかは自分で確かめます。
公式アプリはどうするか
iPhone では、公式アプリの中は変えられません。minulo は公式アプリを Screen Time(FamilyControls)で止めます。選んだアプリは中身の分からないトークンで渡るので、ここでも開発者には分かりません。
minulo の限界
作者として限界も書きます(minulo にできないこと より)。
- リールやショートが消えるのは minulo の中で開いたときだけで、公式アプリの中は変わりません。
- 公式アプリを止める機能は、無料では1回2時間までです。毎晩の予定で自動で止めるのは Pro です。
- 公式アプリを止めている間は、そのアプリの通知が届きません。minulo 自身も通知を送りません。
- ストーリーズの投稿・ボイスメッセージ・通話は、ウェブ版に無いので使えません。
- iPhone のアプリで、Mac 版と Chrome・Safari の機能拡張もあります。Android 版はありません。
送るもの・残すものの全体は minulo は安全? にあります。別のやり方をしている方の話も聞きたいです。