Skip to content

Commit

Permalink
feat: implement encrypted export (#22)
Browse files Browse the repository at this point in the history
* WIP

* Added encrypted export
  • Loading branch information
joeldavidw authored Jul 3, 2024
1 parent ecec243 commit e90b640
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 44 deletions.
49 changes: 49 additions & 0 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -99,6 +104,10 @@
6BD6D2002C11FEB4004512BF /* OTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPService.swift; sourceTree = "<group>"; };
6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordLoginView.swift; sourceTree = "<group>"; };
6BE122912BD6413D008636D2 /* ChronosCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronosCrypto.swift; sourceTree = "<group>"; };
6BF53E512C317F1C00356461 /* ExportSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSelectionView.swift; sourceTree = "<group>"; };
6BF53E532C31856A00356461 /* EncryptedExportPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedExportPasswordView.swift; sourceTree = "<group>"; };
6BF53E562C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedExportConfirmPasswordView.swift; sourceTree = "<group>"; };
6BF53E5A2C33FD8F00356461 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -114,6 +123,7 @@
6B7383E02B9C3962008E8867 /* Factory in Frameworks */,
6B3C7A3D2BE9E0600043FEBD /* Logging in Frameworks */,
6B193C8A2C27F03300E759B7 /* CloudKitSyncMonitor in Frameworks */,
6BF53E4F2C317AA400356461 /* ZipArchive in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -136,6 +146,7 @@
6B2583AD2B96047700938F3A /* Settings */ = {
isa = PBXGroup;
children = (
6BF53E502C317EFF00356461 /* Export */,
6B2583AE2B96048700938F3A /* SettingsTab.swift */,
);
path = Settings;
Expand Down Expand Up @@ -308,6 +319,7 @@
isa = PBXGroup;
children = (
6B9581E12B62AFF80029EF3C /* ViewModifier.swift */,
6BF53E5A2C33FD8F00356461 /* ActivityView.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand All @@ -320,6 +332,24 @@
path = Login;
sourceTree = "<group>";
};
6BF53E502C317EFF00356461 /* Export */ = {
isa = PBXGroup;
children = (
6BF53E552C32EF5800356461 /* EncryptedExport */,
6BF53E512C317F1C00356461 /* ExportSelectionView.swift */,
);
path = Export;
sourceTree = "<group>";
};
6BF53E552C32EF5800356461 /* EncryptedExport */ = {
isa = PBXGroup;
children = (
6BF53E532C31856A00356461 /* EncryptedExportPasswordView.swift */,
6BF53E562C32EF7700356461 /* EncryptedExportConfirmPasswordView.swift */,
);
path = EncryptedExport;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand All @@ -346,6 +376,7 @@
6B7383DF2B9C3962008E8867 /* Factory */,
6B3C7A3C2BE9E0600043FEBD /* Logging */,
6B193C892C27F03300E759B7 /* CloudKitSyncMonitor */,
6BF53E4E2C317AA400356461 /* ZipArchive */,
);
productName = Chronos;
productReference = 6B3BB0AD2B4EA87C00DCEF0B /* Chronos.app */;
Expand Down Expand Up @@ -384,6 +415,7 @@
6B7383DE2B9C3962008E8867 /* XCRemoteSwiftPackageReference "Factory" */,
6B3C7A3B2BE9E0600043FEBD /* XCRemoteSwiftPackageReference "swift-log" */,
6B193C882C27F03300E759B7 /* XCRemoteSwiftPackageReference "CloudKitSyncMonitor" */,
6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */,
);
productRefGroup = 6B3BB0AE2B4EA87C00DCEF0B /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "e744487b2e4216f33f17ad69d0ef61baaf0ab4ced9df8591f51d30bcabba5561",
"originHash" : "83b69a06fc95c43243855fb2ff571a8bf2cf54943f4ffc4a10a2c75a6d98f73b",
"pins" : [
{
"identity" : "alertkit",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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 = ""
}
}
}
Loading

0 comments on commit e90b640

Please sign in to comment.