diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index dcf3138..5872c25 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -54,6 +54,11 @@ 6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD6D2002C11FEB4004512BF /* OTPService.swift */; }; 6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */; }; 6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */; }; + 6BF53E4F2C317AA400356461 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 6BF53E4E2C317AA400356461 /* ZipArchive */; }; + 6BF53E522C317F1C00356461 /* ExportSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF53E512C317F1C00356461 /* ExportSelectionView.swift */; }; + 6BF53E542C31856A00356461 /* EncryptedExportPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF53E532C31856A00356461 /* EncryptedExportPasswordView.swift */; }; + 6BF53E572C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF53E562C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift */; }; + 6BF53E5B2C33FD8F00356461 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF53E5A2C33FD8F00356461 /* ActivityView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -99,6 +104,10 @@ 6BD6D2002C11FEB4004512BF /* OTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPService.swift; sourceTree = ""; }; 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordLoginView.swift; sourceTree = ""; }; 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronosCrypto.swift; sourceTree = ""; }; + 6BF53E512C317F1C00356461 /* ExportSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSelectionView.swift; sourceTree = ""; }; + 6BF53E532C31856A00356461 /* EncryptedExportPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedExportPasswordView.swift; sourceTree = ""; }; + 6BF53E562C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedExportConfirmPasswordView.swift; sourceTree = ""; }; + 6BF53E5A2C33FD8F00356461 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -114,6 +123,7 @@ 6B7383E02B9C3962008E8867 /* Factory in Frameworks */, 6B3C7A3D2BE9E0600043FEBD /* Logging in Frameworks */, 6B193C8A2C27F03300E759B7 /* CloudKitSyncMonitor in Frameworks */, + 6BF53E4F2C317AA400356461 /* ZipArchive in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,6 +146,7 @@ 6B2583AD2B96047700938F3A /* Settings */ = { isa = PBXGroup; children = ( + 6BF53E502C317EFF00356461 /* Export */, 6B2583AE2B96048700938F3A /* SettingsTab.swift */, ); path = Settings; @@ -308,6 +319,7 @@ isa = PBXGroup; children = ( 6B9581E12B62AFF80029EF3C /* ViewModifier.swift */, + 6BF53E5A2C33FD8F00356461 /* ActivityView.swift */, ); path = Extensions; sourceTree = ""; @@ -320,6 +332,24 @@ path = Login; sourceTree = ""; }; + 6BF53E502C317EFF00356461 /* Export */ = { + isa = PBXGroup; + children = ( + 6BF53E552C32EF5800356461 /* EncryptedExport */, + 6BF53E512C317F1C00356461 /* ExportSelectionView.swift */, + ); + path = Export; + sourceTree = ""; + }; + 6BF53E552C32EF5800356461 /* EncryptedExport */ = { + isa = PBXGroup; + children = ( + 6BF53E532C31856A00356461 /* EncryptedExportPasswordView.swift */, + 6BF53E562C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift */, + ); + path = EncryptedExport; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -346,6 +376,7 @@ 6B7383DF2B9C3962008E8867 /* Factory */, 6B3C7A3C2BE9E0600043FEBD /* Logging */, 6B193C892C27F03300E759B7 /* CloudKitSyncMonitor */, + 6BF53E4E2C317AA400356461 /* ZipArchive */, ); productName = Chronos; productReference = 6B3BB0AD2B4EA87C00DCEF0B /* Chronos.app */; @@ -384,6 +415,7 @@ 6B7383DE2B9C3962008E8867 /* XCRemoteSwiftPackageReference "Factory" */, 6B3C7A3B2BE9E0600043FEBD /* XCRemoteSwiftPackageReference "swift-log" */, 6B193C882C27F03300E759B7 /* XCRemoteSwiftPackageReference "CloudKitSyncMonitor" */, + 6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */, ); productRefGroup = 6B3BB0AE2B4EA87C00DCEF0B /* Products */; projectDirPath = ""; @@ -445,6 +477,7 @@ 6B66D5E72B526315006DB79D /* AddTokenView.swift in Sources */, 6B2583AF2B96048700938F3A /* SettingsTab.swift in Sources */, 6B39629C2BF5EB3E000410B0 /* AuthenticationView.swift in Sources */, + 6BF53E5B2C33FD8F00356461 /* ActivityView.swift in Sources */, 6B3F92A92C1B45AA004125A8 /* Vault.swift in Sources */, 6B9581E22B62AFF80029EF3C /* ViewModifier.swift in Sources */, 6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */, @@ -453,17 +486,20 @@ 6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */, 6B9D74682C1553A1008E6582 /* SecureBytes.swift in Sources */, 6B3BB0E22B4ED19300DCEF0B /* TokensTab.swift in Sources */, + 6BF53E542C31856A00356461 /* EncryptedExportPasswordView.swift in Sources */, 6B12B0A02C19DB7800E9ED2D /* ExportService.swift in Sources */, 6B3BB0E02B4ECE6F00DCEF0B /* TOTPRowView.swift in Sources */, 6B65F80B2C21C2EA00AC8606 /* VaultSetupView.swift in Sources */, 6B5E41CE2BD790F80045DBC6 /* EncryptedToken.swift in Sources */, 6B4B48F32BD7BB3C007D357D /* Token.swift in Sources */, + 6BF53E572C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift in Sources */, 6B842DD52BE33E2E00056F0F /* RestoreBackupView.swift in Sources */, 6BA5DA5C2B9E94F8009908E5 /* SecureEnclaveService.swift in Sources */, 6B66D5EF2B52BAC2006DB79D /* HOTPRowView.swift in Sources */, 6B39629E2BF63F27000410B0 /* SwiftDataService.swift in Sources */, 6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */, 6B39629A2BF5E935000410B0 /* MainAppView.swift in Sources */, + 6BF53E522C317F1C00356461 /* ExportSelectionView.swift in Sources */, 6B65F8082C21C17200AC8606 /* VaultSelectionView.swift in Sources */, 6B3962A02BF6423B000410B0 /* CryptoService.swift in Sources */, 6B27317A2B53E23800F30621 /* UpdateTokenView.swift in Sources */, @@ -880,6 +916,14 @@ minimumVersion = 1.8.2; }; }; + 6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ZipArchive/ZipArchive"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.5.5; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -923,6 +967,11 @@ package = 6B8ABEEA2B8F6A4F00F5B514 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; + 6BF53E4E2C317AA400356461 /* ZipArchive */ = { + isa = XCSwiftPackageProductDependency; + package = 6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */; + productName = ZipArchive; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 6B3BB0A52B4EA87C00DCEF0B /* Project object */; diff --git a/Chronos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Chronos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6ec5734..13b0865 100644 --- a/Chronos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Chronos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e744487b2e4216f33f17ad69d0ef61baaf0ab4ced9df8591f51d30bcabba5561", + "originHash" : "83b69a06fc95c43243855fb2ff571a8bf2cf54943f4ffc4a10a2c75a6d98f73b", "pins" : [ { "identity" : "alertkit", @@ -81,6 +81,15 @@ "revision" : "3df8eaa90e1fa0d80830733cdc3c9e098146af3d", "version" : "4.3.0" } + }, + { + "identity" : "ziparchive", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ZipArchive/ZipArchive", + "state" : { + "revision" : "79d4dc9729096c6ad83dd3cee2b9f354d1b4ab7b", + "version" : "2.5.5" + } } ], "version" : 3 diff --git a/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift new file mode 100644 index 0000000..d7cef32 --- /dev/null +++ b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift @@ -0,0 +1,128 @@ +import Factory +import SwiftUI + +struct EncryptedExportConfirmPasswordView: View { + let exportService = Container.shared.exportService() + @StateObject private var viewModel = ConfirmPasswordViewModel() + + @EnvironmentObject var exportNav: ExportNavigation + + @State var password: String + @State private var verifyPassword: String = "" + @State private var passwordInvalidMsg: String = "" + @State private var isPasswordValid: Bool = false + @State private var exportDisabled: Bool = false + + @FocusState private var focusedField: FocusedField? + + var body: some View { + VStack { + Spacer() + + Text("Confirm password") + .padding(.top, 24) + Group { + SecureField("", text: $verifyPassword) + .multilineTextAlignment(.center) + .background(Color.clear) + .focused($focusedField, equals: .verifyPassword) + .submitLabel(.done) + .onSubmit { + doSubmit() + } + .onChange(of: verifyPassword) { _, _ in + validatePasswords() + } + .onAppear { + focusedField = .verifyPassword + } + } + .frame(height: 48) + .background(Color(.systemGray6)) + .cornerRadius(8) + + if !isPasswordValid { + Text(passwordInvalidMsg) + .multilineTextAlignment(.center) + .foregroundStyle(.red) + .font(.subheadline) + .padding(.top, 4) + } + + Spacer() + } + .padding([.horizontal], 24) + .navigationTitle("Encrypted Export") + .background(Color(red: 0.04, green: 0, blue: 0.11)) + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: + Button { + doSubmit() + } label: { + Text("Export") + } + .disabled(!isPasswordValid || exportDisabled) + .sheet(isPresented: $viewModel.showEncryptedExportSheet) { + if let fileUrl = viewModel.exportFileUrl { + ActivityView(fileUrl: fileUrl) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(Visibility.hidden) + .onDisappear { + exportNav.showSheet = false + } + } else { + VStack { + Image(systemName: "xmark.circle") + .fontWeight(.light) + .font(.system(size: 64)) + .padding(.bottom, 8) + Text("An error occurred while during the export process") + } + } + } + ) + } + + func doSubmit() { + if !isPasswordValid { + UINotificationFeedbackGenerator().notificationOccurred(.error) + exportDisabled = false + return + } + + exportDisabled = true + viewModel.exportToEncryptedZip(password: password) + } + + private func validatePasswords() { + if password.count < 10 { + isPasswordValid = false + passwordInvalidMsg = "Passwords must be at least 10 characters long" + } else if password != verifyPassword { + isPasswordValid = false + passwordInvalidMsg = "Passwords do not match" + } else { + isPasswordValid = true + passwordInvalidMsg = "" + } + } +} + +class ConfirmPasswordViewModel: ObservableObject { + @Published var exportFileUrl: URL? + @Published var showEncryptedExportSheet: Bool = false + + let exportService = Container.shared.exportService() + + func exportToEncryptedZip(password: String) { + guard let fileUrl = exportService.exportToEncryptedZip(password: password) else { + exportFileUrl = nil + showEncryptedExportSheet = false + + return + } + + exportFileUrl = fileUrl + showEncryptedExportSheet = true + } +} diff --git a/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportPasswordView.swift b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportPasswordView.swift new file mode 100644 index 0000000..ad0a8e7 --- /dev/null +++ b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportPasswordView.swift @@ -0,0 +1,79 @@ +import Factory +import SwiftUI + +struct EncryptedExportPasswordView: View { + @State private var password: String = "" + + @State private var passwordInvalidMsg: String = "" + @State private var isPasswordValid: Bool = false + @State private var navigateToNextPage: Bool = false + + @FocusState private var focusedField: FocusedField? + + var body: some View { + VStack { + Image(systemName: "lock.square") + .font(.system(size: 44)) + .padding(.bottom, 16) + + Text("This password is used to securely encrypt your data. Choose a memorable, random, and unique password with at least 10 characters.") + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + + Text("Password") + .padding(.top, 32) + + Group { + SecureField("", text: $password) + .multilineTextAlignment(.center) + .background(Color.clear) + .focused($focusedField, equals: .password) + .submitLabel(.next) + .onChange(of: password) { _, _ in + validatePasswords() + } + .onAppear { + focusedField = .password + } + .onSubmit { + doSubmit() + } + } + .frame(height: 48) + .background(Color(.systemGray6)) + .cornerRadius(8) + + Spacer() + } + .padding([.horizontal], 24) + .navigationTitle("Encrypted Export") + .background(Color(red: 0.04, green: 0, blue: 0.11)) + .navigationBarTitleDisplayMode(.inline) + .scrollIndicators(.never) + .navigationBarItems(trailing: Button("Next", action: { + doSubmit() + }).disabled(!isPasswordValid)) + .navigationDestination(isPresented: $navigateToNextPage) { + EncryptedExportConfirmPasswordView(password: password) + } + } + + func doSubmit() { + if !isPasswordValid { + UINotificationFeedbackGenerator().notificationOccurred(.error) + return + } + + navigateToNextPage = true + } + + private func validatePasswords() { + if password.count < 10 { + isPasswordValid = false + passwordInvalidMsg = "Passwords must be at least 10 characters long" + } else { + isPasswordValid = true + passwordInvalidMsg = "" + } + } +} diff --git a/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift b/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift new file mode 100644 index 0000000..7406fcb --- /dev/null +++ b/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift @@ -0,0 +1,82 @@ +import Factory +import SwiftUI + +struct ExportSelectionView: View { + let exportService = Container.shared.exportService() + + @State private var showPlainTextExportConfirmation: Bool = false + @State private var showPlainTextExportSheet: Bool = false + @State private var encryptedBackupBtnPressed: Bool = false + + var body: some View { + NavigationStack { + VStack { + Image(systemName: "square.and.arrow.down") + .font(.system(size: 44)) + .padding(.bottom, 16) + + Text("A backup contains all your token data for this vault. Back up your vault regularly and keep it in a secure location to prevent any data loss.") + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + + Spacer() + + Button { + encryptedBackupBtnPressed = true + } label: { + Text("Encrypted Zip Archive") + .bold() + .frame(minWidth: 0, maxWidth: .infinity) + .frame(height: 32) + } + .buttonStyle(.bordered) + .navigationDestination(isPresented: $encryptedBackupBtnPressed) { + EncryptedExportPasswordView() + } + + Button { + showPlainTextExportConfirmation = true + } label: { + Text("Plaintext") + .bold() + .frame(minWidth: 0, maxWidth: .infinity) + .frame(height: 32) + } + .buttonStyle(.borderless) + .padding(.top, 4) + .confirmationDialog("Confirm Export", isPresented: $showPlainTextExportConfirmation, titleVisibility: .visible) { + Button("Confirm", role: .destructive, action: { + showPlainTextExportConfirmation = false + showPlainTextExportSheet = true + }) + + Button("Cancel", role: .cancel, action: { + showPlainTextExportConfirmation = false + showPlainTextExportSheet = false + }) + } message: { + Text("This export contains your token data in an unencrypted format. This file should not be stored or sent over unsecured channels.") + } + .sheet(isPresented: $showPlainTextExportSheet) { + if let fileurl = exportService.exportToUnencryptedJson() { + ActivityView(fileUrl: fileurl) + .presentationDetents([.medium, .large]) + } else { + VStack { + Image(systemName: "xmark.circle") + .fontWeight(.light) + .font(.system(size: 64)) + .padding(.bottom, 8) + Text("An error occurred while during the export process") + } + } + } + } + .navigationTitle("Export Selection") + .padding([.horizontal], 24) + .padding([.bottom], 32) + .background(Color(red: 0.04, green: 0, blue: 0.11)) + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/Chronos/App/Tabs/Settings/SettingsTab.swift b/Chronos/App/Tabs/Settings/SettingsTab.swift index ae7ed5e..974c173 100644 --- a/Chronos/App/Tabs/Settings/SettingsTab.swift +++ b/Chronos/App/Tabs/Settings/SettingsTab.swift @@ -2,6 +2,10 @@ import CloudKitSyncMonitor import Factory import SwiftUI +final class ExportNavigation: ObservableObject { + @Published var showSheet = false +} + struct SettingsTab: View { @EnvironmentObject private var loginStatus: LoginStatus @Environment(\.scenePhase) private var scenePhase @@ -10,13 +14,13 @@ struct SettingsTab: View { @AppStorage(StateEnum.ICLOUD_BACKUP_ENABLED.rawValue) private var isICloudEnabled: Bool = false @AppStorage(StateEnum.ICLOUD_SYNC_LAST_ATTEMPT.rawValue) private var iCloudSyncLastAttempt: TimeInterval = 0 + @StateObject private var exportNav = ExportNavigation() + private let secureEnclaveService = Container.shared.secureEnclaveService() private let swiftDataService = Container.shared.swiftDataService() private let stateService = Container.shared.stateService() - private let exportService = Container.shared.exportService() @State private var showExportJsonConfirmation: Bool = false - @State private var showExportJsonSheet: Bool = false @State private var showLogoutConfirmation = false @State private var lastSyncedText = "Syncing..." @@ -55,44 +59,19 @@ struct SettingsTab: View { Section { Button { - showExportJsonConfirmation = true + exportNav.showSheet = true } label: { Text("Export") .foregroundStyle(.blue) .frame(maxWidth: .infinity) } - .confirmationDialog("Confirm Export", isPresented: $showExportJsonConfirmation, titleVisibility: .visible) { - Button("Confirm", role: .destructive, action: { - self.showExportJsonConfirmation = false - self.showExportJsonSheet = true - }) - - Button("Cancel", role: .cancel, action: { - self.showExportJsonConfirmation = false - self.showExportJsonSheet = false - }) - } message: { - Text("This export contains your token data in an unencrypted format. This file should not be stored or sent over unsecured channels.") - } - .sheet(isPresented: $showExportJsonSheet) { - if let fileurl = exportService.exportToUnencryptedJson() { - ActivityView(fileUrl: fileurl) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(Visibility.hidden) - } else { - VStack { - Image(systemName: "xmark.circle") - .fontWeight(.light) - .font(.system(size: 64)) - .padding(.bottom, 8) - Text("An error occurred while during the export process") - } - } - } + .sheet(isPresented: $exportNav.showSheet, content: { + ExportSelectionView() + .environmentObject(exportNav) + }) .onChange(of: scenePhase) { _, newValue in if newValue != .active { - self.showExportJsonConfirmation = false - self.showExportJsonSheet = false + exportNav.showSheet = false } } } @@ -160,13 +139,3 @@ struct SettingsTab: View { } } } - -struct ActivityView: UIViewControllerRepresentable { - let fileUrl: URL - - func makeUIViewController(context _: Context) -> UIActivityViewController { - UIActivityViewController(activityItems: [fileUrl], applicationActivities: nil) - } - - func updateUIViewController(_: UIActivityViewController, context _: Context) {} -} diff --git a/Chronos/Extensions/ActivityView.swift b/Chronos/Extensions/ActivityView.swift new file mode 100644 index 0000000..cd098c2 --- /dev/null +++ b/Chronos/Extensions/ActivityView.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct ActivityView: UIViewControllerRepresentable { + let fileUrl: URL + + func makeUIViewController(context _: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: [fileUrl], applicationActivities: nil) + } + + func updateUIViewController(_: UIActivityViewController, context _: Context) {} +} diff --git a/Chronos/Services/ExportService.swift b/Chronos/Services/ExportService.swift index 496cd98..864fdfc 100644 --- a/Chronos/Services/ExportService.swift +++ b/Chronos/Services/ExportService.swift @@ -2,6 +2,7 @@ import Factory import Foundation import Logging import SwiftData +import ZipArchive public class ExportService { private let logger = Logger(label: "ExportService") @@ -51,4 +52,61 @@ public class ExportService { return url } + + func exportToEncryptedZip(password: String) -> URL? { + let context = ModelContext(swiftDataService.getModelContainer()) + let vault = vaultService.getVault(context: context) + + let exportVault = ExportVault() + + var tokens: [Token] = [] + + for encToken in vault!.encryptedTokens! { + guard let token = cryptoService.decryptToken(encryptedToken: encToken) else { + logger.error("Unable to decode token") + continue + } + tokens.append(token) + } + + exportVault.tokens = tokens + + let randomDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + + try? FileManager.default.createDirectory(at: randomDir, withIntermediateDirectories: true) + + let jsonUrl = randomDir.appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) + .appendingPathExtension("json") + + guard let jsonData = try? JSONEncoder().encode(exportVault) else { + logger.error("Unable to encode exportVault") + return nil + } + + do { + try jsonData.write(to: jsonUrl) + } catch { + logger.error("Unable to write json file: \(error.localizedDescription)") + return nil + } + + let zipUrl = FileManager.default.temporaryDirectory + .appendingPathComponent("Chronos_Encrypted_" + Date().formatted(verbatimStyle)) + .appendingPathExtension("zip") + + let success = SSZipArchive.createZipFile( + atPath: zipUrl.path(), + withContentsOfDirectory: randomDir.path(), + keepParentDirectory: false, + compressionLevel: 0, + password: password, + aes: true + ) + + if success { + return zipUrl + } + + return nil + } }