diff --git a/cabal.project b/cabal.project index 9bbbfda98d..1a7b52f2ce 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: e4842f4f47fb60ef8843dbce6fd43dec96f157d2 + tag: fb21d9836e07706c7498baa967f932cb11b818e5 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 088650ba66..c744faa348 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e4842f4f47fb60ef8843dbce6fd43dec96f157d2" = "191h2v5jcn51haj0mi5y5pm8x8fi9pz49ydxwwzqr3m6zjp21ngg"; + "https://github.com/simplex-chat/simplexmq.git"."fb21d9836e07706c7498baa967f932cb11b818e5" = "0dl08ag38d1azzil1xxi6xrzqwfcv550wi5kjdmxn4h820icl2ja"; "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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b99e41b437..dc06e6b5ab 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -65,6 +65,7 @@ library Simplex.Chat.Migrations.M20221115_server_cfg Simplex.Chat.Migrations.M20221129_delete_group_feature_items Simplex.Chat.Migrations.M20221130_delete_item_deleted + Simplex.Chat.Migrations.M20221209_verified_connection Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index de5ca2ed1c..bbad35301b 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -758,24 +758,53 @@ processChatCommand = \case case memberConnId m of Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) $> CRCmdOk _ -> throwChatError CEGroupMemberNotActive + APIGetContactCode contactId -> withUser $ \user -> do + ct@Contact {activeConn = conn@Connection {connId}} <- withStore $ \db -> getContact db user contactId + code <- getConnectionCode (contactConnId ct) + ct' <- case contactSecurityCode ct of + Just SecurityCode {securityCode} + | sameVerificationCode code securityCode -> pure ct + | otherwise -> do + withStore' $ \db -> setConnectionVerified db user connId Nothing + pure (ct :: Contact) {activeConn = conn {connectionCode = Nothing}} + _ -> pure ct + pure $ CRContactCode ct' code + APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do + (g, m@GroupMember {activeConn}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId + case activeConn of + Just conn@Connection {connId} -> do + code <- getConnectionCode $ aConnId conn + m' <- case memberSecurityCode m of + Just SecurityCode {securityCode} + | sameVerificationCode code securityCode -> pure m + | otherwise -> do + withStore' $ \db -> setConnectionVerified db user connId Nothing + pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} + _ -> pure m + pure $ CRGroupMemberCode g m' code + _ -> throwChatError CEGroupMemberNotActive + APIVerifyContact contactId code -> withUser $ \user -> do + Contact {activeConn} <- withStore $ \db -> getContact db user contactId + verifyConnectionCode user activeConn code + APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId + case activeConn of + Just conn -> verifyConnectionCode user conn code + _ -> throwChatError CEGroupMemberNotActive ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do chatId <- case cType of CTDirect -> withStore $ \db -> getContactIdByName db user name CTGroup -> withStore $ \db -> getGroupIdByName db user name _ -> throwChatError $ CECommandError "not supported" processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ ChatSettings ntfOn - ContactInfo cName -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - processChatCommand $ APIContactInfo contactId - GroupMemberInfo gName mName -> withUser $ \user -> do - (gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName - processChatCommand $ APIGroupMemberInfo gId mId - SwitchContact cName -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - processChatCommand $ APISwitchContact contactId - SwitchGroupMember gName mName -> withUser $ \user -> do - (gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName - processChatCommand $ APISwitchGroupMember gId mId + ContactInfo cName -> withContactName cName APIContactInfo + GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo + SwitchContact cName -> withContactName cName APISwitchContact + SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember + GetContactCode cName -> withContactName cName APIGetContactCode + GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode + VerifyContact cName code -> withContactName cName (`APIVerifyContact` code) + VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do @@ -802,12 +831,8 @@ processChatCommand = \case ConnectSimplex -> withUser $ \user -> -- [incognito] generate profile to send connectViaContact user adminContactReq - DeleteContact cName -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId) - ClearContact cName -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - processChatCommand $ APIClearChat (ChatRef CTDirect contactId) + DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect + ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) CreateMyAddress -> withUser $ \User {userId} -> withChatLock "createMyAddress" . procCmd $ do (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing @@ -977,12 +1002,8 @@ processChatCommand = \case JoinGroup gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIJoinGroup groupId - MemberRole gName groupMemberName memRole -> do - (groupId, groupMemberId) <- getGroupAndMemberId gName groupMemberName - processChatCommand $ APIMemberRole groupId groupMemberId memRole - RemoveMember gName groupMemberName -> do - (groupId, groupMemberId) <- getGroupAndMemberId gName groupMemberName - processChatCommand $ APIRemoveMember groupId groupMemberId + MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole + RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember LeaveGroup gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APILeaveGroup groupId @@ -1135,6 +1156,20 @@ processChatCommand = \case withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk checkStoreNotChanged :: m ChatResponse -> m ChatResponse checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) + withContactName :: ContactName -> (ContactId -> ChatCommand) -> m ChatResponse + withContactName cName cmd = withUser $ \user -> + withStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd + withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> m ChatResponse + withMemberName gName mName cmd = withUser $ \user -> + getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd + getConnectionCode :: ConnId -> m Text + getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) + verifyConnectionCode :: User -> Connection -> Text -> m ChatResponse + verifyConnectionCode user conn@Connection {connId} code = do + code' <- getConnectionCode $ aConnId conn + let verified = sameVerificationCode code code' + when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code' + pure $ CRCodeVerification verified code' 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) @@ -1253,8 +1288,8 @@ processChatCommand = \case _ -> throwChatError CEFileNotReceived {fileId} where forward = processChatCommand . sendCommand chatName - getGroupAndMemberId :: GroupName -> ContactName -> m (GroupId, GroupMemberId) - getGroupAndMemberId gName groupMemberName = withUser $ \user -> do + getGroupAndMemberId :: User -> GroupName -> ContactName -> m (GroupId, GroupMemberId) + getGroupAndMemberId user gName groupMemberName = withStore $ \db -> do groupId <- getGroupIdByName db user gName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName @@ -3391,6 +3426,14 @@ chatCommandP = "/_switch @" *> (APISwitchContact <$> A.decimal), "/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName), ("/switch @" <|> "/switch ") *> (SwitchContact <$> displayName), + "/_get code @" *> (APIGetContactCode <$> A.decimal), + "/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal), + "/_verify code @" *> (APIVerifyContact <$> A.decimal <* A.space <*> textP), + "/_verify code @" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <* A.space <*> textP), + ("/code @" <|> "/code ") *> (GetContactCode <$> displayName), + "/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* optional (A.char '@') <*> displayName), + ("/verify @" <|> "/verify ") *> (VerifyContact <$> displayName <* A.space <*> textP), + "/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <* A.space <*> textP), ("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles, ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bef8de88c2..1235542409 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -200,11 +200,19 @@ data ChatCommand | APIGroupMemberInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId + | APIGetContactCode ContactId + | APIGetGroupMemberCode GroupId GroupMemberId + | APIVerifyContact ContactId Text + | APIVerifyGroupMember GroupId GroupMemberId Text | ShowMessages ChatName Bool | ContactInfo ContactName | GroupMemberInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName + | GetContactCode ContactName + | GetGroupMemberCode GroupName ContactName + | VerifyContact ContactName Text + | VerifyGroupMember GroupName ContactName Text | ChatHelp HelpSection | Welcome | AddContact @@ -276,6 +284,9 @@ data ChatResponse | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} | CRContactSwitch {contact :: Contact, switchProgress :: SwitchProgress} | CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} + | CRContactCode {contact :: Contact, connectionCode :: Text} + | CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} + | CRCodeVerification {verified :: Bool, expectedCode :: Text} | CRNewChatItem {chatItem :: AChatItem} | CRChatItemStatusUpdated {chatItem :: AChatItem} | CRChatItemUpdated {chatItem :: AChatItem} diff --git a/src/Simplex/Chat/Migrations/M20221209_verified_connection.hs b/src/Simplex/Chat/Migrations/M20221209_verified_connection.hs new file mode 100644 index 0000000000..f7e4a8aee9 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20221209_verified_connection.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20221209_verified_connection where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20221209_verified_connection :: Query +m20221209_verified_connection = + [sql| +ALTER TABLE connections ADD COLUMN security_code TEXT NULL; +ALTER TABLE connections ADD COLUMN security_code_verified_at TEXT NULL; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 2ee771ed44..079fc76345 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -258,6 +258,8 @@ CREATE TABLE connections( local_alias DEFAULT '' CHECK(local_alias NOT NULL), via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL), group_link_id BLOB, + security_code TEXT NULL, + security_code_verified_at TEXT NULL, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 0d92f443c9..9e26e2e5f5 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -47,6 +47,7 @@ module Simplex.Chat.Store updateContactUsed, updateContactUnreadChat, updateGroupUnreadChat, + setConnectionVerified, getUserContacts, getUserContactProfiles, createUserContactLink, @@ -304,6 +305,7 @@ import Simplex.Chat.Migrations.M20221112_server_password import Simplex.Chat.Migrations.M20221115_server_cfg import Simplex.Chat.Migrations.M20221129_delete_group_feature_items import Simplex.Chat.Migrations.M20221130_delete_item_deleted +import Simplex.Chat.Migrations.M20221209_verified_connection import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -354,7 +356,8 @@ schemaMigrations = ("20221112_server_password", m20221112_server_password), ("20221115_server_cfg", m20221115_server_cfg), ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items), - ("20221130_delete_item_deleted", m20221130_delete_item_deleted) + ("20221130_delete_item_deleted", m20221130_delete_item_deleted), + ("20221209_verified_connection", m20221209_verified_connection) ] -- | The list of migrations in ascending order by date @@ -465,7 +468,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id @@ -540,7 +543,7 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing} where ent ct = if connType == ct then entityId else Nothing @@ -722,6 +725,11 @@ updateGroupUnreadChat db User {userId} GroupInfo {groupId} unreadChat = do updatedAt <- getCurrentTime DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (unreadChat, updatedAt, userId, groupId) +setConnectionVerified :: DB.Connection -> User -> Int64 -> Maybe Text -> IO () +setConnectionVerified db User {userId} connId code = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId) + updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime @@ -821,7 +829,7 @@ getUserAddressConnections db User {userId} = do db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL @@ -835,7 +843,7 @@ getUserContactLinks db User {userId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -968,7 +976,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ? @@ -1071,7 +1079,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id @@ -1266,7 +1274,7 @@ getContactConnections db userId Contact {contactId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? @@ -1277,14 +1285,15 @@ getContactConnections db userId Contact {contactId} = type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. Only UTCTime +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. Only (Maybe UTCTime) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) = let entityId = entityId_ connType - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, createdAt} + connectionCode = SecurityCode <$> code_ <*> verifiedAt_ + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -1294,8 +1303,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) = - Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) toMaybeConnection _ = Nothing getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact] @@ -1466,7 +1475,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at FROM connections WHERE user_id = ? AND agent_conn_id = ? |] @@ -1564,7 +1573,7 @@ getConnectionById db User {userId} connId = ExceptT $ do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at FROM connections WHERE user_id = ? AND connection_id = ? |] @@ -1609,7 +1618,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -1891,7 +1900,7 @@ getGroupMember db user@User {userId} groupId groupMemberId = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( @@ -1913,7 +1922,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( @@ -1935,7 +1944,7 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( @@ -2060,7 +2069,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} = ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM contacts ct JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -2365,7 +2374,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -2397,7 +2406,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = SELECT ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -3276,7 +3285,7 @@ getDirectChatPreviews_ db user@User {userId} = do ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat, -- ChatItem @@ -3598,7 +3607,7 @@ getContact db user@User {userId} contactId = ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 2c54be0c1c..f03b3d5c63 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -20,6 +20,7 @@ module Simplex.Chat.Types where import Control.Applicative ((<|>)) +import Crypto.Number.Serialize (os2ip) import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE @@ -126,6 +127,9 @@ directContact Contact {contactUsed, activeConn = Connection {connLevel, viaGroup anyDirectContact :: Contact -> Bool anyDirectContact Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed +contactSecurityCode :: Contact -> Maybe SecurityCode +contactSecurityCode Contact {activeConn} = connectionCode activeConn + data ContactRef = ContactRef { contactId :: ContactId, localDisplayName :: ContactName @@ -866,6 +870,9 @@ groupMemberId' GroupMember {groupMemberId} = groupMemberId memberIncognito :: GroupMember -> Bool memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId +memberSecurityCode :: GroupMember -> Maybe SecurityCode +memberSecurityCode GroupMember {activeConn} = connectionCode =<< activeConn + data NewGroupMember = NewGroupMember { memInfo :: MemberInfo, memCategory :: GroupMemberCategory, @@ -1302,10 +1309,29 @@ data Connection = Connection connStatus :: ConnStatus, localAlias :: Text, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID + connectionCode :: Maybe SecurityCode, createdAt :: UTCTime } deriving (Eq, Show, Generic) +data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} + deriving (Eq, Show, Generic) + +instance ToJSON SecurityCode where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + +verificationCode :: ByteString -> Text +verificationCode = T.pack . unwords . chunks 5 . show . os2ip + where + chunks _ [] = [] + chunks n xs = let (h, t) = splitAt n xs in h : chunks n t + +sameVerificationCode :: Text -> Text -> Bool +sameVerificationCode c1 c2 = noSpaces c1 == noSpaces c2 + where + noSpaces = T.filter (/= ' ') + aConnId :: Connection -> ConnId aConnId Connection {agentConnId = AgentConnId cId} = cId diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3c4382eb81..b12053f2af 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -74,6 +74,9 @@ responseToView user_ testView ts = \case CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats CRContactSwitch ct progress -> viewContactSwitch ct progress CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress + CRCodeVerification verified code -> [plain $ if verified then "connection verified" else "error: current connection code is " <> code] + CRContactCode ct code -> viewContactCode ct code testView + CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems CRChatItemStatusUpdated _ -> [] @@ -497,8 +500,8 @@ viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = ] viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString] -viewDirectMessagesProhibited MDSnd c = [ "direct messages to indirect contact " <> ttyContact' c <> " are prohibited"] -viewDirectMessagesProhibited MDRcv c = [ "received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"] +viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"] +viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"] viewUserJoinedGroup :: GroupInfo -> [StyledString] viewUserJoinedGroup g@GroupInfo {membership = membership@GroupMember {memberProfile}} = @@ -681,21 +684,27 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = ] viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewConnectionStats stats <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) incognitoProfile - <> if localAlias /= "" then ["alias: " <> plain localAlias] else ["alias not set"] + <> ["alias: " <> plain localAlias | localAlias /= ""] + <> [viewConnectionVerified (contactSecurityCode ct)] viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats = +viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats - <> if localAlias /= "" then ["alias: " <> plain localAlias] else ["no alias for contact"] + <> ["alias: " <> plain localAlias | localAlias /= ""] + <> [viewConnectionVerified (memberSecurityCode m) | isJust stats] + +viewConnectionVerified :: Maybe SecurityCode -> StyledString +viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time? +viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code" viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvServers, sndServers} = @@ -720,6 +729,17 @@ viewGroupMemberSwitch g m (SwitchProgress qd phase _) = case qd of QDRcv -> [ttyGroup' g <> ": you " <> viewSwitchPhase phase <> " for " <> ttyMember m] QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"] +viewContactCode :: Contact -> Text -> Bool -> [StyledString] +viewContactCode ct@Contact {localDisplayName = c} = viewSecurityCode (ttyContact' ct) ("/verify " <> c <> " ") + +viewGroupMemberCode :: GroupInfo -> GroupMember -> Text -> Bool -> [StyledString] +viewGroupMemberCode g m@GroupMember {localDisplayName = n} = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> groupName' g <> " " <> n <> " ") + +viewSecurityCode :: StyledString -> Text -> Text -> Bool -> [StyledString] +viewSecurityCode name cmd code testView + | testView = [plain code] + | otherwise = [name <> " security code:", plain code, "pass this code to your contact and use " <> highlight cmd <> " to verify"] + viewSwitchPhase :: SwitchPhase -> StyledString viewSwitchPhase SPCompleted = "changed address" viewSwitchPhase phase = plain (strEncode phase) <> " changing address" diff --git a/stack.yaml b/stack.yaml index ca7450fedf..3685176bae 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: e4842f4f47fb60ef8843dbce6fd43dec96f157d2 + commit: fb21d9836e07706c7498baa967f932cb11b818e5 # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 4eb9159f55..b3faefbef8 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -27,6 +27,7 @@ import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Store (getUserContactProfiles) import Simplex.Chat.Types import Simplex.Messaging.Agent.Store.SQLite (withTransaction) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util (unlessM) import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist) @@ -168,6 +169,13 @@ chatTests = do describe "queue rotation" $ do it "switch contact to a different queue" testSwitchContact it "switch group member to a different queue" testSwitchGroupMember + describe "connection verification code" $ do + it "verificationCode function converts ByteString to series of digits" $ + verificationCode (C.sha256Hash "abcd") `shouldBe` "61889 38426 63934 09576 96390 79389 84124 85253 63658 69469 70853 37788 95900 68296 20156 25" + it "sameVerificationCode function should ignore spaces" $ + sameVerificationCode "123 456 789" "12345 6789" `shouldBe` True + it "mark contact verified" testMarkContactVerified + it "mark group member verified" testMarkGroupMemberVerified versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix2 runTest = do @@ -4551,6 +4559,58 @@ testSwitchGroupMember = bob #> "#team hi" alice <# "#team bob> hi" +testMarkContactVerified :: IO () +testMarkContactVerified = + testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice ##> "/i bob" + bobInfo alice + alice <## "connection not verified, use /code command to see security code" + alice ##> "/code bob" + bCode <- getTermLine alice + bob ##> "/code alice" + aCode <- getTermLine bob + bCode `shouldBe` aCode + alice ##> "/verify bob 123" + alice <##. "error: current connection code is " + alice ##> ("/verify bob " <> aCode) + alice <## "connection verified" + alice ##> "/i bob" + bobInfo alice + alice <## "connection verified" + where + bobInfo alice = do + alice <## "contact ID: 2" + alice <## "receiving messages via: localhost" + alice <## "sending messages via: localhost" + alice <## "you've shared main profile with this contact" + +testMarkGroupMemberVerified :: IO () +testMarkGroupMemberVerified = + testChat2 aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + alice ##> "/i #team bob" + bobInfo alice + alice <## "connection not verified, use /code command to see security code" + alice ##> "/code #team bob" + bCode <- getTermLine alice + bob ##> "/code #team alice" + aCode <- getTermLine bob + bCode `shouldBe` aCode + alice ##> "/verify #team bob 123" + alice <##. "error: current connection code is " + alice ##> ("/verify #team bob " <> aCode) + alice <## "connection verified" + alice ##> "/i #team bob" + bobInfo alice + alice <## "connection verified" + where + bobInfo alice = do + alice <## "group ID: 1" + alice <## "member ID: 2" + alice <## "receiving messages via: localhost" + alice <## "sending messages via: localhost" + withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a withTestChatContactConnected dbPrefix action = withTestChat dbPrefix $ \cc -> do