一覧SC-12-3 · 境界設計と情報保護

データの最小化・仮名化・削除

現在の表・配布用データ・バックアップで、何が残り何を復旧できるか説明します。

説明

必要のない個人情報を保存・配布しないことと、保存する情報を暗号化することは別の対策です。名前を別の値へ置き換えても、他の資料との対応から本人が分かる場合があります。

架空の顧客と注文を使い、集計だけを出す方法と仮名IDを使う方法を比較します。保存期限の削除後にバックアップへ残る情報を調べ、復元時の期限確認と暗号文の改変検出を試します。

現在の表・配布用データ・バックアップで、何が残り何を復旧できるか説明します。

1元の情報と注文を読む

AliceとBobは架空の資料です。expiresはこの演習の保存期限で、法定の保持期間ではありません。

from sc12 import * db = privacy()架空の顧客・注文資料を作る print(db.execute("SELECT * FROM people ORDER BY id").fetchall()) print(db.execute("SELECT * FROM orders ORDER BY person,amount").fetchall())

氏名・メール・地域と、注文の明細を保持しています。集計に必要な項目を先に決めることで、配る範囲を絞れます。

2地域別の集計だけを出す

目的が地域別売上なら、氏名やメールを含めずに合計できます。

print(db.execute("SELECT p.city,SUM(o.amount),COUNT(DISTINCT p.id)必要な集計値だけを選ぶ FROM people p JOIN orders o ON p.id=o.person GROUP BY p.city").fetchall())

地域A、売上600、人数2です。ただし少人数の集計も外部情報と結び付くことがあるため、この結果だけで匿名性を保証するものではありません。

3同じ人物を同じ仮名へ置き換える

HMACで人物IDを別の値へ変換します。教材の鍵は公開試料なので秘密には使えません。

rows = db.execute("SELECT person,amount FROM orders ORDER BY person,amount").fetchall() print([(pseudonym(person)人物IDを仮名へ変換する,amount) for person,amount in rows]) print(pseudonym(7) == pseudonym(7)同一人物の対応が残ることを確認する)

同じ人物の二つの注文は同じ仮名で結び付きます。対応関係や鍵、周辺情報を持つ相手には再識別される可能性が残ります。

4削除前のバックアップを作る

SQLiteのbackup APIで別のデータベースへ複製します。元の表とは独立した状態になります。

backup = sqlite3.connect(":memory:") db.backup(backup)元のデータベースを独立した保存先へ複製する print(backup.execute("SELECT id,email,expires FROM people ORDER BY id").fetchall())

バックアップには氏名やメールを含む元の資料が残っています。集計結果だけを保存したわけではありません。

5期限が来た人物と注文を削除する

現在時刻を10とします。この例では期限以下の人物の注文を先に消し、人物の行も削除します。

with db: db.execute("DELETE FROM orders期限切れ人物の注文を削除する WHERE person IN (SELECT id FROM people WHERE expires<=10)") db.execute("DELETE FROM people期限切れの人物を削除する WHERE expires<=10") print(db.execute("SELECT id FROM people").fetchall(),db.execute("SELECT * FROM orders").fetchall())

現用表にはBobと注文300だけが残ります。関連する明細も同じトランザクションで削除しています。

6バックアップと復元対象を比較する

バックアップをそのまま戻すとAliceの情報も戻ります。復元対象には現在の保存期限を適用します。

print(backup.execute("SELECT id,email FROM people ORDER BY idバックアップに残る元データを確認する").fetchall()) print(backup.execute("SELECT id,email FROM people WHERE expires>10復元時の期限を適用する ORDER BY id").fetchall())

元のバックアップには2人、期限を適用した抽出にはBobだけが含まれます。削除方針を現用表だけで終わらせず、バックアップの期限と復旧手順にも反映します。

7保存する資料を認証付き暗号化する

残っているバックアップ内容を公開試料鍵で暗号化します。AADにbackup-7を付けて対応付けます。

data = json.dumps(backup.execute("SELECT * FROM people ORDER BY id").fetchall(),separators=(",",":")).encode() sealed = AESGCM(bytes(16)).encryptバックアップ内容を認証付き暗号化する(bytes(12),data,b"backup-7") print(len(data),len(sealed),unseal(sealed) == data元情報を復元できることを確認する)

暗号化しても、鍵を持つ側は期限切れの元情報まで復元できます。暗号化は情報削減や期限管理の代わりにはなりません。

8保存中の改変を検出する

暗号文の先頭1ビットを反転します。

changed = bytes([sealed[0]^1保存した暗号文を変更する])+sealed[1:] print(unseal(changed)復元前に整合性を検証する) backup.close()

InvalidTagです。改変検出、復号鍵の管理、保存期限、配布項目の削減はそれぞれ別の目的を持つ対策です。

まとめ

目的に必要な項目へ、保存・配布する情報を絞ります。

仮名化で同一人物の対応が残ることも考慮します。

現用表の削除、バックアップの期限、復旧時の条件、暗号化を組み合わせます。

操作履歴と解説を振り返れます。

Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻

1
from sc12 import * db = privacy() print(db.execute("SELECT * FROM people ORDER BY id").fetchall()) print(db.execute("SELECT * FROM orders ORDER BY person,amount").fetchall())
[(7, 'Alice', 'alice@example.test', 'A', 10), (8, 'Bob', 'bob@example.test', 'A', 20)]
[(7, 100), (7, 200), (8, 300)]
2
print(db.execute("SELECT p.city,SUM(o.amount),COUNT(DISTINCT p.id) FROM people p JOIN orders o ON p.id=o.person GROUP BY p.city").fetchall())
[('A', 600, 2)]
3
rows = db.execute("SELECT person,amount FROM orders ORDER BY person,amount").fetchall() print([(pseudonym(person),amount) for person,amount in rows]) print(pseudonym(7) == pseudonym(7))
[('5364c7de1528697d', 100), ('5364c7de1528697d', 200), ('befb1f5d2f514026', 300)]
True
4
backup = sqlite3.connect(":memory:") db.backup(backup) print(backup.execute("SELECT id,email,expires FROM people ORDER BY id").fetchall())
[(7, 'alice@example.test', 10), (8, 'bob@example.test', 20)]
5
with db: db.execute("DELETE FROM orders WHERE person IN (SELECT id FROM people WHERE expires<=10)") db.execute("DELETE FROM people WHERE expires<=10") print(db.execute("SELECT id FROM people").fetchall(),db.execute("SELECT * FROM orders").fetchall())
[(8,)] [(8, 300)]
6
print(backup.execute("SELECT id,email FROM people ORDER BY id").fetchall()) print(backup.execute("SELECT id,email FROM people WHERE expires>10 ORDER BY id").fetchall())
[(7, 'alice@example.test'), (8, 'bob@example.test')]
[(8, 'bob@example.test')]
7
data = json.dumps(backup.execute("SELECT * FROM people ORDER BY id").fetchall(),separators=(",",":")).encode() sealed = AESGCM(bytes(16)).encrypt(bytes(12),data,b"backup-7") print(len(data),len(sealed),unseal(sealed) == data)
77 93 True
8
changed = bytes([sealed[0]^1])+sealed[1:] print(unseal(changed)) backup.close()
InvalidTag

演習完了