diff --git a/ConversationsClassic/AppData/Client/Client+MartinChats.swift b/ConversationsClassic/AppData/Client/Client+MartinChats.swift index 20befc7..ab93e69 100644 --- a/ConversationsClassic/AppData/Client/Client+MartinChats.swift +++ b/ConversationsClassic/AppData/Client/Client+MartinChats.swift @@ -51,7 +51,8 @@ final class ClientMartinChatsManager: Martin.ChatManager { id: UUID().uuidString, account: context.userBareJid.stringValue, participant: with.stringValue, - type: .chat + type: .chat, + encrypted: UserSettings.secureChatsByDefault ) try Database.shared.dbQueue.write { db in try chat.save(db) diff --git a/ConversationsClassic/AppData/Client/Client.swift b/ConversationsClassic/AppData/Client/Client.swift index 9155f0b..45b4a15 100644 --- a/ConversationsClassic/AppData/Client/Client.swift +++ b/ConversationsClassic/AppData/Client/Client.swift @@ -117,28 +117,34 @@ extension Client { var msg = chat.createMessage(text: message.body ?? "??", id: message.id) msg.oob = message.oobUrl - msg = try await encryptMessage(msg) + if message.secure { + msg = try await encryptMessage(msg) + } try await chat.send(message: msg) } - func uploadFile(_ localURL: URL) async throws -> String { + func uploadFile(_ localURL: URL, needEncrypt: Bool) async throws -> String { // get data from file guard var data = try? Data(contentsOf: localURL) else { throw AppError.noData } // encrypt data if needed - let key = try AESGSMEngine.generateKey() - let iv = try AESGSMEngine.generateIV() - var encrypted = Data() - var tag = Data() - guard AESGSMEngine.shared.encrypt(iv: iv, key: key, message: data, output: &encrypted, tag: &tag) else { - throw AppError.securityError - } + var key = Data() + var iv = Data() + if needEncrypt { + key = try AESGSMEngine.generateKey() + iv = try AESGSMEngine.generateIV() + var encrypted = Data() + var tag = Data() + guard AESGSMEngine.shared.encrypt(iv: iv, key: key, message: data, output: &encrypted, tag: &tag) else { + throw AppError.securityError + } - // attach tag to end of encrypted data - encrypted.append(tag) - data = encrypted + // attach tag to end of encrypted data + encrypted.append(tag) + data = encrypted + } // upload let httpModule = connection.module(HttpFileUploadModule.self) @@ -164,15 +170,19 @@ extension Client { let (_, response) = try await URLSession.shared.data(for: request) switch response { case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201: - guard var parts = URLComponents(url: slot.getUri, resolvingAgainstBaseURL: true) else { - throw URLError(.badServerResponse) + if needEncrypt { + guard var parts = URLComponents(url: slot.getUri, resolvingAgainstBaseURL: true) else { + throw URLError(.badServerResponse) + } + parts.scheme = "aesgcm" + parts.fragment = (iv + key).map { String(format: "%02x", $0) }.joined() + guard let shareUrl = parts.url else { + throw URLError(.badServerResponse) + } + return shareUrl.absoluteString + } else { + return slot.getUri.absoluteString } - parts.scheme = "aesgcm" - parts.fragment = (iv + key).map { String(format: "%02x", $0) }.joined() - guard let shareUrl = parts.url else { - throw URLError(.badServerResponse) - } - return shareUrl.absoluteString default: throw URLError(.badServerResponse) diff --git a/ConversationsClassic/AppData/Model/Chat.swift b/ConversationsClassic/AppData/Model/Chat.swift index 0f49c7b..289b983 100644 --- a/ConversationsClassic/AppData/Model/Chat.swift +++ b/ConversationsClassic/AppData/Model/Chat.swift @@ -14,6 +14,7 @@ struct Chat: DBStorable { var account: String var participant: String var type: ConversationType + var encrypted: Bool } extension Chat: Equatable {} diff --git a/ConversationsClassic/AppData/Model/Message+OMEMO.swift b/ConversationsClassic/AppData/Model/Message+OMEMO.swift index 773496b..1834056 100644 --- a/ConversationsClassic/AppData/Model/Message+OMEMO.swift +++ b/ConversationsClassic/AppData/Model/Message+OMEMO.swift @@ -50,7 +50,7 @@ extension Message { secure = true case .successTransportKey: - break + return nil case .failure(let error): logIt(.error, "Error decoding omemo message: \(error)") diff --git a/ConversationsClassic/AppData/Services/Database+Migrations.swift b/ConversationsClassic/AppData/Services/Database+Migrations.swift index 474acf1..b6a2f7a 100644 --- a/ConversationsClassic/AppData/Services/Database+Migrations.swift +++ b/ConversationsClassic/AppData/Services/Database+Migrations.swift @@ -97,6 +97,10 @@ extension Database { try db.alter(table: "messages") { table in table.add(column: "secure", .boolean).notNull().defaults(to: false) } + + try db.alter(table: "chats") { table in + table.add(column: "encrypted", .boolean).notNull().defaults(to: false) + } } // return migrator diff --git a/ConversationsClassic/AppData/Store/AttachmentsStore.swift b/ConversationsClassic/AppData/Store/AttachmentsStore.swift index 1a124e9..25aa63b 100644 --- a/ConversationsClassic/AppData/Store/AttachmentsStore.swift +++ b/ConversationsClassic/AppData/Store/AttachmentsStore.swift @@ -12,8 +12,10 @@ final class AttachmentsStore: ObservableObject { private let client: Client private let roster: Roster + private var secured: Bool = false private var messagesCancellable: AnyCancellable? + private var chatCancellable: AnyCancellable? private var processing: Set = [] init(roster: Roster, client: Client) { @@ -62,7 +64,7 @@ extension AttachmentsStore { var message = Message.blank message.from = roster.bareJid message.to = roster.contactBareJid - message.secure = true + message.secure = secured switch item.type { case .photo: @@ -111,7 +113,7 @@ extension AttachmentsStore { var message = Message.blank message.from = roster.bareJid message.to = roster.contactBareJid - message.secure = true + message.secure = secured let localName: String let msgType: AttachmentType @@ -177,7 +179,7 @@ extension AttachmentsStore { var message = Message.blank message.from = roster.bareJid message.to = roster.contactBareJid - message.secure = true + message.secure = secured message.contentType = .attachment( Attachment( type: localName.attachmentType, @@ -241,6 +243,18 @@ private extension AttachmentsStore { } } } + + chatCancellable = ValueObservation.tracking(Chat + .filter(Column("bareJid") == roster.bareJid && Column("contactBareJid") == roster.contactBareJid) + .fetchOne + ) + .publisher(in: Database.shared.dbQueue, scheduling: .immediate) + .receive(on: DispatchQueue.main) + .sink { _ in + } receiveValue: { [weak self] chat in + guard let self = self else { return } + self.secured = chat?.encrypted ?? false + } } } @@ -256,7 +270,7 @@ extension AttachmentsStore { guard let localName = attachment.localPath else { throw AppError.invalidLocalName } - let remotePath = try await client.uploadFile(localName) + let remotePath = try await client.uploadFile(localName, needEncrypt: message.secure) message.contentType = .attachment( Attachment( type: attachment.type, diff --git a/ConversationsClassic/AppData/Store/MessagesStore.swift b/ConversationsClassic/AppData/Store/MessagesStore.swift index 4e0cffa..104493c 100644 --- a/ConversationsClassic/AppData/Store/MessagesStore.swift +++ b/ConversationsClassic/AppData/Store/MessagesStore.swift @@ -10,8 +10,10 @@ final class MessagesStore: ObservableObject { private(set) var roster: Roster private let client: Client + private var secured: Bool = false private var messagesCancellable: AnyCancellable? + private var chatCancellable: AnyCancellable? private let archiver = ArchiveMessageFetcher() init(roster: Roster, client: Client) { @@ -29,7 +31,7 @@ extension MessagesStore { msg.from = roster.bareJid msg.to = roster.contactBareJid msg.body = message - msg.secure = true + msg.secure = secured // store as pending on db, and send do { @@ -72,6 +74,18 @@ private extension MessagesStore { await self.archiver.initialFetch(messages, self.roster, self.client) } } + + chatCancellable = ValueObservation.tracking(Chat + .filter(Column("bareJid") == roster.bareJid && Column("contactBareJid") == roster.contactBareJid) + .fetchOne + ) + .publisher(in: Database.shared.dbQueue, scheduling: .immediate) + .receive(on: DispatchQueue.main) + .sink { _ in + } receiveValue: { [weak self] chat in + guard let self = self else { return } + self.secured = chat?.encrypted ?? false + } } } diff --git a/ConversationsClassic/Helpers/UserDefaultsWrapper.swift b/ConversationsClassic/Helpers/UserDefaultsWrapper.swift index de362aa..a72fde2 100644 --- a/ConversationsClassic/Helpers/UserDefaultsWrapper.swift +++ b/ConversationsClassic/Helpers/UserDefaultsWrapper.swift @@ -25,8 +25,12 @@ struct Storage { // Storage private let kOmemoDeviceId = "conversations.classic.user.defaults.omemoDeviceId" +private let kSecureChatsByDefault = "conversations.classic.user.defaults.secureChatsByDefault" enum UserSettings { @Storage(key: kOmemoDeviceId, defaultValue: 0) static var omemoDeviceId: UInt32 + + @Storage(key: kSecureChatsByDefault, defaultValue: false) + static var secureChatsByDefault: Bool }