mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 09:59:44 +00:00
Merge pull request #985 from simplex-chat/sqlcipher
core: switch from SQLite to SQLCipher
This commit is contained in:
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- sqlcipher
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
@@ -67,9 +68,9 @@ jobs:
|
||||
- name: Setup Stack
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: '8.10.7'
|
||||
ghc-version: "8.10.7"
|
||||
enable-stack: true
|
||||
stack-version: 'latest'
|
||||
stack-version: "latest"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
@@ -81,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Unix build
|
||||
id: unix_build
|
||||
if: matrix.os != 'windows-latest'
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04'
|
||||
shell: bash
|
||||
run: |
|
||||
stack build --test
|
||||
@@ -98,6 +99,27 @@ jobs:
|
||||
|
||||
# Unix /
|
||||
|
||||
# / Mac
|
||||
|
||||
- name: Mac build
|
||||
id: mac_build
|
||||
if: matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
stack build --test --extra-include-dirs=/usr/local/opt/openssl@1.1/include --extra-lib-dirs=/usr/local/opt/openssl@1.1/lib
|
||||
echo "::set-output name=local_install_root::$(stack path --local-install-root)"
|
||||
|
||||
- name: Unix upload binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.unix_build.outputs.local_install_root }}/bin/simplex-chat
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
# Mac /
|
||||
|
||||
# / Windows
|
||||
|
||||
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
app/src/main/cpp/libs/
|
||||
|
||||
@@ -53,6 +53,9 @@ add_library( support SHARED IMPORTED )
|
||||
set_target_properties( support PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so)
|
||||
|
||||
add_library( crypto SHARED IMPORTED )
|
||||
set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libcrypto.so)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link multiple libraries, such as libraries you define in this
|
||||
@@ -61,7 +64,7 @@ set_target_properties( support PROPERTIES IMPORTED_LOCATION
|
||||
target_link_libraries( # Specifies the target library.
|
||||
app-lib
|
||||
|
||||
simplex support
|
||||
simplex support crypto
|
||||
|
||||
# Links the target library to the log library
|
||||
# included in the NDK.
|
||||
|
||||
@@ -24,20 +24,42 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
|
||||
// from simplex-chat
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
extern chat_ctrl chat_init(const char *path);
|
||||
extern char *chat_migrate_db(const char *path, const char *key);
|
||||
extern chat_ctrl chat_init_key(const char *path, const char *key);
|
||||
extern chat_ctrl chat_init(const char *path); // deprecated
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
|
||||
extern char *chat_parse_markdown(const char *str);
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) {
|
||||
const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE);
|
||||
jlong res = (jlong)chat_init(_data);
|
||||
(*env)->ReleaseStringUTFChars(env, datadir, _data);
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatMigrateDB(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_db(_dbPath, _dbKey));
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatInitKey(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
|
||||
jlong ctrl = (jlong)chat_init_key(_dbPath, _dbKey);
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring dbPath) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
jlong ctrl = (jlong)chat_init(_dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
|
||||
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
|
||||
|
||||
@@ -32,6 +32,7 @@ import chat.simplex.app.views.call.IncomingCallAlertView
|
||||
import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.ChatListView
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.connectViaUri
|
||||
import chat.simplex.app.views.newchat.withUriAction
|
||||
@@ -56,7 +57,6 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
private val vm by viewModels<SimplexViewModel>()
|
||||
private val chatController by lazy { (application as SimplexApp).chatController }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -251,6 +251,13 @@ fun MainPage(
|
||||
}
|
||||
chatsAccessAuthorized = userAuthorized.value == true
|
||||
}
|
||||
var showChatDatabaseError by rememberSaveable {
|
||||
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
|
||||
}
|
||||
LaunchedEffect(chatModel.chatDbStatus.value) {
|
||||
showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null
|
||||
}
|
||||
|
||||
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(showAdvertiseLAAlert) {
|
||||
if (
|
||||
@@ -296,6 +303,11 @@ fun MainPage(
|
||||
val onboarding = chatModel.onboardingStage.value
|
||||
val userCreated = chatModel.userCreated.value
|
||||
when {
|
||||
showChatDatabaseError -> {
|
||||
chatModel.chatDbStatus.value?.let {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
}
|
||||
}
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
!chatsAccessAuthorized -> {
|
||||
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
|
||||
|
||||
@@ -26,21 +26,46 @@ external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
|
||||
// SimpleX API
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatInit(path: String): ChatCtrl
|
||||
external fun chatMigrateDB(dbPath: String, dbKey: String): String
|
||||
external fun chatInitKey(dbPath: String, dbKey: String): ChatCtrl
|
||||
external fun chatInit(dbPath: String): ChatCtrl
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
|
||||
external fun chatParseMarkdown(str: String): String
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
val chatController: ChatController by lazy {
|
||||
val ctrl = chatInit(getFilesDirectory(applicationContext))
|
||||
ChatController(ctrl, ntfManager, applicationContext, appPreferences)
|
||||
lateinit var chatController: ChatController
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.getDatabaseKey() ?: ""
|
||||
val res = DatabaseUtils.migrateChatDatabase(dbKey)
|
||||
val ctrl = if (res.second is DBMigrationResult.OK) {
|
||||
chatInitKey(getFilesDirectory(applicationContext), dbKey)
|
||||
} else null
|
||||
if (::chatController.isInitialized) {
|
||||
chatController.ctrl = ctrl
|
||||
} else {
|
||||
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
|
||||
}
|
||||
chatModel.chatDbEncrypted.value = res.first
|
||||
chatModel.chatDbStatus.value = res.second
|
||||
if (res.second != DBMigrationResult.OK) {
|
||||
Log.d(TAG, "Unable to migrate successfully: ${res.second}")
|
||||
} else if (startChat) {
|
||||
withApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
chatController.startChat(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chatModel: ChatModel by lazy {
|
||||
chatController.chatModel
|
||||
}
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
private val ntfManager: NtfManager by lazy {
|
||||
NtfManager(applicationContext, appPreferences)
|
||||
@@ -53,15 +78,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
initChatController()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
withApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
chatController.startChat(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -24,7 +24,6 @@ class SimplexService: Service() {
|
||||
private var isStartingService = false
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private var serviceNotification: Notification? = null
|
||||
private val chatController by lazy { (application as SimplexApp).chatController }
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "onStartCommand startId: $startId")
|
||||
@@ -67,19 +66,21 @@ class SimplexService: Service() {
|
||||
val self = this
|
||||
isStartingService = true
|
||||
withApi {
|
||||
val chatController = (application as SimplexApp).chatController
|
||||
try {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatController.chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
Log.w(TAG, "Starting foreground service")
|
||||
chatController.startChat(user)
|
||||
isServiceStarted = true
|
||||
saveServiceState(self, ServiceState.STARTED)
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
|
||||
acquire()
|
||||
}
|
||||
Log.w(TAG, "Starting foreground service")
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
if (chatDbStatus != DBMigrationResult.OK) {
|
||||
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
|
||||
showPassphraseNotification(chatDbStatus)
|
||||
stopService()
|
||||
return@withApi
|
||||
}
|
||||
isServiceStarted = true
|
||||
saveServiceState(self, ServiceState.STARTED)
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
|
||||
acquire()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -227,6 +228,8 @@ class SimplexService: Service() {
|
||||
const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L
|
||||
const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "SimplexAutoRestartWorkerPeriodic" // Do not change!
|
||||
|
||||
private const val PASSPHRASE_NOTIFICATION_ID = 1535
|
||||
|
||||
private const val WAKE_LOCK_TAG = "SimplexService::lock"
|
||||
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_SERVICE_PREFS"
|
||||
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
||||
@@ -271,6 +274,41 @@ class SimplexService: Service() {
|
||||
return ServiceState.valueOf(value!!)
|
||||
}
|
||||
|
||||
fun showPassphraseNotification(chatDbStatus: DBMigrationResult?) {
|
||||
val pendingIntent: PendingIntent = Intent(SimplexApp.context, MainActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(SimplexApp.context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val title = when(chatDbStatus) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_title)
|
||||
is DBMigrationResult.OK -> return
|
||||
else -> generalGetString(R.string.database_initialization_error_title)
|
||||
}
|
||||
|
||||
val description = when(chatDbStatus) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_desc)
|
||||
is DBMigrationResult.OK -> return
|
||||
else -> generalGetString(R.string.database_initialization_error_desc)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(SimplexApp.context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ntf_service_icon)
|
||||
.setColor(0x88FFFF)
|
||||
.setContentTitle(title)
|
||||
.setContentText(description)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setSilent(true)
|
||||
.setShowWhen(false)
|
||||
|
||||
val notificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(PASSPHRASE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun cancelPassphraseNotification() {
|
||||
val notificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.helpers.DBMigrationResult
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.NotificationPreviewMode
|
||||
@@ -27,6 +28,8 @@ class ChatModel(val controller: ChatController) {
|
||||
val userCreated = mutableStateOf<Boolean?>(null)
|
||||
val chatRunning = mutableStateOf<Boolean?>(null)
|
||||
val chatDbChanged = mutableStateOf<Boolean>(false)
|
||||
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
|
||||
// current chat
|
||||
|
||||
@@ -34,13 +34,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
private val msgNtfTimeoutMs = 30000L
|
||||
|
||||
init {
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, "SimpleX Chat messages", NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, "SimpleX Chat calls (lock screen)", NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, generalGetString(R.string.ntf_channel_calls_lockscreen), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(callNotificationChannel())
|
||||
}
|
||||
|
||||
private fun callNotificationChannel(): NotificationChannel {
|
||||
val callChannel = NotificationChannel(CallChannel, "SimpleX Chat calls", NotificationManager.IMPORTANCE_HIGH)
|
||||
val callChannel = NotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls), NotificationManager.IMPORTANCE_HIGH)
|
||||
val attrs = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
|
||||
@@ -106,6 +106,11 @@ class AppPreferences(val context: Context) {
|
||||
val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt)
|
||||
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
|
||||
|
||||
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
|
||||
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
|
||||
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
||||
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
|
||||
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
|
||||
|
||||
@@ -180,6 +185,10 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt"
|
||||
private const val SHARED_PREFS_INCOGNITO = "Incognito"
|
||||
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
|
||||
}
|
||||
@@ -187,7 +196,7 @@ class AppPreferences(val context: Context) {
|
||||
|
||||
private const val MESSAGE_TIMEOUT: Int = 15_000_000
|
||||
|
||||
open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) {
|
||||
open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) {
|
||||
val chatModel = ChatModel(this)
|
||||
private var receiverStarted = false
|
||||
var lastMsgReceivedTimestamp: Long = System.currentTimeMillis()
|
||||
@@ -242,10 +251,12 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
|
||||
suspend fun sendCmd(cmd: CC): CR {
|
||||
val ctrl = ctrl ?: throw Exception("Controller is not initialized")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
if (cmd !is CC.ApiParseMarkdown) {
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(cmd))
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
}
|
||||
val json = chatSendCmd(ctrl, c)
|
||||
@@ -261,7 +272,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recvMsg(): CR? {
|
||||
private suspend fun recvMsg(ctrl: ChatCtrl): CR? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
||||
if (json == "") {
|
||||
@@ -276,7 +287,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
|
||||
private suspend fun recvMspLoop() {
|
||||
val msg = recvMsg()
|
||||
val msg = recvMsg(ctrl ?: return)
|
||||
if (msg != null) processReceivedMsg(msg)
|
||||
recvMspLoop()
|
||||
}
|
||||
@@ -343,6 +354,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
throw Error("failed to delete storage: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiStorageEncryption(currentKey: String = "", newKey: String = ""): CR.ChatCmdError? {
|
||||
val r = sendCmd(CC.ApiStorageEncryption(DBEncryptionConfig(currentKey, newKey)))
|
||||
if (r is CR.CmdOk) return null
|
||||
else if (r is CR.ChatCmdError) return r
|
||||
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
private suspend fun apiGetChats(): List<Chat> {
|
||||
val r = sendCmd(CC.ApiGetChats())
|
||||
if (r is CR.ApiChats ) return r.chats
|
||||
@@ -1197,6 +1215,7 @@ sealed class CC {
|
||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiDeleteStorage: CC()
|
||||
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
|
||||
class ApiGetChats: CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC()
|
||||
@@ -1251,6 +1270,7 @@ sealed class CC {
|
||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||
is ApiDeleteStorage -> "/_db delete"
|
||||
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
|
||||
is ApiGetChats -> "/_get chats pcc=on"
|
||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
|
||||
@@ -1305,6 +1325,7 @@ sealed class CC {
|
||||
is ApiExportArchive -> "apiExportArchive"
|
||||
is ApiImportArchive -> "apiImportArchive"
|
||||
is ApiDeleteStorage -> "apiDeleteStorage"
|
||||
is ApiStorageEncryption -> "apiStorageEncryption"
|
||||
is ApiGetChats -> "apiGetChats"
|
||||
is ApiGetChat -> "apiGetChat"
|
||||
is ApiSendMessage -> "apiSendMessage"
|
||||
@@ -1350,6 +1371,14 @@ sealed class CC {
|
||||
|
||||
class ItemRange(val from: Long, val to: Long)
|
||||
|
||||
val obfuscated: CC
|
||||
get() = when (this) {
|
||||
is ApiStorageEncryption -> ApiStorageEncryption(DBEncryptionConfig(obfuscate(config.currentKey), obfuscate(config.newKey)))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun obfuscate(s: String): String = if (s.isEmpty()) "" else "***"
|
||||
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
|
||||
@@ -1381,6 +1410,9 @@ class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgCon
|
||||
@Serializable
|
||||
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
||||
|
||||
@Serializable
|
||||
class DBEncryptionConfig(val currentKey: String, val newKey: String)
|
||||
|
||||
@Serializable
|
||||
data class NetCfg(
|
||||
val socksProxy: String? = null,
|
||||
@@ -1785,10 +1817,12 @@ sealed class ChatError {
|
||||
is ChatErrorChat -> "chat ${errorType.string}"
|
||||
is ChatErrorAgent -> "agent ${agentError.string}"
|
||||
is ChatErrorStore -> "store ${storeError.string}"
|
||||
is ChatErrorDatabase -> "database ${databaseError.string}"
|
||||
}
|
||||
@Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError()
|
||||
@Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError()
|
||||
@Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError()
|
||||
@Serializable @SerialName("errorDatabase") class ChatErrorDatabase(val databaseError: DatabaseError): ChatError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1813,6 +1847,28 @@ sealed class StoreError {
|
||||
@Serializable @SerialName("groupNotFound") class GroupNotFound: StoreError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class DatabaseError {
|
||||
val string: String get() = when (this) {
|
||||
is ErrorEncrypted -> "errorEncrypted"
|
||||
is ErrorPlaintext -> "errorPlaintext"
|
||||
is ErrorNoFile -> "errorPlaintext"
|
||||
is ErrorExport -> "errorNoFile"
|
||||
is ErrorOpen -> "errorExport"
|
||||
}
|
||||
@Serializable @SerialName("errorEncrypted") object ErrorEncrypted: DatabaseError()
|
||||
@Serializable @SerialName("errorPlaintext") object ErrorPlaintext: DatabaseError()
|
||||
@Serializable @SerialName("errorNoFile") class ErrorNoFile(val dbFile: String): DatabaseError()
|
||||
@Serializable @SerialName("errorExport") class ErrorExport(val sqliteError: SQLiteError): DatabaseError()
|
||||
@Serializable @SerialName("errorOpen") class ErrorOpen(val sqliteError: SQLiteError): DatabaseError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SQLiteError {
|
||||
@Serializable @SerialName("errorNotADatabase") object ErrorNotADatabase: SQLiteError()
|
||||
@Serializable @SerialName("error") class Error(val error: String): SQLiteError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class AgentErrorType {
|
||||
val string: String get() = when (this) {
|
||||
|
||||
@@ -24,5 +24,6 @@ val GroupDark = Color(80, 80, 80, 60)
|
||||
val IncomingCallLight = Color(239, 237, 236, 255)
|
||||
val IncomingCallDark = Color(34, 30, 29, 255)
|
||||
val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 255)
|
||||
val FileDark = Color(101, 101, 106, 255)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.app.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
@@ -7,39 +8,86 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
BackHandler(onBack = close)
|
||||
TerminalLayout(
|
||||
chatModel.terminalItems,
|
||||
composeState,
|
||||
sendCommand = {
|
||||
withApi {
|
||||
// show "in progress"
|
||||
chatModel.controller.sendCmd(CC.Console(composeState.value.message))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
// hide "in progress"
|
||||
val authorized = remember { mutableStateOf(!chatModel.controller.appPrefs.performLA.get()) }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(authorized.value) {
|
||||
if (!authorized.value) {
|
||||
runAuth(authorized = authorized, context)
|
||||
}
|
||||
}
|
||||
if (authorized.value) {
|
||||
TerminalLayout(
|
||||
chatModel.terminalItems,
|
||||
composeState,
|
||||
sendCommand = {
|
||||
withApi {
|
||||
// show "in progress"
|
||||
chatModel.controller.sendCmd(CC.Console(composeState.value.message))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
// hide "in progress"
|
||||
}
|
||||
},
|
||||
close
|
||||
)
|
||||
} else {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
CloseSheetBar(close)
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(authorized = authorized, context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuth(authorized: MutableState<Boolean>, context: Context) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> authorized.value = true
|
||||
is LAResult.Error, LAResult.Failed -> authorized.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+509
@@ -0,0 +1,509 @@
|
||||
package chat.simplex.app.views.database
|
||||
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlin.math.log2
|
||||
|
||||
@Composable
|
||||
fun DatabaseEncryptionView(m: ChatModel) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") }
|
||||
val newKey = rememberSaveable { mutableStateOf("") }
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
DatabaseEncryptionLayout(
|
||||
useKeychain,
|
||||
prefs,
|
||||
m.chatDbEncrypted.value,
|
||||
currentKey,
|
||||
newKey,
|
||||
confirmNewKey,
|
||||
storedKey,
|
||||
initialRandomDBPassphrase,
|
||||
progressIndicator,
|
||||
onConfirmEncrypt = {
|
||||
progressIndicator.value = true
|
||||
withApi {
|
||||
try {
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.wrong_passphrase_title),
|
||||
generalGetString(R.string.enter_correct_current_passphrase)
|
||||
)
|
||||
}
|
||||
}
|
||||
error != null -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database),
|
||||
"failed to set storage encryption: ${error.responseType} ${error.details}"
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
prefs.initialRandomDBPassphrase.set(false)
|
||||
initialRandomDBPassphrase.value = false
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.setDatabaseKey(newKey.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_encrypted))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (progressIndicator.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DatabaseEncryptionLayout(
|
||||
useKeychain: MutableState<Boolean>,
|
||||
prefs: AppPreferences,
|
||||
chatDbEncrypted: Boolean?,
|
||||
currentKey: MutableState<String>,
|
||||
newKey: MutableState<String>,
|
||||
confirmNewKey: MutableState<String>,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
onConfirmEncrypt: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.database_passphrase),
|
||||
Modifier.padding(start = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
|
||||
SectionView(null) {
|
||||
SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked ->
|
||||
if (checked) {
|
||||
setUseKeychain(true, useKeychain, prefs)
|
||||
} else if (storedKey.value) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.remove_passphrase_from_keychain),
|
||||
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.remove_passphrase),
|
||||
onConfirm = {
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
} else {
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
}
|
||||
}
|
||||
|
||||
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
|
||||
DatabaseKeyField(
|
||||
currentKey,
|
||||
generalGetString(R.string.current_passphrase),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseKeyField(
|
||||
newKey,
|
||||
generalGetString(R.string.new_passphrase),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
showStrength = true,
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
)
|
||||
val onClickUpdate = {
|
||||
// Don't do things concurrently. Shouldn't be here concurrently, just in case
|
||||
if (!progressIndicator.value) {
|
||||
if (currentKey.value == "") {
|
||||
if (useKeychain.value)
|
||||
encryptDatabaseSavedAlert(onConfirmEncrypt)
|
||||
else
|
||||
encryptDatabaseAlert(onConfirmEncrypt)
|
||||
} else {
|
||||
if (useKeychain.value)
|
||||
changeDatabaseKeySavedAlert(onConfirmEncrypt)
|
||||
else
|
||||
changeDatabaseKeyAlert(onConfirmEncrypt)
|
||||
}
|
||||
}
|
||||
}
|
||||
val disabled = currentKey.value == newKey.value ||
|
||||
newKey.value != confirmNewKey.value ||
|
||||
newKey.value.isEmpty() ||
|
||||
!validKey(currentKey.value) ||
|
||||
!validKey(newKey.value) ||
|
||||
progressIndicator.value
|
||||
|
||||
DatabaseKeyField(
|
||||
confirmNewKey,
|
||||
generalGetString(R.string.confirm_new_passphrase),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (!disabled) onClickUpdate()
|
||||
defaultKeyboardAction(ImeAction.Done)
|
||||
}),
|
||||
)
|
||||
|
||||
SectionItemViewSpaceBetween(onClickUpdate, padding = PaddingValues(start = 8.dp, end = 12.dp), disabled = disabled) {
|
||||
Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(R.string.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(R.string.keychain_is_storing_securely))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
SectionTextFooter(generalGetString(R.string.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(R.string.keychain_allows_to_receive_ntfs))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(R.string.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.encrypt_database_question),
|
||||
text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(R.string.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun encryptDatabaseAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.encrypt_database_question),
|
||||
text = generalGetString(R.string.database_will_be_encrypted) +"\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.change_database_passphrase_question),
|
||||
text = generalGetString(R.string.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(R.string.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.change_database_passphrase_question),
|
||||
text = generalGetString(R.string.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView() {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (storedKey) Icons.Filled.VpnKey else Icons.Filled.VpnKeyOff,
|
||||
stringResource(R.string.save_passphrase_in_keychain),
|
||||
tint = if (storedKey) SimplexGreen else HighOrLowlight
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(R.string.save_passphrase_in_keychain),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetFormAfterEncryption(
|
||||
m: ChatModel,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
currentKey: MutableState<String>,
|
||||
newKey: MutableState<String>,
|
||||
confirmNewKey: MutableState<String>,
|
||||
storedKey: MutableState<Boolean>,
|
||||
stored: Boolean = false,
|
||||
) {
|
||||
m.chatDbEncrypted.value = true
|
||||
initialRandomDBPassphrase.value = false
|
||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||
currentKey.value = ""
|
||||
newKey.value = ""
|
||||
confirmNewKey.value = ""
|
||||
storedKey.value = stored
|
||||
}
|
||||
|
||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
|
||||
useKeychain.value = value
|
||||
prefs.storeDBPassphrase.set(value)
|
||||
}
|
||||
|
||||
fun storeSecurelySaved() = generalGetString(R.string.store_passphrase_securely)
|
||||
|
||||
fun storeSecurelyDanger() = generalGetString(R.string.store_passphrase_securely_without_recover)
|
||||
|
||||
private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean>, alert: () -> Unit) {
|
||||
m.chatDbChanged.value = true
|
||||
progressIndicator.value = false
|
||||
alert.invoke()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DatabaseKeyField(
|
||||
key: MutableState<String>,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showStrength: Boolean = false,
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
val icon = if (valid) {
|
||||
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
|
||||
} else Icons.Outlined.Error
|
||||
val iconColor = if (valid) {
|
||||
if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else HighOrLowlight
|
||||
} else Color.Red
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password
|
||||
)
|
||||
val state = remember {
|
||||
mutableStateOf(TextFieldValue(key.value))
|
||||
}
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val color = MaterialTheme.colors.onBackground
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
key.value = it.text
|
||||
valid = isValid(it.text)
|
||||
},
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = if (showKey)
|
||||
VisualTransformation.None
|
||||
else
|
||||
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
keyboardActions.onDone?.invoke(this)
|
||||
}),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle.Default.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = { Text(placeholder, color = HighOrLowlight) },
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
isError = !valid,
|
||||
trailingIcon = {
|
||||
IconButton({ showKey = !showKey }) {
|
||||
Icon(icon, null, tint = iconColor)
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// based on https://generatepasswords.org/how-to-calculate-entropy/
|
||||
private fun passphraseEntropy(s: String): Double {
|
||||
var hasDigits = false
|
||||
var hasUppercase = false
|
||||
var hasLowercase = false
|
||||
var hasSymbols = false
|
||||
for (c in s) {
|
||||
if (c.isDigit()) {
|
||||
hasDigits = true
|
||||
} else if (c.isLetter()) {
|
||||
if (c.isUpperCase()) {
|
||||
hasUppercase = true
|
||||
} else {
|
||||
hasLowercase = true
|
||||
}
|
||||
} else if (c.isASCII()) {
|
||||
hasSymbols = true
|
||||
}
|
||||
}
|
||||
val poolSize = (if (hasDigits) 10 else 0) + (if (hasUppercase) 26 else 0) + (if (hasLowercase) 26 else 0) + (if (hasSymbols) 32 else 0)
|
||||
return s.length * log2(poolSize.toDouble())
|
||||
}
|
||||
|
||||
private enum class PassphraseStrength(val color: Color) {
|
||||
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
|
||||
|
||||
companion object {
|
||||
fun check(s: String) = with(passphraseEntropy(s)) {
|
||||
when {
|
||||
this > 100 -> STRONG
|
||||
this > 70 -> REASONABLE
|
||||
this > 40 -> WEAK
|
||||
else -> VERY_WEAK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun validKey(s: String): Boolean {
|
||||
for (c in s) {
|
||||
if (c.isWhitespace() || !c.isASCII()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Char.isASCII() = code in 32..126
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewDatabaseEncryptionLayout() {
|
||||
SimpleXTheme {
|
||||
DatabaseEncryptionLayout(
|
||||
useKeychain = remember { mutableStateOf(true) },
|
||||
prefs = AppPreferences(SimplexApp.context),
|
||||
chatDbEncrypted = true,
|
||||
currentKey = remember { mutableStateOf("") },
|
||||
newKey = remember { mutableStateOf("") },
|
||||
confirmNewKey = remember { mutableStateOf("") },
|
||||
storedKey = remember { mutableStateOf(true) },
|
||||
initialRandomDBPassphrase = remember { mutableStateOf(true) },
|
||||
progressIndicator = remember { mutableStateOf(false) },
|
||||
onConfirmEncrypt = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package chat.simplex.app.views.database
|
||||
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun DatabaseErrorView(
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
appPreferences: AppPreferences,
|
||||
) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val dbKey = remember { mutableStateOf("") }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val saveAndRunChatOnClick: () -> Unit = {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
useKeychain = true
|
||||
appPreferences.initialRandomDBPassphrase.set(false)
|
||||
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
val title = when (chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> ""
|
||||
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
|
||||
generalGetString(R.string.wrong_passphrase)
|
||||
else
|
||||
generalGetString(R.string.encrypted_database)
|
||||
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
|
||||
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
|
||||
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
|
||||
null -> "" // should never be here
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth().fillMaxHeight().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
Text(generalGetString(R.string.passphrase_is_different))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
saveAndRunChatOnClick()
|
||||
}
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
SectionSpacer()
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
} else {
|
||||
Text(generalGetString(R.string.database_passphrase_is_required))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
if (useKeychain) {
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
} else {
|
||||
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
Text(generalGetString(R.string.cannot_access_keychain))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
|
||||
}
|
||||
is DBMigrationResult.OK -> {
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressIndicator.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runChat(
|
||||
dbKey: String,
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
prefs: AppPreferences
|
||||
) = CoroutineScope(Dispatchers.Default).launch {
|
||||
// Don't do things concurrently. Shouldn't be here concurrently, just in case
|
||||
if (progressIndicator.value) return@launch
|
||||
progressIndicator.value = true
|
||||
try {
|
||||
SimplexApp.context.initChatController(dbKey)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
|
||||
}
|
||||
progressIndicator.value = false
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> {
|
||||
SimplexService.cancelPassphraseNotification()
|
||||
when (prefs.notificationsMode.get()) {
|
||||
NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start(SimplexApp.context) }
|
||||
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
AlertManager.shared.showAlertMsg( generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
AlertManager.shared.showAlertMsg( generalGetString(R.string.database_error), status.migrationError)
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
AlertManager.shared.showAlertMsg( generalGetString(R.string.keychain_error))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
AlertManager.shared.showAlertMsg( generalGetString(R.string.unknown_error), status.json)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
DatabaseKeyField(
|
||||
text,
|
||||
generalGetString(R.string.enter_passphrase),
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onDone = if (enabled) {
|
||||
{ onClick?.invoke() }
|
||||
} else null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
|
||||
Text(generalGetString(R.string.save_passphrase_and_open_chat))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
|
||||
Text(generalGetString(R.string.open_chat))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
DatabaseErrorView(
|
||||
remember { mutableStateOf(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) },
|
||||
AppPreferences(SimplexApp.context)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,11 @@ import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -28,13 +29,14 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.*
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -49,6 +51,7 @@ fun DatabaseView(
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
|
||||
val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) }
|
||||
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
|
||||
@@ -68,12 +71,14 @@ fun DatabaseView(
|
||||
DatabaseLayout(
|
||||
progressIndicator.value,
|
||||
runChat.value,
|
||||
m.chatDbChanged.value,
|
||||
useKeychain.value,
|
||||
m.chatDbEncrypted.value,
|
||||
m.controller.appPrefs.initialRandomDBPassphrase,
|
||||
importArchiveLauncher,
|
||||
chatArchiveName,
|
||||
chatArchiveTime,
|
||||
chatLastStart,
|
||||
startChat = { startChat(m, runChat, chatLastStart, context) },
|
||||
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
|
||||
stopChatAlert = { stopChatAlert(m, runChat, context) },
|
||||
exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
|
||||
deleteChatAlert = { deleteChatAlert(m, progressIndicator) },
|
||||
@@ -100,7 +105,9 @@ fun DatabaseView(
|
||||
fun DatabaseLayout(
|
||||
progressIndicator: Boolean,
|
||||
runChat: Boolean,
|
||||
chatDbChanged: Boolean,
|
||||
useKeyChain: Boolean,
|
||||
chatDbEncrypted: Boolean?,
|
||||
initialRandomDBPassphrase: Preference<Boolean>,
|
||||
importArchiveLauncher: ManagedActivityResultLauncher<String, Uri?>,
|
||||
chatArchiveName: MutableState<String?>,
|
||||
chatArchiveTime: MutableState<Instant?>,
|
||||
@@ -112,10 +119,10 @@ fun DatabaseLayout(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
||||
) {
|
||||
val stopped = !runChat
|
||||
val operationsDisabled = !stopped || progressIndicator || chatDbChanged
|
||||
val operationsDisabled = !stopped || progressIndicator
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
@@ -125,15 +132,30 @@ fun DatabaseLayout(
|
||||
)
|
||||
|
||||
SectionView(stringResource(R.string.run_chat_section)) {
|
||||
RunChatSetting(runChat, stopped, chatDbChanged, startChat, stopChatAlert)
|
||||
RunChatSetting(runChat, stopped, startChat, stopChatAlert)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.chat_database_section)) {
|
||||
val unencrypted = chatDbEncrypted == false
|
||||
SettingsActionItem(
|
||||
if (unencrypted) Icons.Outlined.LockOpen else if (useKeyChain) Icons.Filled.VpnKey else Icons.Outlined.Lock,
|
||||
stringResource(R.string.database_passphrase),
|
||||
click = showSettingsModal { DatabaseEncryptionView(it) },
|
||||
iconColor = if (unencrypted) WarningOrange else HighOrLowlight,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SectionDivider()
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.IosShare,
|
||||
stringResource(R.string.export_database),
|
||||
exportArchive,
|
||||
click = {
|
||||
if (initialRandomDBPassphrase.get()) {
|
||||
exportProhibitedAlert()
|
||||
} else {
|
||||
exportArchive()
|
||||
}
|
||||
},
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
@@ -168,14 +190,10 @@ fun DatabaseLayout(
|
||||
)
|
||||
}
|
||||
SectionTextFooter(
|
||||
if (chatDbChanged) {
|
||||
stringResource(R.string.restart_the_app_to_use_new_chat_database)
|
||||
if (stopped) {
|
||||
stringResource(R.string.you_must_use_the_most_recent_version_of_database)
|
||||
} else {
|
||||
if (stopped) {
|
||||
stringResource(R.string.you_must_use_the_most_recent_version_of_database)
|
||||
} else {
|
||||
stringResource(R.string.stop_chat_to_enable_database_actions)
|
||||
}
|
||||
stringResource(R.string.stop_chat_to_enable_database_actions)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -185,7 +203,6 @@ fun DatabaseLayout(
|
||||
fun RunChatSetting(
|
||||
runChat: Boolean,
|
||||
stopped: Boolean,
|
||||
chatDbChanged: Boolean,
|
||||
startChat: () -> Unit,
|
||||
stopChatAlert: () -> Unit
|
||||
) {
|
||||
@@ -195,13 +212,12 @@ fun RunChatSetting(
|
||||
Icon(
|
||||
if (stopped) Icons.Filled.Report else Icons.Filled.PlayArrow,
|
||||
chatRunningText,
|
||||
tint = if (chatDbChanged) HighOrLowlight else if (stopped) Color.Red else MaterialTheme.colors.primary
|
||||
tint = if (stopped) Color.Red else MaterialTheme.colors.primary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
chatRunningText,
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = if (chatDbChanged) HighOrLowlight else Color.Unspecified
|
||||
Modifier.padding(end = 24.dp)
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
@@ -217,7 +233,6 @@ fun RunChatSetting(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
enabled = !chatDbChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -228,16 +243,28 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
||||
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
|
||||
}
|
||||
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, context: Context) {
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
withApi {
|
||||
try {
|
||||
if (chatDbChanged.value) {
|
||||
SimplexApp.context.initChatController()
|
||||
chatDbChanged.value = false
|
||||
}
|
||||
if (m.chatDbStatus.value !is DBMigrationResult.OK) {
|
||||
/** Hide current view and show [DatabaseErrorView] */
|
||||
ModalManager.shared.closeModals()
|
||||
return@withApi
|
||||
}
|
||||
m.controller.apiStartChat()
|
||||
runChat.value = true
|
||||
m.chatRunning.value = true
|
||||
val ts = Clock.System.now()
|
||||
m.controller.appPrefs.chatLastStart.set(ts)
|
||||
chatLastStart.value = ts
|
||||
SimplexService.start(context)
|
||||
when (m.controller.appPrefs.notificationsMode.get()) {
|
||||
NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start(SimplexApp.context) }
|
||||
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
} catch (e: Error) {
|
||||
runChat.value = false
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString())
|
||||
@@ -250,11 +277,42 @@ private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context:
|
||||
title = generalGetString(R.string.stop_chat_question),
|
||||
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
|
||||
confirmText = generalGetString(R.string.stop_chat_confirmation),
|
||||
onConfirm = { stopChat(m, runChat, context) },
|
||||
onConfirm = { authStopChat(m, runChat, context) },
|
||||
onDismiss = { runChat.value = true }
|
||||
)
|
||||
}
|
||||
|
||||
private fun exportProhibitedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.set_password_to_export),
|
||||
text = generalGetString(R.string.set_password_to_export_desc),
|
||||
)
|
||||
}
|
||||
|
||||
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
if (m.controller.appPrefs.performLA.get()) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_stop_chat),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> {
|
||||
stopChat(m, runChat, context)
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
runChat.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
stopChat(m, runChat, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
withApi {
|
||||
try {
|
||||
@@ -262,6 +320,7 @@ private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Cont
|
||||
runChat.value = false
|
||||
m.chatRunning.value = false
|
||||
SimplexService.stop(context)
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
} catch (e: Error) {
|
||||
runChat.value = true
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString())
|
||||
@@ -377,6 +436,7 @@ private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Ur
|
||||
try {
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
|
||||
m.controller.apiImportArchive(config)
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
|
||||
}
|
||||
@@ -429,6 +489,8 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
|
||||
withApi {
|
||||
try {
|
||||
m.controller.apiDeleteStorage()
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))
|
||||
}
|
||||
@@ -458,7 +520,9 @@ fun PreviewDatabaseLayout() {
|
||||
DatabaseLayout(
|
||||
progressIndicator = false,
|
||||
runChat = true,
|
||||
chatDbChanged = false,
|
||||
useKeyChain = false,
|
||||
chatDbEncrypted = false,
|
||||
initialRandomDBPassphrase = Preference({ true }, {}),
|
||||
importArchiveLauncher = rememberGetContentLauncher {},
|
||||
chatArchiveName = remember { mutableStateOf("dummy_archive") },
|
||||
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.util.Log
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.model.json
|
||||
import chat.simplex.app.views.usersettings.Cryptor
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object DatabaseUtils {
|
||||
private val cryptor = Cryptor()
|
||||
|
||||
private val appPreferences: AppPreferences by lazy {
|
||||
AppPreferences(SimplexApp.context)
|
||||
}
|
||||
|
||||
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
|
||||
|
||||
fun hasDatabase(filesDirectory: String): Boolean = File(filesDirectory + File.separator + "files_chat.db").exists()
|
||||
|
||||
fun getDatabaseKey(): String? {
|
||||
return cryptor.decryptData(
|
||||
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
DATABASE_PASSWORD_ALIAS,
|
||||
)
|
||||
}
|
||||
|
||||
fun setDatabaseKey(key: String) {
|
||||
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
|
||||
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun removeDatabaseKey() {
|
||||
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(null)
|
||||
appPreferences.initializationVectorDBPassphrase.set(null)
|
||||
}
|
||||
|
||||
fun migrateChatDatabase(useKey: String? = null): Pair<Boolean, DBMigrationResult> {
|
||||
Log.d(TAG, "migrateChatDatabase ${appPreferences.storeDBPassphrase.get()}")
|
||||
val dbPath = getFilesDirectory(SimplexApp.context)
|
||||
var dbKey = ""
|
||||
val useKeychain = appPreferences.storeDBPassphrase.get()
|
||||
if (useKey != null) {
|
||||
dbKey = useKey
|
||||
} else if (useKeychain) {
|
||||
if (!hasDatabase(dbPath)) {
|
||||
dbKey = randomDatabasePassword()
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
} else {
|
||||
dbKey = getDatabaseKey() ?: ""
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "migrateChatDatabase DB path: $dbPath")
|
||||
val migrated = chatMigrateDB(dbPath, dbKey)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated) }
|
||||
val encrypted = dbKey != ""
|
||||
return encrypted to res
|
||||
}
|
||||
|
||||
private fun randomDatabasePassword(): String = ByteArray(32).apply { SecureRandom().nextBytes(this) }.toBase64String()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class DBMigrationResult {
|
||||
@Serializable @SerialName("ok") object OK: DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
|
||||
}
|
||||
+10
-3
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -51,12 +52,18 @@ class MessagesFetcherWork(
|
||||
return Result.success()
|
||||
}
|
||||
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
|
||||
var shouldReschedule = true
|
||||
try {
|
||||
withTimeout(durationSeconds * 1000L) {
|
||||
val chatController = (applicationContext as SimplexApp).chatController
|
||||
val user = chatController.apiGetActiveUser() ?: return@withTimeout
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
if (chatDbStatus != DBMigrationResult.OK) {
|
||||
Log.w(TAG, "Worker: problem with the database: $chatDbStatus")
|
||||
showPassphraseNotification(chatDbStatus)
|
||||
shouldReschedule = false
|
||||
return@withTimeout
|
||||
}
|
||||
Log.w(TAG, "Worker: starting work")
|
||||
chatController.startChat(user)
|
||||
// Give some time to start receiving messages
|
||||
delay(10_000)
|
||||
while (!isStopped) {
|
||||
@@ -75,7 +82,7 @@ class MessagesFetcherWork(
|
||||
Log.d(TAG, "Worker: unexpected exception: ${e.stackTraceToString()}")
|
||||
}
|
||||
|
||||
reschedule()
|
||||
if (shouldReschedule) reschedule()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
|
||||
@@ -135,9 +135,10 @@ fun <T> SectionItemWithValue(
|
||||
fun SectionTextFooter(text: String) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(horizontal = 16.dp).padding(top = 5.dp).fillMaxWidth(0.9F),
|
||||
Modifier.padding(horizontal = 16.dp).padding(top = 8.dp).fillMaxWidth(0.9F),
|
||||
color = HighOrLowlight,
|
||||
fontSize = 12.sp
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.provider.OpenableColumns
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import android.text.style.*
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.annotation.StringRes
|
||||
@@ -403,3 +404,7 @@ fun removeFile(context: Context, fileName: String): Boolean {
|
||||
}
|
||||
return fileDeleted
|
||||
}
|
||||
|
||||
fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT)
|
||||
|
||||
fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
internal class Cryptor {
|
||||
private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
|
||||
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String {
|
||||
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec)
|
||||
return String(cipher.doFinal(data))
|
||||
}
|
||||
|
||||
fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> {
|
||||
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias))
|
||||
return Pair(cipher.doFinal(text.toByteArray(charset("UTF-8"))), cipher.iv)
|
||||
}
|
||||
|
||||
fun deleteKey(alias: String) {
|
||||
if (!keyStore.containsAlias(alias)) return
|
||||
keyStore.deleteEntry(alias)
|
||||
}
|
||||
|
||||
private fun createSecretKey(alias: String): SecretKey {
|
||||
if (keyStore.containsAlias(alias)) return getSecretKey(alias)
|
||||
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore")
|
||||
keyGenerator.init(
|
||||
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(BLOCK_MODE)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build()
|
||||
)
|
||||
return keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
private fun getSecretKey(alias: String): SecretKey {
|
||||
return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
|
||||
private val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
chatModel.incognito,
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools,
|
||||
@@ -79,6 +80,7 @@ val simplexTeamUri =
|
||||
fun SettingsLayout(
|
||||
profile: LocalProfile,
|
||||
stopped: Boolean,
|
||||
encrypted: Boolean,
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: Preference<Boolean>,
|
||||
developerTools: Preference<Boolean>,
|
||||
@@ -113,7 +115,7 @@ fun SettingsLayout(
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
DatabaseItem(showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
@@ -199,7 +201,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun DatabaseItem(openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
SectionItemView(openDatabaseView) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -207,12 +209,12 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
Icons.Outlined.Archive,
|
||||
contentDescription = stringResource(R.string.database_export_and_import),
|
||||
tint = HighOrLowlight,
|
||||
Icons.Outlined.FolderOpen,
|
||||
contentDescription = stringResource(R.string.database_passphrase_and_export),
|
||||
tint = if (encrypted) HighOrLowlight else WarningOrange,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(stringResource(R.string.database_export_and_import))
|
||||
Text(stringResource(R.string.database_passphrase_and_export))
|
||||
}
|
||||
if (stopped) {
|
||||
Icon(
|
||||
@@ -305,9 +307,9 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, disabled: Boolean = false) {
|
||||
fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, iconColor: Color = HighOrLowlight, disabled: Boolean = false) {
|
||||
SectionItemView(click, disabled = disabled) {
|
||||
Icon(icon, text, tint = HighOrLowlight)
|
||||
Icon(icon, text, tint = iconColor)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(text, color = if (disabled) HighOrLowlight else textColor)
|
||||
}
|
||||
@@ -355,6 +357,7 @@ fun PreviewSettingsLayout() {
|
||||
SettingsLayout(
|
||||
profile = LocalProfile.sampleData,
|
||||
stopped = false,
|
||||
encrypted = false,
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = Preference({ false}, {}),
|
||||
developerTools = Preference({ false }, {}),
|
||||
|
||||
@@ -67,12 +67,21 @@
|
||||
<string name="periodic_notifications">Периодические уведомления</string>
|
||||
<string name="periodic_notifications_disabled">Периодические уведомления выключены!</string>
|
||||
<string name="periodic_notifications_desc">Приложение периодически получает новые сообщения — это потребляет несколько процентов батареи в день. Приложение не использует push уведомления — данные не отправляются с вашего устройства на сервер.</string>
|
||||
<string name="enter_passphrase_notification_title">Введите пароль</string>
|
||||
<string name="enter_passphrase_notification_desc">Для получения уведомлений, пожалуйста, введите пароль от базы данных</string>
|
||||
<string name="database_initialization_error_title">Ошибка базы данных</string>
|
||||
<string name="database_initialization_error_desc">Ошибка при инициализации базы данных. Нажмите чтобы узнать больше</string>
|
||||
|
||||
<!-- SimpleX Chat foreground Service -->
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> сервис</string>
|
||||
<string name="simplex_service_notification_text">Приём сообщений…</string>
|
||||
<string name="hide_notification">Скрыть</string>
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="ntf_channel_messages">SimpleX Chat сообщения</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat звонки</string>
|
||||
<string name="ntf_channel_calls_lockscreen">SimpleX Chat звонки (экран блокировки)</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="settings_notifications_mode_title">Сервис уведомлений</string>
|
||||
<string name="settings_notification_preview_mode_title">Показывать уведомления</string>
|
||||
@@ -112,6 +121,8 @@
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Аутентификация устройства не включена. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</string>
|
||||
<string name="auth_retry">Повторить</string>
|
||||
<string name="auth_stop_chat">Остановить чат</string>
|
||||
<string name="auth_open_chat_console">Открыть консоль</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Ответить</string>
|
||||
@@ -291,7 +302,7 @@
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Настройки</string>
|
||||
<string name="your_simplex_contact_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
|
||||
<string name="database_export_and_import">Экспорт и импорт архива чата</string>
|
||||
<string name="database_passphrase_and_export">Пароль и экспорт базы</string>
|
||||
<string name="about_simplex_chat">Информация о <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="how_to_use_simplex_chat">Как использовать</string>
|
||||
<string name="markdown_help">Форматирование сообщений</string>
|
||||
@@ -512,11 +523,12 @@
|
||||
<string name="settings_section_title_incognito">Режим Инкогнито</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Данные чата</string>
|
||||
<string name="your_chat_database">База данных</string>
|
||||
<string name="run_chat_section">ЗАПУСТИТЬ ЧАТ</string>
|
||||
<string name="chat_is_running">Чат запущен</string>
|
||||
<string name="chat_is_stopped">Чат остановлен</string>
|
||||
<string name="chat_database_section">АРХИВ ЧАТА</string>
|
||||
<string name="chat_database_section">БАЗА ДАННЫХ</string>
|
||||
<string name="database_passphrase">Пароль базы данных</string>
|
||||
<string name="export_database">Экспорт архива чата</string>
|
||||
<string name="import_database">Импорт архива чата</string>
|
||||
<string name="new_database_archive">Новый архив чата</string>
|
||||
@@ -524,8 +536,10 @@
|
||||
<string name="delete_database">Удалить данные чата</string>
|
||||
<string name="error_starting_chat">Ошибка при запуске чата</string>
|
||||
<string name="stop_chat_question">Остановить чат?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Остановите чат, чтобы экспортировать или импортировать архив чата или удалить базу данных. Вы не сможете получать и отправлять сообщения, пока чат остановлен.</string>
|
||||
<string name="stop_chat_confirmation">Остановить</string>
|
||||
<string name="set_password_to_export">Установите пароль</string>
|
||||
<string name="set_password_to_export_desc">База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом.</string>
|
||||
<string name="error_stopping_chat">Ошибка при остановке чата</string>
|
||||
<string name="error_exporting_chat_database">Ошибка при экспорте архива чата</string>
|
||||
<string name="import_database_question">Импортировать архив чата?</string>
|
||||
@@ -541,7 +555,53 @@
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Перезапустите приложение, чтобы создать новый профиль.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Остановите чат, чтобы разблокировать операции с архивом чата.</string>
|
||||
<string name="restart_the_app_to_use_new_chat_database">Перезапустите приложение, чтобы использовать новый архив чата.</string>
|
||||
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
<string name="save_passphrase_in_keychain">Сохранить пароль в Keystore</string>
|
||||
<string name="database_encrypted">База данных зашифрована!</string>
|
||||
<string name="error_encrypting_database">Ошибка при шифровании</string>
|
||||
<string name="remove_passphrase_from_keychain">Удалить пароль из Keystore?</string>
|
||||
<string name="notifications_will_be_hidden">Уведомления будут работать только до остановки приложения!</string>
|
||||
<string name="remove_passphrase">Удалить</string>
|
||||
<string name="encrypt_database">Зашифровать</string>
|
||||
<string name="update_database">Поменять</string>
|
||||
<string name="current_passphrase">Текущий пароль…</string>
|
||||
<string name="new_passphrase">Новый пароль…</string>
|
||||
<string name="confirm_new_passphrase">Подтвердите новый пароль…</string>
|
||||
<string name="update_database_passphrase">Поменять пароль</string>
|
||||
<string name="enter_correct_current_passphrase">Пожалуйста, введите правильный пароль.</string>
|
||||
<string name="database_is_not_encrypted">База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме.</string>
|
||||
<string name="encrypted_with_random_passphrase">База данных зашифрована случайным паролем, вы можете его поменять.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Внимание</b>: вы не сможете восстановить или поменять пароль, если потеряете его.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Пароль базы данных будет безопасно сохранен в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата.</string>
|
||||
<string name="encrypt_database_question">Зашифровать базу данных?</string>
|
||||
<string name="change_database_passphrase_question">Поменять пароль базы данных?</string>
|
||||
<string name="database_will_be_encrypted">База данных будет зашифрована.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">База данных будет зашифрована и пароль сохранен в Keystore.</string>
|
||||
<string name="database_encryption_will_be_updated">Пароль базы данных будет изменен и сохранен в Keystore.</string>
|
||||
<string name="database_passphrase_will_be_updated">Пароль базы данных будет изменен.</string>
|
||||
<string name="store_passphrase_securely">Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его.</string>
|
||||
|
||||
<!-- DatabaseErrorView.kt -->
|
||||
<string name="wrong_passphrase">Неправильный пароль базы данных</string>
|
||||
<string name="encrypted_database">База данных зашифрована</string>
|
||||
<string name="database_error">Ошибка базы данных</string>
|
||||
<string name="keychain_error">Ошибка Keystore</string>
|
||||
<string name="passphrase_is_different">Пароль базы данных отличается от сохраненного в Keystore.</string>
|
||||
<string name="file_with_path">Файл: %s</string>
|
||||
<string name="database_passphrase_is_required">Введите пароль базы данных, чтобы открыть чат.</string>
|
||||
<string name="error_with_info">Ошибка: %s</string>
|
||||
<string name="cannot_access_keychain">Невозможно сохранить пароль в Keystore</string>
|
||||
<string name="unknown_database_error_with_info">Неизвестная ошибка базы данных: %s</string>
|
||||
<string name="wrong_passphrase_title">Неправильный пароль!</string>
|
||||
<string name="enter_correct_passphrase">Введите правильный пароль.</string>
|
||||
<string name="unknown_error">Неизвестная ошибка</string>
|
||||
<string name="enter_passphrase">Введите пароль…</string>
|
||||
<string name="save_passphrase_and_open_chat">Сохранить пароль и открыть чат</string>
|
||||
<string name="open_chat">Открыть чат</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Чат остановлен</string>
|
||||
|
||||
@@ -67,12 +67,21 @@
|
||||
<string name="periodic_notifications">Periodic notifications</string>
|
||||
<string name="periodic_notifications_disabled">Periodic notifications are disabled!</string>
|
||||
<string name="periodic_notifications_desc">The app fetches new messages periodically — it uses a few percent of the battery per day. The app doesn\'t use push notifications — data from your device is not sent to the servers.</string>
|
||||
<string name="enter_passphrase_notification_title">Passphrase is needed</string>
|
||||
<string name="enter_passphrase_notification_desc">To receive notifications, please, enter the database passphrase</string>
|
||||
<string name="database_initialization_error_title">Can\'t initialize the database</string>
|
||||
<string name="database_initialization_error_desc">The database is not working correctly. Tap to learn more</string>
|
||||
|
||||
<!-- SimpleX Chat foreground Service -->
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> service</string>
|
||||
<string name="simplex_service_notification_text">Receiving messages…</string>
|
||||
<string name="hide_notification">Hide</string>
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="ntf_channel_messages">SimpleX Chat messages</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat calls</string>
|
||||
<string name="ntf_channel_calls_lockscreen">SimpleX Chat calls (lock screen)</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="settings_notifications_mode_title">Notification service</string>
|
||||
<string name="settings_notification_preview_mode_title">Show preview</string>
|
||||
@@ -112,6 +121,8 @@
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string>
|
||||
<string name="auth_retry">Retry</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="auth_open_chat_console">Open chat console</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Reply</string>
|
||||
@@ -295,7 +306,7 @@
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Your settings</string>
|
||||
<string name="your_simplex_contact_address">Your <xliff:g id="appName">SimpleX</xliff:g> contact address</string>
|
||||
<string name="database_export_and_import">Database export & import</string>
|
||||
<string name="database_passphrase_and_export">Database passphrase & export</string>
|
||||
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="how_to_use_simplex_chat">How to use it</string>
|
||||
<string name="markdown_help">Markdown help</string>
|
||||
@@ -518,6 +529,7 @@
|
||||
<string name="chat_is_running">Chat is running</string>
|
||||
<string name="chat_is_stopped">Chat is stopped</string>
|
||||
<string name="chat_database_section">CHAT DATABASE</string>
|
||||
<string name="database_passphrase">Database passphrase</string>
|
||||
<string name="export_database">Export database</string>
|
||||
<string name="import_database">Import database</string>
|
||||
<string name="new_database_archive">New database archive</string>
|
||||
@@ -527,6 +539,8 @@
|
||||
<string name="stop_chat_question">Stop chat?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</string>
|
||||
<string name="stop_chat_confirmation">Stop</string>
|
||||
<string name="set_password_to_export">Set passphrase to export</string>
|
||||
<string name="set_password_to_export_desc">Database is encrypted using a random passphrase. Please change it before exporting.</string>
|
||||
<string name="error_stopping_chat">Error stopping chat</string>
|
||||
<string name="error_exporting_chat_database">Error exporting chat database</string>
|
||||
<string name="import_database_question">Import chat database?</string>
|
||||
@@ -542,7 +556,53 @@
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Restart the app to create a new chat profile.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Stop chat to enable database actions.</string>
|
||||
<string name="restart_the_app_to_use_new_chat_database">Restart the app to use new chat database.</string>
|
||||
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
|
||||
<string name="database_encrypted">Database encrypted!</string>
|
||||
<string name="error_encrypting_database">Error encrypting database</string>
|
||||
<string name="remove_passphrase_from_keychain">Remove passphrase from Keystore?</string>
|
||||
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
|
||||
<string name="remove_passphrase">Remove</string>
|
||||
<string name="encrypt_database">Encrypt</string>
|
||||
<string name="update_database">Update</string>
|
||||
<string name="current_passphrase">Current passphrase…</string>
|
||||
<string name="new_passphrase">New passphrase…</string>
|
||||
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
|
||||
<string name="update_database_passphrase">Update database passphrase</string>
|
||||
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
||||
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
||||
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
|
||||
<string name="encrypt_database_question">Encrypt database?</string>
|
||||
<string name="change_database_passphrase_question">Change database passphrase?</string>
|
||||
<string name="database_will_be_encrypted">Database will be encrypted.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Database will be encrypted and the passphrase stored in the Keystore.</string>
|
||||
<string name="database_encryption_will_be_updated">Database encryption passphrase will be updated and stored in the Keystore.</string>
|
||||
<string name="database_passphrase_will_be_updated">Database encryption passphrase will be updated.</string>
|
||||
<string name="store_passphrase_securely">Please store passphrase securely, you will NOT be able to change it if you lose it.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Please store passphrase securely, you will NOT be able to access chat if you lose it.</string>
|
||||
|
||||
<!-- DatabaseErrorView.kt -->
|
||||
<string name="wrong_passphrase">Wrong database passphrase</string>
|
||||
<string name="encrypted_database">Encrypted database</string>
|
||||
<string name="database_error">Database error</string>
|
||||
<string name="keychain_error">Keychain error</string>
|
||||
<string name="passphrase_is_different">Database passphrase is different from saved in the Keystore.</string>
|
||||
<string name="file_with_path">File: %s</string>
|
||||
<string name="database_passphrase_is_required">Database passphrase is required to open chat.</string>
|
||||
<string name="error_with_info">Error: %s</string>
|
||||
<string name="cannot_access_keychain">Cannot access Keystore to save database password</string>
|
||||
<string name="unknown_database_error_with_info">Unknown database error: %s</string>
|
||||
<string name="wrong_passphrase_title">Wrong passphrase!</string>
|
||||
<string name="enter_correct_passphrase">Enter correct passphrase.</string>
|
||||
<string name="unknown_error">Unknown error</string>
|
||||
<string name="enter_passphrase">Enter passphrase…</string>
|
||||
<string name="save_passphrase_and_open_chat">Save passphrase and open chat</string>
|
||||
<string name="open_chat">Open chat</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Chat is stopped</string>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -21,6 +22,8 @@ struct ContentView: View {
|
||||
ZStack {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
} else if let status = chatModel.chatDbStatus, status != .ok {
|
||||
DatabaseErrorView(status: status)
|
||||
} else if !chatModel.v3DBMigration.startChat {
|
||||
MigrateToAppGroupView()
|
||||
} else if let step = chatModel.onboardingStage {
|
||||
|
||||
@@ -18,6 +18,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var currentUser: User?
|
||||
@Published var chatRunning: Bool?
|
||||
@Published var chatDbChanged = false
|
||||
@Published var chatDbEncrypted: Bool?
|
||||
@Published var chatDbStatus: DBMigrationResult?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
@@ -51,6 +53,8 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
func hasChat(_ id: String) -> Bool {
|
||||
chats.first(where: { $0.id == id }) != nil
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd.obfuscated))
|
||||
ChatModel.shared.terminalItems.append(.resp(.now, resp))
|
||||
}
|
||||
return resp
|
||||
@@ -185,6 +185,10 @@ func apiDeleteStorage() async throws {
|
||||
try await sendCommandOkResp(.apiDeleteStorage)
|
||||
}
|
||||
|
||||
func apiStorageEncryption(currentKey: String = "", newKey: String = "") async throws {
|
||||
try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey)))
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats }
|
||||
@@ -654,22 +658,21 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
|
||||
throw r
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool) throws {
|
||||
func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
||||
logger.debug("initializeChat")
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
} else if start {
|
||||
try startChat()
|
||||
} else {
|
||||
m.chatRunning = false
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to initialize chat controller or database: \(responseError(error))")
|
||||
let m = ChatModel.shared
|
||||
(m.chatDbEncrypted, m.chatDbStatus) = migrateChatDatabase(dbKey)
|
||||
if m.chatDbStatus != .ok { return }
|
||||
let _ = getChatCtrl(dbKey)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
} else if start {
|
||||
try startChat()
|
||||
} else {
|
||||
m.chatRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,14 @@ let bgSuspendTimeout: Int = 5 // seconds
|
||||
let terminationTimeout: Int = 3 // seconds
|
||||
|
||||
private func _suspendChat(timeout: Int) {
|
||||
appStateGroupDefault.set(.suspending)
|
||||
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
||||
let endTask = beginBGTask(chatSuspended)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
|
||||
if ChatModel.ok {
|
||||
appStateGroupDefault.set(.suspending)
|
||||
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
||||
let endTask = beginBGTask(chatSuspended)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
|
||||
} else {
|
||||
appStateGroupDefault.set(.suspended)
|
||||
}
|
||||
}
|
||||
|
||||
func suspendChat() {
|
||||
@@ -47,7 +51,7 @@ func terminateChat() {
|
||||
case .suspending:
|
||||
// suspend instantly if already suspending
|
||||
_chatSuspended()
|
||||
apiSuspendChat(timeoutMicroseconds: 0)
|
||||
if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) }
|
||||
case .stopped: ()
|
||||
default:
|
||||
_suspendChat(timeout: terminationTimeout)
|
||||
@@ -74,6 +78,6 @@ private func _chatSuspended() {
|
||||
func activateChat(appState: AppState = .active) {
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
apiActivateChat()
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// DatabaseEncryptionView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 04/09/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
enum DatabaseEncryptionAlert: Identifiable {
|
||||
case keychainRemoveKey
|
||||
case encryptDatabaseSaved
|
||||
case encryptDatabase
|
||||
case changeDatabaseKeySaved
|
||||
case changeDatabaseKey
|
||||
case databaseEncrypted
|
||||
case currentPassphraseError
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .keychainRemoveKey: return "keychainRemoveKey"
|
||||
case .encryptDatabaseSaved: return "encryptDatabaseSaved"
|
||||
case .encryptDatabase: return "encryptDatabase"
|
||||
case .changeDatabaseKeySaved: return "changeDatabaseKeySaved"
|
||||
case .changeDatabaseKey: return "changeDatabaseKey"
|
||||
case .databaseEncrypted: return "databaseEncrypted"
|
||||
case .currentPassphraseError: return "currentPassphraseError"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseEncryptionView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Binding var useKeychain: Bool
|
||||
@State private var alert: DatabaseEncryptionAlert? = nil
|
||||
@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 currentKey = ""
|
||||
@State private var newKey = ""
|
||||
@State private var confirmNewKey = ""
|
||||
@State private var currentKeyShown = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
databaseEncryptionView()
|
||||
if progressIndicator {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func databaseEncryptionView() -> some View {
|
||||
List {
|
||||
Section {
|
||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
||||
.onChange(of: useKeychainToggle) { _ in
|
||||
if useKeychainToggle {
|
||||
setUseKeychain(true)
|
||||
} else if storedKey {
|
||||
alert = .keychainRemoveKey
|
||||
} else {
|
||||
setUseKeychain(false)
|
||||
}
|
||||
}
|
||||
.disabled(initialRandomDBPassphrase)
|
||||
}
|
||||
|
||||
if !initialRandomDBPassphrase && m.chatDbEncrypted == true {
|
||||
DatabaseKeyField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||
}
|
||||
|
||||
DatabaseKeyField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
||||
DatabaseKeyField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
||||
|
||||
settingsRow("lock.rotation") {
|
||||
Button("Update database passphrase") {
|
||||
alert = currentKey == ""
|
||||
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
||||
: (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey)
|
||||
}
|
||||
}
|
||||
.disabled(
|
||||
currentKey == newKey ||
|
||||
newKey != confirmNewKey ||
|
||||
newKey == "" ||
|
||||
!validKey(currentKey) ||
|
||||
!validKey(newKey)
|
||||
)
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if m.chatDbEncrypted == false {
|
||||
Text("Your chat database is not encrypted - set passphrase to encrypt it.")
|
||||
} else if useKeychain {
|
||||
if storedKey {
|
||||
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||
if initialRandomDBPassphrase {
|
||||
Text("Database is encrypted using a random passphrase, you can change it.")
|
||||
} else {
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
}
|
||||
} else {
|
||||
Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.")
|
||||
}
|
||||
} else {
|
||||
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
if m.notificationMode == .instant && m.notificationPreview != .hidden {
|
||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if initialRandomDBPassphrase { currentKey = getDatabaseKey() ?? "" }
|
||||
}
|
||||
.disabled(m.chatRunning != false)
|
||||
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
||||
}
|
||||
|
||||
private func encryptDatabase() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if useKeychain {
|
||||
if setDatabaseKey(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
} else {
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: responseError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetFormAfterEncryption(_ stored: Bool = false) async {
|
||||
await MainActor.run {
|
||||
m.chatDbEncrypted = true
|
||||
initialRandomDBPassphrase = false
|
||||
currentKey = ""
|
||||
newKey = ""
|
||||
confirmNewKey = ""
|
||||
storedKey = stored
|
||||
}
|
||||
}
|
||||
|
||||
private func setUseKeychain(_ value: Bool) {
|
||||
useKeychain = value
|
||||
storeDBPassphraseGroupDefault.set(value)
|
||||
}
|
||||
|
||||
private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert {
|
||||
switch alertItem {
|
||||
case .keychainRemoveKey:
|
||||
return Alert(
|
||||
title: Text("Remove passphrase from keychain?"),
|
||||
message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
if removeDatabaseKey() {
|
||||
setUseKeychain(false)
|
||||
storedKey = false
|
||||
} else {
|
||||
alert = .error(title: "Keychain error", error: "Failed to remove passphrase")
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
withAnimation { useKeychainToggle = true }
|
||||
}
|
||||
)
|
||||
case .encryptDatabaseSaved:
|
||||
return Alert(
|
||||
title: Text("Encrypt database?"),
|
||||
message: Text("Database will be encrypted and the passphrase stored in the keychain.\n") + storeSecurelySaved(),
|
||||
primaryButton: .default(Text("Encrypt")) { encryptDatabase() },
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .encryptDatabase:
|
||||
return Alert(
|
||||
title: Text("Encrypt database?"),
|
||||
message: Text("Database will be encrypted.\n") + storeSecurelyDanger(),
|
||||
primaryButton: .destructive(Text("Encrypt")) { encryptDatabase() },
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .changeDatabaseKeySaved:
|
||||
return Alert(
|
||||
title: Text("Change database passphrase?"),
|
||||
message: Text("Database encryption passphrase will be updated and stored in the keychain.\n") + storeSecurelySaved(),
|
||||
primaryButton: .default(Text("Update")) { encryptDatabase() },
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .changeDatabaseKey:
|
||||
return Alert(
|
||||
title: Text("Change database passphrase?"),
|
||||
message: Text("Database encryption passphrase will be updated.\n") + storeSecurelyDanger(),
|
||||
primaryButton: .destructive(Text("Update")) { encryptDatabase() },
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .databaseEncrypted:
|
||||
return Alert(title: Text("Database encrypted!"))
|
||||
case .currentPassphraseError:
|
||||
return Alert(
|
||||
title: Text("Wrong passphrase!"),
|
||||
message: Text("Please enter correct current passphrase.")
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text("\(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
private func storeSecurelySaved() -> Text {
|
||||
Text("Please store passphrase securely, you will NOT be able to change it if you lose it.")
|
||||
}
|
||||
|
||||
private func storeSecurelyDanger() -> Text {
|
||||
Text("Please store passphrase securely, you will NOT be able to access chat if you lose it.")
|
||||
}
|
||||
|
||||
private func operationEnded(_ dbAlert: DatabaseEncryptionAlert) async {
|
||||
await MainActor.run {
|
||||
m.chatDbChanged = true
|
||||
progressIndicator = false
|
||||
alert = dbAlert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DatabaseKeyField: View {
|
||||
@Binding var key: String
|
||||
var placeholder: LocalizedStringKey
|
||||
var valid: Bool
|
||||
var showStrength = false
|
||||
var onSubmit: () -> Void = {}
|
||||
@State private var showKey = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
let iconColor = valid
|
||||
? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary)
|
||||
: .red
|
||||
Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 22, alignment: .center)
|
||||
.foregroundColor(iconColor)
|
||||
.onTapGesture { showKey = !showKey }
|
||||
textField()
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.submitLabel(.done)
|
||||
.padding(.leading, 36)
|
||||
.onSubmit(onSubmit)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func textField() -> some View {
|
||||
if showKey {
|
||||
TextField(placeholder, text: $key)
|
||||
} else {
|
||||
SecureField(placeholder, text: $key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// based on https://generatepasswords.org/how-to-calculate-entropy/
|
||||
private func passphraseEnthropy(_ s: String) -> Double {
|
||||
var hasDigits = false
|
||||
var hasUppercase = false
|
||||
var hasLowercase = false
|
||||
var hasSymbols = false
|
||||
for c in s {
|
||||
if c.isNumber {
|
||||
hasDigits = true
|
||||
} else if c.isLetter {
|
||||
if c.isUppercase { hasUppercase = true }
|
||||
else { hasLowercase = true }
|
||||
} else if c.isASCII {
|
||||
hasSymbols = true
|
||||
}
|
||||
}
|
||||
let poolSize: Double = (hasDigits ? 10 : 0) + (hasUppercase ? 26 : 0) + (hasLowercase ? 26 : 0) + (hasSymbols ? 32 : 0)
|
||||
return Double(s.count) * log2(poolSize)
|
||||
}
|
||||
|
||||
enum PassphraseStrength {
|
||||
case veryWeak
|
||||
case weak
|
||||
case reasonable
|
||||
case strong
|
||||
|
||||
init(passphrase s: String) {
|
||||
let enthropy = passphraseEnthropy(s)
|
||||
self = enthropy > 60
|
||||
? .strong
|
||||
: enthropy > 45
|
||||
? .reasonable
|
||||
: enthropy > 30
|
||||
? .weak
|
||||
: .veryWeak
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .veryWeak: return .red
|
||||
case .weak: return .orange
|
||||
case .reasonable: return .yellow
|
||||
case .strong: return .green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validKey(_ s: String) -> Bool {
|
||||
for c in s { if c.isWhitespace || !c.isASCII { return false } }
|
||||
return true
|
||||
}
|
||||
|
||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// DatabaseErrorView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 04/09/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct DatabaseErrorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State var status: DBMigrationResult
|
||||
@State private var dbKey = ""
|
||||
@State private var storedDBKey = getDatabaseKey()
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
switch status {
|
||||
case let .errorNotADatabase(dbFile):
|
||||
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
||||
Text("Wrong database passphrase").font(.title)
|
||||
Text("Database passphrase is different from saved in the keychain.")
|
||||
databaseKeyField(onSubmit: saveAndRunChat)
|
||||
saveAndOpenButton()
|
||||
Spacer()
|
||||
Text("File: \(dbFile)")
|
||||
} else {
|
||||
Text("Encrypted database").font(.title)
|
||||
Text("Database passphrase is required to open chat.")
|
||||
if useKeychain {
|
||||
databaseKeyField(onSubmit: saveAndRunChat)
|
||||
saveAndOpenButton()
|
||||
} else {
|
||||
databaseKeyField(onSubmit: runChat)
|
||||
openChatButton()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
case let .error(dbFile, migrationError):
|
||||
Text("Database error")
|
||||
.font(.title)
|
||||
Text("File: \(dbFile)")
|
||||
Text("Error: \(migrationError)")
|
||||
Spacer()
|
||||
case .errorKeychain:
|
||||
Text("Keychain error")
|
||||
.font(.title)
|
||||
Text("Cannot access keychain to save database password")
|
||||
Spacer()
|
||||
case let .unknown(json):
|
||||
Text("Database error")
|
||||
.font(.title)
|
||||
Text("Unknown database error: \(json)")
|
||||
Spacer()
|
||||
case .ok:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
||||
DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
|
||||
}
|
||||
|
||||
private func saveAndOpenButton() -> some View {
|
||||
Button("Save passphrase and open chat") {
|
||||
saveAndRunChat()
|
||||
}
|
||||
}
|
||||
|
||||
private func openChatButton() -> some View {
|
||||
Button("Open chat") {
|
||||
runChat()
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAndRunChat() {
|
||||
if setDatabaseKey(dbKey) {
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
}
|
||||
runChat()
|
||||
}
|
||||
|
||||
private func runChat() {
|
||||
do {
|
||||
try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey)
|
||||
if let s = m.chatDbStatus {
|
||||
status = s
|
||||
let am = AlertManager.shared
|
||||
switch s {
|
||||
case .errorNotADatabase:
|
||||
am.showAlertMsg(
|
||||
title: "Wrong passphrase!",
|
||||
message: "Enter correct passphrase."
|
||||
)
|
||||
case .errorKeychain:
|
||||
am.showAlertMsg(title: "Keychain error")
|
||||
case let .error(_, error):
|
||||
am.showAlert(Alert(
|
||||
title: Text("Database error"),
|
||||
message: Text(error)
|
||||
))
|
||||
case let .unknown(error):
|
||||
am.showAlert(Alert(
|
||||
title: Text("Unknown error"),
|
||||
message: Text(error)
|
||||
))
|
||||
case .ok: ()
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("initializeChat \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseErrorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseErrorView(status: .errorNotADatabase(dbFile: "simplex_v1_chat.db"))
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
enum DatabaseAlert: Identifiable {
|
||||
case stopChat
|
||||
case exportProhibited
|
||||
case importArchive
|
||||
case archiveImported
|
||||
case deleteChat
|
||||
@@ -21,6 +22,7 @@ enum DatabaseAlert: Identifiable {
|
||||
var id: String {
|
||||
switch self {
|
||||
case .stopChat: return "stopChat"
|
||||
case .exportProhibited: return "exportProhibited"
|
||||
case .importArchive: return "importArchive"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .deleteChat: return "deleteChat"
|
||||
@@ -43,6 +45,7 @@ struct DatabaseView: View {
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@State private var dbContainer = dbContainerGroupDefault.get()
|
||||
@State private var legacyDatabase = hasLegacyDatabase()
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -82,18 +85,28 @@ struct DatabaseView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button {
|
||||
exportArchive()
|
||||
let unencrypted = m.chatDbEncrypted == false
|
||||
let color: Color = unencrypted ? .orange : .secondary
|
||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain)
|
||||
.navigationTitle("Database passphrase")
|
||||
} label: {
|
||||
Text("Export database")
|
||||
Text("Database passphrase")
|
||||
}
|
||||
}
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button("Export database") {
|
||||
if initialRandomDBPassphraseGroupDefault.get() {
|
||||
alert = .exportProhibited
|
||||
} else {
|
||||
exportArchive()
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Button(role: .destructive) {
|
||||
Button("Import database", role: .destructive) {
|
||||
showFileImporter = true
|
||||
} label: {
|
||||
Text("Import database")
|
||||
}
|
||||
}
|
||||
if let archiveName = chatArchiveName {
|
||||
@@ -110,10 +123,8 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
settingsRow("trash.slash") {
|
||||
Button(role: .destructive) {
|
||||
Button("Delete database", role: .destructive) {
|
||||
alert = .deleteChat
|
||||
} label: {
|
||||
Text("Delete database")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -130,10 +141,8 @@ struct DatabaseView: View {
|
||||
if case .group = dbContainer, legacyDatabase {
|
||||
Section("Old database") {
|
||||
settingsRow("trash") {
|
||||
Button {
|
||||
Button("Delete old database") {
|
||||
alert = .deleteLegacyDatabase
|
||||
} label: {
|
||||
Text("Delete old database")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +169,17 @@ struct DatabaseView: View {
|
||||
title: Text("Stop chat?"),
|
||||
message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."),
|
||||
primaryButton: .destructive(Text("Stop")) {
|
||||
stopChat()
|
||||
authStopChat()
|
||||
},
|
||||
secondaryButton: .cancel {
|
||||
withAnimation { runChat = true }
|
||||
}
|
||||
)
|
||||
case .exportProhibited:
|
||||
return Alert(
|
||||
title: Text("Set passphrase to export"),
|
||||
message: Text("Database is encrypted using a random passphrase. Please change it before exporting.")
|
||||
)
|
||||
case .importArchive:
|
||||
if let fileURL = importedArchivePath {
|
||||
return Alert(
|
||||
@@ -213,6 +227,20 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func authStopChat() {
|
||||
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
|
||||
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success: stopChat()
|
||||
case .unavailable: stopChat()
|
||||
case .failed: withAnimation { runChat = true }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopChat()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopChat() {
|
||||
Task {
|
||||
do {
|
||||
@@ -254,6 +282,7 @@ struct DatabaseView: View {
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
try await apiImportArchive(config: config)
|
||||
_ = removeDatabaseKey()
|
||||
await operationEnded(.archiveImported)
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
||||
@@ -273,6 +302,8 @@ struct DatabaseView: View {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
_ = removeDatabaseKey()
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
await operationEnded(.chatDeleted)
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
|
||||
|
||||
@@ -17,8 +17,28 @@ struct TerminalView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var composeState: ComposeState = ComposeState()
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
|
||||
var body: some View {
|
||||
if authorized {
|
||||
terminalView()
|
||||
} else {
|
||||
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||
.onAppear(perform: runAuth)
|
||||
}
|
||||
}
|
||||
|
||||
private func runAuth() {
|
||||
authenticate(reason: NSLocalizedString("Open chat console", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success: authorized = true
|
||||
case .unavailable: authorized = true
|
||||
case .failed: authorized = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func terminalView() -> some View {
|
||||
VStack {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
|
||||
@@ -44,7 +44,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
]
|
||||
|
||||
private var indent: CGFloat = 36
|
||||
@@ -92,9 +92,10 @@ struct SettingsView: View {
|
||||
DatabaseView(showSettings: $showSettings)
|
||||
.navigationTitle("Your chat database")
|
||||
} label: {
|
||||
settingsRow("internaldrive") {
|
||||
let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary
|
||||
settingsRow("internaldrive", color: color) {
|
||||
HStack {
|
||||
Text("Database export & import")
|
||||
Text("Database passphrase & export")
|
||||
Spacer()
|
||||
if chatModel.chatRunning == false {
|
||||
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
<array>
|
||||
<string>group.chat.simplex.app</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -127,6 +127,11 @@
|
||||
<target>**Paste received link** or open it in the browser and tap **Open in mobile app**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<target>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</target>
|
||||
@@ -137,6 +142,11 @@
|
||||
<target>**Scan QR code**: to connect to your contact in person or via video call.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
|
||||
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
|
||||
<target>**Warning**: Instant push notifications require passphrase saved in Keychain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>**e2e encrypted** audio call</target>
|
||||
@@ -303,6 +313,16 @@
|
||||
<target>Cancel</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Cannot access keychain to save database password</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
<source>Change database passphrase?</source>
|
||||
<target>Change database passphrase?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat archive" xml:space="preserve">
|
||||
<source>Chat archive</source>
|
||||
<target>Chat archive</target>
|
||||
@@ -346,7 +366,7 @@
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Chats</target>
|
||||
<note>back button to return to chats list</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
@@ -388,6 +408,11 @@
|
||||
<target>Confirm</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>Confirm new passphrase…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>Connect</target>
|
||||
@@ -528,6 +553,11 @@
|
||||
<target>Created on %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Current passphrase…" xml:space="preserve">
|
||||
<source>Current passphrase…</source>
|
||||
<target>Current passphrase…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
|
||||
<source>Currently maximum supported file size is %@.</source>
|
||||
<target>Currently maximum supported file size is %@.</target>
|
||||
@@ -543,9 +573,72 @@
|
||||
<target>Database ID</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database export & import" xml:space="preserve">
|
||||
<source>Database export & import</source>
|
||||
<target>Database export & import</target>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<target>Database encrypted!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve">
|
||||
<source>Database encryption passphrase will be updated and stored in the keychain.
|
||||
</source>
|
||||
<target>Database encryption passphrase will be updated and stored in the keychain.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve">
|
||||
<source>Database encryption passphrase will be updated.
|
||||
</source>
|
||||
<target>Database encryption passphrase will be updated.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<target>Database error</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve">
|
||||
<source>Database is encrypted using a random passphrase, you can change it.</source>
|
||||
<target>Database is encrypted using a random passphrase, you can change it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve">
|
||||
<source>Database is encrypted using a random passphrase. Please change it before exporting.</source>
|
||||
<target>Database is encrypted using a random passphrase. Please change it before exporting.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase" xml:space="preserve">
|
||||
<source>Database passphrase</source>
|
||||
<target>Database passphrase</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase & export" xml:space="preserve">
|
||||
<source>Database passphrase & export</source>
|
||||
<target>Database passphrase & export</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<target>Database passphrase is different from saved in the keychain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<target>Database passphrase is required to open chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve">
|
||||
<source>Database will be encrypted and the passphrase stored in the keychain.
|
||||
</source>
|
||||
<target>Database will be encrypted and the passphrase stored in the keychain.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted. " xml:space="preserve">
|
||||
<source>Database will be encrypted.
|
||||
</source>
|
||||
<target>Database will be encrypted.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be migrated when the app restarts" xml:space="preserve">
|
||||
@@ -743,6 +836,56 @@
|
||||
<target>Enable periodic notifications?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>Encrypt</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt database?" xml:space="preserve">
|
||||
<source>Encrypt database?</source>
|
||||
<target>Encrypt database?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Encrypted database</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message or another event" xml:space="preserve">
|
||||
<source>Encrypted message or another event</source>
|
||||
<target>Encrypted message or another event</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: database error" xml:space="preserve">
|
||||
<source>Encrypted message: database error</source>
|
||||
<target>Encrypted message: database error</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: keychain error" xml:space="preserve">
|
||||
<source>Encrypted message: keychain error</source>
|
||||
<target>Encrypted message: keychain error</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: no passphrase" xml:space="preserve">
|
||||
<source>Encrypted message: no passphrase</source>
|
||||
<target>Encrypted message: no passphrase</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: unexpeсted error" xml:space="preserve">
|
||||
<source>Encrypted message: unexpeсted error</source>
|
||||
<target>Encrypted message: unexpeсted error</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter correct passphrase." xml:space="preserve">
|
||||
<source>Enter correct passphrase.</source>
|
||||
<target>Enter correct passphrase.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>Enter passphrase…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error accessing database file" xml:space="preserve">
|
||||
<source>Error accessing database file</source>
|
||||
<target>Error accessing database file</target>
|
||||
@@ -783,6 +926,11 @@
|
||||
<target>Error enabling notifications</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error encrypting database" xml:space="preserve">
|
||||
<source>Error encrypting database</source>
|
||||
<target>Error encrypting database</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error exporting chat database" xml:space="preserve">
|
||||
<source>Error exporting chat database</source>
|
||||
<target>Error exporting chat database</target>
|
||||
@@ -863,6 +1011,11 @@
|
||||
<target>File will be received when your contact is online, please wait or check later!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File: %@" xml:space="preserve">
|
||||
<source>File: %@</source>
|
||||
<target>File: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
<source>For console</source>
|
||||
<target>For console</target>
|
||||
@@ -1053,6 +1206,13 @@
|
||||
<target>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Instant push notifications will be hidden! " xml:space="preserve">
|
||||
<source>Instant push notifications will be hidden!
|
||||
</source>
|
||||
<target>Instant push notifications will be hidden!
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Instantly" xml:space="preserve">
|
||||
<source>Instantly</source>
|
||||
<target>Instantly</target>
|
||||
@@ -1128,6 +1288,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Joining group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keychain error" xml:space="preserve">
|
||||
<source>Keychain error</source>
|
||||
<target>Keychain error</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Large file!" xml:space="preserve">
|
||||
<source>Large file!</source>
|
||||
<target>Large file!</target>
|
||||
@@ -1278,6 +1443,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>New message</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New passphrase…" xml:space="preserve">
|
||||
<source>New passphrase…</source>
|
||||
<target>New passphrase…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No" xml:space="preserve">
|
||||
<source>No</source>
|
||||
<target>No</target>
|
||||
@@ -1363,6 +1533,16 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Open Settings</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open chat" xml:space="preserve">
|
||||
<source>Open chat</source>
|
||||
<target>Open chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open chat console" xml:space="preserve">
|
||||
<source>Open chat console</source>
|
||||
<target>Open chat console</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<target>Open-source protocol and code – anybody can run the servers.</target>
|
||||
@@ -1423,11 +1603,26 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Please check your network connection and try again.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please enter correct current passphrase." xml:space="preserve">
|
||||
<source>Please enter correct current passphrase.</source>
|
||||
<target>Please enter correct current passphrase.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve">
|
||||
<source>Please restart the app and migrate the database to enable push notifications.</source>
|
||||
<target>Please restart the app and migrate the database to enable push notifications.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve">
|
||||
<source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source>
|
||||
<target>Please store passphrase securely, you will NOT be able to access chat if you lose it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve">
|
||||
<source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source>
|
||||
<target>Please store passphrase securely, you will NOT be able to change it if you lose it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy & security" xml:space="preserve">
|
||||
<source>Privacy & security</source>
|
||||
<target>Privacy & security</target>
|
||||
@@ -1518,6 +1713,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Remove member?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
|
||||
<source>Remove passphrase from keychain?</source>
|
||||
<target>Remove passphrase from keychain?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Reply</target>
|
||||
@@ -1588,6 +1788,16 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Save group profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save passphrase and open chat" xml:space="preserve">
|
||||
<source>Save passphrase and open chat</source>
|
||||
<target>Save passphrase and open chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save passphrase in Keychain" xml:space="preserve">
|
||||
<source>Save passphrase in Keychain</source>
|
||||
<target>Save passphrase in Keychain</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved SMP servers will be removed" xml:space="preserve">
|
||||
<source>Saved SMP servers will be removed</source>
|
||||
<target>Saved SMP servers will be removed</target>
|
||||
@@ -1648,6 +1858,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Set contact name…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>Set passphrase to export</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve">
|
||||
<source>Set timeouts for proxy/VPN</source>
|
||||
<target>Set timeouts for proxy/VPN</target>
|
||||
@@ -1723,6 +1938,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Stop</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop SimpleX" xml:space="preserve">
|
||||
<source>Stop SimpleX</source>
|
||||
<target>Stop SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>Stop chat to enable database actions</target>
|
||||
@@ -1940,6 +2160,16 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Unexpected migration state</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<target>Unknown database error: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown error" xml:space="preserve">
|
||||
<source>Unknown error</source>
|
||||
<target>Unknown error</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve">
|
||||
<source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.
|
||||
To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source>
|
||||
@@ -1957,11 +2187,21 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Unmute</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
<source>Update</source>
|
||||
<target>Update</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update .onion hosts setting?" xml:space="preserve">
|
||||
<source>Update .onion hosts setting?</source>
|
||||
<target>Update .onion hosts setting?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update database passphrase" xml:space="preserve">
|
||||
<source>Update database passphrase</source>
|
||||
<target>Update database passphrase</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update network settings?" xml:space="preserve">
|
||||
<source>Update network settings?</source>
|
||||
<target>Update network settings?</target>
|
||||
@@ -2032,6 +2272,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||
<source>Wrong database passphrase</source>
|
||||
<target>Wrong database passphrase</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong passphrase!" xml:space="preserve">
|
||||
<source>Wrong passphrase!</source>
|
||||
<target>Wrong passphrase!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You" xml:space="preserve">
|
||||
<source>You</source>
|
||||
<target>You</target>
|
||||
@@ -2097,6 +2347,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You could not be verified; please try again.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve">
|
||||
<source>You have to enter passphrase every time the app starts - it is not stored on the device.</source>
|
||||
<target>You have to enter passphrase every time the app starts - it is not stored on the device.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You invited your contact" xml:space="preserve">
|
||||
<source>You invited your contact</source>
|
||||
<target>You invited your contact</target>
|
||||
@@ -2182,6 +2437,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Your chat database</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve">
|
||||
<source>Your chat database is not encrypted - set passphrase to encrypt it.</source>
|
||||
<target>Your chat database is not encrypted - set passphrase to encrypt it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat profile" xml:space="preserve">
|
||||
<source>Your chat profile</source>
|
||||
<target>Your chat profile</target>
|
||||
@@ -2366,7 +2626,7 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>connecting (introduction invitation)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting call…" xml:space="preserve">
|
||||
<trans-unit id="connecting call" xml:space="preserve">
|
||||
<source>connecting call…</source>
|
||||
<target>connecting call…</target>
|
||||
<note>call status</note>
|
||||
@@ -2451,6 +2711,16 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>group profile updated</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve">
|
||||
<source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source>
|
||||
<target>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve">
|
||||
<source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source>
|
||||
<target>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>incognito via contact address link</target>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"~strike~" = "\\~strike~";
|
||||
|
||||
/* call status */
|
||||
"connecting call" = "connecting call…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server…" = "Connecting to server…";
|
||||
|
||||
|
||||
@@ -127,6 +127,11 @@
|
||||
<target>**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**Внимание**: вы не сможете восстановить или поменять пароль, если вы его потеряете.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<target>**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они.</target>
|
||||
@@ -137,6 +142,11 @@
|
||||
<target>**Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
|
||||
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
|
||||
<target>**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>**e2e зашифрованный** аудиозвонок</target>
|
||||
@@ -303,6 +313,16 @@
|
||||
<target>Отменить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Ошибка доступа к Keychain при сохранении пароля</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
<source>Change database passphrase?</source>
|
||||
<target>Поменять пароль базы данных?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat archive" xml:space="preserve">
|
||||
<source>Chat archive</source>
|
||||
<target>Архив чата</target>
|
||||
@@ -346,7 +366,7 @@
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Чаты</target>
|
||||
<note>back button to return to chats list</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
@@ -388,6 +408,11 @@
|
||||
<target>Подтвердить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>Подтвердите новый пароль…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>Соединиться</target>
|
||||
@@ -528,6 +553,11 @@
|
||||
<target>Дата создания %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Current passphrase…" xml:space="preserve">
|
||||
<source>Current passphrase…</source>
|
||||
<target>Текущий пароль…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
|
||||
<source>Currently maximum supported file size is %@.</source>
|
||||
<target>Максимальный размер файла - %@.</target>
|
||||
@@ -543,9 +573,72 @@
|
||||
<target>ID базы данных</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database export & import" xml:space="preserve">
|
||||
<source>Database export & import</source>
|
||||
<target>Экспорт и импорт архива чата</target>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<target>База данных зашифрована!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve">
|
||||
<source>Database encryption passphrase will be updated and stored in the keychain.
|
||||
</source>
|
||||
<target>Пароль базы данных будет изменен и сохранен в Keychain.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve">
|
||||
<source>Database encryption passphrase will be updated.
|
||||
</source>
|
||||
<target>Пароль базы данных будет изменен.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<target>Ошибка базы данных</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve">
|
||||
<source>Database is encrypted using a random passphrase, you can change it.</source>
|
||||
<target>База данных зашифрована случайным паролем, вы можете его поменять.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve">
|
||||
<source>Database is encrypted using a random passphrase. Please change it before exporting.</source>
|
||||
<target>База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase" xml:space="preserve">
|
||||
<source>Database passphrase</source>
|
||||
<target>Пароль базы данных</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase & export" xml:space="preserve">
|
||||
<source>Database passphrase & export</source>
|
||||
<target>Пароль и экспорт базы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<target>Пароль базы данных отличается от сохраненного в Keychain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<target>Введите пароль базы данных чтобы открыть чат.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve">
|
||||
<source>Database will be encrypted and the passphrase stored in the keychain.
|
||||
</source>
|
||||
<target>База данных будет зашифрована и пароль сохранен в Keychain.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted. " xml:space="preserve">
|
||||
<source>Database will be encrypted.
|
||||
</source>
|
||||
<target>База данных будет зашифрована.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be migrated when the app restarts" xml:space="preserve">
|
||||
@@ -743,6 +836,56 @@
|
||||
<target>Включить периодические уведомления?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>Зашифровать</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt database?" xml:space="preserve">
|
||||
<source>Encrypt database?</source>
|
||||
<target>Зашифровать базу данных?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>База данных зашифрована</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message or another event" xml:space="preserve">
|
||||
<source>Encrypted message or another event</source>
|
||||
<target>Зашифрованное сообщение или событие чата</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: database error" xml:space="preserve">
|
||||
<source>Encrypted message: database error</source>
|
||||
<target>Зашифрованное сообщение: ошибка базы данных</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: keychain error" xml:space="preserve">
|
||||
<source>Encrypted message: keychain error</source>
|
||||
<target>Зашифрованное сообщение: ошибка Keychain</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: no passphrase" xml:space="preserve">
|
||||
<source>Encrypted message: no passphrase</source>
|
||||
<target>Зашифрованное сообщение: пароль не сохранен</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: unexpeсted error" xml:space="preserve">
|
||||
<source>Encrypted message: unexpeсted error</source>
|
||||
<target>Зашифрованное сообщение: неожиданная ошибка</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter correct passphrase." xml:space="preserve">
|
||||
<source>Enter correct passphrase.</source>
|
||||
<target>Введите правильный пароль.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>Введите пароль…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error accessing database file" xml:space="preserve">
|
||||
<source>Error accessing database file</source>
|
||||
<target>Ошибка при доступе к данным чата</target>
|
||||
@@ -783,6 +926,11 @@
|
||||
<target>Ошибка при включении уведомлений</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error encrypting database" xml:space="preserve">
|
||||
<source>Error encrypting database</source>
|
||||
<target>Ошибка при шифровании</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error exporting chat database" xml:space="preserve">
|
||||
<source>Error exporting chat database</source>
|
||||
<target>Ошибка при экспорте архива чата</target>
|
||||
@@ -863,6 +1011,11 @@
|
||||
<target>Файл будет принят, когда ваш контакт будет в сети, подождите или проверьте позже!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File: %@" xml:space="preserve">
|
||||
<source>File: %@</source>
|
||||
<target>Файл: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
<source>For console</source>
|
||||
<target>Для консоли</target>
|
||||
@@ -1053,6 +1206,13 @@
|
||||
<target>[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Instant push notifications will be hidden! " xml:space="preserve">
|
||||
<source>Instant push notifications will be hidden!
|
||||
</source>
|
||||
<target>Мгновенные уведомления будут скрыты!
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Instantly" xml:space="preserve">
|
||||
<source>Instantly</source>
|
||||
<target>Мгновенно</target>
|
||||
@@ -1128,6 +1288,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Вступление в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keychain error" xml:space="preserve">
|
||||
<source>Keychain error</source>
|
||||
<target>Ошибка Keychain</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Large file!" xml:space="preserve">
|
||||
<source>Large file!</source>
|
||||
<target>Большой файл!</target>
|
||||
@@ -1278,6 +1443,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Новое сообщение</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New passphrase…" xml:space="preserve">
|
||||
<source>New passphrase…</source>
|
||||
<target>Новый пароль…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No" xml:space="preserve">
|
||||
<source>No</source>
|
||||
<target>Нет</target>
|
||||
@@ -1363,6 +1533,16 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Открыть Настройки</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open chat" xml:space="preserve">
|
||||
<source>Open chat</source>
|
||||
<target>Открыть чат</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open chat console" xml:space="preserve">
|
||||
<source>Open chat console</source>
|
||||
<target>Открыть консоль</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<target>Открытый протокол и код - кто угодно может запустить сервер.</target>
|
||||
@@ -1423,11 +1603,26 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please enter correct current passphrase." xml:space="preserve">
|
||||
<source>Please enter correct current passphrase.</source>
|
||||
<target>Пожалуйста, введите правильный пароль.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve">
|
||||
<source>Please restart the app and migrate the database to enable push notifications.</source>
|
||||
<target>Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve">
|
||||
<source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source>
|
||||
<target>Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve">
|
||||
<source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source>
|
||||
<target>Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy & security" xml:space="preserve">
|
||||
<source>Privacy & security</source>
|
||||
<target>Конфиденциальность</target>
|
||||
@@ -1518,6 +1713,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Удалить члена группы?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
|
||||
<source>Remove passphrase from keychain?</source>
|
||||
<target>Удалить пароль из Keychain?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Ответить</target>
|
||||
@@ -1588,6 +1788,16 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Сохранить профиль группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save passphrase and open chat" xml:space="preserve">
|
||||
<source>Save passphrase and open chat</source>
|
||||
<target>Сохранить пароль и открыть чат</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save passphrase in Keychain" xml:space="preserve">
|
||||
<source>Save passphrase in Keychain</source>
|
||||
<target>Сохранить пароль в Keychain</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved SMP servers will be removed" xml:space="preserve">
|
||||
<source>Saved SMP servers will be removed</source>
|
||||
<target>Сохраненные SMP серверы будут удалены</target>
|
||||
@@ -1648,6 +1858,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Имя контакта…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>Установите пароль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve">
|
||||
<source>Set timeouts for proxy/VPN</source>
|
||||
<target>Установить таймауты для прокси/VPN</target>
|
||||
@@ -1723,6 +1938,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Остановить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop SimpleX" xml:space="preserve">
|
||||
<source>Stop SimpleX</source>
|
||||
<target>Остановить SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>Остановите чат, чтобы разблокировать операции с архивом чата</target>
|
||||
@@ -1940,6 +2160,16 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Неожиданная ошибка при перемещении данных чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<target>Неизвестная ошибка базы данных: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown error" xml:space="preserve">
|
||||
<source>Unknown error</source>
|
||||
<target>Неизвестная ошибка</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve">
|
||||
<source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.
|
||||
To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source>
|
||||
@@ -1957,11 +2187,21 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Уведомлять</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
<source>Update</source>
|
||||
<target>Обновить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update .onion hosts setting?" xml:space="preserve">
|
||||
<source>Update .onion hosts setting?</source>
|
||||
<target>Обновить настройки .onion хостов?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update database passphrase" xml:space="preserve">
|
||||
<source>Update database passphrase</source>
|
||||
<target>Поменять пароль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update network settings?" xml:space="preserve">
|
||||
<source>Update network settings?</source>
|
||||
<target>Обновить настройки сети?</target>
|
||||
@@ -2032,6 +2272,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||
<source>Wrong database passphrase</source>
|
||||
<target>Неправильный пароль базы данных</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong passphrase!" xml:space="preserve">
|
||||
<source>Wrong passphrase!</source>
|
||||
<target>Неправильный пароль!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You" xml:space="preserve">
|
||||
<source>You</source>
|
||||
<target>Вы</target>
|
||||
@@ -2097,6 +2347,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Верификация не удалась; пожалуйста, попробуйте ещё раз.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve">
|
||||
<source>You have to enter passphrase every time the app starts - it is not stored on the device.</source>
|
||||
<target>Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You invited your contact" xml:space="preserve">
|
||||
<source>You invited your contact</source>
|
||||
<target>Вы пригласили ваш контакт</target>
|
||||
@@ -2179,7 +2434,12 @@ To connect, please ask your contact to create another connection link and check
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat database" xml:space="preserve">
|
||||
<source>Your chat database</source>
|
||||
<target>Данные чата</target>
|
||||
<target>База данных</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve">
|
||||
<source>Your chat database is not encrypted - set passphrase to encrypt it.</source>
|
||||
<target>База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat profile" xml:space="preserve">
|
||||
@@ -2366,7 +2626,7 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>соединяется (приглашение по представлению)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting call…" xml:space="preserve">
|
||||
<trans-unit id="connecting call" xml:space="preserve">
|
||||
<source>connecting call…</source>
|
||||
<target>звонок соединяется…</target>
|
||||
<note>call status</note>
|
||||
@@ -2451,6 +2711,16 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>профиль группы обновлен</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve">
|
||||
<source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source>
|
||||
<target>iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve">
|
||||
<source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source>
|
||||
<target>Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>инкогнито через ссылку-контакт</target>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"~strike~" = "\\~strike~";
|
||||
|
||||
/* call status */
|
||||
"connecting call" = "connecting call…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server…" = "Connecting to server…";
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
|
||||
let nonce = ntfData["nonce"] as? String,
|
||||
let encNtfInfo = ntfData["message"] as? String,
|
||||
let _ = startChat() {
|
||||
logger.debug("NotificationService: receiveNtfMessages: chat is started")
|
||||
if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
let dbStatus = startChat() {
|
||||
if case .ok = dbStatus,
|
||||
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
|
||||
if let connEntity = ntfMsgInfo.connEntity {
|
||||
setBestAttemptNtf(createConnectionEventNtf(connEntity))
|
||||
@@ -118,9 +118,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count)
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
||||
}
|
||||
}
|
||||
deliverBestAttemptNtf()
|
||||
@@ -151,20 +153,26 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
func startChat() -> User? {
|
||||
var chatStarted = false
|
||||
|
||||
func startChat() -> DBMigrationResult? {
|
||||
hs_init(0, nil)
|
||||
if chatStarted { return .ok }
|
||||
let (_, dbStatus) = migrateChatDatabase()
|
||||
if dbStatus != .ok { return dbStatus }
|
||||
if let user = apiGetActiveUser() {
|
||||
logger.debug("active user \(String(describing: user))")
|
||||
do {
|
||||
try setNetworkConfig(getNetCfg())
|
||||
let justStarted = try apiStartChat()
|
||||
chatStarted = true
|
||||
if justStarted {
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
Task { await receiveMessages() }
|
||||
}
|
||||
return user
|
||||
return .ok
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
}
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>group.chat.simplex.app</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"calling…" = "входящий звонок…";
|
||||
|
||||
/* call status */
|
||||
"connecting call…" = "звонок соединяется…";
|
||||
"connecting call" = "звонок соединяется…";
|
||||
|
||||
/* chat list item title */
|
||||
"connecting…" = "соединяется…";
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
||||
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
|
||||
5C00166A28C119300094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166528C119300094D739 /* libgmp.a */; };
|
||||
5C00166B28C119300094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166628C119300094D739 /* libffi.a */; };
|
||||
5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166728C119300094D739 /* libgmpxx.a */; };
|
||||
5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */; };
|
||||
5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */; };
|
||||
5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */; };
|
||||
5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167028C28A6B0094D739 /* libgmpxx.a */; };
|
||||
5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167128C28A6B0094D739 /* libgmp.a */; };
|
||||
5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167228C28A6B0094D739 /* libffi.a */; };
|
||||
5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */; };
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
||||
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
||||
@@ -61,6 +62,8 @@
|
||||
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */; };
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */; };
|
||||
5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */; };
|
||||
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */; };
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||
@@ -197,11 +200,12 @@
|
||||
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
|
||||
5C00166528C119300094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C00166628C119300094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C00166728C119300094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a"; sourceTree = "<group>"; };
|
||||
5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a"; sourceTree = "<group>"; };
|
||||
5C00167028C28A6B0094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C00167128C28A6B0094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C00167228C28A6B0094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
|
||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||
@@ -247,6 +251,9 @@
|
||||
5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = "<group>"; };
|
||||
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedNetworkSettings.swift; sourceTree = "<group>"; };
|
||||
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = "<group>"; };
|
||||
5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = "<group>"; };
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9CC7B128D1F8F400BEF955 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
@@ -351,13 +358,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */,
|
||||
5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */,
|
||||
5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */,
|
||||
5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */,
|
||||
5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */,
|
||||
5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */,
|
||||
5C00166A28C119300094D739 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C00166B28C119300094D739 /* libffi.a in Frameworks */,
|
||||
5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -412,11 +419,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C00166628C119300094D739 /* libffi.a */,
|
||||
5C00166528C119300094D739 /* libgmp.a */,
|
||||
5C00166728C119300094D739 /* libgmpxx.a */,
|
||||
5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */,
|
||||
5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */,
|
||||
5C00167228C28A6B0094D739 /* libffi.a */,
|
||||
5C00167128C28A6B0094D739 /* libgmp.a */,
|
||||
5C00167028C28A6B0094D739 /* libgmpxx.a */,
|
||||
5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */,
|
||||
5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -596,6 +603,7 @@
|
||||
5CDCAD7D2818941F00503DA2 /* API.swift */,
|
||||
5CDCAD80281A7E2700503DA2 /* Notifications.swift */,
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */,
|
||||
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */,
|
||||
5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
|
||||
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */,
|
||||
@@ -642,6 +650,8 @@
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */,
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */,
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */,
|
||||
5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */,
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
@@ -858,6 +868,7 @@
|
||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
|
||||
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||
@@ -934,6 +945,7 @@
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
|
||||
5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */,
|
||||
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
|
||||
@@ -962,6 +974,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */,
|
||||
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
|
||||
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
|
||||
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
|
||||
@@ -1014,6 +1027,7 @@
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
5CB0BA8A2826CB3A00B3292C /* ru */,
|
||||
5C9CC7B128D1F8F400BEF955 /* en */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
|
||||
@@ -10,16 +10,46 @@ import Foundation
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
public func getChatCtrl() -> chat_ctrl {
|
||||
public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl {
|
||||
if let controller = chatController { return controller }
|
||||
let dbPath = getAppDatabasePath().path
|
||||
let dbKey = useKey ?? getDatabaseKey() ?? ""
|
||||
logger.debug("getChatCtrl DB path: \(dbPath)")
|
||||
var cstr = dbPath.cString(using: .utf8)!
|
||||
chatController = chat_init(&cstr)
|
||||
logger.debug("getChatCtrl: chat_init")
|
||||
var cPath = dbPath.cString(using: .utf8)!
|
||||
var cKey = dbKey.cString(using: .utf8)!
|
||||
chatController = chat_init_key(&cPath, &cKey)
|
||||
logger.debug("getChatCtrl: chat_init_key")
|
||||
return chatController!
|
||||
}
|
||||
|
||||
public func migrateChatDatabase(_ useKey: String? = nil) -> (Bool, DBMigrationResult) {
|
||||
logger.debug("migrateChatDatabase \(storeDBPassphraseGroupDefault.get())")
|
||||
let dbPath = getAppDatabasePath().path
|
||||
var dbKey = ""
|
||||
let useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
if let key = useKey {
|
||||
dbKey = key
|
||||
} else if useKeychain {
|
||||
if !hasDatabase() {
|
||||
dbKey = randomDatabasePassword()
|
||||
initialRandomDBPassphraseGroupDefault.set(true)
|
||||
} else if let key = getDatabaseKey() {
|
||||
dbKey = key
|
||||
}
|
||||
}
|
||||
logger.debug("migrateChatDatabase DB path: \(dbPath)")
|
||||
// logger.debug("migrateChatDatabase DB key: \(dbKey)")
|
||||
var cPath = dbPath.cString(using: .utf8)!
|
||||
var cKey = dbKey.cString(using: .utf8)!
|
||||
let cjson = chat_migrate_db(&cPath, &cKey)!
|
||||
let res = dbMigrationResult(fromCString(cjson))
|
||||
let encrypted = dbKey != ""
|
||||
if case .ok = res, useKeychain && encrypted && !setDatabaseKey(dbKey) {
|
||||
return (encrypted, .errorKeychain)
|
||||
}
|
||||
return (encrypted, res)
|
||||
}
|
||||
|
||||
public func resetChatCtrl() {
|
||||
chatController = nil
|
||||
}
|
||||
@@ -103,3 +133,24 @@ public func responseError(_ err: Error) -> String {
|
||||
return err.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
public enum DBMigrationResult: Decodable, Equatable {
|
||||
case ok
|
||||
case errorNotADatabase(dbFile: String)
|
||||
case error(dbFile: String, migrationError: String)
|
||||
case errorKeychain
|
||||
case unknown(json: String)
|
||||
}
|
||||
|
||||
func dbMigrationResult(_ s: String) -> DBMigrationResult {
|
||||
let d = s.data(using: .utf8)!
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
do {
|
||||
return try jsonDecoder.decode(DBMigrationResult.self, from: d)
|
||||
} catch let error {
|
||||
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
return .unknown(json: s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public enum ChatCommand {
|
||||
case apiExportArchive(config: ArchiveConfig)
|
||||
case apiImportArchive(config: ArchiveConfig)
|
||||
case apiDeleteStorage
|
||||
case apiStorageEncryption(config: DBEncryptionConfig)
|
||||
case apiGetChats
|
||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
|
||||
@@ -88,6 +89,7 @@ public enum ChatCommand {
|
||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||
case .apiDeleteStorage: return "/_db delete"
|
||||
case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))"
|
||||
case .apiGetChats: return "/_get chats pcc=on"
|
||||
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
||||
(search == "" ? "" : " search=\(search)")
|
||||
@@ -156,6 +158,7 @@ public enum ChatCommand {
|
||||
case .apiExportArchive: return "apiExportArchive"
|
||||
case .apiImportArchive: return "apiImportArchive"
|
||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||
case .apiStorageEncryption: return "apiStorageEncryption"
|
||||
case .apiGetChats: return "apiGetChats"
|
||||
case .apiGetChat: return "apiGetChat"
|
||||
case .apiSendMessage: return "apiSendMessage"
|
||||
@@ -214,6 +217,18 @@ public enum ChatCommand {
|
||||
func smpServersStr(smpServers: [String]) -> String {
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
|
||||
}
|
||||
|
||||
public var obfuscated: ChatCommand {
|
||||
switch self {
|
||||
case let .apiStorageEncryption(cfg):
|
||||
return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey)))
|
||||
default: return self
|
||||
}
|
||||
}
|
||||
|
||||
private func obfuscate(_ s: String) -> String {
|
||||
s == "" ? "" : "***"
|
||||
}
|
||||
}
|
||||
|
||||
struct APIResponse: Decodable {
|
||||
@@ -527,6 +542,16 @@ public struct ArchiveConfig: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct DBEncryptionConfig: Encodable {
|
||||
public init(currentKey: String, newKey: String) {
|
||||
self.currentKey = currentKey
|
||||
self.newKey = newKey
|
||||
}
|
||||
|
||||
public var currentKey: String
|
||||
public var newKey: String
|
||||
}
|
||||
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public var socksProxy: String? = nil
|
||||
public var hostMode: HostMode = .publicHost
|
||||
@@ -710,6 +735,7 @@ public enum ChatError: Decodable {
|
||||
case error(errorType: ChatErrorType)
|
||||
case errorAgent(agentError: AgentErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
case errorDatabase(databaseError: DatabaseError)
|
||||
}
|
||||
|
||||
public enum ChatErrorType: Decodable {
|
||||
@@ -779,6 +805,19 @@ public enum StoreError: Decodable {
|
||||
case chatItemNotFoundByFileId(fileId: Int64)
|
||||
}
|
||||
|
||||
public enum DatabaseError: Decodable {
|
||||
case errorEncrypted
|
||||
case errorPlaintext
|
||||
case errorNoFile(dbFile: String)
|
||||
case errorExport(sqliteError: SQLiteError)
|
||||
case errorOpen(sqliteError: SQLiteError)
|
||||
}
|
||||
|
||||
public enum SQLiteError: Decodable {
|
||||
case errorNotADatabase
|
||||
case error(String)
|
||||
}
|
||||
|
||||
public enum AgentErrorType: Decodable {
|
||||
case CMD(cmdErr: CommandErrorType)
|
||||
case CONN(connErr: ConnectionErrorType)
|
||||
|
||||
@@ -24,6 +24,8 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
|
||||
let GROUP_DEFAULT_INCOGNITO = "incognito"
|
||||
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
|
||||
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
||||
|
||||
let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||
|
||||
@@ -39,7 +41,9 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle,
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl,
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt,
|
||||
GROUP_DEFAULT_INCOGNITO: false
|
||||
GROUP_DEFAULT_INCOGNITO: false,
|
||||
GROUP_DEFAULT_STORE_DB_PASSPHRASE: true,
|
||||
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false
|
||||
])
|
||||
}
|
||||
|
||||
@@ -96,6 +100,10 @@ public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
withDefault: .no
|
||||
)
|
||||
|
||||
public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE)
|
||||
|
||||
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
|
||||
|
||||
public class DateDefault {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
|
||||
@@ -1355,7 +1355,7 @@ public enum CICallStatus: String, Decodable {
|
||||
case .missed: return NSLocalizedString("missed call", comment: "call status")
|
||||
case .rejected: return NSLocalizedString("rejected call", comment: "call status")
|
||||
case .accepted: return NSLocalizedString("accepted call", comment: "call status")
|
||||
case .negotiated: return NSLocalizedString("connecting call…", comment: "call status")
|
||||
case .negotiated: return NSLocalizedString("connecting call", comment: "call status")
|
||||
case .progress: return NSLocalizedString("call in progress", comment: "call status")
|
||||
case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended call %@", comment: "call status"), CICallStatus.durationText(sec))
|
||||
case .error: return NSLocalizedString("call error", comment: "call status")
|
||||
|
||||
@@ -42,11 +42,17 @@ public func getAppDatabasePath() -> URL {
|
||||
dbContainerGroupDefault.get() == .group
|
||||
? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false)
|
||||
: getLegacyDatabasePath()
|
||||
// getLegacyDatabasePath()
|
||||
}
|
||||
|
||||
public func hasLegacyDatabase() -> Bool {
|
||||
let dbPath = getLegacyDatabasePath()
|
||||
hasDatabaseAtPath(getLegacyDatabasePath())
|
||||
}
|
||||
|
||||
public func hasDatabase() -> Bool {
|
||||
hasDatabaseAtPath(getAppDatabasePath())
|
||||
}
|
||||
|
||||
func hasDatabaseAtPath(_ dbPath: URL) -> Bool {
|
||||
let fm = FileManager.default
|
||||
return fm.isReadableFile(atPath: dbPath.path + "_agent.db") &&
|
||||
fm.isReadableFile(atPath: dbPath.path + "_chat.db")
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// KeyChain.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 04/09/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app"
|
||||
private let DATABASE_PASSWORD_ITEM: String = "databasePassword"
|
||||
|
||||
public func getDatabaseKey() -> String? {
|
||||
getItemString(forKey: DATABASE_PASSWORD_ITEM)
|
||||
}
|
||||
|
||||
public func setDatabaseKey(_ key: String) -> Bool {
|
||||
setItemString(key, forKey: DATABASE_PASSWORD_ITEM)
|
||||
}
|
||||
|
||||
public func removeDatabaseKey() -> Bool {
|
||||
deleteItem(forKey: DATABASE_PASSWORD_ITEM)
|
||||
}
|
||||
|
||||
func randomDatabasePassword() -> String {
|
||||
var keyData = Data(count: 32)
|
||||
let status = keyData.withUnsafeMutableBytes {
|
||||
SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
|
||||
}
|
||||
if status == errSecSuccess {
|
||||
return keyData.base64EncodedString()
|
||||
} else {
|
||||
logger.error("randomDatabasePassword: error \(status)")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private func getItemData(forKey key: String) -> Data? {
|
||||
var query = baseItemQuery(forKey: key)
|
||||
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||
query[kSecReturnData] = true as AnyObject?
|
||||
|
||||
var dataRef: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &dataRef)
|
||||
if status != errSecSuccess && status != errSecItemNotFound {
|
||||
logger.error("getItemData: error getting data for key '\(key)', error: \(status)")
|
||||
}
|
||||
return dataRef as? Data
|
||||
}
|
||||
|
||||
private func getItemString(forKey key: String) -> String? {
|
||||
if let data = getItemData(forKey: key) {
|
||||
return NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func setItemData(_ data: Data, forKey key: String) -> Bool {
|
||||
var query = baseItemQuery(forKey: key)
|
||||
var update = [NSString : AnyObject]()
|
||||
update[kSecValueData] = data as AnyObject?
|
||||
update[kSecAttrAccessible] = ACCESS_POLICY
|
||||
var status: OSStatus
|
||||
if getItemData(forKey: key) == nil {
|
||||
for (key, value) in update { query[key] = value }
|
||||
status = SecItemAdd(query as CFDictionary, nil)
|
||||
} else {
|
||||
status = SecItemUpdate(query as CFDictionary, update as CFDictionary)
|
||||
}
|
||||
if status != errSecSuccess {
|
||||
logger.error("setItemData: error setting data for key '\(key)', error: \(status)")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func setItemString(_ s: String, forKey key: String) -> Bool {
|
||||
if let data = s.data(using: .utf8) {
|
||||
return setItemData(data, forKey: key)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func deleteItem(forKey key: String) -> Bool {
|
||||
let query = baseItemQuery(forKey: key)
|
||||
if getItemData(forKey: key) != nil {
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
if status != errSecSuccess {
|
||||
logger.error("deleteItem: error deleting data for key '\(key)', error: \(status)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func baseItemQuery(forKey key: String) -> [NSString : AnyObject] {
|
||||
var query = [NSString : AnyObject]()
|
||||
query[kSecClass] = kSecClassGenericPassword
|
||||
query[kSecAttrAccount] = key as AnyObject?
|
||||
#if TARGET_OS_IOS && !TARGET_OS_SIMULATOR
|
||||
query[kSecAttrAccessGroup] = ACCESS_GROUP
|
||||
#endif
|
||||
return query
|
||||
}
|
||||
@@ -119,6 +119,26 @@ public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutabl
|
||||
)
|
||||
}
|
||||
|
||||
public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent {
|
||||
var title: String
|
||||
switch dbStatus {
|
||||
case .errorNotADatabase:
|
||||
title = NSLocalizedString("Encrypted message: no passphrase", comment: "notification")
|
||||
case .error:
|
||||
title = NSLocalizedString("Encrypted message: database error", comment: "notification")
|
||||
case .errorKeychain:
|
||||
title = NSLocalizedString("Encrypted message: keychain error", comment: "notification")
|
||||
case .unknown:
|
||||
title = NSLocalizedString("Encrypted message: unexpeсted error", comment: "notification")
|
||||
case .ok:
|
||||
title = NSLocalizedString("Encrypted message or another event", comment: "notification")
|
||||
}
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||
title: title
|
||||
)
|
||||
}
|
||||
|
||||
private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember, hideContent: Bool) -> String {
|
||||
hideContent
|
||||
? NSLocalizedString("Group message:", comment: "notification")
|
||||
|
||||
@@ -15,7 +15,8 @@ extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
extern chat_ctrl chat_init(char *path);
|
||||
extern char *chat_migrate_db(char *path, char *key);
|
||||
extern chat_ctrl chat_init_key(char *path, char *key);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"~strike~" = "\\~strike~";
|
||||
|
||||
/* call status */
|
||||
"connecting call" = "connecting call…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server…" = "Connecting to server…";
|
||||
|
||||
|
||||
@@ -52,12 +52,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Внимание**: вы не сможете восстановить или поменять пароль, если вы его потеряете.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Scan QR code**: to connect to your contact in person or via video call." = "**Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*жирный*";
|
||||
|
||||
@@ -218,6 +224,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Отменить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change database passphrase?" = "Поменять пароль базы данных?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat archive" = "Архив чата";
|
||||
|
||||
@@ -275,6 +287,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Confirm" = "Подтвердить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Confirm new passphrase…" = "Подтвердите новый пароль…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect" = "Соединиться";
|
||||
|
||||
@@ -315,7 +330,7 @@
|
||||
"connecting (introduction invitation)" = "соединяется (приглашение по представлению)";
|
||||
|
||||
/* call status */
|
||||
"connecting call…" = "звонок соединяется…";
|
||||
"connecting call" = "звонок соединяется…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server…" = "Устанавливается соединение с сервером…";
|
||||
@@ -401,6 +416,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"creator" = "создатель";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Current passphrase…" = "Текущий пароль…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Currently maximum supported file size is %@." = "Максимальный размер файла - %@.";
|
||||
|
||||
@@ -408,11 +426,44 @@
|
||||
"Dark" = "Тёмная";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database export & import" = "Экспорт и импорт архива чата";
|
||||
"Database encrypted!" = "База данных зашифрована!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database encryption passphrase will be updated and stored in the keychain.\n" = "Пароль базы данных будет изменен и сохранен в Keychain.\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database encryption passphrase will be updated.\n" = "Пароль базы данных будет изменен.\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database error" = "Ошибка базы данных";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database ID" = "ID базы данных";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database is encrypted using a random passphrase, you can change it." = "База данных зашифрована случайным паролем, вы можете его поменять.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database is encrypted using a random passphrase. Please change it before exporting." = "База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase" = "Пароль базы данных";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase & export" = "Пароль и экспорт базы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохраненного в Keychain.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is required to open chat." = "Введите пароль базы данных чтобы открыть чат.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database will be encrypted and the passphrase stored in the keychain.\n" = "База данных будет зашифрована и пароль сохранен в Keychain.\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database will be encrypted.\n" = "База данных будет зашифрована.\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database will be migrated when the app restarts" = "Данные чата будут мигрированы при перезапуске";
|
||||
|
||||
@@ -545,12 +596,42 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Enable TCP keep-alive" = "Включить TCP keep-alive";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Encrypt" = "Зашифровать";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Encrypt database?" = "Зашифровать базу данных?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Encrypted database" = "База данных зашифрована";
|
||||
|
||||
/* notification */
|
||||
"Encrypted message or another event" = "Зашифрованное сообщение или событие чата";
|
||||
|
||||
/* notification */
|
||||
"Encrypted message: database error" = "Зашифрованное сообщение: ошибка базы данных";
|
||||
|
||||
/* notification */
|
||||
"Encrypted message: keychain error" = "Зашифрованное сообщение: ошибка Keychain";
|
||||
|
||||
/* notification */
|
||||
"Encrypted message: no passphrase" = "Зашифрованное сообщение: пароль не сохранен";
|
||||
|
||||
/* notification */
|
||||
"Encrypted message: unexpeсted error" = "Зашифрованное сообщение: неожиданная ошибка";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"ended" = "завершён";
|
||||
|
||||
/* call status */
|
||||
"ended call %@" = "завершённый звонок %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Enter correct passphrase." = "Введите правильный пароль.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Enter passphrase…" = "Введите пароль…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"error" = "ошибка";
|
||||
|
||||
@@ -578,6 +659,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error enabling notifications" = "Ошибка при включении уведомлений";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error encrypting database" = "Ошибка при шифровании";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error exporting chat database" = "Ошибка при экспорте архива чата";
|
||||
|
||||
@@ -626,6 +710,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"File will be received when your contact is online, please wait or check later!" = "Файл будет принят, когда ваш контакт будет в сети, подождите или проверьте позже!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"File: %@" = "Файл: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"For console" = "Для консоли";
|
||||
|
||||
@@ -755,6 +842,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Instantly" = "Мгновенно";
|
||||
|
||||
@@ -782,6 +872,12 @@
|
||||
/* chat list item title */
|
||||
"invited to connect" = "приглашение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.";
|
||||
|
||||
@@ -812,6 +908,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Joining group" = "Вступление в группу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keychain error" = "Ошибка Keychain";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Large file!" = "Большой файл!";
|
||||
|
||||
@@ -920,6 +1019,9 @@
|
||||
/* notification */
|
||||
"New message" = "Новое сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New passphrase…" = "Новый пароль…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No" = "Нет";
|
||||
|
||||
@@ -971,6 +1073,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open chat" = "Открыть чат";
|
||||
|
||||
/* authentication reason */
|
||||
"Open chat console" = "Открыть консоль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open Settings" = "Открыть Настройки";
|
||||
|
||||
@@ -1019,9 +1127,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Please check your network connection and try again." = "Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please enter correct current passphrase." = "Пожалуйста, введите правильный пароль.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please restart the app and migrate the database to enable push notifications." = "Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Конфиденциальность";
|
||||
|
||||
@@ -1085,6 +1202,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Remove member?" = "Удалить члена группы?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Remove passphrase from keychain?" = "Удалить пароль из Keychain?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"removed" = "удален(а)";
|
||||
|
||||
@@ -1130,6 +1250,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Save group profile" = "Сохранить профиль группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save passphrase and open chat" = "Сохранить пароль и открыть чат";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save passphrase in Keychain" = "Сохранить пароль в Keychain";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Saved SMP servers will be removed" = "Сохраненные SMP серверы будут удалены";
|
||||
|
||||
@@ -1172,6 +1298,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Set contact name…" = "Имя контакта…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Set passphrase to export" = "Установите пароль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN";
|
||||
|
||||
@@ -1235,6 +1364,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Stop chat?" = "Остановить чат?";
|
||||
|
||||
/* authentication reason */
|
||||
"Stop SimpleX" = "Остановить SimpleX";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"strike" = "зачеркнуть";
|
||||
|
||||
@@ -1364,6 +1496,12 @@
|
||||
/* connection info */
|
||||
"unknown" = "неизвестно";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unknown database error: %@" = "Неизвестная ошибка базы данных: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unknown error" = "Неизвестная ошибка";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью.";
|
||||
|
||||
@@ -1373,9 +1511,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Unmute" = "Уведомлять";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update" = "Обновить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update .onion hosts setting?" = "Обновить настройки .onion хостов?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update database passphrase" = "Поменять пароль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update network settings?" = "Обновить настройки сети?";
|
||||
|
||||
@@ -1445,6 +1589,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wrong database passphrase" = "Неправильный пароль базы данных";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wrong passphrase!" = "Неправильный пароль!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You" = "Вы";
|
||||
|
||||
@@ -1487,6 +1637,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You invited your contact" = "Вы пригласили ваш контакт";
|
||||
|
||||
@@ -1545,7 +1698,10 @@
|
||||
"Your chat address" = "Ваш SimpleX адрес";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat database" = "Данные чата";
|
||||
"Your chat database" = "База данных";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat database is not encrypted - set passphrase to encrypt it." = "База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile" = "Ваш профиль";
|
||||
|
||||
+13
-1
@@ -1,11 +1,23 @@
|
||||
packages: .
|
||||
-- packages: . ../simplexmq
|
||||
-- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple
|
||||
|
||||
constraints: zip +disable-bzip2 +disable-zstd
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: e328ae5d060645a8ef090b1b3d88bc20a5902e45
|
||||
tag: f08d81252deb68eb4a1ce4502712b99264b6749d
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/direct-sqlcipher.git
|
||||
tag: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: 5e154a2aeccc33ead6c243ec07195ab673137221
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Contributing guide
|
||||
|
||||
## Compiling with SQLCipher encryption enabled
|
||||
|
||||
Add `cabal.project.local` to project root with the location of OpenSSL headers and libraries and flag setting encryption mode:
|
||||
|
||||
```
|
||||
ignore-project: False
|
||||
|
||||
package direct-sqlcipher
|
||||
extra-include-dirs: /opt/homebrew/opt/openssl@3/include
|
||||
extra-lib-dirs: /opt/homebrew/opt/openssl@3/lib
|
||||
flags: +openssl
|
||||
```
|
||||
|
||||
OpenSSL can be installed with `brew install openssl`
|
||||
@@ -0,0 +1,25 @@
|
||||
# Database encryption
|
||||
|
||||
## Approach
|
||||
|
||||
Using SQLCipher - it is a drop in replacement for SQLite that works for non-encrypted databases without any changes (TODO test on iOS/Android).
|
||||
|
||||
`direct-sqlite` and `sqlite-simple` libraries are forked and renamed to `direct-sqlcipher` and `sqlcipher-simple`, with replaced cbits in `direct-sqlcipher` (TODO include SQLCipher as git submodule with a script to upgrade cbits).
|
||||
|
||||
While SQLCipher provides additional C functions to set and change database key, they do not necessarily need to be exported as they are available as PRAGMAs.
|
||||
|
||||
Moving from plaintext to encrypted database (and back) requires migration process using [sqlcipher_export() function](https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/868).
|
||||
|
||||
The approach would be similar to database migration for the notifications:
|
||||
|
||||
1. the current users will be offered to migrate to encrypted database once, with a notice that it can be done later via settings.
|
||||
2. the new users will be asked to enter a pass-phrase to create a new database (it can be empty, in which case the database won't be encrypted).
|
||||
3. during the migration the database backup will be created and the old database files will be preserved - in case of the app failing to open the new database right after the migration it should revert to using the previous database.
|
||||
|
||||
When opening the database the key must be passed via chat command / agent configuration, some test query must be performed to check that the key is correct: https://www.zetetic.net/sqlcipher/sqlcipher-api/#PRAGMA_key
|
||||
|
||||
Options to support in chat settings:
|
||||
|
||||
- encrypt database (with automatic rollback in case of failure)
|
||||
- decrypt database (-"-)
|
||||
- change key (using [PRAGMA rekey](https://www.zetetic.net/sqlcipher/sqlcipher-api/#rekey))
|
||||
@@ -25,7 +25,7 @@
|
||||
};
|
||||
sha256map = import ./scripts/nix/sha256map.nix;
|
||||
modules = [{
|
||||
packages.direct-sqlite.patches = [ ./scripts/nix/direct-sqlite-2.3.26.patch ];
|
||||
packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ];
|
||||
packages.entropy.patches = [ ./scripts/nix/entropy.patch ];
|
||||
}
|
||||
({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) {
|
||||
@@ -118,13 +118,25 @@
|
||||
> $out/nix-support/hydra-build-products
|
||||
'';
|
||||
};
|
||||
"aarch64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override {
|
||||
"aarch64-android:lib:simplex-chat" = (drv' {
|
||||
pkgs' = androidPkgs;
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.openssl = true;
|
||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
||||
(androidPkgs.openssl.override { static = true; })
|
||||
];
|
||||
packages.direct-sqlcipher.patches = [
|
||||
./scripts/nix/direct-sqlcipher-android-log.patch
|
||||
];
|
||||
}];
|
||||
}).simplex-chat.components.library.override {
|
||||
smallAddressSpace = true; enableShared = false;
|
||||
# for android we build a shared library, passing these arguments is a bit tricky, as
|
||||
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
||||
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
|
||||
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"];
|
||||
postInstall = ''
|
||||
set -x
|
||||
${pkgs.tree}/bin/tree $out
|
||||
mkdir -p $out/_pkg
|
||||
# copy over includes, we might want those, but maybe not.
|
||||
@@ -138,8 +150,26 @@
|
||||
# find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \;
|
||||
# find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \;
|
||||
# find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \;
|
||||
echo ${androidPkgs.openssl}
|
||||
find ${androidPkgs.openssl.out}/lib -name "*.so" -exec cp {} $out/_pkg \;
|
||||
|
||||
${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so
|
||||
# remove the .1 and other version suffixes from .so's. Androids linker
|
||||
# doesn't play nice with them.
|
||||
for lib in $out/_pkg/*.so; do
|
||||
for dep in $(${pkgs.patchelf}/bin/patchelf --print-needed "$lib"); do
|
||||
if [[ "''${dep##*.so}" ]]; then
|
||||
echo "$lib : $dep -> ''${dep%%.so*}.so"
|
||||
chmod +w "$lib"
|
||||
${pkgs.patchelf}/bin/patchelf --replace-needed "$dep" "''${dep%%.so*}.so" "$lib"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
for lib in $out/_pkg/*.so; do
|
||||
chmod +w "$lib"
|
||||
${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib"
|
||||
[[ "$lib" != *libsimplex.so ]] && ${pkgs.patchelf}/bin/patchelf --set-soname "$(basename -a $lib)" "$lib"
|
||||
done
|
||||
|
||||
${pkgs.tree}/bin/tree $out/_pkg
|
||||
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsimplex.zip *)
|
||||
@@ -165,7 +195,12 @@
|
||||
> $out/nix-support/hydra-build-products
|
||||
'';
|
||||
};
|
||||
"x86_64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override {
|
||||
"x86_64-android:lib:simplex-chat" = (drv' {
|
||||
pkgs' = androidPkgs;
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.openssl = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override {
|
||||
smallAddressSpace = true; enableShared = false;
|
||||
# for android we build a shared library, passing these arguments is a bit tricky, as
|
||||
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
||||
@@ -212,7 +247,12 @@
|
||||
> $out/nix-support/hydra-build-products
|
||||
'';
|
||||
};
|
||||
"x86_64-linux:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override {
|
||||
"x86_64-linux:lib:simplex-chat" = (drv' {
|
||||
pkgs' = androidPkgs;
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.openssl = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override {
|
||||
smallAddressSpace = true; enableShared = false;
|
||||
# for android we build a shared library, passing these arguments is a bit tricky, as
|
||||
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
||||
@@ -250,25 +290,41 @@
|
||||
# aarch64-darwin iOS build (to be patched with mac2ios)
|
||||
"aarch64-darwin-ios:lib:simplex-chat" = (drv' {
|
||||
pkgs' = pkgs;
|
||||
extra-modules = [{ packages.simplexmq.flags.swift = true; }];
|
||||
extra-modules = [{
|
||||
packages.simplexmq.flags.swift = true;
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override (
|
||||
iosOverrides "pkg-ios-aarch64-swift-json"
|
||||
);
|
||||
# aarch64-darwin build with tagged JSON format (for Mac & Flutter)
|
||||
"aarch64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override (
|
||||
"aarch64-darwin:lib:simplex-chat" = (drv' {
|
||||
pkgs' = pkgs;
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override (
|
||||
iosOverrides "pkg-ios-aarch64-tagged-json"
|
||||
);
|
||||
};
|
||||
"x86_64-darwin" = {
|
||||
# this is the x86_64-darwin iOS simulator build (to be patched with mac2ios)
|
||||
# x86_64-darwin iOS simulator build (to be patched with mac2ios)
|
||||
"x86_64-darwin-ios:lib:simplex-chat" = (drv' {
|
||||
pkgs' = pkgs;
|
||||
extra-modules = [{ packages.simplexmq.flags.swift = true; }];
|
||||
extra-modules = [{
|
||||
packages.simplexmq.flags.swift = true;
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override (
|
||||
iosOverrides "pkg-ios-x86_64-swift-json"
|
||||
);
|
||||
# This is the x86_64-darwin build with tagged JSON format (for Mac & Flutter iOS simulator)
|
||||
"x86_64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override (
|
||||
# x86_64-darwin build with tagged JSON format (for Mac & Flutter iOS simulator)
|
||||
"x86_64-darwin:lib:simplex-chat" = (drv' {
|
||||
pkgs' = pkgs;
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
}];
|
||||
}).simplex-chat.components.library.override (
|
||||
iosOverrides "pkg-ios-x86_64-tagged-json"
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ dependencies:
|
||||
- containers == 0.6.*
|
||||
- cryptonite >= 0.27 && < 0.30
|
||||
- directory == 1.3.*
|
||||
- direct-sqlcipher == 2.3.*
|
||||
- email-validate == 2.3.*
|
||||
- exceptions == 0.10.*
|
||||
- filepath == 1.4.*
|
||||
@@ -35,7 +36,7 @@ dependencies:
|
||||
- simple-logger == 0.1.*
|
||||
- simplexmq >= 3.0
|
||||
- socks == 0.6.*
|
||||
- sqlite-simple == 0.4.*
|
||||
- sqlcipher-simple == 0.4.*
|
||||
- stm == 2.5.*
|
||||
- terminal == 0.2.*
|
||||
- text == 1.2.*
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
# libsimplex.so and libsupport.so binaries should be in ~/Downloads folder
|
||||
rm ./apps/android/app/src/main/cpp/libs/arm64-v8a/*
|
||||
cp ~/Downloads/libsimplex.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/
|
||||
cp ~/Downloads/libsupport.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/
|
||||
cp ~/Downloads/pkg-aarch64-android-libsimplex/libsimplex.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/
|
||||
cp ~/Downloads/pkg-aarch64-android-libsimplex/libcrypto.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/direct-sqlcipher.cabal b/direct-sqlcipher.cabal
|
||||
index 728ba3e..c63745e 100644
|
||||
--- a/direct-sqlcipher.cabal
|
||||
+++ b/direct-sqlcipher.cabal
|
||||
@@ -84,6 +84,8 @@ library
|
||||
cc-options: -DSQLITE_TEMP_STORE=2
|
||||
-DSQLITE_HAS_CODEC
|
||||
|
||||
+ extra-libraries: dl
|
||||
+
|
||||
if !os(windows) && !os(android)
|
||||
extra-libraries: pthread
|
||||
@@ -0,0 +1,68 @@
|
||||
diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c
|
||||
index 66bb609..00c33c1 100644
|
||||
--- a/cbits/sqlite3.c
|
||||
+++ b/cbits/sqlite3.c
|
||||
@@ -101739,9 +101739,9 @@ sqlite3_mutex* sqlcipher_mutex(int);
|
||||
/* #include "pager.h" */
|
||||
/* #include "vdbeInt.h" */
|
||||
|
||||
-#ifdef __ANDROID__
|
||||
-#include <android/log.h>
|
||||
-#endif
|
||||
+// #ifdef __ANDROID__
|
||||
+// #include <android/log.h>
|
||||
+// #endif
|
||||
|
||||
/* #include <time.h> */
|
||||
|
||||
@@ -104934,11 +104934,11 @@ static int sqlcipher_profile_callback(unsigned int trace, void *file, void *stmt
|
||||
FILE *f = (FILE*) file;
|
||||
char *fmt = "Elapsed time:%.3f ms - %s\n";
|
||||
double elapsed = (*((sqlite3_uint64*)run_time))/1000000.0;
|
||||
-#ifdef __ANDROID__
|
||||
- if(f == NULL) {
|
||||
- __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt));
|
||||
- }
|
||||
-#endif
|
||||
+// #ifdef __ANDROID__
|
||||
+// if(f == NULL) {
|
||||
+// __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt));
|
||||
+// }
|
||||
+// #endif
|
||||
if(f) fprintf(f, fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt));
|
||||
return SQLITE_OK;
|
||||
}
|
||||
@@ -104988,12 +104988,12 @@ void sqlcipher_log(unsigned int level, const char *message, ...) {
|
||||
va_start(params, message);
|
||||
|
||||
#ifdef CODEC_DEBUG
|
||||
-#ifdef __ANDROID__
|
||||
- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params);
|
||||
-#else
|
||||
+// #ifdef __ANDROID__
|
||||
+// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params);
|
||||
+// #else
|
||||
vfprintf(stderr, message, params);
|
||||
fprintf(stderr, "\n");
|
||||
-#endif
|
||||
+// #endif
|
||||
#endif
|
||||
|
||||
if(level > sqlcipher_log_level || (sqlcipher_log_logcat == 0 && sqlcipher_log_file == NULL)) {
|
||||
@@ -105026,11 +105026,11 @@ void sqlcipher_log(unsigned int level, const char *message, ...) {
|
||||
fprintf((FILE*)sqlcipher_log_file, "\n");
|
||||
}
|
||||
}
|
||||
-#ifdef __ANDROID__
|
||||
- if(sqlcipher_log_logcat) {
|
||||
- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params);
|
||||
- }
|
||||
-#endif
|
||||
+// #ifdef __ANDROID__
|
||||
+// if(sqlcipher_log_logcat) {
|
||||
+// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params);
|
||||
+// }
|
||||
+// #endif
|
||||
end:
|
||||
va_end(params);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
diff --git a/direct-sqlite.cabal b/direct-sqlite.cabal
|
||||
index 96f26b7..996198e 100644
|
||||
--- a/direct-sqlite.cabal
|
||||
+++ b/direct-sqlite.cabal
|
||||
@@ -69,7 +69,9 @@ library
|
||||
install-includes: sqlite3.h, sqlite3ext.h
|
||||
include-dirs: cbits
|
||||
|
||||
- if !os(windows) && !os(android)
|
||||
+ extra-libraries: dl
|
||||
+
|
||||
+ if !os(windows) && !os(android)
|
||||
extra-libraries: pthread
|
||||
|
||||
if flag(fulltextsearch)
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."e328ae5d060645a8ef090b1b3d88bc20a5902e45" = "0y9k1v2ss7f68md01azhh55b7xbk654hcpmyjkxkazy261nn9rxg";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."f08d81252deb68eb4a1ce4502712b99264b6749d" = "0v3z9sfy7vjbgm3cznb7vaycn2r2yav83x74khf6a001cs1zv171";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
|
||||
|
||||
+10
-5
@@ -78,6 +78,7 @@ library
|
||||
, composition ==1.0.*
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
@@ -91,7 +92,7 @@ library
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq >=3.0
|
||||
, socks ==0.6.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, terminal ==0.2.*
|
||||
, text ==1.2.*
|
||||
@@ -119,6 +120,7 @@ executable simplex-bot
|
||||
, composition ==1.0.*
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
@@ -133,7 +135,7 @@ executable simplex-bot
|
||||
, simplex-chat
|
||||
, simplexmq >=3.0
|
||||
, socks ==0.6.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, terminal ==0.2.*
|
||||
, text ==1.2.*
|
||||
@@ -161,6 +163,7 @@ executable simplex-bot-advanced
|
||||
, composition ==1.0.*
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
@@ -175,7 +178,7 @@ executable simplex-bot-advanced
|
||||
, simplex-chat
|
||||
, simplexmq >=3.0
|
||||
, socks ==0.6.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, terminal ==0.2.*
|
||||
, text ==1.2.*
|
||||
@@ -204,6 +207,7 @@ executable simplex-chat
|
||||
, composition ==1.0.*
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
@@ -218,7 +222,7 @@ executable simplex-chat
|
||||
, simplex-chat
|
||||
, simplexmq >=3.0
|
||||
, socks ==0.6.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, terminal ==0.2.*
|
||||
, text ==1.2.*
|
||||
@@ -255,6 +259,7 @@ test-suite simplex-chat-test
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
@@ -270,7 +275,7 @@ test-suite simplex-chat-test
|
||||
, simplex-chat
|
||||
, simplexmq >=3.0
|
||||
, socks ==0.6.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, terminal ==0.2.*
|
||||
, text ==1.2.*
|
||||
|
||||
+17
-9
@@ -86,6 +86,7 @@ defaultChatConfig =
|
||||
defaultAgentConfig
|
||||
{ tcpPort = undefined, -- agent does not listen to TCP
|
||||
dbFile = "simplex_v1",
|
||||
dbKey = "",
|
||||
yesToMigrations = False
|
||||
},
|
||||
yesToMigrations = False,
|
||||
@@ -124,7 +125,7 @@ logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
newChatController :: SQLiteStore -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
|
||||
newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, smpServers, networkConfig, logConnections, logServerHosts} sendToast = do
|
||||
newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, dbKey, smpServers, networkConfig, logConnections, logServerHosts} sendToast = do
|
||||
let f = chatStoreFile dbFilePrefix
|
||||
config = cfg {subscriptionEvents = logConnections, hostEvents = logServerHosts}
|
||||
sendNotification = fromMaybe (const $ pure ()) sendToast
|
||||
@@ -132,7 +133,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
|
||||
firstTime <- not <$> doesFileExist f
|
||||
currentUser <- newTVarIO user
|
||||
servers <- resolveServers defaultServers
|
||||
smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db"} servers {netCfg = networkConfig}
|
||||
smpAgent <- getSMPAgentClient aCfg {dbFile = agentStoreFile dbFilePrefix, dbKey} servers {netCfg = networkConfig}
|
||||
agentAsync <- newTVarIO Nothing
|
||||
idsDrg <- newTVarIO =<< drgNew
|
||||
inputQ <- newTBQueueIO tbqSize
|
||||
@@ -216,11 +217,7 @@ processChatCommand = \case
|
||||
StartChat subConns -> withUser' $ \user ->
|
||||
asks agentAsync >>= readTVarIO >>= \case
|
||||
Just _ -> pure CRChatRunning
|
||||
_ ->
|
||||
ifM
|
||||
(asks chatStoreChanged >>= readTVarIO)
|
||||
(throwChatError CEChatStoreChanged)
|
||||
(startChatController user subConns $> CRChatStarted)
|
||||
_ -> checkStoreNotChanged $ startChatController user subConns $> CRChatStarted
|
||||
APIStopChat -> do
|
||||
ask >>= stopChatController
|
||||
pure CRChatStopped
|
||||
@@ -239,8 +236,9 @@ processChatCommand = \case
|
||||
atomically . writeTVar incognito $ onOff
|
||||
pure CRCmdOk
|
||||
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk
|
||||
APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk
|
||||
APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk
|
||||
APIImportArchive cfg -> withStoreChanged $ importArchive cfg
|
||||
APIDeleteStorage -> withStoreChanged $ deleteStorage
|
||||
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
|
||||
APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore' $ \db -> getChatPreviews db user withPCC)
|
||||
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
|
||||
CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination search)
|
||||
@@ -947,6 +945,10 @@ processChatCommand = \case
|
||||
checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped)
|
||||
setStoreChanged :: m ()
|
||||
setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True)
|
||||
withStoreChanged :: m () -> m ChatResponse
|
||||
withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk
|
||||
checkStoreNotChanged :: m ChatResponse -> m ChatResponse
|
||||
checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged)
|
||||
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
|
||||
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
|
||||
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg)
|
||||
@@ -2635,6 +2637,10 @@ chatCommandP =
|
||||
"/_db export " *> (APIExportArchive <$> jsonP),
|
||||
"/_db import " *> (APIImportArchive <$> jsonP),
|
||||
"/_db delete" $> APIDeleteStorage,
|
||||
"/_db encryption " *> (APIStorageEncryption <$> jsonP),
|
||||
"/db encrypt " *> (APIStorageEncryption . DBEncryptionConfig "" <$> dbKeyP),
|
||||
"/db key " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)),
|
||||
"/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP),
|
||||
"/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)),
|
||||
"/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP),
|
||||
"/_get items count=" *> (APIGetChatItems <$> A.decimal),
|
||||
@@ -2784,6 +2790,8 @@ chatCommandP =
|
||||
t_ <- optional $ " timeout=" *> A.decimal
|
||||
let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_
|
||||
pure $ fullNetworkConfig socksProxy tcpTimeout
|
||||
dbKeyP = nonEmptyKey <$?> strP
|
||||
nonEmptyKey k@(DBEncryptionKey s) = if null s then Left "empty key" else Right k
|
||||
|
||||
adminContactReq :: ConnReqContact
|
||||
adminContactReq =
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Chat.Archive where
|
||||
module Simplex.Chat.Archive
|
||||
( exportArchive,
|
||||
importArchive,
|
||||
deleteStorage,
|
||||
sqlCipherExport,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Codec.Archive.Zip as Z
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.SQLite3 as SQL
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Agent.Client (agentDbPath)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..))
|
||||
import Simplex.Messaging.Util (whenM)
|
||||
import Simplex.Messaging.Agent.Client (agentStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString)
|
||||
import Simplex.Messaging.Util (unlessM, whenM)
|
||||
import System.FilePath
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.Exception (SomeException, bracket, catch)
|
||||
import UnliftIO.STM
|
||||
import UnliftIO.Temporary
|
||||
|
||||
@@ -73,14 +87,66 @@ deleteStorage = do
|
||||
|
||||
data StorageFiles = StorageFiles
|
||||
{ chatDb :: FilePath,
|
||||
chatEncrypted :: TVar Bool,
|
||||
agentDb :: FilePath,
|
||||
agentEncrypted :: TVar Bool,
|
||||
filesPath :: Maybe FilePath
|
||||
}
|
||||
|
||||
storageFiles :: ChatMonad m => m StorageFiles
|
||||
storageFiles = do
|
||||
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
||||
let SQLiteStore {dbFilePath = chatDb} = chatStore
|
||||
agentDb = agentDbPath smpAgent
|
||||
let SQLiteStore {dbFilePath = chatDb, dbEncrypted = chatEncrypted} = chatStore
|
||||
SQLiteStore {dbFilePath = agentDb, dbEncrypted = agentEncrypted} = agentStore smpAgent
|
||||
filesPath <- readTVarIO filesFolder
|
||||
pure StorageFiles {chatDb, agentDb, filesPath}
|
||||
pure StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted, filesPath}
|
||||
|
||||
sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m ()
|
||||
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key'} =
|
||||
when (key /= key') $ do
|
||||
fs@StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted} <- storageFiles
|
||||
checkFile `with` fs
|
||||
backup `with` fs
|
||||
(export chatDb chatEncrypted >> export agentDb agentEncrypted)
|
||||
`catchError` \e -> (restore `with` fs) >> throwError e
|
||||
where
|
||||
action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb
|
||||
backup f = copyFile f (f <> ".bak")
|
||||
restore f = copyFile (f <> ".bak") f
|
||||
checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f
|
||||
export f dbEnc = do
|
||||
enc <- readTVarIO dbEnc
|
||||
when (enc && null key) $ throwDBError DBErrorEncrypted
|
||||
when (not enc && not (null key)) $ throwDBError DBErrorPlaintext
|
||||
withDB (`SQL.exec` exportSQL) DBErrorExport
|
||||
renameFile (f <> ".exported") f
|
||||
withDB (`SQL.exec` testSQL) DBErrorOpen
|
||||
atomically $ writeTVar dbEnc $ not (null key')
|
||||
where
|
||||
withDB a err =
|
||||
liftIO (bracket (SQL.open $ T.pack f) SQL.close a $> Nothing)
|
||||
`catch` checkSQLError
|
||||
`catch` (\(e :: SomeException) -> sqliteError' e)
|
||||
>>= mapM_ (throwDBError . err)
|
||||
where
|
||||
checkSQLError e = case SQL.sqlError e of
|
||||
SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase
|
||||
_ -> sqliteError' e
|
||||
sqliteError' :: Show e => e -> m (Maybe SQLiteError)
|
||||
sqliteError' = pure . Just . SQLiteError . show
|
||||
exportSQL =
|
||||
T.unlines $
|
||||
keySQL key
|
||||
<> [ "ATTACH DATABASE " <> sqlString (f <> ".exported") <> " AS exported KEY " <> sqlString key' <> ";",
|
||||
"SELECT sqlcipher_export('exported');",
|
||||
"DETACH DATABASE exported;"
|
||||
]
|
||||
testSQL =
|
||||
T.unlines $
|
||||
keySQL key'
|
||||
<> [ "PRAGMA foreign_keys = ON;",
|
||||
"PRAGMA secure_delete = ON;",
|
||||
"PRAGMA auto_vacuum = FULL;",
|
||||
"SELECT count(*) FROM sqlite_master;"
|
||||
]
|
||||
keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)]
|
||||
|
||||
@@ -17,9 +17,13 @@ import Control.Monad.Reader
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (ord)
|
||||
import Data.Int (Int64)
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import Data.Time (ZonedTime)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
@@ -38,8 +42,9 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, InitialAgentServers, Net
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -112,6 +117,7 @@ data ChatCommand
|
||||
| APIExportArchive ArchiveConfig
|
||||
| APIImportArchive ArchiveConfig
|
||||
| APIDeleteStorage
|
||||
| APIStorageEncryption DBEncryptionConfig
|
||||
| APIGetChats {pendingConnections :: Bool}
|
||||
| APIGetChat ChatRef ChatPagination (Maybe String)
|
||||
| APIGetChatItems Int
|
||||
@@ -323,6 +329,21 @@ instance ToJSON ChatResponse where
|
||||
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
data DBEncryptionConfig = DBEncryptionConfig {currentKey :: DBEncryptionKey, newKey :: DBEncryptionKey}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
newtype DBEncryptionKey = DBEncryptionKey String
|
||||
deriving (Show)
|
||||
|
||||
instance IsString DBEncryptionKey where fromString = parseString $ parseAll strP
|
||||
|
||||
instance StrEncoding DBEncryptionKey where
|
||||
strEncode (DBEncryptionKey s) = B.pack s
|
||||
strP = DBEncryptionKey . B.unpack <$> A.takeWhile (\c -> c /= ' ' && ord c >= 0x21 && ord c <= 0x7E)
|
||||
|
||||
instance FromJSON DBEncryptionKey where
|
||||
parseJSON = strParseJSON "DBEncryptionKey"
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
contactError :: Maybe ChatError
|
||||
@@ -372,6 +393,7 @@ data ChatError
|
||||
= ChatError {errorType :: ChatErrorType}
|
||||
| ChatErrorAgent {agentError :: AgentErrorType}
|
||||
| ChatErrorStore {storeError :: StoreError}
|
||||
| ChatErrorDatabase {databaseError :: DatabaseError}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON ChatError where
|
||||
@@ -430,6 +452,28 @@ instance ToJSON ChatErrorType where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CE"
|
||||
|
||||
data DatabaseError
|
||||
= DBErrorEncrypted
|
||||
| DBErrorPlaintext
|
||||
| DBErrorNoFile {dbFile :: String}
|
||||
| DBErrorExport {sqliteError :: SQLiteError}
|
||||
| DBErrorOpen {sqliteError :: SQLiteError}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON DatabaseError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DB"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DB"
|
||||
|
||||
data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON SQLiteError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SQLite"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SQLite"
|
||||
|
||||
throwDBError :: ChatMonad m => DatabaseError -> m ()
|
||||
throwDBError = throwError . ChatErrorDatabase
|
||||
|
||||
type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m)
|
||||
|
||||
chatCmdError :: String -> ChatResponse
|
||||
|
||||
@@ -23,7 +23,7 @@ simplexChatCore cfg@ChatConfig {yesToMigrations} opts sendToast chat
|
||||
where
|
||||
initRun = do
|
||||
let f = chatStoreFile $ dbFilePrefix opts
|
||||
st <- createStore f yesToMigrations
|
||||
st <- createChatStore f (dbKey opts) yesToMigrations
|
||||
u <- getCreateActiveUser st
|
||||
cc <- newChatController st (Just u) cfg opts sendToast
|
||||
runSimplexChat opts u cc chat
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Chat.Mobile where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (catch)
|
||||
import Control.Monad.Reader
|
||||
import Data.Aeson (ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Functor (($>))
|
||||
import Data.List (find)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Database.SQLite.Simple (SQLError (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types (CInt (..))
|
||||
import Foreign.StablePtr
|
||||
@@ -24,13 +29,21 @@ import Simplex.Chat.Options
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations), createAgentStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore)
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (CorrId (..))
|
||||
import Simplex.Messaging.Util (catchAll)
|
||||
import System.Timeout (timeout)
|
||||
|
||||
foreign export ccall "chat_migrate_db" cChatMigrateDB :: CString -> CString -> IO CJSONString
|
||||
|
||||
-- chat_init is deprecated
|
||||
foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController)
|
||||
|
||||
foreign export ccall "chat_init_key" cChatInitKey :: CString -> CString -> IO (StablePtr ChatController)
|
||||
|
||||
foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString
|
||||
@@ -39,11 +52,23 @@ foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatCont
|
||||
|
||||
foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO CJSONString
|
||||
|
||||
-- | initialize chat controller
|
||||
-- | check and migrate the database
|
||||
-- This function validates that the encryption is correct and runs migrations - it should be called before cChatInitKey
|
||||
cChatMigrateDB :: CString -> CString -> IO CJSONString
|
||||
cChatMigrateDB fp key =
|
||||
((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatMigrateDB >>= newCAString . LB.unpack . J.encode
|
||||
|
||||
-- | initialize chat controller (deprecated)
|
||||
-- The active user has to be created and the chat has to be started before most commands can be used.
|
||||
cChatInit :: CString -> IO (StablePtr ChatController)
|
||||
cChatInit fp = peekCAString fp >>= chatInit >>= newStablePtr
|
||||
|
||||
-- | initialize chat controller with encrypted database
|
||||
-- The active user has to be created and the chat has to be started before most commands can be used.
|
||||
cChatInitKey :: CString -> CString -> IO (StablePtr ChatController)
|
||||
cChatInitKey fp key =
|
||||
((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatInitKey >>= newStablePtr
|
||||
|
||||
-- | send command to chat (same syntax as in terminal for now)
|
||||
cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString
|
||||
cChatSendCmd cPtr cCmd = do
|
||||
@@ -67,6 +92,7 @@ mobileChatOpts :: ChatOpts
|
||||
mobileChatOpts =
|
||||
ChatOpts
|
||||
{ dbFilePrefix = undefined,
|
||||
dbKey = "",
|
||||
smpServers = [],
|
||||
networkConfig = defaultNetworkConfig,
|
||||
logConnections = False,
|
||||
@@ -90,12 +116,41 @@ type CJSONString = CString
|
||||
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
|
||||
getActiveUser_ st = find activeUser <$> withTransaction st getUsers
|
||||
|
||||
data DBMigrationResult
|
||||
= DBMOk
|
||||
| DBMErrorNotADatabase {dbFile :: String}
|
||||
| DBMError {dbFile :: String, migrationError :: String}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON DBMigrationResult where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DBM"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DBM"
|
||||
|
||||
chatMigrateDB :: String -> String -> IO DBMigrationResult
|
||||
chatMigrateDB dbFilePrefix dbKey =
|
||||
migrate createChatStore (chatStoreFile dbFilePrefix) >>= \case
|
||||
DBMOk -> migrate createAgentStore (agentStoreFile dbFilePrefix)
|
||||
e -> pure e
|
||||
where
|
||||
migrate createStore dbFile =
|
||||
((createStore dbFile dbKey True >>= closeSQLiteStore) $> DBMOk)
|
||||
`catch` (pure . checkDBError)
|
||||
`catchAll` (pure . dbError)
|
||||
where
|
||||
checkDBError e = case sqlError e of
|
||||
DB.ErrorNotADatabase -> DBMErrorNotADatabase dbFile
|
||||
_ -> dbError e
|
||||
dbError e = DBMError dbFile $ show e
|
||||
|
||||
chatInit :: String -> IO ChatController
|
||||
chatInit dbFilePrefix = do
|
||||
chatInit = (`chatInitKey` "")
|
||||
|
||||
chatInitKey :: String -> String -> IO ChatController
|
||||
chatInitKey dbFilePrefix dbKey = do
|
||||
let f = chatStoreFile dbFilePrefix
|
||||
chatStore <- createStore f (yesToMigrations (defaultMobileConfig :: ChatConfig))
|
||||
chatStore <- createChatStore f dbKey (yesToMigrations (defaultMobileConfig :: ChatConfig))
|
||||
user_ <- getActiveUser_ chatStore
|
||||
newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing
|
||||
newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix, dbKey} Nothing
|
||||
|
||||
chatSendCmd :: ChatController -> String -> IO JSONString
|
||||
chatSendCmd cc s = LB.unpack . J.encode . APIResponse Nothing <$> runReaderT (execChatCommand $ B.pack s) cc
|
||||
|
||||
@@ -25,6 +25,7 @@ import System.FilePath (combine)
|
||||
|
||||
data ChatOpts = ChatOpts
|
||||
{ dbFilePrefix :: String,
|
||||
dbKey :: String,
|
||||
smpServers :: [SMPServer],
|
||||
networkConfig :: NetworkConfig,
|
||||
logConnections :: Bool,
|
||||
@@ -47,6 +48,14 @@ chatOpts appDir defaultDbFileName = do
|
||||
<> value defaultDbFilePath
|
||||
<> showDefault
|
||||
)
|
||||
dbKey <-
|
||||
strOption
|
||||
( long "key"
|
||||
<> short 'k'
|
||||
<> metavar "KEY"
|
||||
<> help "Database encryption key/pass-phrase"
|
||||
<> value ""
|
||||
)
|
||||
smpServers <-
|
||||
option
|
||||
parseSMPServers
|
||||
@@ -126,6 +135,7 @@ chatOpts appDir defaultDbFileName = do
|
||||
pure
|
||||
ChatOpts
|
||||
{ dbFilePrefix,
|
||||
dbKey,
|
||||
smpServers,
|
||||
networkConfig = fullNetworkConfig socksProxy $ useTcpTimeout socksProxy t,
|
||||
logConnections,
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
module Simplex.Chat.Store
|
||||
( SQLiteStore,
|
||||
StoreError (..),
|
||||
createStore,
|
||||
createChatStore,
|
||||
chatStoreFile,
|
||||
agentStoreFile,
|
||||
createUser,
|
||||
getUsers,
|
||||
setActiveUser,
|
||||
@@ -287,12 +288,15 @@ migrations = sortBy (compare `on` name) $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, query) = Migration {name = name, up = fromQuery query}
|
||||
|
||||
createStore :: FilePath -> Bool -> IO SQLiteStore
|
||||
createStore dbFilePath = createSQLiteStore dbFilePath migrations
|
||||
createChatStore :: FilePath -> String -> Bool -> IO SQLiteStore
|
||||
createChatStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey migrations
|
||||
|
||||
chatStoreFile :: FilePath -> FilePath
|
||||
chatStoreFile = (<> "_chat.db")
|
||||
|
||||
agentStoreFile :: FilePath -> FilePath
|
||||
agentStoreFile = (<> "_agent.db")
|
||||
|
||||
checkConstraint :: StoreError -> ExceptT StoreError IO a -> ExceptT StoreError IO a
|
||||
checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left . handleSQLError err)
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
|
||||
module Simplex.Chat.Terminal where
|
||||
|
||||
import Control.Exception (handle, throwIO)
|
||||
import Control.Monad.Except
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Database.SQLite.Simple (SQLError (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Simplex.Chat (defaultChatConfig)
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Core
|
||||
@@ -18,6 +21,7 @@ import Simplex.Chat.Terminal.Output
|
||||
import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..))
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
terminalChatConfig :: ChatConfig
|
||||
terminalChatConfig =
|
||||
@@ -38,10 +42,17 @@ terminalChatConfig =
|
||||
simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO ()
|
||||
simplexChatTerminal cfg opts t = do
|
||||
sendToast <- initializeNotifications
|
||||
simplexChatCore cfg opts (Just sendToast) $ \u cc -> do
|
||||
handle checkDBKeyError . simplexChatCore cfg opts (Just sendToast) $ \u cc -> do
|
||||
ct <- newChatTerminal t
|
||||
when (firstTime cc) . printToTerminal ct $ chatWelcome u
|
||||
runChatTerminal ct cc
|
||||
|
||||
checkDBKeyError :: SQLError -> IO ()
|
||||
checkDBKeyError e = case sqlError e of
|
||||
DB.ErrorNotADatabase -> do
|
||||
putStrLn "Database file is invalid or you passed an incorrect encryption key"
|
||||
exitFailure
|
||||
_ -> throwIO e
|
||||
|
||||
runChatTerminal :: ChatTerminal -> ChatController -> IO ()
|
||||
runChatTerminal ct cc = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc, runInputLoop ct cc]
|
||||
|
||||
@@ -886,7 +886,7 @@ viewChatError = \case
|
||||
CEActiveUserExists -> ["error: active user already exists"]
|
||||
CEChatNotStarted -> ["error: chat not started"]
|
||||
CEChatNotStopped -> ["error: chat not stopped"]
|
||||
CEChatStoreChanged -> ["error: chat store changed"]
|
||||
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
||||
CEInvalidConnReq -> viewInvalidConnReq
|
||||
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
|
||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||
@@ -944,6 +944,12 @@ viewChatError = \case
|
||||
SEConnectionNotFound _ -> [] -- TODO mutes delete group error, but also mutes any error from getConnectionEntity
|
||||
SEQuotedChatItemNotFound -> ["message not found - reply is not sent"]
|
||||
e -> ["chat db error: " <> sShow e]
|
||||
ChatErrorDatabase err -> case err of
|
||||
DBErrorEncrypted -> ["error: chat database is already encrypted"]
|
||||
DBErrorPlaintext -> ["error: chat database is not encrypted"]
|
||||
DBErrorExport e -> ["error encrypting database: " <> sqliteError' e]
|
||||
DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e]
|
||||
e -> ["chat database error: " <> sShow e]
|
||||
ChatErrorAgent err -> case err of
|
||||
SMP SMP.AUTH ->
|
||||
[ "error: connection authorization failed - this could happen if connection was deleted,\
|
||||
@@ -955,6 +961,9 @@ viewChatError = \case
|
||||
e -> ["smp agent error: " <> sShow e]
|
||||
where
|
||||
fileNotFound fileId = ["file " <> sShow fileId <> " not found"]
|
||||
sqliteError' = \case
|
||||
SQLiteErrorNotADatabase -> "wrong passphrase or invalid database file"
|
||||
SQLiteError e -> sShow e
|
||||
|
||||
ttyContact :: ContactName -> StyledString
|
||||
ttyContact = styled $ colored Green
|
||||
|
||||
+9
-2
@@ -49,7 +49,13 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: e328ae5d060645a8ef090b1b3d88bc20a5902e45
|
||||
commit: f08d81252deb68eb4a1ce4502712b99264b6749d
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
# - ../sqlcipher-simple
|
||||
- github: simplex-chat/sqlcipher-simple
|
||||
commit: 5e154a2aeccc33ead6c243ec07195ab673137221
|
||||
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
|
||||
- github: simplex-chat/aeson
|
||||
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
|
||||
@@ -63,6 +69,8 @@ flags:
|
||||
zip:
|
||||
disable-bzip2: true
|
||||
disable-zstd: true
|
||||
direct-sqlcipher:
|
||||
openssl: true
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
@@ -78,7 +86,6 @@ flags:
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
|
||||
+6
-4
@@ -48,6 +48,8 @@ testOpts :: ChatOpts
|
||||
testOpts =
|
||||
ChatOpts
|
||||
{ dbFilePrefix = undefined,
|
||||
dbKey = "",
|
||||
-- dbKey = "this is a pass-phrase to encrypt the database",
|
||||
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"],
|
||||
networkConfig = defaultNetworkConfig,
|
||||
logConnections = False,
|
||||
@@ -101,16 +103,16 @@ testCfgV1 :: ChatConfig
|
||||
testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
|
||||
|
||||
createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
|
||||
createTestChat cfg opts dbPrefix profile = do
|
||||
createTestChat cfg opts@ChatOpts {dbKey} dbPrefix profile = do
|
||||
let dbFilePrefix = testDBPrefix <> dbPrefix
|
||||
st <- createStore (dbFilePrefix <> "_chat.db") False
|
||||
st <- createChatStore (dbFilePrefix <> "_chat.db") dbKey False
|
||||
Right user <- withTransaction st $ \db -> runExceptT $ createUser db profile True
|
||||
startTestChat_ st cfg opts dbFilePrefix user
|
||||
|
||||
startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC
|
||||
startTestChat cfg opts dbPrefix = do
|
||||
startTestChat cfg opts@ChatOpts {dbKey} dbPrefix = do
|
||||
let dbFilePrefix = testDBPrefix <> dbPrefix
|
||||
st <- createStore (dbFilePrefix <> "_chat.db") False
|
||||
st <- createChatStore (dbFilePrefix <> "_chat.db") dbKey False
|
||||
Just user <- find activeUser <$> withTransaction st getUsers
|
||||
startTestChat_ st cfg opts dbFilePrefix user
|
||||
|
||||
|
||||
+46
-2
@@ -115,6 +115,7 @@ chatTests = do
|
||||
describe "maintenance mode" $ do
|
||||
it "start/stop/export/import chat" testMaintenanceMode
|
||||
it "export/import chat with files" testMaintenanceModeWithFiles
|
||||
it "encrypt/decrypt database" testDatabaseEncryption
|
||||
describe "mute/unmute messages" $ do
|
||||
it "mute/unmute contact" testMuteContact
|
||||
it "mute/unmute group" testMuteGroup
|
||||
@@ -2717,7 +2718,7 @@ testMaintenanceMode = withTmpFiles $ do
|
||||
alice <## "ok"
|
||||
-- cannot start chat after import
|
||||
alice ##> "/_start"
|
||||
alice <## "error: chat store changed"
|
||||
alice <## "error: chat store changed, please restart chat"
|
||||
-- works after full restart
|
||||
withTestChat "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
@@ -2752,7 +2753,7 @@ testMaintenanceModeWithFiles = withTmpFiles $ do
|
||||
alice <## "ok"
|
||||
-- cannot start chat after delete
|
||||
alice ##> "/_start"
|
||||
alice <## "error: chat store changed"
|
||||
alice <## "error: chat store changed, please restart chat"
|
||||
doesDirectoryExist "./tests/tmp/alice_files" `shouldReturn` False
|
||||
alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "ok"
|
||||
@@ -2760,6 +2761,49 @@ testMaintenanceModeWithFiles = withTmpFiles $ do
|
||||
-- works after full restart
|
||||
withTestChat "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
testDatabaseEncryption :: IO ()
|
||||
testDatabaseEncryption = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
connectUsers alice bob
|
||||
alice #> "@bob hi"
|
||||
bob <# "alice> hi"
|
||||
alice ##> "/db encrypt mykey"
|
||||
alice <## "error: chat not stopped"
|
||||
alice ##> "/db decrypt mykey"
|
||||
alice <## "error: chat not stopped"
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/db decrypt mykey"
|
||||
alice <## "error: chat database is not encrypted"
|
||||
alice ##> "/db encrypt mykey"
|
||||
alice <## "ok"
|
||||
alice ##> "/_start"
|
||||
alice <## "error: chat store changed, please restart chat"
|
||||
withTestChatOpts testOpts {maintenance = True, dbKey = "mykey"} "alice" $ \alice -> do
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
testChatWorking alice bob
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/db key wrongkey nextkey"
|
||||
alice <## "error encrypting database: wrong passphrase or invalid database file"
|
||||
alice ##> "/db key mykey nextkey"
|
||||
alice <## "ok"
|
||||
alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}"
|
||||
alice <## "ok"
|
||||
withTestChatOpts testOpts {maintenance = True, dbKey = "anotherkey"} "alice" $ \alice -> do
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
testChatWorking alice bob
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/db decrypt anotherkey"
|
||||
alice <## "ok"
|
||||
withTestChat "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
testMuteContact :: IO ()
|
||||
testMuteContact =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
|
||||
@@ -73,7 +73,9 @@ parsedMarkdown = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\
|
||||
|
||||
testChatApiNoUser :: IO ()
|
||||
testChatApiNoUser = withTmpFiles $ do
|
||||
DBMOk <- chatMigrateDB testDBPrefix ""
|
||||
cc <- chatInit testDBPrefix
|
||||
DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix "myKey"
|
||||
chatSendCmd cc "/u" `shouldReturn` noActiveUser
|
||||
chatSendCmd cc "/_start" `shouldReturn` noActiveUser
|
||||
chatSendCmd cc "/u alice Alice" `shouldReturn` activeUser
|
||||
@@ -81,10 +83,14 @@ testChatApiNoUser = withTmpFiles $ do
|
||||
|
||||
testChatApi :: IO ()
|
||||
testChatApi = withTmpFiles $ do
|
||||
let f = chatStoreFile $ testDBPrefix <> "1"
|
||||
st <- createStore f True
|
||||
let dbPrefix = testDBPrefix <> "1"
|
||||
f = chatStoreFile dbPrefix
|
||||
st <- createChatStore f "myKey" True
|
||||
Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile True
|
||||
cc <- chatInit $ testDBPrefix <> "1"
|
||||
DBMOk <- chatMigrateDB testDBPrefix "myKey"
|
||||
cc <- chatInitKey dbPrefix "myKey"
|
||||
DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix ""
|
||||
DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix "anotherKey"
|
||||
chatSendCmd cc "/u" `shouldReturn` activeUser
|
||||
chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists
|
||||
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ module SchemaDump where
|
||||
import ChatClient (withTmpFiles)
|
||||
import Control.DeepSeq
|
||||
import Control.Monad (void)
|
||||
import Simplex.Chat.Store (createStore)
|
||||
import Simplex.Chat.Store (createChatStore)
|
||||
import System.Process (readCreateProcess, shell)
|
||||
import Test.Hspec
|
||||
|
||||
@@ -22,7 +22,7 @@ schemaDumpTest =
|
||||
testVerifySchemaDump :: IO ()
|
||||
testVerifySchemaDump =
|
||||
withTmpFiles $ do
|
||||
void $ createStore testDB False
|
||||
void $ createChatStore testDB "" False
|
||||
void $ readCreateProcess (shell $ "touch " <> schema) ""
|
||||
savedSchema <- readFile schema
|
||||
savedSchema `deepseq` pure ()
|
||||
|
||||
Reference in New Issue
Block a user