Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement vault model #4

Merged
merged 9 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/* Begin PBXBuildFile section */
6B12B0A02C19DB7800E9ED2D /* ExportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B09F2C19DB7800E9ED2D /* ExportService.swift */; };
6B12B0A22C19F1E600E9ED2D /* Vault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B0A12C19F1E600E9ED2D /* Vault.swift */; };
6B12B0A22C19F1E600E9ED2D /* ExportVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */; };
6B19CA722B7A70B800690390 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B19CA712B7A70B800690390 /* WelcomeView.swift */; };
6B19CA742B7A769900690390 /* PasswordSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B19CA732B7A769900690390 /* PasswordSetupView.swift */; };
6B2583AF2B96048700938F3A /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2583AE2B96048700938F3A /* SettingsTab.swift */; };
Expand All @@ -30,6 +30,8 @@
6B3BB1202B511A9E00DCEF0B /* SwiftOTP in Frameworks */ = {isa = PBXBuildFile; productRef = 6B3BB11F2B511A9E00DCEF0B /* SwiftOTP */; };
6B3C7A382BE764E70043FEBD /* StorageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3C7A372BE764E70043FEBD /* StorageSetupView.swift */; };
6B3C7A3D2BE9E0600043FEBD /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 6B3C7A3C2BE9E0600043FEBD /* Logging */; };
6B3F92A92C1B45AA004125A8 /* Vault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3F92A82C1B45AA004125A8 /* Vault.swift */; };
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 */; };
6B66D5E72B526315006DB79D /* AddTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B66D5E62B526315006DB79D /* AddTokenView.swift */; };
Expand All @@ -53,7 +55,7 @@

