mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-16 06:28:27 +02:00
279 lines
8.6 KiB
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)
|
|
}
|
|
|
|
}
|
|
|
|
}
|