データの最小化・仮名化・削除
現在の表・配布用データ・バックアップで、何が残り何を復旧できるか説明します。
説明
必要のない個人情報を保存・配布しないことと、保存する情報を暗号化することは別の対策です。名前を別の値へ置き換えても、他の資料との対応から本人が分かる場合があります。
架空の顧客と注文を使い、集計だけを出す方法と仮名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 / 公開試料と固定時刻
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)]
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)]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)]
Truebackup = 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)]
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)]
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')]
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
changed = bytes([sealed[0]^1])+sealed[1:]
print(unseal(changed))
backup.close()InvalidTag
演習完了