/* Begin PBXFileReference section */
6B12B09F2C19DB7800E9ED2D /* ExportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportService.swift; sourceTree = "<group>"; };
6B12B0A12C19F1E600E9ED2D /* Vault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vault.swift; sourceTree = "<group>"; };
6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportVault.swift; sourceTree = "<group>"; };
6B19CA712B7A70B800690390 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
6B19CA732B7A769900690390 /* PasswordSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSetupView.swift; sourceTree = "<group>"; };
6B2583AE2B96048700938F3A /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
Expand All @@ -73,6 +75,8 @@
6B3BB0DF2B4ECE6F00DCEF0B /* TOTPRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPRowView.swift; sourceTree = "<group>"; };
6B3BB0E12B4ED19300DCEF0B /* TokensTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensTab.swift; sourceTree = "<group>"; };
6B3C7A372BE764E70043FEBD /* StorageSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSetupView.swift; sourceTree = "<group>"; };
6B3F92A82C1B45AA004125A8 /* Vault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vault.swift; sourceTree = "<group>"; };
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>"; };
6B66D5E62B526315006DB79D /* AddTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTokenView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -219,6 +223,7 @@
children = (
6BE122912BD6413D008636D2 /* ChronosCrypto.swift */,
6B5E41CD2BD790F80045DBC6 /* EncryptedToken.swift */,
6B3F92A82C1B45AA004125A8 /* Vault.swift */,
);
path = Database;
sourceTree = "<group>";
Expand All @@ -242,6 +247,7 @@
6BD6D2002C11FEB4004512BF /* OTPService.swift */,
6B9D74642C14ADDC008E6582 /* StateService.swift */,
6B12B09F2C19DB7800E9ED2D /* ExportService.swift */,
6B3F92AA2C1C7987004125A8 /* VaultService.swift */,
);
path = Services;
sourceTree = "<group>";
Expand All @@ -258,7 +264,7 @@
isa = PBXGroup;
children = (
6B4B48F22BD7BB3C007D357D /* Token.swift */,
6B12B0A12C19F1E600E9ED2D /* Vault.swift */,
6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */,
);
path = Data;
sourceTree = "<group>";
Expand Down Expand Up @@ -410,7 +416,8 @@
files = (
6B3BB0B32B4EA87C00DCEF0B /* ContentView.swift in Sources */,
6B3BB0B12B4EA87C00DCEF0B /* ChronosApp.swift in Sources */,
6B12B0A22C19F1E600E9ED2D /* Vault.swift in Sources */,
6B3F92AB2C1C7987004125A8 /* VaultService.swift in Sources */,
6B12B0A22C19F1E600E9ED2D /* ExportVault.swift in Sources */,
6B7383E52B9C4230008E8867 /* Container.swift in Sources */,
6B3C7A382BE764E70043FEBD /* StorageSetupView.swift in Sources */,
6BC3C3B52BA6B91E00B181B9 /* BiometricsSetupView.swift in Sources */,
Expand All @@ -420,6 +427,7 @@
6B66D5E72B526315006DB79D /* AddTokenView.swift in Sources */,
6B2583AF2B96048700938F3A /* SettingsTab.swift in Sources */,
6B39629C2BF5EB3E000410B0 /* AuthenticationView.swift in Sources */,
6B3F92A92C1B45AA004125A8 /* Vault.swift in Sources */,
6B9581E22B62AFF80029EF3C /* ViewModifier.swift in Sources */,
6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */,
6B19CA742B7A769900690390 /* PasswordSetupView.swift in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions Chronos/App/Onboarding/PasswordSetupView.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import Factory
import SwiftData
import SwiftUI

enum FocusedField {
case password, verifyPassword
}

struct PasswordSetupView: View {
@Environment(\.modelContext) private var modelContext

@State private var password: String = ""
@State private var verifyPassword: String = ""
@State private var nextBtnPressed: Bool = false
@State private var isEncrypting: Bool = false

@Query() private var vaults: [Vault]

@FocusState private var focusedField: FocusedField?

let cryptoService = Container.shared.cryptoService()
Expand Down Expand Up @@ -97,6 +102,12 @@ extension PasswordSetupView {
func generateAndEncryptMasterKey() async {
stateService.masterKey = try! cryptoService.generateRandomMasterKey()

let vault = Vault(vaultId: UUID(), createdAt: Date(), chronosCryptos: [], encryptedTokens: [])
modelContext.insert(vault)
try? modelContext.save()

stateService.setVaultId(vaultId: vault.vaultId!)

await cryptoService.wrapMasterKeyWithUserPassword(password: Array(password.utf8))
nextBtnPressed = true
}
Expand Down
8 changes: 6 additions & 2 deletions Chronos/App/Onboarding/RestoreBackupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct RestoreBackupView: View {
@FocusState private var focusedField: FocusedField?

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

var body: some View {
VStack {
Expand Down Expand Up @@ -58,11 +60,13 @@ struct RestoreBackupView: View {
restoreBtnPressed = false

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

isICloudEnabled = true
} else {
passwordInvalid = true
let notificationGenerator = UINotificationFeedbackGenerator()
notificationGenerator.notificationOccurred(.error)
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
}
} label: {
Expand Down
4 changes: 0 additions & 4 deletions Chronos/App/Privacy/PrivacyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@ struct PrivacyView: View {
.background(Color(red: 0.04, green: 0, blue: 0.11))
}
}

#Preview {
PrivacyView()
}
1 change: 0 additions & 1 deletion Chronos/App/Tabs/Settings/SettingsTab.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Factory
import LinkPresentation
import SwiftUI

struct SettingsTab: View {
Expand Down
10 changes: 9 additions & 1 deletion Chronos/App/Tabs/Tokens/TokensTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ struct TokensTab: View {
@State private var selectedTokenForUpdate: Token? = nil
@State var detentHeight: CGFloat = 0

let stateService = Container.shared.stateService()

private var filteredEncyptedTokens: [EncryptedToken] {
return encyptedTokens.compactMap { encToken in
encToken.vault?.vaultId == stateService.getVaultId() ? encToken : nil
}
}

var body: some View {
ZStack {
NavigationStack {
ScrollViewReader { _ in
List(encyptedTokens) { encyptedToken in
List(filteredEncyptedTokens) { encyptedToken in
TokenRowView(tokenRowViewModel: TokenRowViewModel(encyptedToken: encyptedToken))
}
.listStyle(.plain)
Expand Down
2 changes: 1 addition & 1 deletion Chronos/Chronos.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.joeldavidw.chronos.test</string>
<string>iCloud.com.joeldavidw.ChronosApp</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

class Vault: Codable {
class ExportVault: Codable {
var tokens: [Token]?
var errors: [String]?
}
14 changes: 7 additions & 7 deletions Chronos/Database/ChronosCrypto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ struct PasswordParams: Codable {
var salt: String
}

enum KdfEnum: String, Codable, CaseIterable, Identifiable {
var id: String { rawValue }

case SCRYPT
}
// Type:
// 0: Scrypt

struct KdfParams: Codable {
var type: KdfEnum
var type: Int
var n: Int
var r: Int
var p: Int
}

@Model
class ChronosCrypto {
var vault: Vault?

var key: [UInt8]?
var keyParams: KeyParams?
var passwordParams: PasswordParams?
var kdfParams: KdfParams?

init(key: [UInt8], keyParams: KeyParams, passwordParams: PasswordParams, kdfParams: KdfParams) {
init(vault: Vault, key: [UInt8]? = nil, keyParams: KeyParams? = nil, passwordParams: PasswordParams? = nil, kdfParams: KdfParams? = nil) {
self.vault = vault
self.key = key
self.keyParams = keyParams
self.passwordParams = passwordParams
Expand Down
5 changes: 4 additions & 1 deletion Chronos/Database/EncryptedToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import SwiftData

@Model
class EncryptedToken {
var vault: Vault?

var encryptedTokenCiper: [UInt8]?
var iv: [UInt8]?
var authenticationTag: [UInt8]?
var createdAt: Date?

init(encryptedTokenCiper: [UInt8], iv: [UInt8], authenticationTag: [UInt8], createdAt: Date) {
init(vault: Vault, encryptedTokenCiper: [UInt8]? = nil, iv: [UInt8]? = nil, authenticationTag: [UInt8]? = nil, createdAt: Date? = nil) {
self.vault = vault
self.encryptedTokenCiper = encryptedTokenCiper
self.iv = iv
self.authenticationTag = authenticationTag
Expand Down
21 changes: 21 additions & 0 deletions Chronos/Database/Vault.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import SwiftData

@Model
class Vault {
var vaultId: UUID?
var createdAt: Date?

@Relationship(deleteRule: .cascade, inverse: \ChronosCrypto.vault)
var chronosCryptos: [ChronosCrypto]?

@Relationship(deleteRule: .cascade, inverse: \EncryptedToken.vault)
var encryptedTokens: [EncryptedToken]?

init(vaultId: UUID? = nil, createdAt: Date? = nil, chronosCryptos: [ChronosCrypto]? = nil, encryptedTokens: [EncryptedToken]? = nil) {
self.vaultId = vaultId
self.createdAt = createdAt
self.chronosCryptos = chronosCryptos
self.encryptedTokens = encryptedTokens
}
}
4 changes: 4 additions & 0 deletions Chronos/Services/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ extension Container {
.singleton
}

var vaultService: Factory<VaultService> {
Factory(self) { VaultService() }
}

var cryptoService: Factory<CryptoService> {
Factory(self) { CryptoService() }
}
Expand Down
41 changes: 29 additions & 12 deletions Chronos/Services/CryptoService.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CryptoSwift
import Factory
import Foundation
import Logging
import SwiftData

enum CryptoError: Error {
Expand All @@ -9,13 +10,14 @@ enum CryptoError: Error {
}

public class CryptoService {
let stateService = Container.shared.stateService()
let swiftDataService = Container.shared.swiftDataService()
private let logger = Logger(label: "CryptoService")

let defaults = UserDefaults.standard
private let stateService = Container.shared.stateService()
private let vaultService = Container.shared.vaultService()
private let swiftDataService = Container.shared.swiftDataService()

// scrypt paramaters - n: 2^17, r: 8, p: 1
let kdfParams = KdfParams(type: KdfEnum.SCRYPT, n: 1 << 17, r: 8, p: 1)
private let kdfParams = KdfParams(type: 0, n: 1 << 17, r: 8, p: 1)

func wrapMasterKeyWithUserPassword(password: [UInt8]) async {
let passwordSalt = try! generateRandomSaltHexString()
Expand All @@ -31,9 +33,10 @@ public class CryptoService {

let keyParams = KeyParams(iv: iv, tag: encrypt.authenticationTag)

let newPasswordCrypto = ChronosCrypto(key: encrypt.cipherText, keyParams: keyParams, passwordParams: passwordParams, kdfParams: kdfParams)
let newPasswordCrypto = ChronosCrypto(vault: vaultService.getVault()!, key: encrypt.cipherText, keyParams: keyParams, passwordParams: passwordParams, kdfParams: kdfParams)

let context = ModelContext(swiftDataService.getModelContainer())

context.insert(newPasswordCrypto)
try context.save()
} catch {
Expand All @@ -42,36 +45,50 @@ public class CryptoService {
}

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

let cryptoArr = try! context.fetch(FetchDescriptor<ChronosCrypto>())
guard let cryptoArr = vault.chronosCryptos else {
logger.error("No crypto found in vault")
return false
}

if cryptoArr.isEmpty {
stateService.resetAllStates()
logger.error("Empty crypto found in vault")
return false
}

let crypto: ChronosCrypto = cryptoArr.first!
guard let crypto = cryptoArr.first else {
logger.error("Empty crypto found in vault")
return false
}

let passwordHash = Array(createPasswordHash(password: password, salt: crypto.passwordParams!.salt, kdfParms: kdfParams)!)

let header = Array("".utf8)

do {
guard let cryptoKey = crypto.key, let keyParams = crypto.keyParams else {
fatalError("var nil")
logger.error("cryptoKey or keyParams is nil")
return false
}

var decrypt = try AEADXChaCha20Poly1305.decrypt(cryptoKey, key: passwordHash, iv: keyParams.iv, authenticationHeader: header, authenticationTag: keyParams.tag)

if decrypt.success {
stateService.masterKey = SecureBytes(bytes: decrypt.plainText)
stateService.setVaultId(vaultId: vault.vaultId!)

decrypt.plainText.removeAll()
return true
} else {
logger.error("Unable to decrypt crypto with passwordHash")
return false
}
} catch {
fatalError(error.localizedDescription)
logger.error("Error encountered while decrypting crypto. Error: \(error.localizedDescription)")
return false
}
}
}
Expand All @@ -85,7 +102,7 @@ extension CryptoService {
let tokenJson = try JSONEncoder().encode(token)
let encrypt = try AEADXChaCha20Poly1305.encrypt(Array(tokenJson), key: Array(stateService.masterKey), iv: iv, authenticationHeader: header)

return EncryptedToken(encryptedTokenCiper: encrypt.cipherText, iv: iv, authenticationTag: encrypt.authenticationTag, createdAt: Date())
return EncryptedToken(vault: vaultService.getVault()!, encryptedTokenCiper: encrypt.cipherText, iv: iv, authenticationTag: encrypt.authenticationTag, createdAt: Date())
} catch {
fatalError(error.localizedDescription)
}
Expand Down
Loading