Merge branch 'master' into av/ios-migrate-to-device

This commit is contained in:
Evgeny Poberezkin 2024-02-25 22:36:08 +00:00
commit 8cc605ffb9
No known key found for this signature in database
GPG Key ID: 494BDDD9A28B577D
14 changed files with 475 additions and 15 deletions

View File

@ -24,7 +24,7 @@ _Please note_: when you change the servers in the app configuration, it only aff
- Semi-automatic deployment: - Semi-automatic deployment:
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
- [Linode StackScript](https://github.com/simplex-chat/simplexmq#deploy-smp-server-on-linode) - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/)
Manual installation requires some preliminary actions: Manual installation requires some preliminary actions:
@ -33,7 +33,7 @@ Manual installation requires some preliminary actions:
- Using offical binaries: - Using offical binaries:
```sh ```sh
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server
``` ```
- Compiling from source: - Compiling from source:
@ -417,6 +417,63 @@ To import `csv` to `Grafana` one should:
For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/) For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/)
# Updating your SMP server
To update your smp-server to latest version, choose your installation method and follow the steps:
- Manual deployment
1. Stop the server:
```sh
sudo systemctl stop smp-server
```
2. Update the binary:
```sh
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server
```
3. Start the server:
```sh
sudo systemctl start smp-server
```
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
1. Execute the followin command:
```sh
sudo simplex-servers-update
```
2. Done!
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
1. Stop and remove the container:
```sh
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="{{.ID}}"))
```
2. Pull latest image:
```sh
docker pull simplexchat/smp-server:latest
```
3. Start new container:
```sh
docker run -d \
-p 5223:5223 \
-v $HOME/simplex/smp/config:/etc/opt/simplex:z \
-v $HOME/simplex/smp/logs:/var/opt/simplex:z \
simplexchat/smp-server:latest
```
- [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/)
1. Pull latest images:
```sh
docker-compose --project-directory /etc/docker/compose/simplex pull
```
2. Restart the containers:
```sh
docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans
```
3. Remove obsolete images:
```sh
docker image prune
```
### Configuring the app to use the server ### Configuring the app to use the server
To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them.

View File

