Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f916c5c
issue 852 no prv found - added logs + refactor
flowcrypt-machine-user Oct 30, 2021
974122f
fix build
flowcrypt-machine-user Oct 30, 2021
3c9703d
fix input pass phrase for signing - not tested
flowcrypt-machine-user Oct 30, 2021
198cec1
refactored / fixed handligh pass phrases
flowcrypt-machine-user Oct 30, 2021
a8717e1
add more logs
flowcrypt-machine-user Oct 30, 2021
56fe1e5
fix previous test, added broken test to fix
flowcrypt-machine-user Oct 30, 2021
f2beadf
added log
flowcrypt-machine-user Oct 30, 2021
7efa634
fixing concurrency, refactoring core to async/await
flowcrypt-machine-user Oct 31, 2021
6132336
fix build to some exctent
flowcrypt-machine-user Oct 31, 2021
c81e2e7
fix tests wip
flowcrypt-machine-user Oct 31, 2021
9313875
fixed more tests
flowcrypt-machine-user Oct 31, 2021
6ddd95c
fixed more tests
flowcrypt-machine-user Oct 31, 2021
b7ce07c
render on main thread
flowcrypt-machine-user Oct 31, 2021
46ef5d4
handle errors on main thread if rendering
flowcrypt-machine-user Oct 31, 2021
da08a86
fix main thread rendering
flowcrypt-machine-user Nov 1, 2021
a3d52a7
fix test
flowcrypt-machine-user Nov 1, 2021
7bb2ac9
Realm thread problem and alert thread problem
ivan-ushakov Nov 1, 2021
8e44a7f
Merge branch 'issue-852-no-prv-found' of github.com:FlowCrypt/flowcry…
ivan-ushakov Nov 1, 2021
de8a423
KeyServiceTests fixed and some code tweaks
ivan-ushakov Nov 1, 2021
2b4a7f1
Merge branch 'master' into issue-852-no-prv-found
ivan-ushakov Nov 1, 2021
a41f14c
Merge fixed
ivan-ushakov Nov 1, 2021
4a7def5
UI thread and Realm thread problems fixed
ivan-ushakov Nov 1, 2021
eb5ecb6
Merge branch 'master' into issue-852-no-prv-found
flowcrypt-machine-user Nov 1, 2021
f422b5f
updated comment
flowcrypt-machine-user Nov 1, 2021
436c4b1
fixed appium deps
flowcrypt-machine-user Nov 1, 2021
0328a3c
Pass phrase confirmation alert crash fixed
ivan-ushakov Nov 2, 2021
f777238
Merge branch 'master' into issue-852-no-prv-found
ivan-ushakov Nov 2, 2021
c60ec99
UI test crash fixed
ivan-ushakov Nov 2, 2021
1c6eaa4
Merge branch 'master' into issue-852-no-prv-found
sosnovsky Nov 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

191 changes: 100 additions & 91 deletions FlowCrypt/Controllers/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private struct ComposedDraft: Equatable {
let contextToSend: ComposeMessageContext
}

