【Swift】CryptoKitで実ファイルの暗号化・復号を行う

#概要
サンドボックス内に保存しているファイルについて、iOS13から追加されたフレームワーク「CryptoKit」を使って「暗号化・復号」する方法を紹介したいと思います。

暗号化・復号というとパディングやらなんやらで面倒な印象を受けますが、
CryptoKitを使うと簡単に相互に変換出来てしまいます。

参考プロジェクトを作ったので、こちらを元に紹介していきます。
https://github.com/o-mo-te/Swift_CryptoKit

※ CryptoKitはiOS13以降でしか動作しないのでご注意を！

#環境
Xcode: 11.3.1
Swift: Swift5

#SymmetricKeyの生成
まず暗号化・復号を行うには鍵が必要です。
鍵の生成は以下のコードで生成できます。
(今回はSymmetricKeySizeを256にしました。)

```swift:Sample.swift
// 鍵の生成
let encryptionKey: SymmetricKey = SymmetricKey(size: .bits256)
```

#暗号化を実行
生成したSymmetricKeyで暗号化を行います。
CryptoKitは「AES-GCM」と「ChaChaPoly」の両方のアルゴリズムをサポートしています。
この記事では「AES-GCM」を使用しています。
※「ChaChaPoly」の方が一般的には高速だそうです！

暗号化・復号の動作確認のため、サンドボックス内、ドキュメントディレクトリ配下に「Sample.text」というファイルを配置しています。

```swift:Sample.swift
// 鍵の生成
let symmetricKey: SymmetricKey = SymmetricKey(size: .bits256)

// ファイルパスを取得
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let filePath = "\(documentDirectory)/Sample.text"

// ファイルパスからデータを取得
guard let encryptData = FileManager.default.contents(atPath: filePath) else {
    return
}

// 暗号化の実行
do {
    let sealedBox = try AES.GCM.seal(encryptData, using: symmetricKey)
    guard let data = sealedBox.combined else {
        return
    }
    // ここで取れる"data"が暗号化された「Sample.text」
} catch _ {
   return
}
```

#復号を実行
続いては復号です。

```swift:Sample.swift
// 鍵の生成
let symmetricKey: SymmetricKey = SymmetricKey(size: .bits256)

// ファイルパスを取得
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let filePath = "\(documentDirectory)/Sample.text"

// ファイルパスからデータを取得
guard let decryptData = FileManager.default.contents(atPath: filePath) else {
    return
}

// 復号を実行
do {
    let sealedBox = try AES.GCM.SealedBox(combined: decryptData)
    let data = try AES.GCM.open(sealedBox, using: encryptionKey)
    // ここで取れる"data"が復号された「Sample.text」
} catch _ {
    return
}
```

#SymmetricKeyを永続化するには？
さて、暗号化・復号はできましたが、SymmetricKeyを永続化しないと、鍵の情報が変わってしまい、
二度と暗号化・復号ができなくなってしまいます。

そこで、SymmetricKeyからBase64EncodedStringを生成し、文字列としてローカルに保存する方法を取りました。
また、Base64EncodedStringからSymmetricKeyのインスタンスを返却するInitializerも用意します。

SymmetricKeyのExtensionを作り関数を追加します。

```swift:SymmetricKey+Initializer.swift
extension SymmetricKey {
    
    // Create symetricKey with base64Encoded string
    init?(base64EncodedString string: String) {
        guard let data = Data(base64Encoded: string) else {
            return nil
        }
        self.init(data: data)
    }
    
    // CreateBase64String from symetricKey
    func serialize() -> String {
        return self.withUnsafeBytes { body in
            Data(body).base64EncodedString()
        }
    }
}
```

以下のように使用します。

```swift:Sample.swift
let symmetricKey = SymmetricKey(size: .bits256)

// Base64EncodedStringになったSymmetricKey
let stringValue = symmetricKey.serialize()

// Base64EncodedStringからSymmetricKeyに戻す
let key = SymmetricKey(base64EncodedString: stringValue)
```

