決済用の Webhook が失敗し続けていた。プロバイダーのサポートフォームはダッシュボードのスクリーンショット(スクショ)を求めてきた。そこにはエラーだけでなく、有効な秘密鍵、アカウント所有者のメールアドレス、イベントログに残った顧客名まで写っていた。そこで画像編集ソフトで鍵にモザイクをかけ、メールアドレスにはぼかしを入れて、チケットを送信した。
3週間後、そのチケットは公開ステータス投稿で引用された。鍵にかけたモザイクは、既知のフォント・既知のサイズ・既知のブラウザでレンダリングされた等幅テキストに対する、16ピクセルのボックス平均化セルの並びだった。これはまさに、モザイク除去ツールが対象として作られた入力そのものだ。
モザイク加工とぼかしは、ぱっと見では中身を隠せる。しかし元の情報の多くをそのまま保持している。画像にモザイクをかけたり、一部をぼかしたり、スクリーンショットを黒塗りしたりする前に、それぞれのエフェクトがピクセルに何をしているのか、どれが復元可能なのか、正しく処理した後でも何が漏れるのかを知っておくと役に立つ。
画像の一部を隠す必要があるとき
適切なエフェクトは、隠したい内容がテキストかどうかで決まる。
| 状況 | 隠す必要があるもの | 使うべきエフェクト |
|---|---|---|
| AI チャット、サポートチケット、フォーラム投稿用のスクショ | API キー、トークン、パスワード、アカウント ID | 黒塗り |
| 本番データが写り込んだバグレポート | 顧客名、メールアドレス、注文番号、社内ホスト名 | 黒塗り |
| チュートリアルやドキュメント用のスクリーンショット | 自分のメールアドレス、ワークスペース名、請求先情報 | 黒塗り |
| Slack や LINE のスクショを共有する前、個人情報保護の文脈 | 個人名、社内チャンネル名、ユーザー ID などの個人情報 | 黒塗り |
| ブログ用の街並み・イベント・オフィスの写真 | 通行人の顔 | 大きめのブロックサイズでモザイクかぼかし、本人特定が重要な場合は黒塗り |
| 車や駐車場の写真 | ナンバープレート | 黒塗り(プレートは標準的な書体の短いテキストのため) |
| プレースホルダー、サムネイル、レトロ風アートワーク | 見た目のための画像全体 | 「画像全体に適用」でモザイク |
| ネタバレや閲覧注意のプレビュー画像 | ソフトにした画像全体 | 「画像全体に適用」でぼかし |
人間が読める文字情報は黒塗りを使う。モザイクとぼかしは、テキスト以外のものや見た目の演出のために使う。
3つのエフェクトの仕組み
Pixelate Image ツールは画像を Canvas 2D コンテキストに読み込み、その上に1つ以上の矩形をドラッグして描ける。選択したエフェクトはすべての矩形に一括で適用される。設定を変更するたびに、デコード済みの元画像から描き直してエフェクトを再適用するため、結果が前のバージョンの上に重なって蓄積することはない。
モザイク(ブロック平均化)
モザイクは、選択した範囲を指定したブロックサイズ(4〜64 ピクセル、デフォルト 16)の正方形セルに分割する。各セルについて、内側の全ピクセルの赤・緑・青・アルファ値を合計し、ピクセル数で割って、セル全体をその平均色で塗りつぶす。半透明のピクセルでは色チャンネルをアルファで重み付けするため、完全に透明なピクセルがセルを暗くすることはない。
知っておくべき点が2つある。
- グリッドは各範囲の左上角から始まる。 異なる位置に描いた2つの矩形は、互いにも画像の原点にも揃わないグリッドを持つ。
- 端のセルは合わせて縮む。 範囲の幅が 100 ピクセルでブロックサイズが 16 の場合、最後の列のセルは幅 4 ピクセルになる。これらの端のセルは自分自身のピクセルだけを平均化し、矩形の外側のピクセルを読むことは決してない。
これは線形ボックスフィルターであり、出力される各セルは入力ピクセルの固定的で均等な重み付け平均になる。決定的な処理でもある。同じ入力と同じグリッドは常に同じ出力を生み、これこそが復元攻撃が依存している性質だ。
320 × 32 ピクセルの範囲を 16 ピクセルでモザイク処理すると、20 × 2 セル、つまり 40 個の平均色になる。等幅フォントのテキスト1行分としては、そこにどの文字があったかについて、かなり多くの情報が残っていることになる。
ぼかし(縮小してから拡大)
ぼかしエフェクトは 2〜40 ピクセルの半径(デフォルト 12)を使う。ガウシアン畳み込みは使っていない。代わりに、選択した範囲を ceil(width / radius) × ceil(height / radius) ピクセルの小さな一時キャンバスに高品質のスムージングを効かせて描画し、その小さな画像を再びスムージングを効かせながら元の範囲いっぱいに描き戻す。
見た目は柔らかい。セル同士の境界は補間によってなめらかになっている。しかし実際に保持されているデータを見ると、元画像のおおよそ radius × radius の正方形ごとに1サンプルであり、その半径と同じブロックサイズでモザイク処理した場合とほぼ同じ桁数の情報量になる。ぼかしとは、境界をなめらかにしたモザイクにすぎず、保持する情報量もほぼ同じだ。
モザイクと同様、ぼかしも選択した矩形の内側のピクセルしか読まないため、外側の色が滲み出ることはない。
黒塗り(置き換え)
黒塗りは、カラーピッカーで選んだ単一の不透明色(デフォルトは黒)で矩形を塗りつぶす。元のピクセルの平均値もサンプルも痕跡も一切残らない。
プレビューに表示される破線の枠は画面上にのみ描画される。ダウンロードやコピーの際は、画像とエフェクトを別のキャンバス上でこの枠なしに再描画する。
各エフェクトの後に何が残るか
| エフェクト | 範囲内に残るもの | 復元は可能か |
|---|---|---|
| モザイク(ブロックサイズ b) | b × b セルごとに1つの平均色 | テキスト:可能。研究や公開ツールで繰り返し実証されている。顔:学習済みモデルによって高い確率で特定可能 |
| ぼかし(半径 r) | r × r の正方形ごとにおよそ1つのスムージング済みサンプル | モザイクと同様。スムージングは情報を除去しない |
| 黒塗り | 何も残らない | 不可能 |
なぜモザイクとぼかしは復元されうるのか
どちらのエフェクトも非可逆だ。多数の元画像が同じブロックを生み出しうるため、数式を逆算することはできない。しかし攻撃手法はそもそも逆算しようとしない。候補を推測し、その候補に同じフィルターを適用し、結果を比較する。既知のフォントで書かれたテキストのように、あり得る元画像の候補数が少ない場合、この推測は非常によく機能する。
Depix:レンダリングしたアルファベットとブロックを照合する
Depix は 2020年12月に GitHub に公開された、作者 spipm によるプロジェクトだ。プロジェクト自身の説明によれば「モザイク化されたスクリーンショットから平文を復元する技術の PoC(概念実証)」である。線形ボックスフィルターでモザイク処理された画像を対象としており、これは Pixelate エフェクトが使っているのと同じ種類のフィルターだ。
使い方は、想定される文字の De Bruijn 列を含む検索用画像を、対象と同じフォント設定(同じ文字サイズ、似たフォント、同じ色)でレンダリングして作成する。Depix はその検索用画像をモザイク処理し、対象のスクリーンショット内のブロックと一致するブロックを見つけ、隣接するブロックを手がかりに候補を絞り込む。
README には制約も列挙されている。テキストはブロック境界に完全に揃っている必要があり、フォントや場合によっては画面設定が既知でなければならず、追加の圧縮がかかると機能しなくなる。GitHub のリポジトリは現在アーカイブされており、開発は Codeberg に移った。
Unredacter:オフセットを総当たりする
2022年2月、Bishop Fox の Dan Petro が Never Use Text Pixelation To Redact Sensitive Information を発表し、Unredacter というツールを公開した。対象としたのは Jumpsec の研究者が用意した、5 ピクセルのブロックでモザイク処理されたチャレンジ画像だ。Unredacter は1文字ずつ推測する。候補の文字をレンダリングし、同じようにモザイク処理を施し、対象の画像とどれだけ一致するかを測定する。テキストがブロックグリッドに揃っていなかったため、あり得るグリッドオフセットもすべて試した。ブロックサイズが 5 の場合、オフセットの候補数はわずか 25 通りだ。
本ツールでは、グリッドは描いた各矩形の角から始まるため、テキストとの位置関係は事実上ランダムになる。それでも Unredacter はすべてのオフセットを試す。
記事の推奨は明快だ。「テキストを黒塗りする必要があるときは、テキスト全体を覆う黒い帯を使うこと。それ以外の方法は絶対に使わないこと」。
隠れマルコフモデル:大きなブロック、ノイズ、ぼかしにも通用する
学術的な成果はこちらの方が先だ。Steven Hill、Zhimin Zhou、Lawrence Saul、Hovav Shacham による On the (In)effectiveness of Mosaicing and Blurring as Tools for Document Redaction は、Proceedings on Privacy Enhancing Technologies 2016年 第4号に掲載された。音声認識から借りてきた隠れマルコフモデルを使って、黒塗りされたテキストをモデル化する。
- この手法は、18 ポイントのフォントでレンダリングされた英文を、24 ポイントのモザイクの下からでも読める形で復元した。フォントサイズより粗いグリッドでも、テキストは安全にならなかった。
- 画質 0% の JPEG 圧縮で画像が劣化した後でも、読める形でテキストを復元できた。黒塗りした画像を低品質の JPEG として保存しても保護にはならない。
- ぼかされたテキストを攻撃する際は、まずぼかされた領域にモザイクを適用してから手法を適用する。ぼかしもテキストを保護しなかった。
論文の結論は明確だ。「モザイク処理とぼかしは、広く使われているにもかかわらず、テキストの黒塗り手法として実用に耐えない」。
顔も安全ではない
顔画像に関する関連研究は、Richard McPherson、Reza Shokri、Vitaly Shmatikov による Defeating Image Obfuscation with Deep Learning(arXiv、2016年9月)だ。彼らはニューラルネットワークを訓練し、モザイク処理、YouTube 方式のぼかし、P3 暗号化方式で保護された画像から、顔を特定し、物体や手書き数字を認識させた。
この攻撃は鮮明な顔を復元するわけではない。ぼかされた顔が既知の誰のものかを特定する。大きめのブロックサイズは、何気なく見た第三者が見知らぬ人物と気づくのを防ぐ。しかし、その人物の写真で学習済みのモデルが本人と一致させられないことまでは保証しない。本人特定が本当に重要な場合は、顔にも黒塗りを使うこと。
落とし穴とエッジケース
-
モザイク化したテキストは、どのブロックサイズでも復元されうる。 ブロックを大きくすれば推測は難しくなるが、上記の HMM の結果はフォントより粗いモザイクの下からでもテキストを復元した。どの設定であっても、テキストに対するモザイクは安全ではないと考えること。
-
ぼかしはモザイクより強力ではない。 見た目がなめらかだと、より多くの情報が失われたように感じる。しかしここでのぼかしは、同じサイズのモザイクとほぼ同量の情報を保持する縮小・拡大処理にすぎない。HMM 論文でも、ガウシアンぼかしされたテキストはまずモザイクに変換してから攻撃している。
-
小さいブロックサイズとぎりぎりの矩形。 通常の UI テキストに対して 4〜8 ピクセルのブロックサイズを使うと、単語の形が肉眼で読めてしまうことが多い。矩形には余白を持たせて描くこと。テキストの1ピクセル手前で止まった範囲では、背の高い文字の上部や、ディセンダー(下に伸びる部分)の下端が処理されないまま残る。本ツールは、縦横どちらかが 2 ピクセル未満の矩形を破棄するため、誤クリックで空の範囲が追加されることはない。
-
黒い帯は長さを漏らす。 テキストにぴったり合わせた黒帯は、そこに何文字あったかを示してしまう。等幅フォントであれば、それは正確な文字数になる。PIN のような短い機密情報には、テキストより幅広く帯を描くこと。
-
元ファイルに残るメタデータ。 写真には EXIF データ——カメラ機種、撮影日時、しばしば GPS 座標——が付随している。本ツールから書き出されるファイルは、Canvas API がピクセルのみから新規にエンコードしたものなので、元画像の EXIF・GPS・カメラ情報は一切書き込まれない。画像は EXIF の向き情報を適用した状態でデコードされるため、向きタグが失われた後でも、回転したスマホ写真は正しい向きで書き出される。それでもピクセルの外側に情報を残すものが2つある。
- ファイル名。 ダウンロードされるファイルは元のベース名を保ったまま
-pixelatedを付加する。たとえばpassport-jane-doe.jpgはpassport-jane-doe-pixelated.pngになる。貼り付けた画像はpasted-image-pixelatedという名前になる。ファイル名が個人を特定してしまう場合は、共有前にリネームすること。 - 元ファイル。 黒塗り処理は新しいファイルを作るだけだ。処理前の元画像はディスク上に、スクリーンショットフォルダに、あるいはクラウドの写真バックアップにも残ったままになる。
- ファイル名。 ダウンロードされるファイルは元のベース名を保ったまま
-
ファイルコンテナ内に隠れたデータ。 見えているピクセルが、ファイルが持つデータのすべてとは限らない。2023年、aCropalypse 脆弱性により、Google Pixel の Markup エディタ(CVE-2023-21036)と Windows のスニッピングツール(CVE-2023-28303)が、切り抜いた画像を元ファイルの上に、切り詰めずに上書きしてしまうことが判明した。その結果、切り抜く前の画像の一部がファイル末尾に残り、復元可能な状態になっていた。Canvas でのエクスポートにはこの問題はない。レンダリングされたピクセルから新規に構築されたファイルだからだ。しかし、上書き保存を行う別のエディタで画像を黒塗りした場合は、クリーンなコピーを書き出しておく価値がある。
-
スクリーンショットの他の場所に残る二次的な漏洩。 黒塗りした機密情報は、同じ画像の別の場所にも写り込んでいることが多い。
- ドキュメント名、チケットの件名、ユーザー名を表示するブラウザのタブタイトルやウィンドウタイトル。
- クエリ文字列にトークンやメールアドレスが含まれるアドレスバー。
- ショートカットを押した瞬間に画面上にあったオートコンプリートのドロップダウン、通知トースト、チャットのプレビュー。
- サムネイル。最近使ったファイルの一覧、画像プレビューのペイン、サイドバーのメッセージプレビューなど。
- ターミナル、ログ行、ネットワークパネルのさらに下の方に繰り返し表示された同じ鍵。
- 映り込み。Private Eye(Yan Long ほか、2022年)は、眼鏡に映り込んだテキストを 720p の Web カメラで撮影し、画面上でわずか 10mm の高さしかない文字でも 75% 以上の精度で復元できることを示した。デスクにいる人物の写真も、眼鏡・窓・光沢のある面に映り込んだモニターを捉えてしまうことがある。
すでに隠した範囲だけでなく、共有前に画像全体を見渡すこと。
-
1回のパスにつき1エフェクト、そして画像全体スイッチ。 選択したエフェクトは、すべての範囲に一括で適用される。同じ画像内で顔にモザイクをかけつつパスワードを黒塗りしたい場合は、まず一方のエフェクトを完了して結果をダウンロードし、そのファイルを再び読み込んでからもう一方のエフェクトを適用する。範囲は描いた後に移動・リサイズできない。取り消し(Undo)で直前の範囲を削除して描き直すこと。「画像全体に適用」がオンの間はエフェクトが画像全体に及び、範囲の描画は無効になる。チェックを外すと、描いた範囲が元に戻る。
-
アニメーション GIF は1フレームだけ書き出される。 ブラウザはアニメーション GIF を最初のフレームにデコードし、書き出されるのは静止画になる。これは仕様どおりの動作だ。機密情報が後のフレームに現れる場合は、先に動画や GIF ツールでそのフレームを抽出すること。
-
HEIC は Safari でしか開けない。 本ツールは、ブラウザがデコードできる形式であればどれでも受け付ける。一般的には PNG、JPEG、WebP、GIF、BMP、AVIF だ。Can I use によれば、HEIF/HEIC をサポートするのは macOS と iOS の Safari 17 以降のみだ。Chrome、Edge、Firefox では、HEIC 写真の読み込みはエラーになって失敗する。先に JPEG に変換するか、Safari を使うこと。
-
書き出し形式による違い。 PNG がデフォルトであり、スクリーンショットには最適な選択肢だ。可逆圧縮でテキストの輪郭がシャープなまま保たれる。JPEG は透過をサポートしないため、透明な範囲は白で塗りつぶされる。WebP の書き出しはブラウザの canvas エンコーダーに依存する。MDN の互換性データでは、Safari の canvas は WebP エンコーディングに対応していないとされている。Safari では、WebP を選んでも PNG として保存されるため、そこでは PNG か JPEG を選ぶこと。画像をコピーは常にクリップボードに PNG を置く。ブラウザがクリップボードへのアクセスをブロックした場合は、本ツールがその旨を表示するので、代わりにダウンロードを使うこと。
-
非常に大きな画像。 ブラウザには canvas サイズの上限がある。書き出しに失敗した場合、本ツールは「書き出せませんでした — もっと小さい画像で試してください」と表示する。リサイズしてから再度試すこと。
コードで実装する
同じ3つの処理は、どの画像ライブラリを使っても数行で書ける。
JavaScript:canvas 上でのブロック平均化
これは本ツールの Pixelate エフェクトと同じアルゴリズムだ。矩形の角に固定されたグリッド、合わせて縮む端のセル、RGBA に対する平均。ツール側は色をアルファで重み付けしており、結果が変わるのは半透明のピクセルを含む画像だけだ。getImageData と putImageData を使う。
// 2D canvas コンテキストの矩形 (x, y, w, h) をその場でモザイク処理する。
function pixelateRegion(ctx, x, y, w, h, block) {
const img = ctx.getImageData(x, y, w, h);
const d = img.data; // フラットな RGBA、1ピクセルあたり4バイト
for (let by = 0; by < h; by += block) {
for (let bx = 0; bx < w; bx += block) {
const bw = Math.min(block, w - bx); // 端のセルは合わせて縮む
const bh = Math.min(block, h - by);
const sum = [0, 0, 0, 0];
for (let yy = by; yy < by + bh; yy++) {
for (let xx = bx; xx < bx + bw; xx++) {
const i = (yy * w + xx) * 4;
for (let c = 0; c < 4; c++) sum[c] += d[i + c];
}
}
const avg = sum.map((s) => Math.round(s / (bw * bh)));
for (let yy = by; yy < by + bh; yy++) {
for (let xx = bx; xx < bx + bw; xx++) {
const i = (yy * w + xx) * 4;
for (let c = 0; c < 4; c++) d[i + c] = avg[c];
}
}
}
}
ctx.putImageData(img, x, y);
}
// 黒塗りは1回の呼び出しで済む:
// ctx.fillStyle = "#000"; ctx.fillRect(x, y, w, h);
// canvas のピクセルからクリーンなファイル(EXIF なし)を書き出す:
// canvas.toBlob((blob) => { /* download or upload blob */ }, "image/png");
getImageData は、CORS の許可なしにクロスオリジン画像を canvas に描画していた場合、セキュリティエラーを投げる。ローカルファイルやクリップボードから読み込んだ画像であれば、この問題は起きない。
Python:Pillow
Pillow にはボックスフィルターが組み込まれている。Image.Resampling.BOX で縮小すると各セルが平均化され、NEAREST で拡大すると、平均化された各ピクセルがそのまま平坦な正方形に戻る。Image.Resampling を使うには Pillow 9.1 以降が必要だ。
from PIL import Image, ImageDraw
def pixelate(img, box, block=16):
"""box = (left, top, right, bottom) の範囲をその場でモザイク処理する。"""
region = img.crop(box)
w, h = region.size
small = region.resize(
(max(1, round(w / block)), max(1, round(h / block))),
Image.Resampling.BOX, # 各セルの単純平均
)
img.paste(small.resize((w, h), Image.Resampling.NEAREST), box[:2])
def black_out(img, box, color="black"):
# rectangle() は右端と下端を含む
ImageDraw.Draw(img).rectangle(box, fill=color)
img = Image.open("screenshot.png").convert("RGB")
pixelate(img, (40, 60, 360, 140), block=16) # 写真に写った顔
black_out(img, (40, 200, 520, 232)) # API キー
img.save("redacted.png") # exif= 引数を渡していないため、EXIF は書き込まれない
目標サイズは丸められるため、セルは block ピクセルちょうどではなく、それに近い値になる。テキストに対しては、いずれにせよ black_out を使うこと。
Bash:ImageMagick
ImageMagick の -scale オプションは、縮小時に「ピクセルブロック平均化」を行い、拡大時にはピクセル複製を行う。つまり2段階でモザイク処理を実現できる。特定の範囲だけに適用するには、画像を複製し、その範囲を切り抜き、縮小・拡大してから元の位置に合成する。
# (40,60) の位置にある 320x128 の範囲を 16px ブロックでモザイク処理する: 320/16 = 20, 128/16 = 8
magick screenshot.png \
\( +clone -crop 320x128+40+60 +repage -scale 20x8! -scale 320x128! \) \
-geometry +40+60 -composite \
-fill black -draw "rectangle 40,200 520,232" \
-strip redacted.png
! は寸法を厳密に強制するため、範囲のサイズがブロックサイズの倍数であれば、各ブロックはちょうど 16 × 16 ピクセルになる。-draw "rectangle ..." の行が黒塗りに相当する。-strip は必ず残すこと。EXIF を含む、出力からプロファイルやコメントを除去してくれる。
比較
よくある代替手段として、オンラインのモザイク処理サービスとデスクトップエディタの2つがある。以下の表は、各製品自身のページやドキュメントに記載されている内容のみをまとめたものだ。
| ZeroTool Pixelate Image | ResizePixel Pixelate | GIMP Pixelize フィルター | |
|---|---|---|---|
| 画像の処理場所 | ブラウザタブ内。画像に対するネットワークリクエストは発生しない | ページには「サイトにアップロード」と記載。アップロードフォームはファイルをサイトへ送信する | 自分のコンピューター上(デスクトップアプリ) |
| エフェクト | モザイク、ぼかし、黒塗り | ページには「画像または画像の一部」をモザイク処理すると記載 | Pixelize、および GIMP の他のフィルター |
| 範囲選択 | 好きな数の矩形をドラッグ、または画像全体 | ブロックサイズを「画像の選択した部分」に適用 | GIMP 自身のツールを使用 |
| モザイクのオプション | ブロックサイズ 4〜64px | ブロックサイズ | ブロックの幅と高さ、オフセット X・Y、形状(正方形、円形、ダイヤモンド) |
| 対応する入力形式 | ブラウザがデコードできるものすべて | JPG、PNG、WEBP、GIF、TIFF、BMP | GIMP が対応する形式 |
| セットアップ | 不要 | 不要 | アプリケーションのインストールが必要 |
出典:ResizePixel の pixelate ページ、GIMP 3.0 の Pixelize ドキュメント。
認証情報が写ったスクリーンショットの場合、タブ内で処理することで、黒塗り前のバージョンが自分のマシンから外に出ることはなく、ZeroTool のページはアナリティクスや広告のスクリプトも読み込まない。円形のモザイク、精密なグリッドオフセット、さらなる編集が必要なら、GIMP の方がコントロールが豊富だ。
関連ツール
- Secret Redactor — テキストやログ内の API キーやトークンを、貼り付ける前にマスクし、返信では元に戻す
- EXIF Metadata Viewer — 元画像を共有する前に、カメラ・撮影日時・GPS データが正確に何を含んでいるかを確認する
- Image Compressor — 黒塗りしたスクリーンショットを添付する前にファイルサイズを削減する
参考文献
- MDN — Pixel manipulation with canvas
- MDN — createImageBitmap()
- MDN — HTMLCanvasElement.toBlob()
- MDN — CanvasRenderingContext2D.imageSmoothingQuality
- Hill, Zhou, Saul, Shacham — On the (In)effectiveness of Mosaicing and Blurring as Tools for Document Redaction (PoPETs 2016)
- McPherson, Shokri, Shmatikov — Defeating Image Obfuscation with Deep Learning (2016)
- Bishop Fox — Never Use Text Pixelation To Redact Sensitive Information
- Depix on Codeberg