@MainActor
final class ComposeViewController: TableNodeViewController {
private enum Constants {
static let endTypingCharacters = [",", " ", "\n", ";"]
Expand All @@ -47,6 +48,7 @@ final class ComposeViewController: TableNodeViewController {
private let filesManager: FilesManagerType
private let photosManager: PhotosManagerType
private let keyService: KeyServiceType
private let keyMethods: KeyMethodsType
private let service: ServiceActor
private let passPhraseService: PassPhraseService

Expand Down Expand Up @@ -76,7 +78,8 @@ final class ComposeViewController: TableNodeViewController {
filesManager: FilesManagerType = FilesManager(),
photosManager: PhotosManagerType = PhotosManager(),
keyService: KeyServiceType = KeyService(),
passPhraseService: PassPhraseService = PassPhraseService()
passPhraseService: PassPhraseService = PassPhraseService(),
keyMethods: KeyMethodsType = KeyMethods()
) {
self.email = email
self.notificationCenter = notificationCenter
Expand All @@ -88,6 +91,7 @@ final class ComposeViewController: TableNodeViewController {
self.filesManager = filesManager
self.photosManager = photosManager
self.keyService = keyService
self.keyMethods = keyMethods
self.service = ServiceActor(
composeMessageService: composeMessageService,
contactsService: contactsService,
Expand Down Expand Up @@ -154,55 +158,52 @@ final class ComposeViewController: TableNodeViewController {
// MARK: - Drafts
extension ComposeViewController {
@objc private func startTimer() {
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
self.saveDraftTimer = Timer.scheduledTimer(
timeInterval: 1,
target: self,
selector: #selector(self.saveDraftIfNeeded),
userInfo: nil,
repeats: true)
self.saveDraftTimer?.fire()
saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.saveDraftIfNeeded()
}
saveDraftTimer?.fire()
}

@objc private func stopTimer() {
saveDraftTimer?.invalidate()
saveDraftTimer = nil

saveDraftIfNeeded()
}

private func shouldSaveDraft() -> Bool {
// todo - that draft should only be saved when one of the fields are dirty (edited by user). Right now, a draft may get saved right after rendering compose (or reply), which is annoying.
let newDraft = ComposedDraft(email: email, input: input, contextToSend: contextToSend)

guard let oldDraft = composedLatestDraft else {
composedLatestDraft = newDraft
return true
}

let result = newDraft != oldDraft
composedLatestDraft = newDraft
return result
}

@objc private func saveDraftIfNeeded() {
private func saveDraftIfNeeded() {
guard shouldSaveDraft() else { return }
Task {
guard shouldSaveDraft() else { return }
do {
let signingPrv = try await prepareSigningKey()
let messagevalidationResult = composeMessageService.validateMessage(
let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg(
input: input,
contextToSend: contextToSend,
email: email,
includeAttachments: false,
signingPrv: signingPrv
)
guard case let .success(message) = messagevalidationResult else {
return
try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId)
} catch {
if !(error is MessageValidationError) {
// no need to save or notify user if validation error
// for other errors show toast
// todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open?
showToast("Error saving draft: \(error.localizedDescription)")
}
try await composeMessageService.encryptAndSaveDraft(message: message, threadId: input.threadId)
} catch {}
}
}
}
}
Expand Down Expand Up @@ -329,106 +330,114 @@ extension ComposeViewController {
Task {
do {
let key = try await prepareSigningKey()
sendMessage(key)
} catch {}
try await sendMessage(key)
} catch {
handle(error: error)
}
}
}
}

// MARK: - Message Sending

extension ComposeViewController {
private func prepareSigningKey() async throws -> PrvKeyInfo {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<PrvKeyInfo, Error>) in
guard let signingKey = try? keyService.getSigningKey() else {
let message = "No available private key has your user id \"\(email)\" in it. Please import the appropriate private key."
showAlert(message: message)
continuation.resume(throwing: MessageServiceError.unknown)
return
}
@MainActor private func prepareSigningKey() async throws -> PrvKeyInfo {
guard let signingKey = try await keyService.getSigningKey() else {
throw AppErr.general("None of your private keys have your user id \"\(email)\". Please import the appropriate key.")
}

guard let passphrase = signingKey.passphrase else {
let alert = AlertsFactory.makePassPhraseAlert(
onCancel: { [weak self] in
self?.showAlert(message: "Passphrase is required for message signing")
continuation.resume(throwing: MessageServiceError.unknown)
},
onCompletion: { [weak self] passPhrase in
// save passphrase
let keyInfo = signingKey.copy(with: passPhrase)
self?.savePassPhrases(value: passPhrase, with: [keyInfo])
continuation.resume(returning: keyInfo)
})
present(alert, animated: true, completion: nil)
return
}
continuation.resume(returning: signingKey.copy(with: passphrase))
guard let existingPassPhrase = signingKey.passphrase else {
return signingKey.copy(with: try await self.requestMissingPassPhraseWithModal(for: signingKey))
}

return signingKey.copy(with: existingPassPhrase)
}

private func requestMissingPassPhraseWithModal(for signingKey: PrvKeyInfo) async throws -> String {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) in
let alert = AlertsFactory.makePassPhraseAlert(
onCancel: {
continuation.resume(throwing: AppErr.user("Passphrase is required for message signing"))
},
onCompletion: { [weak self] passPhrase in
guard let self = self else {
continuation.resume(throwing: AppErr.nilSelf)
return
}
Task {
do {
let matched = try await self.handlePassPhraseEntry(passPhrase, for: signingKey)
if matched {
continuation.resume(returning: passPhrase)
} else {
throw AppErr.user("This pass phrase did not match your signing private key")
}
} catch {
continuation.resume(throwing: error)
}
}
}
)
present(alert, animated: true, completion: nil)
}
}

private func savePassPhrases(value passPhrase: String, with privateKeys: [PrvKeyInfo]) {
privateKeys
.map { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) }
.forEach { self.passPhraseService.savePassPhrase(with: $0, storageMethod: .memory) }
private func handlePassPhraseEntry(_ passPhrase: String, for signingKey: PrvKeyInfo) async throws -> Bool {
// since pass phrase was entered (an inconvenient thing for user to do),
// let's find all keys that match and save the pass phrase for all
let allKeys = try await self.keyService.getPrvKeyInfo()
guard allKeys.isNotEmpty else {
// tom - todo - nonsensical error type choice https://github.com/FlowCrypt/flowcrypt-ios/issues/859
// I copied it from another usage, but has to be changed
throw KeyServiceError.retrieve
}
let matchingKeys = try await self.keyMethods.filterByPassPhraseMatch(keys: allKeys, passPhrase: passPhrase)
// save passphrase for all matching keys
self.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys)
// now figure out if the pass phrase also matched the signing prv itself
let matched = matchingKeys.first(where: { $0.fingerprints.first == signingKey.fingerprints.first })
return matched != nil// true if the pass phrase matched signing key
}

private func sendMessage(_ signingKey: PrvKeyInfo) {
private func sendMessage(_ signingKey: PrvKeyInfo) async throws {
view.endEditing(true)
navigationItem.rightBarButtonItem?.isEnabled = false

let spinnerTitle = contextToSend.attachments.isEmpty ? "sending_title" : "encrypting_title"
showSpinner(spinnerTitle.localized)

let selectedRecipients = contextToSend.recipients.filter(\.state.isSelected)
selectedRecipients.forEach(evaluate)
for selectedRecipient in selectedRecipients {
evaluate(recipient: selectedRecipient)
}

// TODO: - fix for spinner
// https://github.com/FlowCrypt/flowcrypt-ios/issues/291
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let result = self.composeMessageService.validateMessage(
input: self.input,
contextToSend: self.contextToSend,
email: self.email,
signingPrv: signingKey
)
switch result {
case .success(let message):
UIApplication.shared.isIdleTimerDisabled = true
self.encryptAndSend(message)
case .failure(let error):
self.handle(error: error)
}
}
}

private func encryptAndSend(_ message: SendableMsg) {
Task {
do {
try await service.encryptAndSend(message: message,
threadId: input.threadId,
progressHandler: { [weak self] progress in
self?.updateSpinner(progress: progress)
})
handleSuccessfullySentMessage()
} catch {
if let error = error as? ComposeMessageError {
handle(error: error)
}
try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg(
input: self.input,
contextToSend: self.contextToSend,
email: self.email,
signingPrv: signingKey
)
UIApplication.shared.isIdleTimerDisabled = true
try await service.encryptAndSend(
message: sendableMsg,
threadId: input.threadId,
progressHandler: { [weak self] progress in
self?.updateSpinner(progress: progress)
}
}
)
handleSuccessfullySentMessage()
}

private func handle(error: ComposeMessageError) {
private func handle(error: Error) {
UIApplication.shared.isIdleTimerDisabled = false
hideSpinner()
navigationItem.rightBarButtonItem?.isEnabled = true

let message = "compose_error".localized
+ "\n\n"
+ error.description

showAlert(message: message)
let err = error as? ComposeMessageError
let description = err?.description ?? error.localizedDescription
showAlert(message: "compose_error".localized + "\n\n" + description)
}

private func handleSuccessfullySentMessage() {
Expand Down
Loading