// /* * Copyright (c) 2021 BWI GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Foundation import MatrixSDK import Security class SecureFileStorage { static let shared: SecureFileStorage = SecureFileStorage(withFilename: "shared") private let fileOperationQueue = DispatchQueue(label: "SecureFileStoreQueue") private(set) var locked: Bool = true private var fileURL: URL! private let fileName: String private let directoryName: String = "SecureFiles" private var crypto: Cryptable! private let saltKeyName: String = "salt" private let iterationsKeyName: String = "iterations" private var memoryVault: MemoryVault! private var dict: [String: Data] = [:] private enum SecureStorageError: Error { case locked case noData } init(withFilename fileName: String) { self.fileName = fileName setupFilePath() let fileContent = loadFromFile() memoryVault = MemoryVault(fileContent) let salt = try? memoryVault.string(forKey: saltKeyName) let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName) if iterations == nil && salt == nil { do { try memoryVault.set(makeSalt(), forKey: saltKeyName) dict[saltKeyName] = makeSalt().data(using: .utf8) try memoryVault.set(BWIBuildSettings.shared.iterationsForSecureStorage, forKey: iterationsKeyName) dict[iterationsKeyName] = String(BWIBuildSettings.shared.iterationsForSecureStorage).data(using: .utf8) saveToFile() } catch { } } } // MARK: - private func setupFilePath() { var cachePath: URL! if let appGroupIdentifier = MXSDKOptions.sharedInstance().applicationGroupIdentifier { cachePath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) } else { cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first } fileURL = cachePath.appendingPathComponent(directoryName).appendingPathComponent(fileName) fileOperationQueue.async { try? FileManager.default.createDirectory(at: self.fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) } } private func loadFromFile() -> [String: Any] { var fileContent: [String: Any] = [:] fileOperationQueue.sync { if let dict = NSDictionary(contentsOf: self.fileURL) as? [String: Any] { fileContent = dict } } return fileContent } private func saveToFile() { fileOperationQueue.async { (self.memoryVault.dict as NSDictionary).write(to: self.fileURL, atomically: true) } } // MARK: - private func makeSalt() -> String { return generateSecureRandomString(length: 32)! } func generateSecureRandomString(length: Int) -> String? { let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") let charactersCount = characters.count var randomBytes = [UInt8](repeating: 0, count: length) let status = SecRandomCopyBytes(kSecRandomDefault, length, &randomBytes) guard status == errSecSuccess else { MXLog.error("generateSecureRandomString failed") return nil } let randomString = randomBytes.map { byte in String(characters[Int(byte) % charactersCount]) }.joined() return randomString } func update(passphrase: String) throws { guard !locked else { throw SecureStorageError.locked } guard let salt = try? memoryVault.string(forKey: saltKeyName), let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName) else { throw SecureStorageError.noData } let encryptionKey = try MXKeyBackupPassword.retrievePrivateKey(withPassword: passphrase, salt: salt, iterations: iterations) crypto = try CryptoAES(key: encryptionKey) try dict.forEach { (key: String, value: Data) in if key == saltKeyName || key == iterationsKeyName { return } let encrypted = try crypto.encrypt(value) try memoryVault.set(encrypted, forKey: key) } saveToFile() } func unlock(passphrase: String) throws { guard let salt = try? memoryVault.string(forKey: saltKeyName), let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName) else { throw SecureStorageError.noData } let encryptionKey = try MXKeyBackupPassword.retrievePrivateKey(withPassword: passphrase, salt: salt, iterations: iterations) crypto = try CryptoAES(key: encryptionKey) try memoryVault.dict.forEach { (key: String, value: Any) in guard let data = value as? Data else { return } if key == saltKeyName || key == iterationsKeyName { dict[key] = data return } dict[key] = try crypto.decrypt(data) } locked = false } } extension SecureFileStorage: KeyValueVault { func objectExists(withKey key: String) -> Bool { return memoryVault.objectExists(withKey: key) } func removeObject(forKey key: String) throws { dict.removeValue(forKey: key) try memoryVault.removeObject(forKey: key) saveToFile() } func reset() throws { dict.removeAll() try memoryVault.reset() saveToFile() } // MARK: - Getter func bool(forKey key: String) throws -> Bool? { if let stringValue = try string(forKey: key) { return Bool(stringValue) } else { return nil } } func integer(forKey key: String) throws -> Int? { if let stringValue = try string(forKey: key) { return Int(stringValue) } else { return nil } } func unsignedInteger(forKey key: String) throws -> UInt? { if let stringValue = try string(forKey: key) { return UInt(stringValue) } else { return nil } } func string(forKey key: String) throws -> String? { if let data = try data(forKey: key) { return String(data: data, encoding: .utf8) } else { return nil } } func data(forKey key: String) throws -> Data? { guard !locked else { throw SecureStorageError.locked } return dict[key] } // MARK: - Setter func set(_ value: Bool?, forKey key: String) throws { if let value = value { try set(String(value).data(using: .utf8), forKey: key) } else { try set(nil as Data?, forKey: key) } } func set(_ value: Int?, forKey key: String) throws { if let value = value { try set(String(value).data(using: .utf8), forKey: key) } else { try set(nil as Data?, forKey: key) } } func set(_ value: UInt?, forKey key: String) throws { if let value = value { try set(String(value).data(using: .utf8), forKey: key) } else { try set(nil as Data?, forKey: key) } } func set(_ value: String?, forKey key: String) throws { try set(value?.data(using: .utf8), forKey: key) } func set(_ value: Data?, forKey key: String) throws { guard !locked else { throw SecureStorageError.locked } if let value = value { dict[key] = value let encrypted = try crypto.encrypt(value) try memoryVault.set(encrypted, forKey: key) saveToFile() } else { try removeObject(forKey: key) } } }