This commit is contained in:
fmodf 2024-09-19 17:14:05 +02:00
parent 5f5e20b462
commit d0cbe63eb1
8 changed files with 75 additions and 27 deletions

View file

@ -51,7 +51,8 @@ final class ClientMartinChatsManager: Martin.ChatManager {
id: UUID().uuidString, id: UUID().uuidString,
account: context.userBareJid.stringValue, account: context.userBareJid.stringValue,
participant: with.stringValue, participant: with.stringValue,
type: .chat type: .chat,
encrypted: UserSettings.secureChatsByDefault
) )
try Database.shared.dbQueue.write { db in try Database.shared.dbQueue.write { db in
try chat.save(db) try chat.save(db)

View file

@ -117,28 +117,34 @@ extension Client {
var msg = chat.createMessage(text: message.body ?? "??", id: message.id) var msg = chat.createMessage(text: message.body ?? "??", id: message.id)
msg.oob = message.oobUrl msg.oob = message.oobUrl
msg = try await encryptMessage(msg) if message.secure {
msg = try await encryptMessage(msg)
}
try await chat.send(message: 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 // get data from file
guard var data = try? Data(contentsOf: localURL) else { guard var data = try? Data(contentsOf: localURL) else {
throw AppError.noData throw AppError.noData
} }
// encrypt data if needed // encrypt data if needed
let key = try AESGSMEngine.generateKey() var key = Data()
let iv = try AESGSMEngine.generateIV() var iv = Data()
var encrypted = Data() if needEncrypt {
var tag = Data() key = try AESGSMEngine.generateKey()
guard AESGSMEngine.shared.encrypt(iv: iv, key: key, message: data, output: &encrypted, tag: &tag) else { iv = try AESGSMEngine.generateIV()
throw AppError.securityError 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 // attach tag to end of encrypted data
encrypted.append(tag) encrypted.append(tag)
data = encrypted data = encrypted
}
// upload // upload
let httpModule = connection.module(HttpFileUploadModule.self) let httpModule = connection.module(HttpFileUploadModule.self)
@ -164,15 +170,19 @@ extension Client {
let (_, response) = try await URLSession.shared.data(for: request) let (_, response) = try await URLSession.shared.data(for: request)
switch response { switch response {
case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201: case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201:
guard var parts = URLComponents(url: slot.getUri, resolvingAgainstBaseURL: true) else { if needEncrypt {
throw URLError(.badServerResponse) 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: default:
throw URLError(.badServerResponse) throw URLError(.badServerResponse)

View file

@ -14,6 +14,7 @@ struct Chat: DBStorable {
var account: String var account: String
var participant: String var participant: String
var type: ConversationType var type: ConversationType
var encrypted: Bool
} }
extension Chat: Equatable {} extension Chat: Equatable {}

View file

@ -50,7 +50,7 @@ extension Message {
secure = true secure = true
case .successTransportKey: case .successTransportKey:
break return nil
case .failure(let error): case .failure(let error):
logIt(.error, "Error decoding omemo message: \(error)") logIt(.error, "Error decoding omemo message: \(error)")

View file

@ -97,6 +97,10 @@ extension Database {
try db.alter(table: "messages") { table in try db.alter(table: "messages") { table in
table.add(column: "secure", .boolean).notNull().defaults(to: false) 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 // return migrator

View file

@ -12,8 +12,10 @@ final class AttachmentsStore: ObservableObject {
private let client: Client private let client: Client
private let roster: Roster private let roster: Roster
private var secured: Bool = false
private var messagesCancellable: AnyCancellable? private var messagesCancellable: AnyCancellable?
private var chatCancellable: AnyCancellable?
private var processing: Set<String> = [] private var processing: Set<String> = []
init(roster: Roster, client: Client) { init(roster: Roster, client: Client) {
@ -62,7 +64,7 @@ extension AttachmentsStore {
var message = Message.blank var message = Message.blank
message.from = roster.bareJid message.from = roster.bareJid
message.to = roster.contactBareJid message.to = roster.contactBareJid
message.secure = true message.secure = secured
switch item.type { switch item.type {
case .photo: case .photo:
@ -111,7 +113,7 @@ extension AttachmentsStore {
var message = Message.blank var message = Message.blank
message.from = roster.bareJid message.from = roster.bareJid
message.to = roster.contactBareJid message.to = roster.contactBareJid
message.secure = true message.secure = secured
let localName: String let localName: String
let msgType: AttachmentType let msgType: AttachmentType
@ -177,7 +179,7 @@ extension AttachmentsStore {
var message = Message.blank var message = Message.blank
message.from = roster.bareJid message.from = roster.bareJid
message.to = roster.contactBareJid message.to = roster.contactBareJid
message.secure = true message.secure = secured
message.contentType = .attachment( message.contentType = .attachment(
Attachment( Attachment(
type: localName.attachmentType, 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 { guard let localName = attachment.localPath else {
throw AppError.invalidLocalName throw AppError.invalidLocalName
} }
let remotePath = try await client.uploadFile(localName) let remotePath = try await client.uploadFile(localName, needEncrypt: message.secure)
message.contentType = .attachment( message.contentType = .attachment(
Attachment( Attachment(
type: attachment.type, type: attachment.type,

View file

@ -10,8 +10,10 @@ final class MessagesStore: ObservableObject {
private(set) var roster: Roster private(set) var roster: Roster
private let client: Client private let client: Client
private var secured: Bool = false
private var messagesCancellable: AnyCancellable? private var messagesCancellable: AnyCancellable?
private var chatCancellable: AnyCancellable?
private let archiver = ArchiveMessageFetcher() private let archiver = ArchiveMessageFetcher()
init(roster: Roster, client: Client) { init(roster: Roster, client: Client) {
@ -29,7 +31,7 @@ extension MessagesStore {
msg.from = roster.bareJid msg.from = roster.bareJid
msg.to = roster.contactBareJid msg.to = roster.contactBareJid
msg.body = message msg.body = message
msg.secure = true msg.secure = secured
// store as pending on db, and send // store as pending on db, and send
do { do {
@ -72,6 +74,18 @@ private extension MessagesStore {
await self.archiver.initialFetch(messages, self.roster, self.client) 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
}
} }
} }

View file

@ -25,8 +25,12 @@ struct Storage<T> {
// Storage // Storage
private let kOmemoDeviceId = "conversations.classic.user.defaults.omemoDeviceId" private let kOmemoDeviceId = "conversations.classic.user.defaults.omemoDeviceId"
private let kSecureChatsByDefault = "conversations.classic.user.defaults.secureChatsByDefault"
enum UserSettings { enum UserSettings {
@Storage(key: kOmemoDeviceId, defaultValue: 0) @Storage(key: kOmemoDeviceId, defaultValue: 0)
static var omemoDeviceId: UInt32 static var omemoDeviceId: UInt32
@Storage(key: kSecureChatsByDefault, defaultValue: false)
static var secureChatsByDefault: Bool
} }