core, iOS: support for self-destruct password (#2412)

* core, iOS: support for self-destruct password

* disable test logging

* core: fix tests, iOS: remove notifications on removal

* change alerts
This commit is contained in:
Evgeny Poberezkin 2023-05-09 10:33:30 +02:00 committed by GitHub
parent 57801fde1f
commit 0b8d9d11e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 318 additions and 106 deletions

View File

@ -192,7 +192,7 @@ struct ContentView: View {
private func justAuthenticate() {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason")) { laResult in
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:

View File

@ -42,7 +42,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
changeActiveUser(userId, viewPwd: nil)
}
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let chatId = content.userInfo["chatId"] as? String {
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(contactRequest) }
} else {
@ -107,8 +107,8 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
// in another chat
return recent ? [.banner, .list] : [.sound, .banner, .list]
}
// this notification is deliverd from the notifications server
// when the app is in foreground it does not need to be shown
// this notification is deliverd from the notifications server
// when the app is in foreground it does not need to be shown
case ntfCategoryCheckMessage: return []
case ntfCategoryCallInvitation: return []
default: return [.sound, .banner, .list]
@ -247,8 +247,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
}
func removeNotifications(_ ids : [String]){
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
func removeAllNotifications() async {
let nc = UNUserNotificationCenter.current()
let settings = await nc.notificationSettings()
if settings.authorizationStatus == .authorized {
nc.removeAllPendingNotificationRequests()
nc.removeAllDeliveredNotifications()
}
}
}

View File

