Skip to content

Commit

Permalink
feat: implemented vault creation & restore (#9)
Browse files Browse the repository at this point in the history
* implemented vault creation & restore screens

* fix writing

* Updated

* cleanup vaultservice

* revert doesICloudBackupExist
  • Loading branch information
joeldavidw authored Jun 18, 2024
1 parent 8e275ae commit a4005c1
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 51 deletions.
22 changes: 19 additions & 3 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
6B3F92AB2C1C7987004125A8 /* VaultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3F92AA2C1C7987004125A8 /* VaultService.swift */; };
6B4B48F32BD7BB3C007D357D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4B48F22BD7BB3C007D357D /* Token.swift */; };
6B5E41CE2BD790F80045DBC6 /* EncryptedToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E41CD2BD790F80045DBC6 /* EncryptedToken.swift */; };
6B65F8082C21C17200AC8606 /* VaultSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B65F8072C21C17200AC8606 /* VaultSelectionView.swift */; };
6B65F80B2C21C2EA00AC8606 /* VaultSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B65F80A2C21C2EA00AC8606 /* VaultSetupView.swift */; };
6B66D5E72B526315006DB79D /* AddTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B66D5E62B526315006DB79D /* AddTokenView.swift */; };
6B66D5ED2B526648006DB79D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 6B66D5EC2B526648006DB79D /* CodeScanner */; };
6B66D5EF2B52BAC2006DB79D /* HOTPRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B66D5EE2B52BAC2006DB79D /* HOTPRowView.swift */; };
Expand Down Expand Up @@ -79,6 +81,8 @@
6B3F92AA2C1C7987004125A8 /* VaultService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultService.swift; sourceTree = "<group>"; };
6B4B48F22BD7BB3C007D357D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
6B5E41CD2BD790F80045DBC6 /* EncryptedToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedToken.swift; sourceTree = "<group>"; };
6B65F8072C21C17200AC8606 /* VaultSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultSelectionView.swift; sourceTree = "<group>"; };
6B65F80A2C21C2EA00AC8606 /* VaultSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultSetupView.swift; sourceTree = "<group>"; };
6B66D5E62B526315006DB79D /* AddTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTokenView.swift; sourceTree = "<group>"; };
6B66D5EE2B52BAC2006DB79D /* HOTPRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HOTPRowView.swift; sourceTree = "<group>"; };
6B7383E42B9C4230008E8867 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -117,11 +121,12 @@
6B19CA6E2B7A695200690390 /* Onboarding */ = {
isa = PBXGroup;
children = (
6B65F8062C21C15E00AC8606 /* Restore */,
6B19CA712B7A70B800690390 /* WelcomeView.swift */,
6B19CA732B7A769900690390 /* PasswordSetupView.swift */,
6B842DD42BE33E2E00056F0F /* RestoreBackupView.swift */,
6B3C7A372BE764E70043FEBD /* StorageSetupView.swift */,
6BC3C3B42BA6B91E00B181B9 /* BiometricsSetupView.swift */,
6B65F80A2C21C2EA00AC8606 /* VaultSetupView.swift */,
);
path = Onboarding;
sourceTree = "<group>";
Expand Down Expand Up @@ -228,6 +233,15 @@
path = Database;
sourceTree = "<group>";
};
6B65F8062C21C15E00AC8606 /* Restore */ = {
isa = PBXGroup;
children = (
6B842DD42BE33E2E00056F0F /* RestoreBackupView.swift */,
6B65F8072C21C17200AC8606 /* VaultSelectionView.swift */,
);
path = Restore;
sourceTree = "<group>";
};
6B66D5E52B525D1B006DB79D /* AddToken */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -437,6 +451,7 @@
6B3BB0E22B4ED19300DCEF0B /* TokensTab.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 */,
6B842DD52BE33E2E00056F0F /* RestoreBackupView.swift in Sources */,
Expand All @@ -445,6 +460,7 @@
6B39629E2BF63F27000410B0 /* SwiftDataService.swift in Sources */,
6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */,
6B39629A2BF5E935000410B0 /* MainAppView.swift in Sources */,
6B65F8082C21C17200AC8606 /* VaultSelectionView.swift in Sources */,
6B3962A02BF6423B000410B0 /* CryptoService.swift in Sources */,
6B27317A2B53E23800F30621 /* UpdateTokenView.swift in Sources */,
6B27317C2B53F0B200F30621 /* TokenRowView.swift in Sources */,
Expand Down Expand Up @@ -607,7 +623,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.Chronos;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -653,7 +669,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.Chronos;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
3 changes: 2 additions & 1 deletion Chronos/App/AuthenticationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ struct AuthenticationView: View {
Group {
if !stateOnboardingCompleted {
WelcomeView()
// Defaults to a cloud container for SwiftUI on the first load. Allows SwiftData in SwiftUI to retrieve vaults from CloudKit.
.modelContainer(swiftDataService.getCloudModelContainer())
} else {
PasswordLoginView()
}
}
.modelContainer(swiftDataService.getCloudModelContainer())
}
}
1 change: 0 additions & 1 deletion Chronos/App/Login/PasswordLoginView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import AlertKit
import Factory
import SwiftData
import SwiftUI

