SafeManager.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2018-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. import CocoaLumberjackSwift
  22. @objc class SafeManager: NSObject {
  23. private var safeConfigManager: SafeConfigManagerProtocol
  24. private var safeStore: SafeStore
  25. private var safeApiService: SafeApiService
  26. private var logger: ValidationLogger
  27. //trigger safe backup states
  28. private static var backupObserver: NSObjectProtocol?
  29. private static var backupDelay: Timer?
  30. private static let backupProcessLock: DispatchQueue = DispatchQueue(label: "backupProcessLock")
  31. private static var backupProcessStart: Bool = false
  32. private static var backupIsRunning: Bool = false
  33. private var backupForce: Bool = false
  34. private var backupCompletionHandler: (() -> Void)? = nil
  35. private var checksum: [UInt8]?
  36. enum SafeError: Error {
  37. case activateFailed(message: String)
  38. case backupFailed(message: String)
  39. case restoreError(message: String)
  40. case restoreFailed(message: String)
  41. }
  42. init(safeConfigManager: SafeConfigManagerProtocol, safeStore: SafeStore, safeApiService: SafeApiService) {
  43. self.safeConfigManager = safeConfigManager
  44. self.safeStore = safeStore
  45. self.safeApiService = safeApiService
  46. self.logger = ValidationLogger.shared()
  47. }
  48. //NSObject thereby not the whole SafeConfigManagerProtocol interface must be like @objc
  49. @objc convenience init(safeConfigManagerAsObject safeConfigManager: NSObject, safeStore: SafeStore, safeApiService: SafeApiService) {
  50. self.init(safeConfigManager: safeConfigManager as! SafeConfigManagerProtocol, safeStore: safeStore, safeApiService: safeApiService)
  51. }
  52. @objc var isActivated: Bool {
  53. get {
  54. if let key = self.safeConfigManager.getKey() {
  55. return key.count == self.safeStore.masterKeyLength
  56. }
  57. return false
  58. }
  59. }
  60. var isBackupRunning: Bool {
  61. get {
  62. return SafeManager.backupIsRunning
  63. }
  64. }
  65. @objc func activate(identity: String, password: String) throws {
  66. self.safeConfigManager.setKey(self.safeStore.createKey(identity: identity, password: password))
  67. self.safeConfigManager.setIsTriggered(true)
  68. initTrigger()
  69. }
  70. @objc func activate(identity: String, password: String, customServer: String?, server: String?, maxBackupBytes: NSNumber?, retentionDays: NSNumber?) throws {
  71. if let key = self.safeStore.createKey(identity: identity, password: password) {
  72. try activate(key: key, customServer: customServer, server: server, maxBackupBytes: maxBackupBytes?.intValue, retentionDays: retentionDays?.intValue)
  73. }
  74. }
  75. func activate(key: [UInt8], customServer: String?, server: String?, maxBackupBytes: Int?, retentionDays: Int?) throws {
  76. if let customServer = customServer,
  77. let server = server {
  78. self.safeConfigManager.setKey(key)
  79. self.safeConfigManager.setCustomServer(customServer)
  80. self.safeConfigManager.setServer(server)
  81. self.safeConfigManager.setMaxBackupBytes(maxBackupBytes)
  82. self.safeConfigManager.setRetentionDays(retentionDays)
  83. } else {
  84. if let defaultServer = self.safeStore.getSafeDefaultServer(key: key) {
  85. let result = testServer(serverUrl: defaultServer)
  86. if let errorMessage = result.errorMessage {
  87. throw SafeError.activateFailed(message: "Test default server: \(errorMessage)")
  88. } else {
  89. self.safeConfigManager.setKey(key)
  90. self.safeConfigManager.setCustomServer(nil)
  91. self.safeConfigManager.setServer(defaultServer.absoluteString)
  92. self.safeConfigManager.setMaxBackupBytes(result.maxBackupBytes)
  93. self.safeConfigManager.setRetentionDays(result.retentionDays)
  94. }
  95. }
  96. }
  97. initTrigger()
  98. }
  99. @objc func deactivate() {
  100. if let observer = SafeManager.backupObserver {
  101. SafeManager.backupObserver = nil
  102. NotificationCenter.default.removeObserver(observer, name: Notification.Name(kSafeBackupTrigger), object: nil)
  103. }
  104. if let key = self.safeConfigManager.getKey(),
  105. let backupId = self.safeStore.getBackupId(key: key) {
  106. if let safeServer = self.safeStore.getSafeServer(key: key) {
  107. let safeServerAuth = self.safeStore.extractSafeServerAuth(server: safeServer)
  108. let safeBackupUrl = safeServerAuth.server.appendingPathComponent("backups/\(SafeStore.dataToHexString(backupId))")
  109. if let errorMessage = safeApiService.delete(server: safeBackupUrl, user: safeServerAuth.user, password: safeServerAuth.password) {
  110. self.logger.logString("Safe backup could not be deleted: \(errorMessage)")
  111. }
  112. }
  113. }
  114. self.safeConfigManager.setKey(nil)
  115. self.safeConfigManager.setCustomServer(nil)
  116. self.safeConfigManager.setServer(nil)
  117. self.safeConfigManager.setMaxBackupBytes(nil)
  118. self.safeConfigManager.setRetentionDays(nil)
  119. self.safeConfigManager.setLastBackup(nil)
  120. self.safeConfigManager.setLastChecksum(nil)
  121. self.safeConfigManager.setLastResult(nil)
  122. self.safeConfigManager.setLastAlertBackupFailed(nil)
  123. self.safeConfigManager.setBackupStartedAt(nil)
  124. self.safeConfigManager.setIsTriggered(false)
  125. DispatchQueue.main.async {
  126. self.setBackupReminder()
  127. }
  128. }
  129. func isPasswordBad(password: String) -> Bool {
  130. if password.count < 8 {
  131. return true
  132. }
  133. else if checkPasswordToRegEx(password: password) {
  134. return true
  135. }
  136. return checkPasswordToFile(password: password)
  137. }
  138. private func checkPasswordToFile(password: String) -> Bool {
  139. guard let filePath = Bundle.main.path(forResource: "bad_passwords", ofType: "txt"),
  140. let fileHandle = FileHandle(forReadingAtPath: filePath) else {
  141. return false
  142. }
  143. defer {
  144. fileHandle.closeFile()
  145. }
  146. let delimiter: Data = String(stringLiteral: "\n").data(using: .utf8)!
  147. let chunkSize = 4096
  148. var isEof: Bool = false
  149. var lineStart: String = ""
  150. while !isEof {
  151. var position: Int = 0
  152. let chunk = fileHandle.readData(ofLength: chunkSize)
  153. if chunk.count == 0 {
  154. isEof = true
  155. }
  156. //compare password with all lines within the chunk
  157. repeat {
  158. var line: String = ""
  159. if let range = chunk.subdata(in: position..<chunk.count).range(of: delimiter) {
  160. if lineStart.count > 0 {
  161. line.append(lineStart)
  162. lineStart = ""
  163. }
  164. line.append(String(data: chunk.subdata(in: position..<position+range.lowerBound), encoding: .utf8)!)
  165. position += range.upperBound
  166. } else {
  167. //store start characters of next line/chunk
  168. if chunk.count > position {
  169. lineStart = String(data: chunk.subdata(in: position..<chunk.count), encoding: .utf8)!
  170. }
  171. position = chunk.count
  172. }
  173. if line.count > 0 && line == password {
  174. return true
  175. }
  176. } while chunk.count > position
  177. }
  178. return false
  179. }
  180. private func checkPasswordToRegEx(password: String) -> Bool {
  181. let checks = [
  182. "(.)\\1+", //do not allow single repeating characters
  183. "^[0-9]{1,15}$"] //do not allow numbers only
  184. do {
  185. for check in checks {
  186. let regex = try NSRegularExpression(pattern: check, options: .caseInsensitive)
  187. let result = regex.matches(in: password, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSRange(location: 0, length: password.count))
  188. //result must match once the whole password/string
  189. if result.count == 1 && result[0].range.location == 0 && result[0].range.length == password.count {
  190. return true
  191. }
  192. }
  193. } catch let error {
  194. print("regex faild to check password: \(error.localizedDescription)")
  195. }
  196. return false
  197. }
  198. static func isPasswordPatternValid(password: String, regExPattern: String) throws -> Bool {
  199. var regExMatches: Int = 0
  200. let regEx = try NSRegularExpression(pattern: regExPattern)
  201. regExMatches = regEx.numberOfMatches(in: password, options: [], range: NSRange.init(location: 0, length: password.count))
  202. return regExMatches == 1
  203. }
  204. @objc func setBackupReminder() {
  205. // remove safe backup notification anyway
  206. let notificationKey = "safe-backup-notification"
  207. let oneDayInSeconds = 24 * 60 * 60
  208. UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationKey])
  209. DDLogNotice("Threema Safe: Reminder notification removed")
  210. // add new safe backup notification, if is Threema Safe activated and is set backup retention days
  211. if self.isActivated,
  212. let lastBackup = self.safeConfigManager.getLastBackup(),
  213. let retentionDays = self.safeConfigManager.getRetentionDays() {
  214. let notification = UNMutableNotificationContent()
  215. notification.title = BundleUtil.localizedString(forKey: "safe_setup_backup_title")
  216. notification.body = BundleUtil.localizedString(forKey: "safe_expired_notification")
  217. notification.categoryIdentifier = "SAFE_SETUP"
  218. notification.userInfo = ["threema": ["nil": "nil"], "key": notificationKey]
  219. var trigger: UNTimeIntervalNotificationTrigger?
  220. var fireDate = lastBackup.addingTimeInterval(TimeInterval(oneDayInSeconds * (retentionDays / 2)))
  221. if fireDate.timeIntervalSinceNow <= 0 { // Fire date is in the past
  222. fireDate = lastBackup.addingTimeInterval(TimeInterval(oneDayInSeconds * retentionDays))
  223. if fireDate.timeIntervalSinceNow <= 0 { // Safe backup it outside of retention days
  224. let seconds = lastBackup.timeIntervalSinceNow
  225. let days = Double(exactly: seconds / Double(oneDayInSeconds))?.rounded(.up)
  226. notification.body = String(format: BundleUtil.localizedString(forKey: "safe_failed_notification"), abs(days!))
  227. } else {
  228. trigger = UNTimeIntervalNotificationTrigger(timeInterval: fireDate.timeIntervalSinceNow, repeats: false)
  229. }
  230. } else { // Fire date is in the future
  231. trigger = UNTimeIntervalNotificationTrigger(timeInterval: fireDate.timeIntervalSinceNow, repeats: false)
  232. }
  233. let notificationRequest = UNNotificationRequest(identifier: notificationKey, content: notification, trigger: trigger)
  234. UNUserNotificationCenter.current().add(notificationRequest) { error in
  235. if let error = error {
  236. DDLogError("Threema Safe: Error adding reminder to fire at \(DateFormatter.getFullDate(for: fireDate)): \(error.localizedDescription)")
  237. } else {
  238. DDLogNotice("Threema Safe: Reminder notification added, fire at: \(DateFormatter.getFullDate(for: fireDate))")
  239. }
  240. }
  241. }
  242. }
  243. func testServer(serverUrl: URL) -> (errorMessage: String?, maxBackupBytes: Int?, retentionDays: Int?) {
  244. let safeServerAuth = self.safeStore.extractSafeServerAuth(server: serverUrl)
  245. let result = self.safeApiService.testServer(server: safeServerAuth.server, user: safeServerAuth.user, password: safeServerAuth.password)
  246. if let errorMessage = result.errorMessage {
  247. return (errorMessage: errorMessage, maxBackupBytes: nil, retentionDays: nil)
  248. } else {
  249. let parser = SafeJsonParser()
  250. guard let data = result.serverConfig,
  251. let config = parser.getSafeServerConfig(from: data) else {
  252. return (errorMessage: "Invalid response data", maxBackupBytes: nil, retentionDays: nil)
  253. }
  254. return (errorMessage: nil, maxBackupBytes: config.maxBackupBytes, retentionDays: config.retentionDays)
  255. }
  256. }
  257. /// Apply Threema Safe server it has changed
  258. @objc func applyServer(server: String?, username: String?, password: String?) {
  259. if self.isActivated {
  260. var newServerUrl: URL?
  261. if let customServer = server {
  262. newServerUrl = self.safeStore.composeSafeServerAuth(server: customServer, user: username, password: password)
  263. } else {
  264. newServerUrl = self.safeStore.getSafeDefaultServer(key: self.safeConfigManager.getKey()!)
  265. }
  266. if let newServerUrl = newServerUrl {
  267. if self.safeConfigManager.getServer() != newServerUrl.absoluteString {
  268. // Save Threema Safe server config and reset result and control config
  269. self.safeConfigManager.setCustomServer(server)
  270. self.safeConfigManager.setServer(newServerUrl.absoluteString)
  271. self.safeConfigManager.setMaxBackupBytes(nil)
  272. self.safeConfigManager.setRetentionDays(nil)
  273. self.safeConfigManager.setLastChecksum(nil)
  274. self.safeConfigManager.setBackupSize(nil)
  275. self.safeConfigManager.setBackupStartedAt(nil)
  276. self.safeConfigManager.setLastAlertBackupFailed(nil)
  277. self.safeConfigManager.setIsTriggered(true)
  278. self.safeConfigManager.setLastResult(nil)
  279. self.safeConfigManager.setLastBackup(nil)
  280. }
  281. } else {
  282. self.logger.logString("Error while apply Threema Safe server: could not calculate server")
  283. }
  284. }
  285. }
  286. private func startBackup(force: Bool, completionHandler: @escaping () -> Void) {
  287. self.backupCompletionHandler = completionHandler
  288. do {
  289. if let key = self.safeConfigManager.getKey(),
  290. let backupId = self.safeStore.getBackupId(key: key) {
  291. // get backup data and and its checksum
  292. if let data = self.safeStore.backupData() {
  293. self.checksum = self.safeStore.sha1(data: Data(data))
  294. // do backup is forced or if data has changed or last backup (nearly) out of date
  295. if force || self.safeConfigManager.getLastChecksum() != self.checksum || self.safeStore.isDateOlderThenDays(date: self.safeConfigManager.getLastBackup(), days: self.safeConfigManager.getRetentionDays() ?? 180 / 2) {
  296. self.safeConfigManager.setBackupStartedAt(Date())
  297. // test server and save its config
  298. if let safeServerUrl = self.safeStore.getSafeServer(key: key) {
  299. let safeServerAuth = self.safeStore.extractSafeServerAuth(server: safeServerUrl)
  300. let safeBackupUrl = safeServerAuth.server.appendingPathComponent("backups/\(SafeStore.dataToHexString(backupId))")
  301. let result = testServer(serverUrl: safeServerUrl)
  302. if let errorMessage = result.errorMessage {
  303. throw SafeError.backupFailed(message: errorMessage)
  304. } else {
  305. self.safeConfigManager.setMaxBackupBytes(result.maxBackupBytes)
  306. self.safeConfigManager.setRetentionDays(result.retentionDays)
  307. }
  308. // encrypt backup data and upload it
  309. let encryptedData = try self.safeStore.encryptBackupData(key: key, data: data)
  310. // set actual backup size anyway
  311. self.safeConfigManager.setBackupSize(Int64(encryptedData.count))
  312. if encryptedData.count < self.safeConfigManager.getMaxBackupBytes() ?? 524288 {
  313. self.safeApiService.upload(backup: safeBackupUrl, user: safeServerAuth.user, password: safeServerAuth.password, encryptedData: encryptedData) { (data, errorMessage) in
  314. if let errorMessage = errorMessage {
  315. self.logger.logString(errorMessage)
  316. self.safeConfigManager.setLastResult(errorMessage.contains("Payload Too Large") ? BundleUtil.localizedString(forKey: "safe_upload_size_exceeded") : "\(BundleUtil.localizedString(forKey: "safe_upload_failed")!) (\(errorMessage))")
  317. } else {
  318. self.safeConfigManager.setLastChecksum(self.checksum)
  319. self.safeConfigManager.setLastBackup(Date())
  320. self.safeConfigManager.setLastResult(BundleUtil.localizedString(forKey: "safe_successful"))
  321. self.safeConfigManager.setLastAlertBackupFailed(nil)
  322. }
  323. self.backupCompletionHandler!()
  324. }
  325. } else {
  326. throw SafeError.backupFailed(message: BundleUtil.localizedString(forKey: "safe_upload_size_exceeded"))
  327. }
  328. } else {
  329. throw SafeError.backupFailed(message: "Invalid safe server url")
  330. }
  331. // cancel background task here, because the upload it's a background task too
  332. BackgroundTaskManager.shared.cancelBackgroundTask(key: kSafeBackgroundTask)
  333. } else {
  334. self.backupCompletionHandler!()
  335. }
  336. } else {
  337. throw SafeError.backupFailed(message: "Missing private key")
  338. }
  339. } else {
  340. throw SafeStore.SafeError.invalidMasterKey
  341. }
  342. } catch SafeError.backupFailed(let message) {
  343. self.logger.logString(message)
  344. self.safeConfigManager.setLastResult("\(BundleUtil.localizedString(forKey: "safe_unsuccessful")!): \(message)")
  345. self.backupCompletionHandler!()
  346. } catch let error {
  347. self.logger.logString(error.localizedDescription)
  348. self.safeConfigManager.setLastResult("\(BundleUtil.localizedString(forKey: "safe_unsuccessful")!): \(error.localizedDescription)")
  349. self.backupCompletionHandler!()
  350. }
  351. }
  352. func startRestore(identity:String, password: String, customServer: String?, server: String?, restoreIdentityOnly: Bool, activateSafeAnyway: Bool, completionHandler: @escaping (SafeError?) -> Swift.Void) {
  353. if let key = self.safeStore.createKey(identity: identity, password: password),
  354. let backupId = self.safeStore.getBackupId(key: key) {
  355. var safeServerUrl: URL
  356. if let server = server,
  357. server.count > 0 {
  358. safeServerUrl = URL(string: server)!
  359. } else {
  360. safeServerUrl = self.safeStore.getSafeDefaultServer(key: key)!
  361. }
  362. let safeServerAuth = self.safeStore.extractSafeServerAuth(server: safeServerUrl)
  363. let backupUrl = safeServerAuth.server.appendingPathComponent("backups/\(SafeStore.dataToHexString(backupId))")
  364. var decryptedData: [UInt8]?
  365. do {
  366. let safeApiService = SafeApiService()
  367. let encryptedData = try safeApiService.download(backup: backupUrl, user: safeServerAuth.user, password: safeServerAuth.password)
  368. if encryptedData != nil {
  369. decryptedData = try self.safeStore.decryptBackupData(key: key, data: Array(encryptedData!))
  370. try self.safeStore.restoreData(identity: identity, data: decryptedData!, onlyIdentity: restoreIdentityOnly, completionHandler: { (error) in
  371. if let error = error {
  372. switch error {
  373. case .restoreError(let message):
  374. completionHandler(SafeError.restoreError(message: message))
  375. case .restoreFailed(let message):
  376. completionHandler(SafeError.restoreFailed(message: message))
  377. default: break
  378. }
  379. } else {
  380. do {
  381. if (!restoreIdentityOnly || activateSafeAnyway) {
  382. //activate Threema Safe
  383. try self.activate(key: key, customServer: customServer, server: safeServerUrl.absoluteString, maxBackupBytes: nil, retentionDays: nil)
  384. } else {
  385. //show Threema Safe-Intro
  386. UserSettings.shared()?.safeIntroShown = false
  387. }
  388. //trigger backup
  389. NotificationCenter.default.post(name: NSNotification.Name(kSafeBackupTrigger), object: nil)
  390. completionHandler(nil)
  391. } catch {
  392. completionHandler(SafeError.restoreError(message: BundleUtil.localizedString(forKey: "safe_activation_failed")))
  393. }
  394. }
  395. })
  396. }
  397. } catch SafeApiService.SafeApiError.requestFailed(let message) {
  398. completionHandler(SafeError.restoreFailed(message: "\(BundleUtil.localizedString(forKey: "safe_no_backup_found")!) (\(message))"))
  399. } catch SafeStore.SafeError.restoreFailed(let message) {
  400. completionHandler(SafeError.restoreFailed(message: message))
  401. if let decryptedData = decryptedData {
  402. // Save decrypted backup data into application documents folder, for analyzing failures
  403. _ = FileUtility.write(fileUrl: DocumentManager.applicationDocumentsDirectory()?.appendingPathComponent("safe-backup.json"), text: String(bytes: decryptedData, encoding: .utf8)!)
  404. }
  405. } catch {
  406. completionHandler(SafeError.restoreFailed(message: BundleUtil.localizedString(forKey: "safe_no_backup_found")))
  407. }
  408. } else {
  409. completionHandler(SafeError.restoreFailed(message: BundleUtil.localizedString(forKey: "safe_no_backup_found")))
  410. }
  411. }
  412. @objc func initTrigger() {
  413. DDLogVerbose("Threema Safe triggered")
  414. if isActivated {
  415. if SafeManager.backupObserver == nil {
  416. SafeManager.backupObserver = NotificationCenter.default.addObserver(forName: Notification.Name(kSafeBackupTrigger), object: nil, queue: nil) { (notification) in
  417. if !AppDelegate.shared().isAppInBackground() && self.isActivated {
  418. //start background task to give time to create backup file, if the app is going into background
  419. BackgroundTaskManager.shared.newBackgroundTask(key: kSafeBackgroundTask, timeout: 60, completionHandler: {
  420. if SafeManager.backupDelay != nil {
  421. SafeManager.backupDelay?.invalidate()
  422. }
  423. // set 5s delay timer to start backup (if delay time 0s, then force backup)
  424. var interval: Int = 5
  425. if notification.object is Int {
  426. interval = notification.object as! Int
  427. }
  428. self.backupForce = interval == 0
  429. //async is necessary if the call is already within an operation queue (like after setup completion)
  430. SafeManager.backupDelay = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(self.trigger), userInfo: nil, repeats: false)
  431. })
  432. }
  433. }
  434. }
  435. if self.safeConfigManager.getIsTriggered() || self.safeStore.isDateOlderThenDays(date: self.safeConfigManager.getLastBackup(), days: 1) {
  436. NotificationCenter.default.post(name: NSNotification.Name(kSafeBackupTrigger), object: nil)
  437. }
  438. // Show alert once a day, if is last successful backup older than 7 days
  439. if self.safeConfigManager.getLastResult() != BundleUtil.localizedString(forKey: "safe_successful") && self.safeConfigManager.getLastBackup() != nil && self.safeStore.isDateOlderThenDays(date: self.safeConfigManager.getLastBackup(), days: 7) {
  440. DDLogWarn("WARNING Threema Safe backup not successfully since 7 days or more")
  441. self.logger.logString("WARNING Threema Safe backup not successfully since 7 days or more")
  442. if self.safeStore.isDateOlderThenDays(date: self.safeConfigManager.getLastAlertBackupFailed(), days: 1) {
  443. if let topViewController = AppDelegate.shared()?.currentTopViewController(),
  444. let seconds = self.safeConfigManager.getLastBackup()?.timeIntervalSinceNow,
  445. let days = Double(exactly: seconds / 86400)?.rounded(FloatingPointRoundingRule.up) {
  446. self.safeConfigManager.setLastAlertBackupFailed(Date())
  447. UIAlertTemplate.showAlert(owner: topViewController, title: BundleUtil.localizedString(forKey: "safe_setup_backup_title"), message: String(format: BundleUtil.localizedString(forKey: "safe_failed_notification"), abs(days)))
  448. }
  449. }
  450. }
  451. }
  452. }
  453. @objc private func trigger() {
  454. DispatchQueue(label: "backupProcess").async {
  455. //if forced, try to start backup immediately, otherwise when backup process is already running or last backup not older then a day then just mark as triggered
  456. SafeManager.backupProcessLock.sync {
  457. SafeManager.backupProcessStart = false
  458. if self.backupForce && SafeManager.backupIsRunning {
  459. self.safeConfigManager.setLastResult("\(NSLocalizedString("safe_unsuccessful", comment: "")): is already running")
  460. } else if !self.backupForce && (SafeManager.backupIsRunning || !self.safeStore.isDateOlderThenDays(date: self.safeConfigManager.getLastBackup(), days: 1)) {
  461. self.safeConfigManager.setIsTriggered(true)
  462. self.logger.logString("Safe backup just triggered")
  463. } else {
  464. SafeManager.backupProcessStart = true
  465. SafeManager.backupIsRunning = true
  466. self.safeConfigManager.setIsTriggered(false)
  467. }
  468. }
  469. if SafeManager.backupProcessStart {
  470. self.logger.logString("Safe backup start, force \(self.backupForce)")
  471. self.startBackup(force: self.backupForce) {
  472. SafeManager.backupProcessLock.sync {
  473. SafeManager.backupIsRunning = false
  474. BackgroundTaskManager.shared.cancelBackgroundTask(key: kSafeBackgroundTask)
  475. }
  476. DispatchQueue.main.async {
  477. self.setBackupReminder()
  478. NotificationCenter.default.post(name: NSNotification.Name(kSafeBackupUIRefresh), object: nil)
  479. }
  480. self.logger.logString("Safe backup completed")
  481. }
  482. } else {
  483. BackgroundTaskManager.shared.cancelBackgroundTask(key: kSafeBackgroundTask)
  484. }
  485. DispatchQueue.main.async {
  486. NotificationCenter.default.post(name: NSNotification.Name(kSafeBackupUIRefresh), object: nil)
  487. }
  488. }
  489. }
  490. }