@ -24,6 +24,7 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba
- Semi-automatic deployment: - Semi-automatic deployment:
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
- [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/)
Manual installation requires some preliminary actions: Manual installation requires some preliminary actions:
@ -32,7 +33,7 @@ Manual installation requires some preliminary actions:
- Using offical binaries: - Using offical binaries:
```sh ```sh
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server && chmod +x /usr/local/bin/xftp-server
``` ```
- Compiling from source: - Compiling from source:
@ -418,6 +419,65 @@ To import `csv` to `Grafana` one should:
For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/) For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/)
# Updating your XFTP server
To update your XFTP server to latest version, choose your installation method and follow the steps:
- Manual deployment
1. Stop the server:
```sh
sudo systemctl stop xftp-server
```
2. Update the binary:
```sh
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server && chmod +x /usr/local/bin/xftp-server
```
3. Start the server:
```sh
sudo systemctl start xftp-server
```
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
1. Execute the followin command:
```sh
sudo simplex-servers-update
```
2. Done!
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
1. Stop and remove the container:
```sh
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="{{.ID}}"))
```
2. Pull latest image:
```sh
docker pull simplexchat/xftp-server:latest
```
3. Start new container:
```sh
docker run -d \
-p 443:443 \
-v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \
-v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \
-v $HOME/simplex/xftp/files:/srv/xftp:z \
simplexchat/xftp-server:latest
```
- [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/)
1. Pull latest images:
```sh
docker-compose --project-directory /etc/docker/compose/simplex pull
```
2. Restart the containers:
```sh
docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans
```
3. Remove obsolete images:
```sh
docker image prune
```
### Configuring the app to use the server ### Configuring the app to use the server
Please see: [SMP Server: Configuring the app to use the server](./SERVER.md#configuring-the-app-to-use-the-server). Please see: [SMP Server: Configuring the app to use the server](./SERVER.md#configuring-the-app-to-use-the-server).

View File

@ -82,7 +82,7 @@ def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb,
state.PQRs = GENERATE_PQKEM() state.PQRs = GENERATE_PQKEM()
state.PQRr = bob_pq_kem_encapsulation_key state.PQRr = bob_pq_kem_encapsulation_key
state.PQRss = random // shared secret for KEM state.PQRss = random // shared secret for KEM
state.PQRenc_ss = PQKEM-ENC(state.PQRr.encaps, state.PQRss) // encapsulated additional shared secret state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret
// above added for KEM // above added for KEM
// below augments DH key agreement with PQ shared secret // below augments DH key agreement with PQ shared secret
state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss) state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss)
@ -103,7 +103,7 @@ def RatchetInitBobPQ2HE(state, SK, bob_dh_key_pair, shared_hka, shared_nhkb, bob
state.PQRs = bob_pq_kem_key_pair state.PQRs = bob_pq_kem_key_pair
state.PQRr = None state.PQRr = None
state.PQRss = None state.PQRss = None
state.PQRenc_ss = None state.PQRct = None
// above added for KEM // above added for KEM
state.RK = SK state.RK = SK
state.CKs = None state.CKs = None
@ -132,10 +132,10 @@ def RatchetEncryptPQ2HE(state, plaintext, AD):
// encapsulation key from PQRs and encapsulated shared secret is added to header // encapsulation key from PQRs and encapsulated shared secret is added to header
header = HEADER_PQ2( header = HEADER_PQ2(
dh = state.DHRs.public, dh = state.DHRs.public,
kem = state.PQRs.public, // added for KEM #2
ct = state.PQRct // added for KEM #1
pn = state.PN, pn = state.PN,
n = state.Ns, n = state.Ns,
encaps = state.PQRs.encaps, // added for KEM #1
enc_ss = state.PQRenc_ss // added for KEM #2
) )
enc_header = HENCRYPT(state.HKs, header) enc_header = HENCRYPT(state.HKs, header)
state.Ns += 1 state.Ns += 1
@ -162,6 +162,16 @@ def RatchetDecryptPQ2HE(state, enc_header, ciphertext, AD):
state.Nr += 1 state.Nr += 1
return DECRYPT(mk, ciphertext, CONCAT(AD, enc_header)) return DECRYPT(mk, ciphertext, CONCAT(AD, enc_header))
// DecryptHeader is the same as in double ratchet specification
def DecryptHeader(state, enc_header):
header = HDECRYPT(state.HKr, enc_header)
if header != None:
return header, False
header = HDECRYPT(state.NHKr, enc_header)
if header != None:
return header, True
raise Error()
def DHRatchetPQ2HE(state, header): def DHRatchetPQ2HE(state, header):
state.PN = state.Ns state.PN = state.Ns
state.Ns = 0 state.Ns = 0
@ -170,16 +180,16 @@ def DHRatchetPQ2HE(state, header):
state.HKr = state.NHKr state.HKr = state.NHKr
state.DHRr = header.dh state.DHRr = header.dh
// save new encapsulation key from header // save new encapsulation key from header
state.PQRr = header.encaps state.PQRr = header.kem
// decapsulate shared secret from header - KEM #2 // decapsulate shared secret from header - KEM #2
ss = PQKEM-DEC(state.PQRs.decaps, header.enc_ss) ss = PQKEM-DEC(state.PQRs.private, header.ct)
// use decapsulated shared secret with receiving ratchet // use decapsulated shared secret with receiving ratchet
state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss) state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss)
state.DHRs = GENERATE_DH() state.DHRs = GENERATE_DH()
// below is added for KEM // below is added for KEM
state.PQRs = GENERATE_PQKEM() // generate new PQ key pair state.PQRs = GENERATE_PQKEM() // generate new PQ key pair
state.PQRss = random // shared secret for KEM state.PQRss = random // shared secret for KEM
state.PQRenc_ss = PQKEM-ENC(state.PQRr.encaps, state.PQRss) // encapsulated additional shared secret KEM #1 state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret KEM #1
// above is added for KEM // above is added for KEM
// use new shared secret with sending ratchet // use new shared secret with sending ratchet
state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss) state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss)
@ -201,7 +211,7 @@ The main downside is the absense of performance-efficient implementation for aar
## Implementation considerations for SimpleX Chat ## Implementation considerations for SimpleX Chat
As SimpleX Chat pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme will not cause additional traffic in most cases. For large texts it may require additional messages to be sent. Similarly, for media previews it may require either reducing the preview size (and quality) or sending additional messages. As SimpleX Chat pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme will not cause additional traffic in most cases. For large texts it may require additional messages to be sent. Similarly, for media previews it may require either reducing the preview size (and quality), or sending additional messages, or compressing the current JSON encoding, e.g. with zstd algorithm.
That might be the primary reason why this scheme was not adopted by Signal, as it would have resulted in substantial traffic growth to the best of our knowledge, Signal messages are not padded to a fixed size. That might be the primary reason why this scheme was not adopted by Signal, as it would have resulted in substantial traffic growth to the best of our knowledge, Signal messages are not padded to a fixed size.
@ -209,6 +219,8 @@ Sharing the initial keys in case of SimpleX Chat it is equivalent to sharing the
It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm. To mitigate it, the hadnshake protocol can be modified to postpone sending the user profile until the second message from Alice (HELLO message in SMP protocol). It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm. To mitigate it, the hadnshake protocol can be modified to postpone sending the user profile until the second message from Alice (HELLO message in SMP protocol).
Another consideration is pairwise ratchets in groups. Key generation in sntrup761 is quite slow - on slow devices it can probably be as slow as 10 keys per second, so using this primitive in groups larger than 10 members would result in slow performance. An option could be not to use ratchets in groups at all, but that would result in the lack of protection in small groups that simply combine multiple devices of 1-3 people. So a better option would be to support dynamically adding and removing sntrup761 keys for pairwise ratchets in groups, which means that when sending each message a boolean flag needs to be passed whether to use PQ KEM or not.
## Summary ## Summary
If chosen PQ KEM proves secure against quantum computer attacks, then the proposed augmented double ratchet will also be secure against quantum computer attack, including break-in recovery property, while keeping deniability and forward secrecy, because the [same proof](https://eprint.iacr.org/2016/1013.pdf) as for double ratchet algorithm would hold here, provided KEM is secure. If chosen PQ KEM proves secure against quantum computer attacks, then the proposed augmented double ratchet will also be secure against quantum computer attack, including break-in recovery property, while keeping deniability and forward secrecy, because the [same proof](https://eprint.iacr.org/2016/1013.pdf) as for double ratchet algorithm would hold here, provided KEM is secure.

View File

@ -0,0 +1,60 @@
# Migrating app settings to another device
## Problem
This is related to simplified database migration UX in the [previous RFC](./2024-02-12-database-migration.md).
Currently, when database is imported after the onboarding is complete, users can configure the app prior to the import.
Some of the settings are particularly important for privacy and security:
- SOCKS proxy settings
- Automatic image etc. downloads
- Link previews
With the new UX, the chat will start automatically, without giving users a chance to configure the app. That means that we have to migrate settings to a new device as well, as part of the archive.
## Solution
There are several possible approaches:
- put settings to the database via the API
- save settings as some file with cross-platform format (e.g. JSON or YAML or properties used on desktop).
The second approach seems much simpler than maintaining the settings in the database.
If we save a file, then there are two options:
- native apps maintain cross-platform schemas for this file, support any JSON and parse it in a safe way (so that even invalid or incorrect JSON - e.g., array instead of object - or invalid types in some properties do not cause the failure of properties that are correct).
- this schema and type will be maintained in the core library, that will be responsible for storing and reading the settings and passing to native UI as correct record of a given type.
The downside of the second approach is that addition of any property that needs to be migrated will have to be done on any change in either of the platforms. The downside of the first approach is that neither app platform will be self-sufficient any more, and not only iOS/Android would have to take into account code, but also each other code.
If we go with the second approach, there will be these types:
```haskell
data AppSettings = AppSettings
{ networkConfig :: NetworkConfig, -- existing type in Haskell and all UIs
privacyConfig :: PrivacyConfig -- new type, etc.
-- ... additional properties after the initial release should be added as Maybe, as all extensions
}
data ArchiveConfig = ArchiveConfig
{ -- existing properties
archivePath :: FilePath,
disableCompression :: Maybe Bool,
parentTempDirectory :: Maybe FilePath,
-- new property
appSettings :: AppSettings
-- for export, these settings will contain the settings passed from the UI and will be saved to JSON file as simplex_v1_settings.json in the archive
-- for import, these settings will contain the defaults that will be used if some property or subproperty is missing in JSON
}
-- importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError] -- current type
importArchive :: ChatMonad m => ArchiveConfig -> m ArchiveImportResult -- new type
-- | CRArchiveImported {archiveErrors :: [ArchiveError]} -- current type
| CRArchiveImported {importResult :: ArchiveImportResult} -- new type
data ArchiveImportResult = ArchiveImportResult
{ archiveErrors :: [ArchiveError],
appSettings :: Maybe AppSettings
}
```

View File

@ -26,6 +26,7 @@ flag swift
library library
exposed-modules: exposed-modules:
Simplex.Chat Simplex.Chat
Simplex.Chat.AppSettings
Simplex.Chat.Archive Simplex.Chat.Archive
Simplex.Chat.Bot Simplex.Chat.Bot
Simplex.Chat.Bot.KnownContacts Simplex.Chat.Bot.KnownContacts
@ -134,6 +135,7 @@ library
Simplex.Chat.Migrations.M20240115_block_member_for_all Simplex.Chat.Migrations.M20240115_block_member_for_all
Simplex.Chat.Migrations.M20240122_indexes Simplex.Chat.Migrations.M20240122_indexes
Simplex.Chat.Migrations.M20240214_redirect_file_id Simplex.Chat.Migrations.M20240214_redirect_file_id
Simplex.Chat.Migrations.M20240222_app_settings
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
@ -149,6 +151,7 @@ library
Simplex.Chat.Remote.Transport Simplex.Chat.Remote.Transport
Simplex.Chat.Remote.Types Simplex.Chat.Remote.Types
Simplex.Chat.Store Simplex.Chat.Store
Simplex.Chat.Store.AppSettings
Simplex.Chat.Store.Connections Simplex.Chat.Store.Connections
Simplex.Chat.Store.Direct Simplex.Chat.Store.Direct
Simplex.Chat.Store.Files Simplex.Chat.Store.Files

View File

@ -68,6 +68,7 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Remote import Simplex.Chat.Remote
import Simplex.Chat.Remote.Types import Simplex.Chat.Remote.Types
import Simplex.Chat.Store import Simplex.Chat.Store
import Simplex.Chat.Store.AppSettings
import Simplex.Chat.Store.Connections import Simplex.Chat.Store.Connections
import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Files import Simplex.Chat.Store.Files
@ -597,9 +598,11 @@ processChatCommand' vr = \case
fileErrs <- importArchive cfg fileErrs <- importArchive cfg
setStoreChanged setStoreChanged
pure $ CRArchiveImported fileErrs pure $ CRArchiveImported fileErrs
APISaveAppSettings as -> withStore' (`saveAppSettings` as) >> ok_
APIGetAppSettings platformDefaults -> CRAppSettings <$> withStore' (`getAppSettings` platformDefaults)
APIDeleteStorage -> withStoreChanged deleteStorage APIDeleteStorage -> withStoreChanged deleteStorage
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
TestStorageEncryption key -> withStoreChanged $ sqlCipherTestKey key TestStorageEncryption key -> sqlCipherTestKey key >> ok_
ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query) ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query)
ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query) ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query)
SlowSQLQueries -> do SlowSQLQueries -> do
@ -6469,6 +6472,8 @@ chatCommandP =
"/db key " *> (APIStorageEncryption <$> (dbEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), "/db key " *> (APIStorageEncryption <$> (dbEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)),
"/db decrypt " *> (APIStorageEncryption . (`dbEncryptionConfig` "") <$> dbKeyP), "/db decrypt " *> (APIStorageEncryption . (`dbEncryptionConfig` "") <$> dbKeyP),
"/db test key " *> (TestStorageEncryption <$> dbKeyP), "/db test key " *> (TestStorageEncryption <$> dbKeyP),
"/_save app settings" *> (APISaveAppSettings <$> jsonP),
"/_get app settings" *> (APIGetAppSettings <$> optional (A.space *> jsonP)),
"/sql chat " *> (ExecChatStoreSQL <$> textP), "/sql chat " *> (ExecChatStoreSQL <$> textP),
"/sql agent " *> (ExecAgentStoreSQL <$> textP), "/sql agent " *> (ExecAgentStoreSQL <$> textP),
"/sql slow" $> SlowSQLQueries, "/sql slow" $> SlowSQLQueries,

View File

@ -0,0 +1,190 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.AppSettings where
import Control.Applicative ((<|>))
import Data.Aeson (FromJSON (..), (.:?))
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
import Simplex.Messaging.Util (catchAll_)
data AppPlatform = APIOS | APAndroid | APDesktop deriving (Show)
data NotificationMode = NMOff | NMPeriodic | NMInstant deriving (Show)
data NotificationPreviewMode = NPMHidden | NPMContact | NPMMessage deriving (Show)
data LockScreenCalls = LSCDisable | LSCShow | LSCAccept deriving (Show)
data AppSettings = AppSettings
{ appPlatform :: Maybe AppPlatform,
networkConfig :: Maybe NetworkConfig,
privacyEncryptLocalFiles :: Maybe Bool,
privacyAcceptImages :: Maybe Bool,
privacyLinkPreviews :: Maybe Bool,
privacyShowChatPreviews :: Maybe Bool,
privacySaveLastDraft :: Maybe Bool,
privacyProtectScreen :: Maybe Bool,
notificationMode :: Maybe NotificationMode,
notificationPreviewMode :: Maybe NotificationPreviewMode,
webrtcPolicyRelay :: Maybe Bool,
webrtcICEServers :: Maybe [Text],
confirmRemoteSessions :: Maybe Bool,
connectRemoteViaMulticast :: Maybe Bool,
connectRemoteViaMulticastAuto :: Maybe Bool,
developerTools :: Maybe Bool,
confirmDBUpgrades :: Maybe Bool,
androidCallOnLockScreen :: Maybe LockScreenCalls,
iosCallKitEnabled :: Maybe Bool,
iosCallKitCallsInRecents :: Maybe Bool
}
deriving (Show)
defaultAppSettings :: AppSettings
defaultAppSettings =
AppSettings
{ appPlatform = Nothing,
networkConfig = Just defaultNetworkConfig,
privacyEncryptLocalFiles = Just True,
privacyAcceptImages = Just True,
privacyLinkPreviews = Just True,
privacyShowChatPreviews = Just True,
privacySaveLastDraft = Just True,
privacyProtectScreen = Just False,
notificationMode = Just NMInstant,
notificationPreviewMode = Just NPMMessage,
webrtcPolicyRelay = Just True,
webrtcICEServers = Just [],
confirmRemoteSessions = Just False,
connectRemoteViaMulticast = Just True,
connectRemoteViaMulticastAuto = Just True,
developerTools = Just False,
confirmDBUpgrades = Just False,
androidCallOnLockScreen = Just LSCShow,
iosCallKitEnabled = Just True,
iosCallKitCallsInRecents = Just False
}
defaultParseAppSettings :: AppSettings
defaultParseAppSettings =
AppSettings
{ appPlatform = Nothing,
networkConfig = Nothing,
privacyEncryptLocalFiles = Nothing,
privacyAcceptImages = Nothing,
privacyLinkPreviews = Nothing,
privacyShowChatPreviews = Nothing,
privacySaveLastDraft = Nothing,
privacyProtectScreen = Nothing,
notificationMode = Nothing,
notificationPreviewMode = Nothing,
webrtcPolicyRelay = Nothing,
webrtcICEServers = Nothing,
confirmRemoteSessions = Nothing,
connectRemoteViaMulticast = Nothing,
connectRemoteViaMulticastAuto = Nothing,
developerTools = Nothing,
confirmDBUpgrades = Nothing,
androidCallOnLockScreen = Nothing,
iosCallKitEnabled = Nothing,
iosCallKitCallsInRecents = Nothing
}
combineAppSettings :: AppSettings -> AppSettings -> AppSettings
combineAppSettings platformDefaults storedSettings =
AppSettings
{ appPlatform = p appPlatform,
networkConfig = p networkConfig,
privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
privacyAcceptImages = p privacyAcceptImages,
privacyLinkPreviews = p privacyLinkPreviews,
privacyShowChatPreviews = p privacyShowChatPreviews,
privacySaveLastDraft = p privacySaveLastDraft,
privacyProtectScreen = p privacyProtectScreen,
notificationMode = p notificationMode,
notificationPreviewMode = p notificationPreviewMode,
webrtcPolicyRelay = p webrtcPolicyRelay,
webrtcICEServers = p webrtcICEServers,
confirmRemoteSessions = p confirmRemoteSessions,
connectRemoteViaMulticast = p connectRemoteViaMulticast,
connectRemoteViaMulticastAuto = p connectRemoteViaMulticastAuto,
developerTools = p developerTools,
confirmDBUpgrades = p confirmDBUpgrades,
iosCallKitEnabled = p iosCallKitEnabled,
iosCallKitCallsInRecents = p iosCallKitCallsInRecents,
androidCallOnLockScreen = p androidCallOnLockScreen
}
where
p :: (AppSettings -> Maybe a) -> Maybe a
p sel = sel storedSettings <|> sel platformDefaults <|> sel defaultAppSettings
$(JQ.deriveJSON (enumJSON $ dropPrefix "AP") ''AppPlatform)
$(JQ.deriveJSON (enumJSON $ dropPrefix "NM") ''NotificationMode)
$(JQ.deriveJSON (enumJSON $ dropPrefix "NPM") ''NotificationPreviewMode)
$(JQ.deriveJSON (enumJSON $ dropPrefix "LSC") ''LockScreenCalls)
$(JQ.deriveToJSON defaultJSON ''AppSettings)
instance FromJSON AppSettings where
parseJSON (J.Object v) = do
appPlatform <- p "appPlatform"
networkConfig <- p "networkConfig"
privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles"
privacyAcceptImages <- p "privacyAcceptImages"
privacyLinkPreviews <- p "privacyLinkPreviews"
privacyShowChatPreviews <- p "privacyShowChatPreviews"
privacySaveLastDraft <- p "privacySaveLastDraft"
privacyProtectScreen <- p "privacyProtectScreen"
notificationMode <- p "notificationMode"
notificationPreviewMode <- p "notificationPreviewMode"
webrtcPolicyRelay <- p "webrtcPolicyRelay"
webrtcICEServers <- p "webrtcICEServers"
confirmRemoteSessions <- p "confirmRemoteSessions"
connectRemoteViaMulticast <- p "connectRemoteViaMulticast"
connectRemoteViaMulticastAuto <- p "connectRemoteViaMulticastAuto"
developerTools <- p "developerTools"
confirmDBUpgrades <- p "confirmDBUpgrades"
iosCallKitEnabled <- p "iosCallKitEnabled"
iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents"
androidCallOnLockScreen <- p "androidCallOnLockScreen"
pure
AppSettings
{ appPlatform,
networkConfig,
privacyEncryptLocalFiles,
privacyAcceptImages,
privacyLinkPreviews,
privacyShowChatPreviews,
privacySaveLastDraft,
privacyProtectScreen,
notificationMode,
notificationPreviewMode,
webrtcPolicyRelay,
webrtcICEServers,
confirmRemoteSessions,
connectRemoteViaMulticast,
connectRemoteViaMulticastAuto,
developerTools,
confirmDBUpgrades,
iosCallKitEnabled,
iosCallKitCallsInRecents,
androidCallOnLockScreen
}
where
p key = v .:? key <|> pure Nothing
parseJSON _ = pure defaultParseAppSettings
readAppSettings :: FilePath -> Maybe AppSettings -> IO AppSettings
readAppSettings f platformDefaults =
combineAppSettings (fromMaybe defaultAppSettings platformDefaults) . fromMaybe defaultParseAppSettings
<$> (J.decodeFileStrict f `catchAll_` pure Nothing)

View File

@ -49,6 +49,7 @@ import Data.Word (Word16)
import Language.Haskell.TH (Exp, Q, runIO) import Language.Haskell.TH (Exp, Q, runIO)
import Numeric.Natural import Numeric.Natural
import qualified Paths_simplex_chat as SC import qualified Paths_simplex_chat as SC
import Simplex.Chat.AppSettings
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Markdown (MarkdownList)
import Simplex.Chat.Messages import Simplex.Chat.Messages
@ -245,6 +246,8 @@ data ChatCommand
| APIExportArchive ArchiveConfig | APIExportArchive ArchiveConfig
| ExportArchive | ExportArchive
| APIImportArchive ArchiveConfig | APIImportArchive ArchiveConfig
| APISaveAppSettings AppSettings
| APIGetAppSettings (Maybe AppSettings)
| APIDeleteStorage | APIDeleteStorage
| APIStorageEncryption DBEncryptionConfig | APIStorageEncryption DBEncryptionConfig
| TestStorageEncryption DBEncryptionKey | TestStorageEncryption DBEncryptionKey
@ -711,6 +714,7 @@ data ChatResponse
| CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRChatError {user_ :: Maybe User, chatError :: ChatError}
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
| CRArchiveImported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]}
| CRAppSettings {appSettings :: AppSettings}
| CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRTimedAction {action :: String, durationMilliseconds :: Int64}
deriving (Show) deriving (Show)