@ -125,7 +125,7 @@ func apiGetActiveUser() throws -> User? {
}
}
func apiCreateActiveUser(_ p: Profile) throws -> User {
func apiCreateActiveUser(_ p: Profile?) throws -> User {
let r = chatSendCmdSync(.createActiveUser(profile: p))
if case let .activeUser(user) = r { return user }
throw r

View File

@ -317,10 +317,7 @@ struct DatabaseView: View {
private func stopChat() {
Task {
do {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { m.chatRunning = false }
appStateGroupDefault.set(.stopped)
try await stopChatAsync()
} catch let error {
await MainActor.run {
runChat = true
@ -374,9 +371,7 @@ struct DatabaseView: View {
progressIndicator = true
Task {
do {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
try await deleteChatAsync()
await operationEnded(.chatDeleted)
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
} catch let error {
@ -468,6 +463,19 @@ struct DatabaseView: View {
}
}
func stopChatAsync() async throws {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { ChatModel.shared.chatRunning = false }
appStateGroupDefault.set(.stopped)
}
func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
}
struct DatabaseView_Previews: PreviewProvider {
static var previews: some View {
DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)

View File

@ -30,19 +30,26 @@ struct LocalAuthRequest {
var title: LocalizedStringKey? // if title is null, reason is shown
var reason: String
var password: String
var selfDestruct: Bool
var completed: (LAResult) -> Void
static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", completed: { _ in })
static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", selfDestruct: false, completed: { _ in })
}
func authenticate(title: LocalizedStringKey? = nil, reason: String, completed: @escaping (LAResult) -> Void) {
func authenticate(title: LocalizedStringKey? = nil, reason: String, selfDestruct: Bool = false, 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)
ChatModel.shared.laRequest = LocalAuthRequest(
title: title,
reason: reason,
password: password,
selfDestruct: selfDestruct && UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT),
completed: completed
)
}
} else {
completed(.unavailable(authError: NSLocalizedString("No app password", comment: "Authentication unavailable")))

View File

@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct LocalAuthView: View {
@EnvironmentObject var m: ChatModel
@ -15,6 +16,13 @@ struct LocalAuthView: View {
var body: some View {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
deleteStorageAndRestart(sdPassword) { r in
m.laRequest = nil
authRequest.completed(r)
}
return
}
let r: LAResult = password == authRequest.password
? .success
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
@ -25,6 +33,41 @@ struct LocalAuthView: View {
authRequest.completed(.failed(authError: NSLocalizedString("Authentication cancelled", comment: "PIN entry")))
}
}
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task {
do {
try await stopChatAsync()
try await deleteChatAsync()
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications()
let displayName = UserDefaults.standard.string(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME)
UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT)
UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME)
await MainActor.run {
m.chatDbChanged = true
m.chatInitialized = false
}
resetChatCtrl()
try initializeChat(start: true)
m.chatDbChanged = false
appStateGroupDefault.set(.active)
if m.currentUser != nil { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "")
}
m.currentUser = try apiCreateActiveUser(profile)
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
try startChat()
completed(.success)
} catch {
completed(.failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")))
}
}
}
}
struct LocalAuthView_Previews: PreviewProvider {

View File

@ -10,6 +10,9 @@ import SwiftUI
import SimpleXChat
struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword
var title: LocalizedStringKey = "New Passcode"
var reason: String?
var submit: () -> Void
var cancel: () -> Void
@Environment(\.dismiss) var dismiss: DismissAction
@ -27,7 +30,7 @@ struct SetAppPasscodeView: View {
submitEnabled: { pwd in pwd == enteredPassword }
) {
if passcode == enteredPassword {
if kcAppPassword.set(passcode) {
if passcodeKeychain.set(passcode) {
enteredPassword = ""
passcode = ""
dismiss()
@ -38,7 +41,7 @@ struct SetAppPasscodeView: View {
}
}
} else {
setPasswordView(title: "New Passcode", submitLabel: "Save") {
setPasswordView(title: title, submitLabel: "Save") {
enteredPassword = passcode
passcode = ""
confirming = true
@ -51,7 +54,7 @@ struct SetAppPasscodeView: View {
}
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) {
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
dismiss()
cancel()
}

View File

@ -129,6 +129,8 @@ struct CreateProfile: View {
m.onboardingStage = .step3_CreateSimpleXAddress
}
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()

View File

@ -13,6 +13,7 @@ struct IncognitoHelp: View {
VStack(alignment: .leading) {
Text("Incognito mode")
.font(.largeTitle)
.bold()
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {

View File

@ -102,9 +102,13 @@ struct SimplexLockView: View {
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = 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 performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
@State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
@AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = ""
@State private var performLAToggleReset = false
@State private var performLAModeReset = false
@State private var performLASelfDestructReset = false
@State private var showPasswordAction: PasswordAction? = nil
@State private var showChangePassword = false
@State var laAlert: LASettingViewAlert? = nil
@ -116,6 +120,8 @@ struct SimplexLockView: View {
case laUnavailableTurningOffAlert
case laPasscodeSetAlert
case laPasscodeChangedAlert
case laSeldDestructPasscodeSetAlert
case laSeldDestructPasscodeChangedAlert
case laPasscodeNotChangedAlert
var id: Self { self }
@ -124,7 +130,10 @@ struct SimplexLockView: View {
enum PasswordAction: Identifiable {
case enableAuth
case toggleMode
case changePassword
case changePasscode
case enableSelfDestruct
case changeSelfDestructPasscode
case selfDestructInfo
var id: Self { self }
}
@ -159,12 +168,34 @@ struct SimplexLockView: View {
}
}
if showChangePassword && laMode == .passcode {
Button("Change Passcode") {
Button("Change passcode") {
changeLAPassword()
}
}
}
}
if performLA && laMode == .passcode {
Section("Self-destruct passcode") {
Toggle(isOn: $selfDestruct) {
HStack(spacing: 6) {
Text("Enable self-destruct")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showPasswordAction = .selfDestructInfo
}
}
if selfDestruct {
TextField("New display name", text: $selfDestructDisplayName)
Button("Change self-destruct passcode") {
changeSelfDestructPassword()
}
}
}
}
}
}
.onChange(of: performLA) { performLAToggle in
@ -192,6 +223,13 @@ struct SimplexLockView: View {
updateLAMode()
}
}
.onChange(of: selfDestruct) { _ in
if performLASelfDestructReset {
performLASelfDestructReset = false
} else if prefPerformLA {
toggleSelfDestruct()
}
}
.alert(item: $laAlert) { alertItem in
switch alertItem {
case .laTurnedOnAlert: return laTurnedOnAlert()
@ -200,6 +238,8 @@ struct SimplexLockView: View {
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
case .laPasscodeSetAlert: return passcodeAlert("Passcode set!")
case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!")
case .laSeldDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!")
case .laSeldDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!")
case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!")
}
}
@ -223,12 +263,27 @@ struct SimplexLockView: View {
} cancel: {
revertLAMode()
}
case .changePassword:
case .changePasscode:
SetAppPasscodeView {
showLAAlert(.laPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .enableSelfDestruct:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
updateSelfDestruct()
showLAAlert(.laSeldDestructPasscodeSetAlert)
} cancel: {
revertSelfDestruct()
}
case .changeSelfDestructPasscode:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
showLAAlert(.laSeldDestructPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .selfDestructInfo:
selfDestructInfoView()
}
}
.onAppear {
@ -239,6 +294,30 @@ struct SimplexLockView: View {
}
}
private func selfDestructInfoView() -> some View {
VStack(alignment: .leading) {
Text("Self-desctruct")
.font(.largeTitle)
.bold()
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {
Group {
Text("If you enter your self-desctruct passcode while opening the app:")
VStack(spacing: 8) {
textListItem("1.", "All app data is deleted.")
textListItem("2.", "App passcode is replaced with self-desctruct passcode.")
textListItem("3.", "An empty chat profile with the provided name is created, and the app opens as usual.")
}
}
.padding(.bottom)
}
}
}
.frame(maxWidth: .infinity)
.padding()
}
private func showLAAlert(_ a: LASettingViewAlert) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
laAlert = a
@ -276,11 +355,39 @@ struct SimplexLockView: View {
}
}
private func toggleSelfDestruct() {
authenticate(reason: NSLocalizedString("Change self-destruct mode", comment: "authentication reason")) { laResult in
switch laResult {
case .failed:
revertSelfDestruct()
laAlert = .laFailedAlert
case .success:
if selfDestruct {
showPasswordAction = .enableSelfDestruct
} else {
resetSelfDestruct()
}
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 .success: showPasswordAction = .changePasscode
case .unavailable: disableUnavailableLA()
}
}
}
private func changeSelfDestructPassword() {
authenticate(reason: NSLocalizedString("Change self-destruct passcode", comment: "authentication reason")) { laResult in
switch laResult {
case .failed: laAlert = .laFailedAlert
case .success: showPasswordAction = .changeSelfDestructPasscode
case .unavailable: disableUnavailableLA()
}
}
@ -329,6 +436,7 @@ struct SimplexLockView: View {
_ = kcAppPassword.remove()
laLockDelay = 30
showChangePassword = false
resetSelfDestruct()
}
private func resetLAEnabled(_ onOff: Bool) {
@ -347,9 +455,29 @@ struct SimplexLockView: View {
privacyLocalAuthModeDefault.set(laMode)
}
private func resetSelfDestruct() {
_ = kcSelfDestructPassword.remove()
selfDestruct = false
updateSelfDestruct()
}
private func revertSelfDestruct() {
performLASelfDestructReset = true
withAnimation { selfDestruct = currentSelfDestruct }
}
private func updateSelfDestruct() {
UserDefaults.standard.set(selfDestruct, forKey: DEFAULT_LA_SELF_DESTRUCT)
currentSelfDestruct = selfDestruct
}
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!")
}
private func selfDestructPasscodeAlert(_ title: LocalizedStringKey) -> Alert {
mkAlert(title: title, message: "If you enter this passcode when opening the app, all app data will be irreversibly removed!")
}
}
struct PrivacySettings_Previews: PreviewProvider {

View File

@ -21,6 +21,8 @@ let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown"
let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_LA_MODE = "localAuthenticationMode"
let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay"
let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct"
let DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME = "localAuthenticationSelfDestructDisplayName"
let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
@ -53,6 +55,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PERFORM_LA: false,
DEFAULT_LA_MODE: LAMode.system.rawValue,
DEFAULT_LA_LOCK_DELAY: 30,
DEFAULT_LA_SELF_DESTRUCT: false,
DEFAULT_NOTIFICATION_ALERT_SHOWN: false,
DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false,
@ -298,9 +301,8 @@ struct SettingsView: View {
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(chatModel.incognito ? Color.indigo : .secondary)
Toggle(isOn: $chatModel.incognito) {
HStack {
HStack(spacing: 6) {
Text("Incognito")
Spacer().frame(width: 4)
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))

View File

@ -730,8 +730,8 @@ Dostupné ve verzi 5.1</target>
<target>Změnit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Změna hesla</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -730,8 +730,8 @@ Verfügbar ab v5.1</target>
<target>Ändern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Passwort ändern</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -739,9 +739,9 @@ Available in v5.1</target>
<target>Change</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<target>Change Passcode</target>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Change passcode</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Disponible en v5.1</target>
<target>Cambiar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Cambiar el código de acceso</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -730,8 +730,8 @@ Disponible dans la v5.1</target>
<target>Changer</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Modifier le code d'accès</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Disponibile nella v5.1</target>
<target>Cambia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Cambia codice di accesso</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -730,8 +730,8 @@ Beschikbaar in v5.1</target>
<target>Veranderen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Toegangscode wijzigen</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -730,8 +730,8 @@ Dostępny w v5.1</target>
<target>Zmień</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Zmień kod dostępu</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Available in v5.1</source>
<target>Поменять</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>Изменить код доступа</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -730,8 +730,8 @@ Available in v5.1</source>
<target>更改</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve">
<source>Change passcode</source>
<target>更改密码</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -4207,8 +4207,8 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">已取消認證</target>
<note>PIN entry</note>
</trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve" approved="no">
<source>Change Passcode</source>
<trans-unit id="Change passcode" xml:space="preserve" approved="no">
<source>Change passcode</source>
<target state="translated">修改密碼</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View File

@ -14,7 +14,7 @@ let jsonEncoder = getJSONEncoder()
public enum ChatCommand {
case showActiveUser
case createActiveUser(profile: Profile)
case createActiveUser(profile: Profile?)
case listUsers
case apiSetActiveUser(userId: Int64, viewPwd: String?)
case apiHideUser(userId: Int64, viewPwd: String)
@ -110,7 +110,11 @@ public enum ChatCommand {
get {
switch self {
case .showActiveUser: return "/u"
case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)"
case let .createActiveUser(profile):
if let profile = profile {
return "/create user \(profile.displayName) \(profile.fullName)"
}
return "/create user"
case .listUsers: return "/users"
case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"

View File

@ -13,11 +13,14 @@ private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDevi
private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app"
private let DATABASE_PASSWORD_ITEM: String = "databasePassword"
private let APP_PASSWORD_ITEM: String = "appPassword"
private let SELF_DESTRUCT_PASSWORD_ITEM: String = "selfDestructPassword"
public let kcDatabasePassword = KeyChainItem(forKey: DATABASE_PASSWORD_ITEM)
public let kcAppPassword = KeyChainItem(forKey: APP_PASSWORD_ITEM)
public let kcSelfDestructPassword = KeyChainItem(forKey: SELF_DESTRUCT_PASSWORD_ITEM)
public struct KeyChainItem {
var forKey: String

View File

@ -477,7 +477,7 @@
"Change passcode" = "Passwort ändern";
/* No comment provided by engineer. */
"Change Passcode" = "Passwort ändern";
"Change passcode" = "Passwort ändern";
/* No comment provided by engineer. */
"Change receiving address" = "Wechseln der Empfängeradresse";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Cambiar el código de acceso";
/* No comment provided by engineer. */
"Change Passcode" = "Cambiar el código de acceso";
"Change passcode" = "Cambiar el código de acceso";
/* No comment provided by engineer. */
"Change receiving address" = "Cambiar servidor de recepción";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Modifier le code d'accès";
/* No comment provided by engineer. */
"Change Passcode" = "Modifier le code d'accès";
"Change passcode" = "Modifier le code d'accès";
/* No comment provided by engineer. */
"Change receiving address" = "Changer d'adresse de réception";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Cambia codice di accesso";
/* No comment provided by engineer. */
"Change Passcode" = "Cambia codice di accesso";
"Change passcode" = "Cambia codice di accesso";
/* No comment provided by engineer. */
"Change receiving address" = "Cambia indirizzo di ricezione";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Toegangscode wijzigen";
/* No comment provided by engineer. */
"Change Passcode" = "Toegangscode wijzigen";
"Change passcode" = "Toegangscode wijzigen";
/* No comment provided by engineer. */
"Change receiving address" = "Ontvangst adres wijzigen";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Zmień pin";
/* No comment provided by engineer. */
"Change Passcode" = "Zmień kod dostępu";
"Change passcode" = "Zmień kod dostępu";
/* No comment provided by engineer. */
"Change receiving address" = "Zmień adres odbioru";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Изменить код доступа";
/* No comment provided by engineer. */
"Change Passcode" = "Изменить код доступа";
"Change passcode" = "Изменить код доступа";
/* No comment provided by engineer. */
"Change receiving address" = "Поменять адрес получения";

View File

@ -477,7 +477,7 @@
"Change passcode" = "更改密码";
/* No comment provided by engineer. */
"Change Passcode" = "更改密码";
"Change passcode" = "更改密码";
/* No comment provided by engineer. */
"Change receiving address" = "更改接收地址";

View File

@ -83,6 +83,7 @@ import Simplex.Messaging.Util
import System.Exit (exitFailure, exitSuccess)
import System.FilePath (combine, splitExtensions, takeFileName, (</>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe)
import UnliftIO.Async
import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay)
@ -318,7 +319,8 @@ toView event = do
processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse
processChatCommand = \case
ShowActiveUser -> withUser' $ pure . CRActiveUser
CreateActiveUser p@Profile {displayName} sameServers -> do
CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
u <- asks currentUser
(smp, smpServers) <- chooseServers SPSMP
(xftp, xftpServers) <- chooseServers SPXFTP
@ -329,7 +331,8 @@ processChatCommand = \case
when (any (\User {localDisplayName = n} -> n == displayName) users) $
throwChatError $ CEUserExists displayName
withAgent (\a -> createUser a smp xftp)
user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
storeServers user smpServers
storeServers user xftpServers
setActive ActiveNone
@ -351,6 +354,8 @@ processChatCommand = \case
storeServers user servers =
unless (null servers) $
withStore $ \db -> overwriteProtocolServers db user servers
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withStore' getUsersInfo
APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId'
@ -4584,12 +4589,8 @@ chatCommandP =
choice
[ "/mute " *> ((`ShowMessages` False) <$> chatNameP),
"/unmute " *> ((`ShowMessages` True) <$> chatNameP),
"/create user"
*> ( do
sameSmp <- (A.space *> "same_smp=" *> onOffP) <|> pure False
uProfile <- A.space *> userProfile
pure $ CreateActiveUser uProfile sameSmp
),
"/_create user " *> (CreateActiveUser <$> jsonP),
"/create user " *> (CreateActiveUser <$> newUserP),
"/users" $> ListUsers,
"/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)),
("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)),
@ -4784,7 +4785,7 @@ chatCommandP =
("/welcome" <|> "/w") $> Welcome,
"/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
"/profile_image" $> UpdateProfileImage Nothing,
("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> userNames),
("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames),
("/profile" <|> "/p") $> ShowProfile,
"/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)),
"/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)),
@ -4823,23 +4824,19 @@ chatCommandP =
refChar c = c > ' ' && c /= '#' && c /= '@'
liveMessageP = " live=" *> onOffP <|> pure False
onOffP = ("on" $> True) <|> ("off" $> False)
userNames = do
cName <- displayName
fullName <- fullNameP cName
pure (cName, fullName)
userProfile = do
(cName, fullName) <- userNames
pure Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing}
profileNames = (,) <$> displayName <*> fullNameP
newUserP = do
sameServers <- "same_smp=" *> onOffP <* A.space <|> pure False
(cName, fullName) <- profileNames
let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing}
pure NewUser {profile, sameServers, pastTimestamp = False}
jsonP :: J.FromJSON a => Parser a
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
groupProfile = do
gName <- displayName
fullName <- fullNameP gName
(gName, fullName) <- profileNames
let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just DirectMessagesGroupPreference {enable = FEOn}}
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
fullNameP name = do
n <- (A.space *> A.takeByteString) <|> pure ""
pure $ if B.null n then name else safeDecodeUtf8 n
fullNameP = A.space *> textP <|> pure ""
textP = safeDecodeUtf8 <$> A.takeByteString
pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' '))
msgTextP = jsonP <|> textP

View File

@ -180,7 +180,7 @@ instance ToJSON HelpSection where
data ChatCommand
= ShowActiveUser
| CreateActiveUser Profile Bool
| CreateActiveUser NewUser
| ListUsers
| APISetActiveUser UserId (Maybe UserPwd)
| SetActiveUser UserName (Maybe UserPwd)

View File

@ -27,6 +27,7 @@ module Simplex.Chat.Store
chatStoreFile,
agentStoreFile,
createUserRecord,
createUserRecordAt,
getUsersInfo,
getUsers,
setActiveUser,
@ -490,9 +491,11 @@ insertedRowId :: DB.Connection -> IO Int64
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()"
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser =
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
createUserRecordAt :: DB.Connection -> AgentUserId -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User
createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser currentTs =
checkConstraint SEDuplicateName . liftIO $ do
currentTs <- getCurrentTime
when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0"
DB.execute
db

View File

@ -120,6 +120,13 @@ instance ToJSON User where
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
data NewUser = NewUser
{ profile :: Maybe Profile,
sameServers :: Bool,
pastTimestamp :: Bool
}
deriving (Show, Generic, FromJSON)
newtype B64UrlByteString = B64UrlByteString ByteString
deriving (Eq, Show)

View File

@ -457,7 +457,7 @@ testGroupSameName =
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/g team"
alice <## "group #team_1 (team) is created"
alice <## "group #team_1 is created"
alice <## "to add members use /a team_1 <name> or /create link #team_1"
testGroupDeleteWhenInvited :: HasCallStack => FilePath -> IO ()
@ -518,7 +518,7 @@ testGroupReAddInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team_1 (team): alice invites you to join the group as admin"
bob <## "#team_1: alice invites you to join the group as admin"
bob <## "use /j team_1 to accept"
]
@ -678,7 +678,7 @@ testGroupRemoveAdd =
]
alice ##> "/a team bob"
alice <## "invitation to join the group #team sent to bob"
bob <## "#team_1 (team): alice invites you to join the group as admin"
bob <## "#team_1: alice invites you to join the group as admin"
bob <## "use /j team_1 to accept"
bob ##> "/j team_1"
concurrentlyN_
@ -1058,13 +1058,13 @@ testGroupLiveMessage =
alice <## "message history:"
alice .<## ": hello 2"
alice .<## ":"
bobItemId <- lastItemId bob
bob ##> ("/_get item info " <> bobItemId)
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hello 2"
bob .<## ":"
-- bobItemId <- lastItemId bob
-- bob ##> ("/_get item info " <> bobItemId)
-- bob <##. "sent at: "
-- bob <##. "received at: "
-- bob <## "message history:"
-- bob .<## ": hello 2"
-- bob .<## ":"
testUpdateGroupProfile :: HasCallStack => FilePath -> IO ()
testUpdateGroupProfile =

View File

@ -1147,7 +1147,7 @@ testUpdateGroupPrefs =
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")])
threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:"
alice <## "Full deletion: on"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")])
@ -1156,7 +1156,7 @@ testUpdateGroupPrefs =
bob <## "Full deletion: on"
threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:"
alice <## "Full deletion: off"
alice <## "Voice messages: off"
@ -1167,7 +1167,7 @@ testUpdateGroupPrefs =
bob <## "Voice messages: off"
threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")])
-- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}"
-- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}"
alice ##> "/set voice #team on"
alice <## "updated group preferences:"
alice <## "Voice messages: on"
@ -1178,7 +1178,7 @@ testUpdateGroupPrefs =
threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")])
threadDelay 500000
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
-- no update
threadDelay 500000
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
@ -1343,7 +1343,7 @@ testEnableTimedMessagesGroup =
\alice bob -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}"
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:"
alice <## "Disappearing messages: on (1 sec)"
bob <## "alice updated group #team:"