ios: digital password (instead of device auth) (#2169)
* ios: digital password (instead of device auth) * set, ask, change password * kind of working, sometimes * ZSTack * fix cancel * update title * fix password showing after settings dismissed * disable button when 16 digits entered * fixes * layout on larger screens * do not disable auth when switching to system if system auth failed, refactor * fix enabling auth via the initial alert * support landscape orientation
This commit is contained in:
committed by
GitHub
parent
1d16a19373
commit
ec6cee1389
@@ -23,7 +23,10 @@ struct ContentView: View {
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||
@State private var showSettings = false
|
||||
@State private var showWhatsNew = false
|
||||
@State private var showChooseLAMode = false
|
||||
@State private var showSetPasscode = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -31,6 +34,20 @@ struct ContentView: View {
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
callView(call)
|
||||
}
|
||||
if !showSettings, let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
} else if showSetPasscode {
|
||||
SetAppPasscodeView {
|
||||
prefPerformLA = true
|
||||
showSetPasscode = false
|
||||
privacyLocalAuthModeDefault.set(.passcode)
|
||||
alertManager.showAlert(laTurnedOnAlert())
|
||||
} cancel: {
|
||||
prefPerformLA = false
|
||||
showSetPasscode = false
|
||||
alertManager.showAlert(laPasscodeNotSetAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if prefPerformLA { requestNtfAuthorization() }
|
||||
@@ -40,6 +57,13 @@ struct ContentView: View {
|
||||
initAuthenticate()
|
||||
}
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
}
|
||||
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
|
||||
Button("System authentication") { initialEnableLA() }
|
||||
Button("Passcode entry") { showSetPasscode = true }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contentView() -> some View {
|
||||
@@ -82,7 +106,7 @@ struct ContentView: View {
|
||||
|
||||
private func mainView() -> some View {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView().privacySensitive(protectScreen)
|
||||
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
|
||||
.onAppear {
|
||||
if !prefPerformLA { requestNtfAuthorization() }
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
@@ -132,6 +156,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func initAuthenticate() {
|
||||
logger.debug("initAuthenticate")
|
||||
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
|
||||
userAuthorized = false
|
||||
} else if doAuthenticate {
|
||||
@@ -152,14 +177,18 @@ struct ContentView: View {
|
||||
|
||||
private func justAuthenticate() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
let laMode = privacyLocalAuthModeDefault.get()
|
||||
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason")) { laResult in
|
||||
logger.debug("authenticate callback: \(String(describing: laResult))")
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
canConnectCall = true
|
||||
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
|
||||
case .failed:
|
||||
break
|
||||
if laMode == .passcode {
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
}
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
@@ -185,25 +214,28 @@ struct ContentView: View {
|
||||
Alert(
|
||||
title: Text("SimpleX Lock"),
|
||||
message: Text("To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled."),
|
||||
primaryButton: .default(Text("Turn on")) {
|
||||
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success:
|
||||
prefPerformLA = true
|
||||
alertManager.showAlert(laTurnedOnAlert())
|
||||
case .failed:
|
||||
prefPerformLA = false
|
||||
alertManager.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
prefPerformLA = false
|
||||
alertManager.showAlert(laUnavailableInstructionAlert())
|
||||
}
|
||||
}
|
||||
},
|
||||
primaryButton: .default(Text("Turn on")) { showChooseLAMode = true },
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
private func initialEnableLA () {
|
||||
privacyLocalAuthModeDefault.set(.system)
|
||||
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success:
|
||||
prefPerformLA = true
|
||||
alertManager.showAlert(laTurnedOnAlert())
|
||||
case .failed:
|
||||
prefPerformLA = false
|
||||
alertManager.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
prefPerformLA = false
|
||||
alertManager.showAlert(laUnavailableInstructionAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func notificationAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Notifications are disabled!"),
|
||||
|
||||
@@ -21,6 +21,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatDbChanged = false
|
||||
@Published var chatDbEncrypted: Bool?
|
||||
@Published var chatDbStatus: DBMigrationResult?
|
||||
@Published var laRequest: LocalAuthRequest?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// map of connections network statuses, key is agent connection id
|
||||
|
||||
@@ -106,7 +106,8 @@ struct SimpleXApp: App {
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var showSettings = false
|
||||
@Binding var showSettings: Bool
|
||||
@State private var searchText = ""
|
||||
@State private var showAddChat = false
|
||||
@State var userPickerVisible = false
|
||||
@@ -114,9 +114,6 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
@@ -224,9 +221,9 @@ struct ChatListView_Previews: PreviewProvider {
|
||||
|
||||
]
|
||||
return Group {
|
||||
ChatListView()
|
||||
ChatListView(showSettings: Binding.constant(false))
|
||||
.environmentObject(chatModel)
|
||||
ChatListView()
|
||||
ChatListView(showSettings: Binding.constant(false))
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct DatabaseEncryptionView: View {
|
||||
@State private var progressIndicator = false
|
||||
@State private var useKeychainToggle = storeDBPassphraseGroupDefault.get()
|
||||
@State private var initialRandomDBPassphrase = initialRandomDBPassphraseGroupDefault.get()
|
||||
@State private var storedKey = getDatabaseKey() != nil
|
||||
@State private var storedKey = kcDatabasePassword.get() != nil
|
||||
@State private var currentKey = ""
|
||||
@State private var newKey = ""
|
||||
@State private var confirmNewKey = ""
|
||||
@@ -124,7 +124,7 @@ struct DatabaseEncryptionView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if initialRandomDBPassphrase { currentKey = getDatabaseKey() ?? "" }
|
||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||
}
|
||||
.disabled(m.chatRunning != false)
|
||||
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
||||
@@ -140,7 +140,7 @@ struct DatabaseEncryptionView: View {
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if useKeychain {
|
||||
if setDatabaseKey(newKey) {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
} else {
|
||||
@@ -184,7 +184,7 @@ struct DatabaseEncryptionView: View {
|
||||
title: Text("Remove passphrase from keychain?"),
|
||||
message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
if removeDatabaseKey() {
|
||||
if kcDatabasePassword.remove() {
|
||||
logger.debug("passphrase removed from keychain")
|
||||
setUseKeychain(false)
|
||||
storedKey = false
|
||||
|
||||
@@ -13,7 +13,7 @@ struct DatabaseErrorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State var status: DBMigrationResult
|
||||
@State private var dbKey = ""
|
||||
@State private var storedDBKey = getDatabaseKey()
|
||||
@State private var storedDBKey = kcDatabasePassword.get()
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var showRestoreDbButton = false
|
||||
@State private var starting = false
|
||||
@@ -131,7 +131,7 @@ struct DatabaseErrorView: View {
|
||||
}
|
||||
|
||||
private func saveAndRunChat() {
|
||||
if setDatabaseKey(dbKey) {
|
||||
if kcDatabasePassword.set(dbKey) {
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ struct DatabaseView: View {
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
try await apiImportArchive(config: config)
|
||||
_ = removeDatabaseKey()
|
||||
_ = kcDatabasePassword.remove()
|
||||
await operationEnded(.archiveImported)
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
||||
@@ -375,7 +375,7 @@ struct DatabaseView: View {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
_ = removeDatabaseKey()
|
||||
_ = kcDatabasePassword.remove()
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
await operationEnded(.chatDeleted)
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
import SimpleXChat
|
||||
|
||||
enum LAResult {
|
||||
case success
|
||||
@@ -25,7 +26,31 @@ func authorize(_ text: String, _ authorized: Binding<Bool>) {
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(reason: String, completed: @escaping (LAResult) -> Void) {
|
||||
struct LocalAuthRequest {
|
||||
var title: LocalizedStringKey? // if title is null, reason is shown
|
||||
var reason: String
|
||||
var password: String
|
||||
var completed: (LAResult) -> Void
|
||||
|
||||
static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", completed: { _ in })
|
||||
}
|
||||
|
||||
func authenticate(title: LocalizedStringKey? = nil, reason: String, completed: @escaping (LAResult) -> Void) {
|
||||
logger.debug("authenticate")
|
||||
switch privacyLocalAuthModeDefault.get() {
|
||||
case .system: systemAuthenticate(reason, completed)
|
||||
case .passcode:
|
||||
if let password = kcAppPassword.get() {
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.laRequest = LocalAuthRequest(title: title, reason: reason, password: password, completed: completed)
|
||||
}
|
||||
} else {
|
||||
completed(.unavailable(authError: NSLocalizedString("No app password", comment: "Authentication unavailable")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func systemAuthenticate(_ reason: String, _ completed: @escaping (LAResult) -> Void) {
|
||||
let laContext = LAContext()
|
||||
var authAvailabilityError: NSError?
|
||||
if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) {
|
||||
@@ -52,6 +77,13 @@ func laTurnedOnAlert() -> Alert {
|
||||
)
|
||||
}
|
||||
|
||||
func laPasscodeNotSetAlert() -> Alert {
|
||||
mkAlert(
|
||||
title: "SimpleX Lock not enabled!",
|
||||
message: "You can turn on SimpleX Lock via Settings."
|
||||
)
|
||||
}
|
||||
|
||||
func laFailedAlert() -> Alert {
|
||||
mkAlert(
|
||||
title: "Authentication failed",
|
||||
@@ -72,3 +104,4 @@ func laUnavailableTurningOffAlert() -> Alert {
|
||||
message: "Device authentication is disabled. Turning off SimpleX Lock."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
34
apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
Normal file
34
apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// LocalAuthView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/04/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LocalAuthView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var authRequest: LocalAuthRequest
|
||||
@State private var password = ""
|
||||
|
||||
var body: some View {
|
||||
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
|
||||
let r: LAResult = password == authRequest.password
|
||||
? .success
|
||||
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
||||
m.laRequest = nil
|
||||
authRequest.completed(r)
|
||||
} cancel: {
|
||||
m.laRequest = nil
|
||||
authRequest.completed(.failed(authError: NSLocalizedString("Authentication cancelled", comment: "PIN entry")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalAuthView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LocalAuthView(authRequest: LocalAuthRequest.sample)
|
||||
}
|
||||
}
|
||||
156
apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift
Normal file
156
apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift
Normal file
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// PasscodeEntry.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/04/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PasscodeEntry: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var width: CGFloat
|
||||
var height: CGFloat
|
||||
@Binding var password: String
|
||||
@State private var showPassword = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
passwordView()
|
||||
.padding(.bottom, 4)
|
||||
if width < height * 2 / 3 {
|
||||
verticalPasswordGrid()
|
||||
} else {
|
||||
horizontalPasswordGrid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func passwordView() -> some View {
|
||||
Text(
|
||||
password == ""
|
||||
? " "
|
||||
: splitPassword()
|
||||
)
|
||||
.font(showPassword ? .title2.monospacedDigit() : .body)
|
||||
.onTapGesture {
|
||||
showPassword = !showPassword
|
||||
}
|
||||
.frame(height: 30)
|
||||
}
|
||||
|
||||
private func splitPassword() -> String {
|
||||
let n = password.count < 8 ? 8 : 4
|
||||
return password.enumerated().reduce("") { acc, c in
|
||||
acc
|
||||
+ (showPassword ? String(c.element) : "●")
|
||||
+ ((c.offset + 1) % n == 0 ? " " : "")
|
||||
}
|
||||
}
|
||||
|
||||
private func verticalPasswordGrid() -> some View {
|
||||
let s = width / 3
|
||||
return VStack(spacing: 0) {
|
||||
digitsRow(s, 1, 2, 3)
|
||||
Divider()
|
||||
digitsRow(s, 4, 5, 6)
|
||||
Divider()
|
||||
digitsRow(s, 7, 8, 9)
|
||||
Divider()
|
||||
HStack(spacing: 0) {
|
||||
passwordEdit(s, image: "multiply") {
|
||||
password = ""
|
||||
}
|
||||
Divider()
|
||||
passwordDigit(s, 0)
|
||||
Divider()
|
||||
passwordEdit(s, image: "delete.backward") {
|
||||
if password != "" { password.removeLast() }
|
||||
}
|
||||
}
|
||||
.frame(height: s)
|
||||
}
|
||||
.frame(width: width, height: s * 4 * 0.97)
|
||||
}
|
||||
|
||||
private func horizontalPasswordGrid() -> some View {
|
||||
let s = height / 5
|
||||
return VStack(spacing: 0) {
|
||||
horizontalDigitsRow(s, 1, 2, 3) {
|
||||
passwordEdit(s, image: "multiply") {
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
horizontalDigitsRow(s, 4, 5, 6) {
|
||||
passwordDigit(s, 0)
|
||||
}
|
||||
Divider()
|
||||
horizontalDigitsRow(s, 7, 8, 9) {
|
||||
passwordEdit(s, image: "delete.backward") {
|
||||
if password != "" { password.removeLast() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: s * 4, height: s * 3 * 0.97)
|
||||
}
|
||||
|
||||
private func digitsRow(_ size: CGFloat, _ d1: Int, _ d2: Int, _ d3: Int) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
passwordDigit(size, d1)
|
||||
Divider()
|
||||
passwordDigit(size, d2)
|
||||
Divider()
|
||||
passwordDigit(size, d3)
|
||||
}
|
||||
.frame(height: size * 0.97)
|
||||
}
|
||||
|
||||
private func horizontalDigitsRow<V: View>(_ size: CGFloat, _ d1: Int, _ d2: Int, _ d3: Int, _ button: @escaping () -> V) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
digitsRow(size, d1, d2, d3)
|
||||
Divider()
|
||||
button()
|
||||
}
|
||||
.frame(height: size * 0.97)
|
||||
}
|
||||
|
||||
private func passwordDigit(_ size: CGFloat, _ d: Int) -> some View {
|
||||
let s = String(describing: d)
|
||||
return passwordButton(size) {
|
||||
if password.count < 16 {
|
||||
password = password + s
|
||||
}
|
||||
} label: {
|
||||
Text(s).font(.title)
|
||||
}
|
||||
.disabled(password.count >= 16)
|
||||
}
|
||||
|
||||
private func passwordEdit(_ size: CGFloat, image: String, action: @escaping () -> Void) -> some View {
|
||||
passwordButton(size, action: action) {
|
||||
Image(systemName: image)
|
||||
}
|
||||
}
|
||||
|
||||
private func passwordButton<V: View>(_ size: CGFloat, action: @escaping () -> Void, label: () -> V) -> some View {
|
||||
let h = size * 0.97
|
||||
return Button(action: action) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: h, height: h)
|
||||
.foregroundColor(Color(uiColor: .systemBackground))
|
||||
label()
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: size, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
struct PasscodeEntry_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PasscodeEntry(width: 800, height: 420, password: Binding.constant(""))
|
||||
}
|
||||
}
|
||||
92
apps/ios/Shared/Views/LocalAuth/PasscodeView.swift
Normal file
92
apps/ios/Shared/Views/LocalAuth/PasscodeView.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// PasscodeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 11/04/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PasscodeView: View {
|
||||
@Binding var passcode: String
|
||||
var title: LocalizedStringKey
|
||||
var reason: String? = nil
|
||||
var submitLabel: LocalizedStringKey
|
||||
var submitEnabled: ((String) -> Bool)?
|
||||
var submit: () -> Void
|
||||
var cancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
if g.size.width < g.size.height * 2 / 3 {
|
||||
verticalPasscodeView(g)
|
||||
} else {
|
||||
horizontalPasscodeView(g)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
}
|
||||
|
||||
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
|
||||
VStack(spacing: 8) {
|
||||
passcodeEntry(g)
|
||||
Spacer()
|
||||
HStack(spacing: 48) {
|
||||
buttonsView()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 32)
|
||||
}
|
||||
|
||||
private func horizontalPasscodeView(_ g: GeometryProxy) -> some View {
|
||||
HStack(alignment: .bottom, spacing: 48) {
|
||||
VStack(spacing: 8) {
|
||||
passcodeEntry(g)
|
||||
}
|
||||
VStack(spacing: 48) {
|
||||
buttonsView()
|
||||
}
|
||||
.frame(maxHeight: g.size.height / 5 * 3 * 0.97)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
@ViewBuilder private func passcodeEntry(_ g: GeometryProxy) -> some View {
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.top, 8)
|
||||
if let reason = reason {
|
||||
Text(reason).padding(.top, 4)
|
||||
}
|
||||
Spacer()
|
||||
PasscodeEntry(width: g.size.width, height: g.size.height, password: $passcode)
|
||||
}
|
||||
|
||||
@ViewBuilder private func buttonsView() -> some View {
|
||||
Button(action: cancel) {
|
||||
Label("Cancel", systemImage: "multiply")
|
||||
}
|
||||
Button(action: submit) {
|
||||
Label(submitLabel, systemImage: "checkmark")
|
||||
}
|
||||
.disabled(submitEnabled?(passcode) == false || passcode.count < 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct PasscodeViewView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PasscodeView(
|
||||
passcode: Binding.constant(""),
|
||||
title: "Enter Passcode",
|
||||
reason: "Unlock app",
|
||||
submitLabel: "Submit",
|
||||
submit: {},
|
||||
cancel: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
65
apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift
Normal file
65
apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// SetAppPaswordView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/04/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SetAppPasscodeView: View {
|
||||
var submit: () -> Void
|
||||
var cancel: () -> Void
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State private var showKeychainError = false
|
||||
@State private var passcode = ""
|
||||
@State private var enteredPassword = ""
|
||||
@State private var confirming = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if confirming {
|
||||
setPasswordView(
|
||||
title: "Confirm Passcode",
|
||||
submitLabel: "Confirm",
|
||||
submitEnabled: { pwd in pwd == enteredPassword }
|
||||
) {
|
||||
if passcode == enteredPassword {
|
||||
if kcAppPassword.set(passcode) {
|
||||
enteredPassword = ""
|
||||
passcode = ""
|
||||
dismiss()
|
||||
submit()
|
||||
} else {
|
||||
showKeychainError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPasswordView(title: "New Passcode", submitLabel: "Save") {
|
||||
enteredPassword = passcode
|
||||
passcode = ""
|
||||
confirming = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showKeychainError) {
|
||||
mkAlert(title: "KeyChain error", message: "Error saving passcode")
|
||||
}
|
||||
}
|
||||
|
||||
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
|
||||
PasscodeView(passcode: $passcode, title: title, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
|
||||
dismiss()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SetAppPasscodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SetAppPasscodeView(submit: {}, cancel: {})
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ struct DeveloperView: View {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Show developer options", isOn: $developerTools)
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||
}
|
||||
|
||||
@@ -14,12 +14,27 @@ struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
Section("Device") {
|
||||
SimplexLockSetting()
|
||||
NavigationLink {
|
||||
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
|
||||
.navigationTitle("SimpleX Lock")
|
||||
} label: {
|
||||
if prefPerformLA {
|
||||
settingsRow("lock.fill", color: .green) {
|
||||
simplexLockRow(currentLAMode.text)
|
||||
}
|
||||
} else {
|
||||
settingsRow("lock") {
|
||||
simplexLockRow("Off")
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("eye.slash") {
|
||||
Toggle("Protect app screen", isOn: $protectScreen)
|
||||
}
|
||||
@@ -56,38 +71,125 @@ struct PrivacySettings: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simplexLockRow(_ value: LocalizedStringKey) -> some View {
|
||||
HStack {
|
||||
Text("SimpleX Lock")
|
||||
Spacer()
|
||||
Text(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SimplexLockSetting: View {
|
||||
enum LAMode: String, Identifiable, CaseIterable {
|
||||
case system
|
||||
case passcode
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .system: return "System"
|
||||
case .passcode: return "Passcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SimplexLockView: View {
|
||||
@Binding var prefPerformLA: Bool
|
||||
@Binding var currentLAMode: LAMode
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
|
||||
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
|
||||
@State var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
@State private var performLAToggleReset = false
|
||||
@State var laAlert: laSettingViewAlert? = nil
|
||||
@State private var performLAModeReset = false
|
||||
@State private var showPasswordAction: PasswordAction? = nil
|
||||
@State private var showChangePassword = false
|
||||
@State var laAlert: LASettingViewAlert? = nil
|
||||
|
||||
enum laSettingViewAlert: Identifiable {
|
||||
enum LASettingViewAlert: Identifiable {
|
||||
case laTurnedOnAlert
|
||||
case laFailedAlert
|
||||
case laUnavailableInstructionAlert
|
||||
case laUnavailableTurningOffAlert
|
||||
case laPasscodeSetAlert
|
||||
case laPasscodeChangedAlert
|
||||
case laPasscodeNotChangedAlert
|
||||
|
||||
var id: laSettingViewAlert { get { self } }
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
enum PasswordAction: Identifiable {
|
||||
case enableAuth
|
||||
case toggleMode
|
||||
case changePassword
|
||||
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
let laDelays: [Int] = [10, 30, 60, 180, 0]
|
||||
|
||||
func laDelayText(_ t: Int) -> LocalizedStringKey {
|
||||
let m = t / 60
|
||||
let s = t % 60
|
||||
return t == 0
|
||||
? "Immediately"
|
||||
: m == 0 || s != 0
|
||||
? "\(s) seconds" // there are no options where both minutes and seconds are needed
|
||||
: "\(m) minutes"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
settingsRow("lock") {
|
||||
Toggle("SimpleX Lock", isOn: $performLA)
|
||||
VStack {
|
||||
List {
|
||||
Section("") {
|
||||
Toggle("Enable lock", isOn: $performLA)
|
||||
Picker("Lock mode", selection: $laMode) {
|
||||
ForEach(LAMode.allCases) { mode in
|
||||
Text(mode.text)
|
||||
}
|
||||
}
|
||||
if performLA {
|
||||
Picker("Lock after", selection: $laLockDelay) {
|
||||
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
|
||||
ForEach(delays, id: \.self) { t in
|
||||
Text(laDelayText(t))
|
||||
}
|
||||
}
|
||||
if showChangePassword && laMode == .passcode {
|
||||
Button("Change Passcode") {
|
||||
changeLAPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: performLA) { performLAToggle in
|
||||
prefLANoticeShown = true
|
||||
if performLAToggleReset {
|
||||
performLAToggleReset = false
|
||||
} else {
|
||||
if performLAToggle {
|
||||
} else if performLAToggle {
|
||||
switch currentLAMode {
|
||||
case .system:
|
||||
enableLA()
|
||||
} else {
|
||||
disableLA()
|
||||
case .passcode:
|
||||
resetLA()
|
||||
showPasswordAction = .enableAuth
|
||||
}
|
||||
} else {
|
||||
disableLA()
|
||||
}
|
||||
}
|
||||
.onChange(of: laMode) { _ in
|
||||
if performLAModeReset {
|
||||
performLAModeReset = false
|
||||
} else if performLA {
|
||||
toggleLAMode()
|
||||
} else {
|
||||
updateLAMode()
|
||||
}
|
||||
}
|
||||
.alert(item: $laAlert) { alertItem in
|
||||
@@ -96,46 +198,125 @@ struct SimplexLockSetting: View {
|
||||
case .laFailedAlert: return laFailedAlert()
|
||||
case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert()
|
||||
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
|
||||
case .laPasscodeSetAlert: return passcodeAlert("Passcode set!")
|
||||
case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!")
|
||||
case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!")
|
||||
}
|
||||
}
|
||||
.sheet(item: $showPasswordAction) { a in
|
||||
switch a {
|
||||
case .enableAuth:
|
||||
SetAppPasscodeView {
|
||||
laLockDelay = 30
|
||||
prefPerformLA = true
|
||||
showChangePassword = true
|
||||
showLAAlert(.laPasscodeSetAlert)
|
||||
} cancel: {
|
||||
resetLAEnabled(false)
|
||||
}
|
||||
case .toggleMode:
|
||||
SetAppPasscodeView {
|
||||
laLockDelay = 30
|
||||
updateLAMode()
|
||||
showChangePassword = true
|
||||
showLAAlert(.laPasscodeSetAlert)
|
||||
} cancel: {
|
||||
revertLAMode()
|
||||
}
|
||||
case .changePassword:
|
||||
SetAppPasscodeView {
|
||||
showLAAlert(.laPasscodeChangedAlert)
|
||||
} cancel: {
|
||||
showLAAlert(.laPasscodeNotChangedAlert)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
showChangePassword = prefPerformLA && currentLAMode == .passcode
|
||||
}
|
||||
.onDisappear() {
|
||||
m.laRequest = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func showLAAlert(_ a: LASettingViewAlert) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
laAlert = a
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleLAMode() {
|
||||
authenticate(reason: NSLocalizedString("Change lock mode", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .failed:
|
||||
revertLAMode()
|
||||
laAlert = .laFailedAlert
|
||||
case .success:
|
||||
switch laMode {
|
||||
case .system:
|
||||
updateLAMode()
|
||||
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success:
|
||||
_ = kcAppPassword.remove()
|
||||
laAlert = .laTurnedOnAlert
|
||||
case .failed, .unavailable:
|
||||
currentLAMode = .passcode
|
||||
privacyLocalAuthModeDefault.set(.passcode)
|
||||
revertLAMode()
|
||||
laAlert = .laFailedAlert
|
||||
}
|
||||
}
|
||||
case .passcode:
|
||||
showPasswordAction = .toggleMode
|
||||
}
|
||||
case .unavailable:
|
||||
disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func changeLAPassword() {
|
||||
authenticate(title: "Current Passcode", reason: NSLocalizedString("Change passcode", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .failed: laAlert = .laFailedAlert
|
||||
case .success: showPasswordAction = .changePassword
|
||||
case .unavailable: disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enableLA() {
|
||||
resetLA()
|
||||
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success:
|
||||
prefPerformLA = true
|
||||
laAlert = .laTurnedOnAlert
|
||||
case .failed:
|
||||
prefPerformLA = false
|
||||
withAnimation() {
|
||||
performLA = false
|
||||
}
|
||||
performLAToggleReset = true
|
||||
resetLAEnabled(false)
|
||||
laAlert = .laFailedAlert
|
||||
case .unavailable:
|
||||
prefPerformLA = false
|
||||
withAnimation() {
|
||||
performLA = false
|
||||
}
|
||||
performLAToggleReset = true
|
||||
laAlert = .laUnavailableInstructionAlert
|
||||
disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disableUnavailableLA() {
|
||||
resetLAEnabled(false)
|
||||
laMode = .system
|
||||
updateLAMode()
|
||||
laAlert = .laUnavailableInstructionAlert
|
||||
}
|
||||
|
||||
private func disableLA() {
|
||||
authenticate(reason: NSLocalizedString("Disable SimpleX Lock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
prefPerformLA = false
|
||||
resetLA()
|
||||
case .failed:
|
||||
prefPerformLA = true
|
||||
withAnimation() {
|
||||
performLA = true
|
||||
}
|
||||
performLAToggleReset = true
|
||||
resetLAEnabled(true)
|
||||
laAlert = .laFailedAlert
|
||||
case .unavailable:
|
||||
prefPerformLA = false
|
||||
@@ -143,6 +324,32 @@ struct SimplexLockSetting: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetLA() {
|
||||
_ = kcAppPassword.remove()
|
||||
laLockDelay = 30
|
||||
showChangePassword = false
|
||||
}
|
||||
|
||||
private func resetLAEnabled(_ onOff: Bool) {
|
||||
prefPerformLA = onOff
|
||||
performLAToggleReset = true
|
||||
withAnimation { performLA = onOff }
|
||||
}
|
||||
|
||||
private func revertLAMode() {
|
||||
performLAModeReset = true
|
||||
withAnimation { laMode = currentLAMode }
|
||||
}
|
||||
|
||||
private func updateLAMode() {
|
||||
currentLAMode = laMode
|
||||
privacyLocalAuthModeDefault.set(laMode)
|
||||
}
|
||||
|
||||
private func passcodeAlert(_ title: LocalizedStringKey) -> Alert {
|
||||
mkAlert(title: title, message: "Please remember or store it securely - there is no way to recover a lost passcode!")
|
||||
}
|
||||
}
|
||||
|
||||
struct PrivacySettings_Previews: PreviewProvider {
|
||||
|
||||
@@ -19,6 +19,8 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as?
|
||||
let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice"
|
||||
let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown"
|
||||
let DEFAULT_PERFORM_LA = "performLocalAuthentication"
|
||||
let DEFAULT_LA_MODE = "localAuthenticationMode"
|
||||
let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay"
|
||||
let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
|
||||
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
||||
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
|
||||
@@ -48,22 +50,24 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
DEFAULT_LA_NOTICE_SHOWN: false,
|
||||
DEFAULT_PERFORM_LA: false,
|
||||
DEFAULT_LA_MODE: LAMode.system.rawValue,
|
||||
DEFAULT_LA_LOCK_DELAY: 30,
|
||||
DEFAULT_NOTIFICATION_ALERT_SHOWN: false,
|
||||
DEFAULT_WEBRTC_POLICY_RELAY: true,
|
||||
DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false,
|
||||
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
|
||||
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: "description",
|
||||
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
|
||||
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: "offer",
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: V3DBMigrationState.offer.rawValue,
|
||||
DEFAULT_DEVELOPER_TOOLS: false,
|
||||
DEFAULT_ENCRYPTION_STARTED: false,
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: "scan",
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
|
||||
@@ -99,6 +103,8 @@ let connectViaLinkTabDefault = EnumDefault<ConnectViaLinkTab>(defaults: UserDefa
|
||||
|
||||
let privacySimplexLinkModeDefault = EnumDefault<SimpleXLinkMode>(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_SIMPLEX_LINK_MODE, withDefault: .description)
|
||||
|
||||
let privacyLocalAuthModeDefault = EnumDefault<LAMode>(defaults: UserDefaults.standard, forKey: DEFAULT_LA_MODE, withDefault: .system)
|
||||
|
||||
func setGroupDefaults() {
|
||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||
}
|
||||
@@ -111,8 +117,16 @@ struct SettingsView: View {
|
||||
@State private var settingsSheet: SettingsSheet?
|
||||
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
ZStack {
|
||||
settingsView()
|
||||
if let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func settingsView() -> some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
NavigationView {
|
||||
List {
|
||||
Section("You") {
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
5CB634A429E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349F29E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a */; };
|
||||
5CB634A529E1EE550066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB634A029E1EE550066AD6B /* libgmpxx.a */; };
|
||||
5CB634A629E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB634A129E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a */; };
|
||||
5CB634A829E437960066AD6B /* PasscodeEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634A729E437960066AD6B /* PasscodeEntry.swift */; };
|
||||
5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */; };
|
||||
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */; };
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */; };
|
||||
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
|
||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
|
||||
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
|
||||
@@ -360,6 +364,10 @@
|
||||
5CB6349F29E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a"; sourceTree = "<group>"; };
|
||||
5CB634A029E1EE550066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CB634A129E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CB634A729E437960066AD6B /* PasscodeEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeEntry.swift; sourceTree = "<group>"; };
|
||||
5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthView.swift; sourceTree = "<group>"; };
|
||||
5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAppPasscodeView.swift; sourceTree = "<group>"; };
|
||||
5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = "<group>"; };
|
||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
|
||||
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
|
||||
@@ -509,6 +517,7 @@
|
||||
5CB9250B27A942F300ACCCDD /* ChatList */,
|
||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||
5CFA59C22860B04D00863A68 /* Database */,
|
||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
||||
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
||||
);
|
||||
@@ -659,6 +668,17 @@
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */,
|
||||
5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */,
|
||||
5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */,
|
||||
5CB634A729E437960066AD6B /* PasscodeEntry.swift */,
|
||||
);
|
||||
path = LocalAuth;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CB924DD27A8622200ACCCDD /* NewChat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1048,11 +1068,13 @@
|
||||
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
|
||||
5CC036E029C488D500C0EF20 /* HiddenProfileView.swift in Sources */,
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
||||
5CB634A829E437960066AD6B /* PasscodeEntry.swift in Sources */,
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */,
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */,
|
||||
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */,
|
||||
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */,
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
@@ -1094,6 +1116,7 @@
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
|
||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
||||
@@ -1148,6 +1171,7 @@
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
||||
5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */,
|
||||
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */,
|
||||
18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */,
|
||||
184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */,
|
||||
|
||||
@@ -30,7 +30,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
||||
logger.debug("chatMigrateInit generating a random DB key")
|
||||
dbKey = randomDatabasePassword()
|
||||
initialRandomDBPassphraseGroupDefault.set(true)
|
||||
} else if let key = getDatabaseKey() {
|
||||
} else if let key = kcDatabasePassword.get() {
|
||||
dbKey = key
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
||||
let cjson = chat_migrate_init(&cPath, &cKey, &cConfirm, &chatController)!
|
||||
let dbRes = dbMigrationResult(fromCString(cjson))
|
||||
let encrypted = dbKey != ""
|
||||
let keychainErr = dbRes == .ok && useKeychain && encrypted && !setDatabaseKey(dbKey)
|
||||
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
|
||||
let result = (encrypted, keychainErr ? .errorKeychain : dbRes)
|
||||
migrationResult = result
|
||||
return result
|
||||
|
||||
@@ -12,17 +12,26 @@ import Security
|
||||
private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app"
|
||||
private let DATABASE_PASSWORD_ITEM: String = "databasePassword"
|
||||
private let APP_PASSWORD_ITEM: String = "appPassword"
|
||||
|
||||
public func getDatabaseKey() -> String? {
|
||||
getItemString(forKey: DATABASE_PASSWORD_ITEM)
|
||||
}
|
||||
public let kcDatabasePassword = KeyChainItem(forKey: DATABASE_PASSWORD_ITEM)
|
||||
|
||||
public func setDatabaseKey(_ key: String) -> Bool {
|
||||
setItemString(key, forKey: DATABASE_PASSWORD_ITEM)
|
||||
}
|
||||
public let kcAppPassword = KeyChainItem(forKey: APP_PASSWORD_ITEM)
|
||||
|
||||
public func removeDatabaseKey() -> Bool {
|
||||
deleteItem(forKey: DATABASE_PASSWORD_ITEM)
|
||||
public struct KeyChainItem {
|
||||
var forKey: String
|
||||
|
||||
public func get() -> String? {
|
||||
getItemString(forKey: forKey)
|
||||
}
|
||||
|
||||
public func set(_ value: String) -> Bool {
|
||||
setItemString(value, forKey: forKey)
|
||||
}
|
||||
|
||||
public func remove() -> Bool {
|
||||
deleteItem(forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
func randomDatabasePassword() -> String {
|
||||
|
||||
Reference in New Issue
Block a user