Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1233 Decrypt attachments on tap #1323

Merged
merged 9 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
7,403 changes: 2,615 additions & 4,788 deletions Core/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@types/encoding-japanese": "^1.0.18",
"@types/node": "^17.0.10",
"@types/node-cleanup": "2.1.2",
"ava": "1.4.1",
"ava": "4.0.1",
"babel-loader": "8.2.3",
"babel-preset-env": "1.7.0",
"chai": "4.3.4",
Expand Down
4 changes: 4 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; };
514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; };
5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; };
51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */; };
5168FB0B274F94D300131072 /* MessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5168FB0A274F94D300131072 /* MessageAttachment.swift */; };
51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; };
51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C38270C7D2400D7C944 /* StorageMethod.swift */; };
Expand Down Expand Up @@ -503,6 +504,7 @@
514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = "<group>"; };
514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = "<group>"; };
5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = "<group>"; };
51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessage.swift; sourceTree = "<group>"; };
5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = "<group>"; };
51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = "<group>"; };
51775C38270C7D2400D7C944 /* StorageMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMethod.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1460,6 +1462,7 @@
9F9362182573D10E0009912F /* Imap+Message.swift */,
9FA9C83B264C2D75005A9670 /* MessageService.swift */,
5168FB0A274F94D300131072 /* MessageAttachment.swift */,
51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */,
);
path = "Message Provider";
sourceTree = "<group>";
Expand Down Expand Up @@ -2679,6 +2682,7 @@
9F0C3C1A231819C500299985 /* MessageKindProviderType.swift in Sources */,
21C7DF09266C0D8F00C44800 /* EnterpriseServerApi.swift in Sources */,
9FF0670825520CF800FCC9E6 /* GmailService.swift in Sources */,
51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */,
215897E8267A553300423694 /* FilesManager.swift in Sources */,
9F5C2A77257D705100DE9B4B /* MessageLabel.swift in Sources */,
9F93623F2573D16F0009912F /* Gmail+Message.swift in Sources */,
Expand Down
103 changes: 68 additions & 35 deletions FlowCrypt/Controllers/Attachment/AttachmentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,26 @@ final class AttachmentViewController: UIViewController {
configuration.defaultWebpagePreferences = preferences

let view = WKWebView(frame: .zero, configuration: configuration)
view.translatesAutoresizingMaskIntoConstraints = false
view.navigationDelegate = self
view.backgroundColor = .backgroundColor
view.allowsLinkPreview = false
view.accessibilityIdentifier = "aid-attachment-web-view"
return view
}()

private lazy var textView: UITextView = {
let textView = UITextView(frame: .zero)
textView.textContainerInset = .deviceSpecificTextInsets(top: 16, bottom: 16)
textView.isEditable = false
textView.font = .systemFont(ofSize: 14)
textView.textColor = .textColor
textView.backgroundColor = .backgroundColor
textView.accessibilityIdentifier = "aid-attachment-text-view"
return textView
}()

private let errorLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
label.text = "no_preview_avalable".localized
label.isHidden = true
Expand All @@ -66,10 +76,11 @@ final class AttachmentViewController: UIViewController {
}

private var isNavigated = false
private var didLayoutSubviews = false

init(
file: FileType,
shouldShowDownloadButton: Bool,
shouldShowDownloadButton: Bool = true,
filesManager: FilesManagerType = FilesManager()
) {
self.file = file
Expand All @@ -85,38 +96,76 @@ final class AttachmentViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

setupNavigationBar()
setupUI()
showSpinner()
title = file.name
}

Task {
do {
try await setContentRules()
try load()
} catch {
logger.logError("Preview Failed due to \(error.localizedDescription)")
// todo - exact error should be surfaced to user?
errorLabel.isHidden = false
hideSpinner()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

guard !didLayoutSubviews else { return }
didLayoutSubviews = true

renderAttachment()
}

private func setupNavigationBar() {
title = file.name

guard shouldShowDownloadButton else { return }

let imageConfiguration = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 24, weight: .light))
let image = UIImage(systemName: "square.and.arrow.down", withConfiguration: imageConfiguration)