struct PasswordLoginView: View {
Expand Down
9 changes: 4 additions & 5 deletions Chronos/App/Onboarding/PasswordSetupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct PasswordSetupView: View {

@FocusState private var focusedField: FocusedField?

let vaultName: String

let cryptoService = Container.shared.cryptoService()
let stateService = Container.shared.stateService()
let vaultService = Container.shared.vaultService()
Expand All @@ -24,7 +26,7 @@ struct PasswordSetupView: View {
.font(.system(size: 44))
.padding(.bottom, 16)

Text("Your master password is used to encrypt your data securely. Choose a memorable, random, and unique password with at least 10 characters.")
Text("Your master password is used to securely encrypt your tokens in a vault. Choose a memorable, random, and unique password with at least 10 characters.")
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.center)

Expand Down Expand Up @@ -100,10 +102,7 @@ extension PasswordSetupView {

let chronosCrypto = await cryptoService.wrapMasterKeyWithUserPassword(password: Array(password.utf8))

let vault = vaultService.createVault(chronosCrypto: chronosCrypto)!
stateService.setVaultId(vaultId: vault.vaultId!)

nextBtnPressed = true
nextBtnPressed = vaultService.createVaultCrypto(vaultName: vaultName, chronosCrypto: chronosCrypto)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ struct RestoreBackupView: View {
restoreBtnPressed = false

if passwordVerified {
let vault = vaultService.getFirstVault(isRestore: true)
stateService.setVaultId(vaultId: vault!.vaultId!)

isICloudEnabled = true
} else {
passwordInvalid = true
Expand Down
67 changes: 67 additions & 0 deletions Chronos/App/Onboarding/Restore/VaultSelectionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import AlertKit
import Factory
import SwiftData
import SwiftUI

struct VaultSelectionView: View {
@Query(sort: \Vault.createdAt) var vaults: [Vault]

@State private var moveToNextScreen: Bool = false

let cryptoService = Container.shared.cryptoService()
let vaultService = Container.shared.vaultService()
let stateService = Container.shared.stateService()

let formatter = RelativeDateTimeFormatter()

var body: some View {
VStack {
ScrollViewReader { _ in
List(vaults) { vault in
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(vault.name)
.fontWeight(.semibold)

Text("Created \(formatter.localizedString(for: Date(timeIntervalSince1970: vault.createdAt!.timeIntervalSince1970), relativeTo: Date.now))")
.foregroundStyle(.gray)
.font(.subheadline)
}

Spacer()

VStack(alignment: .trailing) {
Text("\(vault.encryptedTokens?.count ?? 0)")
.font(.system(size: 20))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color(.gray).opacity(0.6))
.cornerRadius(8)
}
}
.contentShape(Rectangle())
.onTapGesture {
guard let vaultId = vault.vaultId else {
AlertKitAPI.present(
title: "Unable to access vault",
icon: .error,
style: .iOS17AppleMusic,
haptic: .error
)
return
}

stateService.setVaultId(vaultId: vaultId)
moveToNextScreen = true
}
.padding(CGFloat(4))
}
.listStyle(.plain)
}
}
.navigationDestination(isPresented: $moveToNextScreen) {
RestoreBackupView()
}
.navigationTitle("Vaults")
}
}
3 changes: 2 additions & 1 deletion Chronos/App/Onboarding/StorageSetupView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Factory
import SwiftData
import SwiftUI

struct StorageSetupView: View {
Expand Down Expand Up @@ -57,7 +58,7 @@ struct StorageSetupView: View {
.navigationTitle("Storage")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(isPresented: $nextBtnPressed) {
PasswordSetupView()
VaultSetupView()
}
.confirmationDialog("Vault Exists", isPresented: $showICloudOverwriteConfirmation, titleVisibility: .visible) {
Button("Continue", role: .destructive, action: {
Expand Down
63 changes: 63 additions & 0 deletions Chronos/App/Onboarding/VaultSetupView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Factory
import SwiftUI

struct VaultSetupView: View {
let vaultService = Container.shared.vaultService()

@State private var vaultName: String = "My Vault"
@State private var isCreatingVault: Bool = false
@State private var nextBtnPressed: Bool = false

var body: some View {
VStack {
Image(systemName: "lock.shield")
.font(.system(size: 44))
.padding(.bottom, 16)

Text("A vault contains all of your Two-Factor Authentication (2FA) tokens and is secured with your own password.")
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.center)

Text("Your vault name")
.padding(.top, 32)

Group {
TextField("", text: $vaultName)
.multilineTextAlignment(.center)
.background(Color.clear)
}
.frame(height: 48)
.background(Color(.systemGray6))
.cornerRadius(8)

Spacer()

Button {
nextBtnPressed = true
} label: {
if !isCreatingVault {
Text("Next")
.foregroundStyle(Color(red: 0.04, green: 0, blue: 0.11))
.bold()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32)
} else {
ProgressView()
.tint(Color(red: 0.04, green: 0, blue: 0.11))
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32)
}
}
.padding(.top, 64)
.buttonStyle(.borderedProminent)
}
.padding(.vertical, 32)
.padding(.horizontal, 24)
.background(Color(red: 0.04, green: 0, blue: 0.11).ignoresSafeArea())
.navigationTitle("Vault")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(isPresented: $nextBtnPressed) {
PasswordSetupView(vaultName: vaultName)
}
}
}
10 changes: 7 additions & 3 deletions Chronos/App/Onboarding/WelcomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SwiftUI
struct WelcomeView: View {
let swiftDataService = Container.shared.swiftDataService()

@Query var chronosCryptos: [ChronosCrypto]
@Query var vaults: [Vault]

@State private var getStartedPressed: Bool = false
@State private var restorePressed: Bool = false
Expand Down Expand Up @@ -40,7 +40,7 @@ struct WelcomeView: View {
.frame(height: 32)
}
.padding(.top, 4)
.disabled(chronosCryptos.isEmpty)
.disabled(vaults.isEmpty)
.buttonStyle(.borderless)
.padding(.bottom, 32)
}
Expand All @@ -52,7 +52,11 @@ struct WelcomeView: View {
StorageSetupView()
}
.navigationDestination(isPresented: $restorePressed) {
RestoreBackupView()
if vaults.count == 1 {
RestoreBackupView()
} else if vaults.count > 1 {
VaultSelectionView()
}
}
}
.onAppear(perform: {
Expand Down
4 changes: 3 additions & 1 deletion Chronos/Database/Vault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import SwiftData
class Vault {
var vaultId: UUID?
var createdAt: Date?
var name: String = "My Vault"

@Relationship(deleteRule: .cascade)
var chronosCryptos: [ChronosCrypto]? = []

@Relationship(deleteRule: .cascade)
var encryptedTokens: [EncryptedToken]? = []

init(vaultId: UUID, createdAt: Date) {
init(vaultId: UUID, name: String, createdAt: Date) {
self.vaultId = vaultId
self.name = name
self.createdAt = createdAt
}
}
4 changes: 2 additions & 2 deletions Chronos/Services/CryptoService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public class CryptoService {
}
}

func unwrapMasterKeyWithUserPassword(password: [UInt8], isRestore: Bool = false) async -> Bool {
guard let vault = vaultService.getFirstVault(isRestore: isRestore) else {
func unwrapMasterKeyWithUserPassword(password: [UInt8], isRestore _: Bool = false) async -> Bool {
guard let vault = vaultService.getVaultFromCloudContainer() else {
return false
}

Expand Down
4 changes: 2 additions & 2 deletions Chronos/Services/SwiftDataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ extension SwiftDataService {
func doesICloudBackupExist() -> Bool {
let container = getCloudModelContainer()
let context = ModelContext(container)
let cryptoArr = try! context.fetch(FetchDescriptor<ChronosCrypto>())
let vaults = try! context.fetch(FetchDescriptor<Vault>())

return !cryptoArr.isEmpty
return !vaults.isEmpty
}

func deleteLocallyPersistedChronosData() {
Expand Down
Loading

0 comments on commit a4005c1

Please sign in to comment.