View File

@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240222_app_settings where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240222_app_settings :: Query
m20240222_app_settings =
[sql|
CREATE TABLE app_settings (
app_settings TEXT NOT NULL
);
|]
down_m20240222_app_settings :: Query
down_m20240222_app_settings =
[sql|
DROP TABLE app_settings;
|]

View File

@ -562,6 +562,7 @@ CREATE TABLE note_folders(
favorite INTEGER NOT NULL DEFAULT 0, favorite INTEGER NOT NULL DEFAULT 0,
unread_chat INTEGER NOT NULL DEFAULT 0 unread_chat INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE app_settings(app_settings TEXT NOT NULL);
CREATE INDEX contact_profiles_index ON contact_profiles( CREATE INDEX contact_profiles_index ON contact_profiles(
display_name, display_name,
full_name full_name

View File

@ -0,0 +1,22 @@
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Store.AppSettings where
import Control.Monad (join)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as J
import Data.Maybe (fromMaybe)
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings)
import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
saveAppSettings :: DB.Connection -> AppSettings -> IO ()
saveAppSettings db appSettings = do
DB.execute_ db "DELETE FROM app_settings"
DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings)
getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings
getAppSettings db platformDefaults = do
stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings")
pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_)

View File

@ -99,6 +99,7 @@ import Simplex.Chat.Migrations.M20240104_members_profile_update
import Simplex.Chat.Migrations.M20240115_block_member_for_all import Simplex.Chat.Migrations.M20240115_block_member_for_all
import Simplex.Chat.Migrations.M20240122_indexes import Simplex.Chat.Migrations.M20240122_indexes
import Simplex.Chat.Migrations.M20240214_redirect_file_id import Simplex.Chat.Migrations.M20240214_redirect_file_id
import Simplex.Chat.Migrations.M20240222_app_settings
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -197,7 +198,8 @@ schemaMigrations =
("20240104_members_profile_update", m20240104_members_profile_update, Just down_m20240104_members_profile_update), ("20240104_members_profile_update", m20240104_members_profile_update, Just down_m20240104_members_profile_update),
("20240115_block_member_for_all", m20240115_block_member_for_all, Just down_m20240115_block_member_for_all), ("20240115_block_member_for_all", m20240115_block_member_for_all, Just down_m20240115_block_member_for_all),
("20240122_indexes", m20240122_indexes, Just down_m20240122_indexes), ("20240122_indexes", m20240122_indexes, Just down_m20240122_indexes),
("20240214_redirect_file_id", m20240214_redirect_file_id, Just down_m20240214_redirect_file_id) ("20240214_redirect_file_id", m20240214_redirect_file_id, Just down_m20240214_redirect_file_id),
("20240222_app_settings", m20240222_app_settings, Just down_m20240222_app_settings)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View File

