[Swift] 64 メモアプリ製作 その16 データ暗号化実装 CryptoKit, キーチェーン

[Mac M2 Pro 12CPU, Ventura 13.6.1, iOS 17.1.2, Xcode 15.0]

データを暗号化してからCore Dataに保存するようにしました。暗号化キーはKeyChainに保存しています。

UIのコーディングでは毎度苦労させられますが、暗号化については情報は簡単に入手できて実装は比較的容易でした。

import CryptoKit
import Security
import Foundation

class CryptoManager {
    static let shared = CryptoManager()
    private let key: SymmetricKey
    
    private init() {
        if let keyData = KeychainManager.load(key: "myEncryptionKey") {
            // Keychainからキーを読み込む
            key = SymmetricKey(data: keyData)
        } else {
            // 新しいキーを生成してKeychainに保存する
            let newKey = SymmetricKey(size: .bits256)
            let newKeyData = newKey.withUnsafeBytes { Data($0) }
            let status = KeychainManager.save(key: "myEncryptionKey", data: newKeyData)
            if status == noErr {
                print("新しいキーをKeychainに保存しました。")
                key = newKey
            } else {
                fatalError("キーの保存に失敗しました。")
            }
        }
    }
    
    func encrypt(_ text: String) -> String? {
        guard let data = text.data(using: .utf8) else { return nil }
        do {
            let sealedBox = try AES.GCM.seal(data, using: key)
            return sealedBox.combined?.base64EncodedString()
        } catch {
            print("Encryption error: \(error)")
            return nil
        }
    }

    func decrypt(_ encryptedText: String) -> String? {
        guard let data = Data(base64Encoded: encryptedText),
              let sealedBox = try? AES.GCM.SealedBox(combined: data) else { return nil }
        do {
            let decryptedData = try AES.GCM.open(sealedBox, using: key)
            return String(data: decryptedData, encoding: .utf8)
        } catch {
            print("Decryption error: \(error)")
            return nil
        }
    }

}

class KeychainManager {
    static func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]

        SecItemDelete(query as CFDictionary) // 既存のアイテムを削除
        return SecItemAdd(query as CFDictionary, nil)
    }
    
    static func load(key: String) -> Data? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue!,
            kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        if status == noErr {
            return item as? Data
        }
        return nil
    }
}