kikuta@dev:~$cat ./notes/spex-privacy-by-architecture.md
·12 min·kikuta

Spreadsheet拡張の診断ログをどう設計したか

セル値・数式・URLを診断ログへ混入させないため、privacy ruleをloggerの型とschemaへ落とし込む。

Google Sheets向けChrome拡張Spexを作っています。

Spreadsheet上で動く拡張機能を公開するとき、かなり気になるのがPrivacyです。

ユーザーから見れば、extensionがGoogle Sheetsへアクセスできる以上、

  • セルの値を読んでいるのか
  • 数式を保存しているのか
  • Spreadsheet URLを送信しているのか
  • AnalyticsやTelemetryがあるのか

は分かりません。

そこでSpexでは、Privacy Policyへ「送りません」と書くだけではなく、そもそも不要なデータを持ちにくい実装構造にすることを重視しました。

この記事では、機密性の高い業務データを扱いうるbrowser extensionで、DiagnosticsとPrivacyをどう両立させたかを書きます。

「アクセスしない」と「外部送信しない」は違う

最初に整理したのがここです。

SpexはGoogle SheetsのUIへキーボード操作を提供するため、ページのDOMや操作に必要なstateへアクセスします。

したがって、公開文書で、

SpexはGoogle Sheetsの情報へアクセスしません

と書くのは正確ではありません。

一方で、現行Spexには利用データやdiagnosticsを受け取る独自backend serverはなく、analytics / telemetry uploadも実装していません。

つまり正確な境界は、

Google Sheets UI / DOM / required stateへはアクセスする

しかし
Spreadsheet contentをSpex backendへ送信する仕組みは持たない

です。

Privacy説明は「安心させる表現」より、実装との一致を優先しました。

Diagnosticsは必要だった

外部Webアプリ上で動くextensionは、host側の変更で壊れることがあります。

そのため、

  • locatorが見つからない
  • command verificationが失敗した
  • unexpected contextだった
  • extension stateが不整合になった

といったdiagnosticsは欲しくなります。

しかし、generic loggerにDOM objectやcontext objectを丸ごと渡せる設計にすると、将来うっかり、

  • Spreadsheet URL
  • cell value
  • formula
  • selection content

などを保存する可能性があります。

「今は保存していない」だけでは少し弱いと考えました。

Generic objectを保存できないschemaにする

Spexのdiagnosticsは、入力schema自体をSpex-owned metadataへ限定しています。

概念的には、

type DiagnosticInput = {
  level: "warning" | "error";
  source: DiagnosticSource;
  operation: string;
  message: string;
  commandId?: CommandId;
  mode?: string;
};

のようにします。

逆に、

context: unknown
metadata: Record<string, unknown>
dom: Element

のような何でも入るescape hatchを作りません。

これにより、developerが便利だからとDOM snapshotをそのままloggerへ入れることを型レベルで難しくします。

Privacy ruleをdocumentationだけでなくAPI shapeへ落とす考え方です。

保存するものをinventory化する

Spexでlocal storageへ保存しているものは、カテゴリとして明示しています。

Settings

  • KeyTips設定
  • Toast duration
  • keymap profile
  • theme
  • locale preference

Onboarding

  • tutorial completed / dismissed state

Diagnostics

  • timestamp
  • level
  • source
  • operation
  • message
  • 限定的なSpex command/status metadata
  • extension version

一方、diagnostics schemaに持たないものを明示します。

spreadsheet URL
cell value
formula
selection contents
arbitrary document object

「何を保存するか」だけでなく、何を保存しないかもinventoryとして管理するようにしています。

Host permissionも機能境界に合わせる

PermissionsもPrivacy設計の一部です。

Spexが対象とするhost accessはGoogle Sheets spreadsheet URLへ限定します。

https://docs.google.com/spreadsheets/*

「将来便利かもしれない」から広いhost permissionを先に取ることはしません。

権限を最小化すると、ユーザーへの説明だけでなく、実装上も触れられるsurfaceを減らせます。

Network APIをRelease時にscanする

「今backendがない」ことは、一度確認して終わりにはしません。

将来別機能を追加したとき、意図せずnetwork transmissionが増える可能性があります。

そのためrelease checklistでは、sourceに、

fetch(
XMLHttpRequest
WebSocket
sendBeacon

などのoutbound network APIが追加されていないかscanする方針にしています。

もちろん文字列grepだけでsecurity auditが完了するわけではありません。

目的は、Privacy boundaryが変わる変更をrelease時に見逃しにくくするgateを作ることです。

Privacy Policyと実装変更を同じ変更単位にする

将来analyticsやcloud serviceを追加する可能性をゼロとは言いません。

重要なのは、追加するときに、

implementation
privacy disclosure
store declaration
website privacy policy

を別々のタイミングで更新しないことです。

Spexではnetwork transmissionを追加するなら、user disclosure / Chrome Web Store declaration / privacy policyも同じ変更で更新する、というruleにしています。

実装だけ先に出て、policyが後追いになる状態を避けます。

Diagnosticsはローカルだから何でも保存してよい、ではない

データをserverへ送らずlocal storageだけに置くと、Privacy上はかなり楽になります。

ただし「ローカルだから何を保存してもよい」とは考えていません。

例えばセル値やformulaを診断ログへ入れると、

  • support時にユーザーがログを共有する
  • browser profile backupへ含まれる
  • 他のdiagnostic export機能を将来作る

といった経路で、当初想定しなかった移動が起こり得ます。

そのため最初から、diagnosticsにSpreadsheet contentを必要としない設計を目指しました。

User-facing errorとDiagnostic detailを分ける

Privacyとは少し別ですが、error handlingでも同じ分離が効きます。

ユーザーには、

Google Sheetsで結果を確認できませんでした

程度の意味のあるmessageを出します。

内部diagnosticsには、

source
operation
commandId
verification status

などを残します。

raw selectorや内部action名を通常Toastへ露出しません。

User surfaceとdeveloper diagnosisを分けることで、表示を分かりやすくしつつ、不要なhost情報を蓄積する誘惑も減ります。

Privacyは機能制限でもある

SpexではSpreadsheet contentのAI分析などを意図的に提供していません。

これはPrivacy Policy上の都合だけではなく、製品のsingle purposeを小さく保つためでもあります。

Keyboard operation layer

という範囲に製品を絞ると、

  • 必要permission
  • 必要storage
  • diagnostics schema
  • userへの説明

も小さくできます。

機能を増やさない判断が、Privacyと保守性の両方に効いています。

「Privacy by Architecture」として考える

今回の設計で意識したのは、Privacyを最後に書くpolicy documentではなく、次の組み合わせとして扱うことでした。

Data inventory
+ Narrow types
+ Minimal permissions
+ No generic diagnostic payload
+ Release network audit
+ Disclosure synced with implementation

この構造なら、Privacy Policyの文章と実装が乖離したときに気づきやすくなります。

「収集しません」という宣言を信じてもらうには、できるだけ収集しづらい構造をコード側に作るのが一番分かりやすいと感じました。

Spexで実際に使っています

SpexはGoogle Sheets上で動くためhost UIへアクセスしますが、現行版にはSpex独自backend、Telemetry、Analyticsはなく、diagnosticsは限定schemaで端末内に保存します。

業務データを扱うWebアプリ向けextensionを作る場合、Privacy文言を書く前に「loggerへ何を渡せる設計になっているか」を見るのはかなり有効だと思います。