Files
bundesmessenger-ios/bwi/SecureStorage/SecureFileStorage.swift

279 lines
8.6 KiB
Swift

//
/*
* 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)
}
}
}