Skip to content

Commit

Permalink
feat: Implement password reminder prompt (#34)
Browse files Browse the repository at this point in the history
* Added password reminder view

* Added settings, timer and haptic feedback
  • Loading branch information
joeldavidw authored Jul 22, 2024
1 parent ad301a0 commit b1ebe5d
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
6B8132F72C4BB2B200DB367E /* EFQRCode in Frameworks */ = {isa = PBXBuildFile; productRef = 6B8132F62C4BB2B200DB367E /* EFQRCode */; };
6B8132FA2C4C0F6300DB367E /* TokenToOtpAuthUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8132F92C4C0F6300DB367E /* TokenToOtpAuthUrl.swift */; };
6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8132FC2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift */; };
6B8133002C4E634100DB367E /* PasswordReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8132FF2C4E634100DB367E /* PasswordReminder.swift */; };
6B842DD52BE33E2E00056F0F /* RestoreBackupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B842DD42BE33E2E00056F0F /* RestoreBackupView.swift */; };
6B8ABEEC2B8F6A4F00F5B514 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6B8ABEEB2B8F6A4F00F5B514 /* CryptoSwift */; };
6B9581E22B62AFF80029EF3C /* ViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9581E12B62AFF80029EF3C /* ViewModifier.swift */; };
Expand Down Expand Up @@ -122,6 +123,7 @@
6B8132F32C4BAD5A00DB367E /* TokenQRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenQRView.swift; sourceTree = "<group>"; };
6B8132F92C4C0F6300DB367E /* TokenToOtpAuthUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenToOtpAuthUrl.swift; sourceTree = "<group>"; };
6B8132FC2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeGenerationAndParsing.swift; sourceTree = "<group>"; };
6B8132FF2C4E634100DB367E /* PasswordReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordReminder.swift; sourceTree = "<group>"; };
6B842DD42BE33E2E00056F0F /* RestoreBackupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreBackupView.swift; sourceTree = "<group>"; };
6B9581E12B62AFF80029EF3C /* ViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifier.swift; sourceTree = "<group>"; };
6B9581E32B633A880029EF3C /* AddManualTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddManualTokenView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -243,6 +245,7 @@
6B3BB0DA2B4EB04600DCEF0B /* App */ = {
isa = PBXGroup;
children = (
6B8132FE2C4E632800DB367E /* Misc */,
6B7A54722B94B7960057DCF9 /* Privacy */,
6BD90AA22B8E349500FABD91 /* Login */,
6B19CA6E2B7A695200690390 /* Onboarding */,
Expand Down Expand Up @@ -360,6 +363,14 @@
path = Token;
sourceTree = "<group>";
};
6B8132FE2C4E632800DB367E /* Misc */ = {
isa = PBXGroup;
children = (
6B8132FF2C4E634100DB367E /* PasswordReminder.swift */,
);
path = Misc;
sourceTree = "<group>";
};
6B8581BB2B99FB4400A19CE1 /* Data */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -629,6 +640,7 @@
6B3962A02BF6423B000410B0 /* CryptoService.swift in Sources */,
6B27317A2B53E23800F30621 /* UpdateTokenView.swift in Sources */,
6B27317C2B53F0B200F30621 /* TokenRowView.swift in Sources */,
6B8133002C4E634100DB367E /* PasswordReminder.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"originHash" : "a10ff4a5835952be5d746ae6edf88cae1d0b20500819d37f98f2f742855d195a",
"pins" : [
{
"identity" : "alertkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparrowcode/AlertKit",
"state" : {
"revision" : "3b73be8db5a7e7efaf474c6ed919f5a437d843c9",
"version" : "5.1.9"
}
},
{
"identity" : "cloudkitsyncmonitor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ggruen/CloudKitSyncMonitor",
"state" : {
"revision" : "d3bca51a84e416a64bd30d65287041a37f0612f3",
"version" : "1.4.0"
}
},
{
"identity" : "codescanner",
"kind" : "remoteSourceControl",
"location" : "https://github.com/twostraws/CodeScanner.git",
"state" : {
"revision" : "34da57fb63b47add20de8a85da58191523ccce57",
"version" : "2.5.0"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0",
"version" : "1.8.2"
}
},
{
"identity" : "efqrcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/EFPrefix/EFQRCode",
"state" : {
"revision" : "2991c2f318ad9529d93b2a73a382a3f9c72c64ce",
"version" : "6.2.2"
}
},
{
"identity" : "factory",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hmlongco/Factory",
"state" : {
"revision" : "587995f7d5cc667951d635fbf6b4252324ba0439",
"version" : "2.3.2"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "46072478ca365fe48370993833cb22de9b41567f",
"version" : "3.5.2"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
}
},
{
"identity" : "swift_qrcodejs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ApolloZhu/swift_qrcodejs.git",
"state" : {
"revision" : "374dc7f7b9e76c6aeb393f6a84590c6d387e1ecb",
"version" : "2.2.2"
}
},
{
"identity" : "swiftotp",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lachlanbell/SwiftOTP.git",
"state" : {
"revision" : "9660551ea3df153c3cbacfa34ac3abbec73a8b84",
"version" : "3.0.2"
}
},
{
"identity" : "swiftyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828",
"version" : "5.0.2"
}
},
{
"identity" : "valet",
"kind" : "remoteSourceControl",
"location" : "https://github.com/square/Valet",
"state" : {
"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
}
14 changes: 14 additions & 0 deletions Chronos/App/MainAppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct MainAppView: View {
@Query private var vaults: [Vault]

@State private var currentTab: String = "Tokens"
@State private var showPasswordReminder = false

@Environment(\.modelContext) private var modelContext
@Environment(\.scenePhase) private var scenePhase
Expand All @@ -15,6 +16,9 @@ struct MainAppView: View {
@ObservedObject var syncMonitor = SyncMonitor.shared

@AppStorage(StateEnum.ICLOUD_SYNC_LAST_ATTEMPT.rawValue) var iCloudSyncLastAttempt: TimeInterval = 0
@AppStorage(StateEnum.NEXT_PASSWORD_REMINDER_TIMESTAMP.rawValue) var nextPasswordReminderTimestamp: TimeInterval = 0
@AppStorage(StateEnum.BIOMETRICS_AUTH_ENABLED.rawValue) var biometricsEnabled: Bool = false
@AppStorage(StateEnum.PASSWORD_REMINDER_ENABLED.rawValue) private var statePasswordReminderEnabled: Bool = false

private let stateService = Container.shared.stateService()

Expand Down Expand Up @@ -49,6 +53,13 @@ struct MainAppView: View {
stateService.resetAllStates()
loginStatus.loggedIn = false
}

if biometricsEnabled && statePasswordReminderEnabled {
print("Fuck \(nextPasswordReminderTimestamp)")
if Date().timeIntervalSince1970 >= nextPasswordReminderTimestamp {
showPasswordReminder = true
}
}
}
.onChange(of: filteredVault) { _, newValue in
if newValue.isEmpty {
Expand All @@ -61,5 +72,8 @@ struct MainAppView: View {
iCloudSyncLastAttempt = Date().timeIntervalSince1970
}
}
.sheet(isPresented: $showPasswordReminder, content: {
PasswordReminderView()
})
}
}
123 changes: 123 additions & 0 deletions Chronos/App/Misc/PasswordReminder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import AlertKit
import Factory
import SwiftData
import SwiftUI

struct PasswordReminderView: View {
@Environment(\.dismiss) var dismiss

@State private var verifyPressed: Bool = false
@State private var password: String = ""
@State private var passwordInvalid: Bool = false

@FocusState private var focusedField: FocusedField?

@AppStorage(StateEnum.PASSWORD_REMINDER_ENABLED.rawValue) private var statePasswordReminderEnabled: Bool = false
@AppStorage(StateEnum.NEXT_PASSWORD_REMINDER_TIMESTAMP.rawValue) var nextPasswordReminderTimestamp: TimeInterval = 0

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

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

Text("This is an occasional prompt to ensure you don’t forget your password.")
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.center)

Group {
HStack {
SecureField("Enter your password", text: $password)
.background(Color.clear)
.focused($focusedField, equals: .password)
.disabled(verifyPressed)
.submitLabel(.done)
.onSubmit {
doSubmit()
}
}
}
.frame(height: 48)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.top, 32)

if passwordInvalid {
Text("Invalid password. Check your password and try again.")
.multilineTextAlignment(.center)
.foregroundStyle(.red)
.font(.subheadline)
.padding(.top, 4)
}

Button {
doSubmit()
} label: {
if !verifyPressed {
Text("Verify")
.bold()
.foregroundStyle(Color(red: 0.04, green: 0, blue: 0.11))
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32)
.disabled(verifyPressed)
} else {
ProgressView()
.tint(Color(red: 0.04, green: 0, blue: 0.11))
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32)
}
}
.padding(.top, 32)
.buttonStyle(.borderedProminent)

Button {
dismiss()
} label: {
Text("Skip")
.bold()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 32)
}
.buttonStyle(.borderless)
}
.padding(16)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color(red: 0.04, green: 0, blue: 0.11).ignoresSafeArea())
.navigationTitle("Password Reminder")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
focusedField = .password

nextPasswordReminderTimestamp = Date().timeIntervalSince1970 + (2 * 7 * 24 * 60 * 60)
}
}
.presentationDragIndicator(.visible)
}

func doSubmit() {
verifyPressed = true

Task {
let context = ModelContext(swiftDataService.getModelContainer())
let vault = vaultService.getVault(context: context)!

let passwordVerified = await cryptoService.unwrapMasterKeyWithUserPassword(vault: vault, password: Array(password.utf8))

if passwordVerified {
dismiss()
await UINotificationFeedbackGenerator().notificationOccurred(.success)
} else {
passwordInvalid = true
await UINotificationFeedbackGenerator().notificationOccurred(.error)
}

verifyPressed = false
}
}
}
14 changes: 14 additions & 0 deletions Chronos/App/Tabs/Settings/SettingsTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ 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

@AppStorage(StateEnum.PASSWORD_REMINDER_ENABLED.rawValue) private var statePasswordReminderEnabled: Bool = false
@AppStorage(StateEnum.NEXT_PASSWORD_REMINDER_TIMESTAMP.rawValue) var nextPasswordReminderTimestamp: TimeInterval = 0

@StateObject private var exportNav = ExportNavigation()
@StateObject private var importNav = ExportNavigation()

Expand Down Expand Up @@ -104,6 +107,17 @@ struct SettingsTab: View {
secureEnclaveService.deleteMasterKey()
}
}

if stateBiometricsAuth {
Toggle(isOn: $statePasswordReminderEnabled, label: {
Text("Password Reminder (2 Weeks)")
})
.onChange(of: statePasswordReminderEnabled) { _, enabled in
if enabled {
nextPasswordReminderTimestamp = Date().timeIntervalSince1970 + (2 * 7 * 24 * 60 * 60)
}
}
}
}

Section {
Expand Down
6 changes: 6 additions & 0 deletions Chronos/Services/StateService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ enum StateEnum: String {
case LAST_BIOMETRICS_AUTH_ATTEMPT

case ICLOUD_SYNC_LAST_ATTEMPT

case PASSWORD_REMINDER_ENABLED
case NEXT_PASSWORD_REMINDER_TIMESTAMP
}

public class StateService {
Expand Down Expand Up @@ -41,6 +44,9 @@ public class StateService {

defaults.setValue(Date().timeIntervalSince1970, forKey: StateEnum.ICLOUD_SYNC_LAST_ATTEMPT.rawValue)

defaults.setValue(4_102_444_800, forKey: StateEnum.NEXT_PASSWORD_REMINDER_TIMESTAMP.rawValue)
defaults.setValue(true, forKey: StateEnum.PASSWORD_REMINDER_ENABLED.rawValue)

masterKey.clear()
}

Expand Down

0 comments on commit b1ebe5d

Please sign in to comment.