navigationItem.rightBarButtonItem = NavigationBarItemsView(
with: [
NavigationBarItemsView.Input(
image: UIImage(named: "download")?.tinted(.gray),
title: "save".localized,
image: image?.tinted(.gray),
accessibilityId: "aid-save-attachment-to-device",
onTap: { [weak self] in self?.downloadAttachment() }
)
]
)
}

private func renderAttachment() {
switch file.type {
case "text/plain":
showTextAttachment()
default:
renderAttachmentData()
}
}

private func showTextAttachment() {
view.addSubview(textView)
view.constrainToEdges(textView)
textView.text = file.data.toStr()
textView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false)
}

private func renderAttachmentData() {
view.addSubview(webView)
webView.addSubview(errorLabel)

view.constrainToEdges(webView)
webView.constrainToEdges(errorLabel)

showSpinner()

Task {
do {
try await setContentRules()
try load()
} catch {
logger.logError("Preview Failed due to \(error.localizedDescription)")
// todo - exact error should be surfaced to user?
errorLabel.isHidden = false
hideSpinner()
}
}
}

private func downloadAttachment() {
Task {
await attachmentManager.download(file)
Expand Down Expand Up @@ -168,20 +217,4 @@ private extension AttachmentViewController {
logger.logDebug("base64 encoding time is \(encoderTrace.finish())")
webView.load(URLRequest(url: url))
}

private func setupUI() {
view.addSubview(webView)
webView.addSubview(errorLabel)

NSLayoutConstraint.activate([
webView.leftAnchor.constraint(equalTo: view.leftAnchor),
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.rightAnchor.constraint(equalTo: view.rightAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
errorLabel.leftAnchor.constraint(equalTo: webView.leftAnchor),
errorLabel.topAnchor.constraint(equalTo: webView.topAnchor),
errorLabel.rightAnchor.constraint(equalTo: webView.rightAnchor),
errorLabel.bottomAnchor.constraint(equalTo: webView.bottomAnchor)
])
}
}
3 changes: 2 additions & 1 deletion FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ extension AttachmentNode.Input {
.attributed(.regular(18), color: .mainTextColor, alignment: .left),
size: attachment.formattedSize
.attributed(.medium(12), color: .mainTextColor, alignment: .left),
index: index
index: index,
isEncrypted: false
)
}
}
7 changes: 4 additions & 3 deletions FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ extension AttachmentNode.Input {
init(msgAttachment: MessageAttachment, index: Int) {
self.init(
name: msgAttachment.name
.attributed(.regular(18), color: .textColor, alignment: .left),
.attributed(.regular(16), color: .textColor, alignment: .left),
size: msgAttachment.formattedSize
.attributed(.medium(12), color: .textColor, alignment: .left),
index: index
.attributed(.medium(12), color: .secondaryLabel, alignment: .left),
index: index,
isEncrypted: msgAttachment.isEncrypted
)
}
}
Expand Down
69 changes: 61 additions & 8 deletions FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ extension ThreadDetailsViewController {
present(alert, animated: true, completion: nil)
}

private func handleAttachmentTap(at indexPath: IndexPath) {
Task {
do {
let attachment = try await getAttachment(at: indexPath)
show(attachment: attachment)
} catch {
handleAttachmentDecryptError(error, at: indexPath)
}

}
}

private func getAttachment(at indexPath: IndexPath) async throws -> MessageAttachment {
let section = input[indexPath.section-1]
let attachmentIndex = indexPath.row - 2

guard let attachment = section.processedMessage?.attachments[attachmentIndex]
else { throw MessageServiceError.attachmentNotFound }

if attachment.isEncrypted {
let decryptedAttachment = try await messageService.decrypt(attachment: attachment)
input[indexPath.section-1].processedMessage?.attachments[attachmentIndex] = decryptedAttachment
node.reloadRows(at: [indexPath], with: .automatic)
return decryptedAttachment
} else {
return attachment
}
}

private func show(attachment: MessageAttachment) {
let attachmentViewController = AttachmentViewController(file: attachment)
navigationController?.pushViewController(attachmentViewController, animated: true)
}

private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) {
guard let input = input[safe: indexPath.section-1],
let processedMessage = input.processedMessage
Expand Down Expand Up @@ -309,6 +343,30 @@ extension ThreadDetailsViewController {
}
}