#こんな感じのもの作りました
指定したファイルの暗号化・復号を行い、任意のディレクトリに書き出すための共通クラスを作りました。
コード自体は概要欄に書きました以下のリポジトリにあります。
https://github.com/o-mo-te/Swift_CryptoKit

```swift:SymmetricKey+Initializer.swift
import Foundation
import CryptoKit

extension SymmetricKey {
    
    // Create symetricKey with base64Encoded string
    init?(base64EncodedString string: String) {
        guard let data = Data(base64Encoded: string) else {
            return nil
        }
        self.init(data: data)
    }
    
    // CreateBase64String from symetricKey
    func serialize() -> String {
        return self.withUnsafeBytes { body in
            Data(body).base64EncodedString()
        }
    }
}
```

```swift:CryptManager.swift
import Foundation
import CryptoKit

enum AppError: Error {
    case error
}

enum KeyType {
    case encryptionKey
    
    var name: String {
        switch self {
        case .encryptionKey:
            return "encryptionKey"
        }
    }
}

final class CryptoManager {
    
    // MARK: - Property
    
    static let shared = CryptoManager()
    private var symmetricKey: SymmetricKey!
    
    // MARK: - private function
    
    private init() {
        
        defer {
            if symmetricKey == nil {
                fatalError()
            }
        }
        
        // If userDefaults has symmetricKey, get value and set to property named "symmetricKey"
        if
            let value = UserDefaults.standard.string(forKey: KeyType.encryptionKey.name),
            let key = SymmetricKey(base64EncodedString: value) {
            symmetricKey = key
            return
        }
        
        // If userDefaults doesn't have symmetricKey, create value and set to userDefaults
        let key = SymmetricKey(size: .bits256)
        symmetricKey = key
        let value = symmetricKey.serialize()
        UserDefaults.standard.set(value, forKey: KeyType.encryptionKey.name)
    }
    
    // MARK: - Public function
    
    /// 指定したパスに暗号化したデータを書き込む
    /// - Parameter path: 書き込み先のパス
    /// - Parameter data: 暗号化して書き込むデータ
    @discardableResult
    func writeEncryptedData(to path: String, data: Data) -> Bool {
        do {
            let data = try encrypt(data: data)
            FileManager.default.createFile(atPath: path, contents: data, attributes: [:])
            return true
        } catch _ {
            return false
        }
    }
    
    /// 指定したパスに復号したデータを書き込む
    /// - Parameter path: 書き込み先のパス
    /// - Parameter data: 復号して書き込むデータ
    @discardableResult
    func writeDecryptedData(to path: String, data: Data) -> Bool {
        do {
            let data = try decrypt(data: data)
            FileManager.default.createFile(atPath: path, contents: data, attributes: [:])
            return true
        } catch _ {
            return false
        }
    }
    
    // MARK: - Private function
    
    /// 暗号化
    /// - Parameter data: 暗号化するデータ
    private func encrypt(data: Data) throws -> Data {
        do {
            let sealedBox = try AES.GCM.seal(data, using: symmetricKey)
            guard let data = sealedBox.combined else {
                throw AppError.error
            }
            return data
        } catch _ {
            throw AppError.error
        }
    }
    
    /// 復号
    /// - Parameter data: 復号するデータ
    private func decrypt(data: Data) throws -> Data {
        do {
            let sealedBox = try AES.GCM.SealedBox(combined: data)
            return try AES.GCM.open(sealedBox, using: symmetricKey)
        } catch _ {
            throw AppError.error
        }
    }
}
```

DB作るのが面倒だったため、UserDefaultsで鍵の保持しちゃってます、、、

#最後に
CryptoKitの出始めの頃に実装してみて、情報が少なく苦労したことを思い出しました。
誰かの助けになれば幸いです、、！

#参考
https://developer.apple.com/videos/play/wwdc2019/709/
https://developer.apple.com/documentation/cryptokit