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:
Evgeny Poberezkin
2023-04-12 12:22:55 +02:00
committed by GitHub
parent 1d16a19373
commit ec6cee1389
18 changed files with 743 additions and 76 deletions

View File

@@ -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!"),

View File

@@ -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

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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."
)
}

View 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)
}
}

View 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(""))
}
}

View 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: {}
)
}
}

View 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: {})
}
}

View File

@@ -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.")
}

View File

@@ -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 {

View File

@@ -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") {

View File

@@ -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 */,

View File

@@ -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

View File

@@ -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 {