LoginSignup
22
13

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-01

概要

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

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

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

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

環境

Xcode: 11.3.1
Swift: Swift5

SymmetricKeyの生成

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

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

暗号化を実行

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

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

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
}

復号を実行

続いては復号です。

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を作り関数を追加します。

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()
        }
    }
}

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

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

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()
        }
    }
}
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

22
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
13