private func handleAttachmentDecryptError(_ error: Error, at indexPath: IndexPath) {
let message = "message_attachment_corrupted_file".localized

let alertController = UIAlertController(
title: "message_attachment_decrypt_error".localized,
message: "\n\(error.errorMessage)\n\n\(message)",
preferredStyle: .alert
)

let downloadAction = UIAlertAction(title: "download".localized, style: .default) { [weak self] _ in
guard let self = self,
let attachment = self.input[indexPath.section-1].processedMessage?.attachments[indexPath.row-2]
else { return }

self.show(attachment: attachment)
}
let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel)

alertController.addAction(downloadAction)
alertController.addAction(cancelAction)

present(alertController, animated: true)
}

private func handleMissedPassPhrase(for rawMimeData: Data, at indexPath: IndexPath) {
let alert = AlertsFactory.makePassPhraseAlert(
onCancel: { [weak self] in
Expand Down Expand Up @@ -417,7 +475,7 @@ extension ThreadDetailsViewController: MessageActionsHandler {
}

func handleMarkUnreadTap() {
let messages = input.filter { $0.isExpanded }.map(\.rawMessage)
let messages = input.filter(\.isExpanded).map(\.rawMessage)

guard messages.isNotEmpty else { return }

Expand Down Expand Up @@ -505,12 +563,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource {
case is ThreadMessageInfoCellNode:
handleExpandTap(at: indexPath)
case is AttachmentNode:
let section = self.input[indexPath.section-1]
guard let attachment = section.processedMessage?.attachments[indexPath.row - 2] else { return }
navigationController?.pushViewController(
AttachmentViewController(file: attachment, shouldShowDownloadButton: true),
animated: true
)
handleAttachmentTap(at: indexPath)
default: return
}
}
Expand All @@ -537,7 +590,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource {
extension ThreadDetailsViewController: NavigationChildController {
func handleBackButtonTap() {
let isRead = input.contains(where: { $0.rawMessage.isMessageRead })
logger.logInfo("Back button. Are all messages read \(isRead) ")
logger.logInfo("Back button. Are all messages read \(isRead)")
onComplete(MessageAction.markAsRead(isRead), .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: self.user.email))
navigationController?.popViewController(animated: true)
}
Expand Down
33 changes: 19 additions & 14 deletions FlowCrypt/Core/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,35 +80,40 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
struct DecryptFileRaw: Decodable {
let decryptSuccess: DecryptSuccess?
let decryptErr: DecryptErr?

struct DecryptSuccess: Decodable {
let name: String
}
}
let json: [String : Any?]? = [

let decrypted = try await call("decryptFile", jsonDict: [
"keys": try keys.map { try $0.toJsonEncodedDict() },
"msgPwd": msgPwd
]
let decrypted = try await call("decryptFile", jsonDict: json, data: encrypted)
], data: encrypted)

let decryptFileRes = try decrypted.json.decodeJson(as: DecryptFileRaw.self)

if let decryptErr = decryptFileRes.decryptErr {
return CoreRes.DecryptFile(
decryptSuccess: nil,
decryptErr: decryptErr
)
}
if let decryptSuccess = decryptFileRes.decryptSuccess {
return CoreRes.DecryptFile(
decryptSuccess: CoreRes.DecryptFile.DecryptSuccess(
name: decryptSuccess.name,
data: decrypted.data
),
decryptErr: nil
)

guard let decryptSuccess = decryptFileRes.decryptSuccess else {
throw AppErr.unexpected("decryptFile: both decryptErr and decryptSuccess were nil")
}
throw AppErr.unexpected("decryptFile: both decryptErr and decryptSuccess were nil")

return CoreRes.DecryptFile(
decryptSuccess: CoreRes.DecryptFile.DecryptSuccess(
name: decryptSuccess.name,
data: decrypted.data
),
decryptErr: nil
)
}

func encrypt(file: Data, name: String, pubKeys: [String]?) async throws -> Data {
func encrypt(file: Data, name: String, pubKeys: [String]?) async throws -> Data {
let json: [String: Any?]? = [
"pubKeys": pubKeys,
"name": name
Expand Down Expand Up @@ -149,7 +154,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
let replyType: ReplyType
let text: String
}
let json: [String : Any?]? = [
let json: [String: Any?]? = [
"keys": try keys.map { try $0.toJsonEncodedDict() },
"isEmail": isEmail,
"msgPwd": msgPwd,
Expand Down
Loading