注意この実装では暗号化が弱いです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import Foundation import CryptoKit // MARK: - CryptoKit ラッパー final class CryptoKitManager { enum CryptoError: LocalizedError { case encryptionFailed case decryptionFailed var errorDescription: String? { switch self { case .encryptionFailed: return "暗号化に失敗しました" case .decryptionFailed: return "復号に失敗しました(パスワードが違う可能性があります)" } } } /// パスワード文字列から AES-256 対称鍵を導出(SHA-256) static func deriveKey(from password: String) -> SymmetricKey { let hash = SHA256.hash(data: Data(password.utf8)) return SymmetricKey(data: hash) } /// AES-GCM 暗号化 → nonce(12B) + ciphertext + tag(16B) を連結して返す static func encrypt(_ data: Data, password: String) throws -> Data { let key = deriveKey(from: password) let sealed = try AES.GCM.seal(data, using: key) guard let combined = sealed.combined else { throw CryptoError.encryptionFailed } return combined } /// AES-GCM 復号 static func decrypt(_ data: Data, password: String) throws -> Data { let key = deriveKey(from: password) do { let box = try AES.GCM.SealedBox(combined: data) return try AES.GCM.open(box, using: key) } catch { throw CryptoError.decryptionFailed } } } |
使い方
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import Foundation // MARK: - テーブルデータの永続化 final class TableDataStore { private static let fileName = "tableData.enc" private static var fileURL: URL { FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent(fileName) } /// 保存(パスワードありなら暗号化、なしは平文 JSON) static func save(_ tableData: [[String]], password: String?) throws { let json = try JSONEncoder().encode(tableData) if let pw = password, !pw.isEmpty { let encrypted = try CryptoKitManager.encrypt(json, password: pw) try encrypted.write(to: fileURL, options: .atomic) } else { try json.write(to: fileURL, options: .atomic) } } /// 読み込み(ファイルがなければ nil) static func load(password: String?) throws -> [[String]]? { guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil } let raw = try Data(contentsOf: fileURL) let json: Data if let pw = password, !pw.isEmpty { json = try CryptoKitManager.decrypt(raw, password: pw) } else { json = raw } return try JSONDecoder().decode([[String]].self, from: json) } } |
| 項目 | 詳細 |
|---|---|
| 暗号化方式 | AES-GCM(256bit)- 認証付き暗号で改ざん検知も可能 |
| 鍵導出 | SHA-256(password) — シンプルな実装。より堅牢にするなら PBKDF2/HKDF を推奨 |
| 保存先 | Documents/tableData.enc(iCloud バックアップ対象) |
| パスワード変更時 | SettingsView でトグルON/OFFが変わるたびに saveTableData() を呼ぶと安全です |
CryptoKitManager、このクラスで暗号化した場合、同じこのアプリであって同じパスワードを使用した場合違うデバイスでも復号できますか?
はい、復号できます。 その理由を説明します。
なぜ別デバイスでも復号できるか
鍵の導出が決定論的
|
1 2 3 4 |
static func deriveKey(from password: String) -> SymmetricKey { let hash = SHA256.hash(data: Data(password.utf8)) return SymmetricKey(data: hash) } |
SHA-256(パスワード文字列) はどのデバイスで実行しても同じ結果になります。つまり同じパスワード → 常に同じ256bitの鍵が生成されます。
nonce(ランダム値)が暗号文に含まれている
|
1 2 |
guard let combined = sealed.combined else { ... } // combined = nonce(12B) + ciphertext + tag(16B) |
AES-GCM の combined は nonce・暗号文・認証タグをひとまとめ にしたものです。復号側は combined から nonce を自動的に取り出すので、デバイス間で別途 nonce を共有する必要がありません。
| 条件 | 結果 |
|---|---|
| 同じアプリ・同じパスワード・同じデバイス | ✅ 復号できる |
| 同じアプリ・同じパスワード・別デバイス | ✅ 復号できる |
| 同じアプリ・違うパスワード | ❌ 復号失敗(CryptoError.decryptionFailed) |
暗号化ファイル(tableData.enc)さえ共有できれば(iCloud Drive 経由など)、同じパスワードで別デバイスからも問題なく開けます。
ひとつ注意点
現在の実装は パスワードがそのまま鍵の素材 になっています。短いパスワードや辞書に載っているような単語はブルートフォース攻撃に弱いため、もし将来セキュリティを強化したい場合は SHA-256 の代わりに PBKDF2 や Argon2 などの鍵導出関数(KDF)を使うとより安全です。ただし現状の用途(個人メモアプリ)であれば十分実用的です。
コメント