@ -385,6 +385,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
CRTimedAction _ _ -> [] CRTimedAction _ _ -> []
where where
ttyUser :: User -> [StyledString] -> [StyledString] ttyUser :: User -> [StyledString] -> [StyledString]

View File

@ -14,6 +14,9 @@ import Data.Aeson (ToJSON)
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Data.Text as T
import Simplex.Chat.AppSettings (defaultAppSettings)
import qualified Simplex.Chat.AppSettings as AS
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Options (ChatOpts (..))
@ -21,6 +24,7 @@ import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode) import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Version import Simplex.Messaging.Version
import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
import System.FilePath ((</>)) import System.FilePath ((</>))
@ -84,8 +88,9 @@ chatDirectTests = do
it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser
it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages
it "user profile privacy: hide profiles and notificaitons" testUserPrivacy it "user profile privacy: hide profiles and notificaitons" testUserPrivacy
describe "chat item expiration" $ do describe "settings" $ do
it "set chat item TTL" testSetChatItemTTL it "set chat item expiration TTL" testSetChatItemTTL
it "save/get app settings" testAppSettings
describe "connection switch" $ do describe "connection switch" $ do
it "switch contact to a different queue" testSwitchContact it "switch contact to a different queue" testSwitchContact
it "stop switching contact to a different queue" testAbortSwitchContact it "stop switching contact to a different queue" testAbortSwitchContact
@ -2195,6 +2200,24 @@ testSetChatItemTTL =
alice #$> ("/ttl none", id, "ok") alice #$> ("/ttl none", id, "ok")
alice #$> ("/ttl", id, "old messages are not being deleted") alice #$> ("/ttl", id, "old messages are not being deleted")
testAppSettings :: HasCallStack => FilePath -> IO ()
testAppSettings tmp =
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
let settings = T.unpack . safeDecodeUtf8 . LB.toStrict $ J.encode defaultAppSettings
settingsApp = T.unpack . safeDecodeUtf8 . LB.toStrict $ J.encode defaultAppSettings {AS.webrtcICEServers = Just ["non-default.value.com"]}
-- app-provided defaults
alice ##> ("/_get app settings " <> settingsApp)
alice <## ("app settings: " <> settingsApp)
-- parser defaults fallback
alice ##> "/_get app settings"
alice <## ("app settings: " <> settings)
-- store
alice ##> ("/_save app settings " <> settingsApp)
alice <## "ok"
-- read back
alice ##> "/_get app settings"
alice <## ("app settings: " <> settingsApp)
testSwitchContact :: HasCallStack => FilePath -> IO () testSwitchContact :: HasCallStack => FilePath -> IO ()
testSwitchContact = testSwitchContact =
testChat2 aliceProfile bobProfile $ testChat2 aliceProfile bobProfile $