[ios] use uploadNative for blob attachments

Problem:
* the mailModel would decide which upload mthod to use according to the
  attachment type (DataFile -> normal, FileReference -> native)
* getLogs returns DataFiles
* blob access tokens reference https:// blobstore URLs
* these shouldn't be used by the webview (only asset: and apis: URLs)

Fix (has at least these possibilities):
 * rewrite blobstore URLs to apis: protocol before giving them to mail
   model. continue using the normal upload, the URL will be rewritten to
   https: on the native side again
 * write the data file to disk and upload the blob with the normal
   native method, which is allowed to use https: URLS.
 * add a method to blobFacade that allows natively uploading byte arrays
 * somehow get a FileRef from getLogs

this commit uses the second method. it mirrors the way other attachments
are uploaded in the app and doesn't complicate the IPC with additional
params or methods.

fix #4361
This commit is contained in:
nig 2022-08-09 15:04:15 +02:00 committed by ganthern
parent a86793e7e8
commit 6f4600f940
3 changed files with 68 additions and 60 deletions

View file

@ -4,7 +4,7 @@ import {Indexer} from "./search/Indexer"
import type {EntityRestInterface} from "./rest/EntityRestClient"
import {EntityRestClient} from "./rest/EntityRestClient"
import {UserManagementFacade} from "./facades/UserManagementFacade"
import {DefaultEntityRestCache} from "./rest/DefaultEntityRestCache.js"
import {CacheStorage, DefaultEntityRestCache} from "./rest/DefaultEntityRestCache.js"
import {GroupManagementFacade} from "./facades/GroupManagementFacade"
import {MailFacade} from "./facades/MailFacade"
import {MailAddressFacade} from "./facades/MailAddressFacade"
@ -42,7 +42,6 @@ import {IServiceExecutor} from "../common/ServiceRequest"
import {ServiceExecutor} from "./rest/ServiceExecutor"
import {BookingFacade} from "./facades/BookingFacade"
import {BlobFacade} from "./facades/BlobFacade"
import {CacheStorage} from "./rest/DefaultEntityRestCache.js"
import {UserFacade} from "./facades/UserFacade"
import {OfflineStorage} from "./offline/OfflineStorage.js"
import {exposeNativeInterface} from "../common/ExposeNativeInterface"
@ -194,7 +193,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
const aesApp = new AesApp(new NativeCryptoFacadeSendDispatcher(worker), random)
locator.blob = new BlobFacade(locator.user, locator.serviceExecutor, locator.restClient, suspensionHandler, fileApp, aesApp, locator.instanceMapper, locator.crypto)
locator.file = new FileFacade(locator.user, locator.restClient, suspensionHandler, fileApp, aesApp, locator.instanceMapper, locator.serviceExecutor, locator.crypto)
locator.mail = new MailFacade(locator.user, locator.file, locator.cachingEntityClient, locator.crypto, locator.serviceExecutor, locator.blob)
locator.mail = new MailFacade(locator.user, locator.file, locator.cachingEntityClient, locator.crypto, locator.serviceExecutor, locator.blob, fileApp)
const nativePushFacade = new NativePushFacadeSendDispatcher(worker)
// not needed for admin client
if (!isAdminClient()) {

View file

@ -42,8 +42,10 @@ import {
createDeleteMailFolderData,
createDraftAttachment,
createDraftCreateData,
createDraftData, createDraftRecipient,
createDraftUpdateData, createEncryptedMailAddress,
createDraftData,
createDraftRecipient,
createDraftUpdateData,
createEncryptedMailAddress,
createExternalUserData,
createListUnsubscribeData,
createMoveMailData,
@ -60,7 +62,8 @@ import {NotFoundError} from "../../common/error/RestError"
import type {EntityUpdate, PublicKeyReturn, User} from "../../entities/sys/TypeRefs.js"
import {
BlobReferenceTokenWrapper,
createPublicKeyData, CustomerTypeRef,
createPublicKeyData,
CustomerTypeRef,
ExternalUserReferenceTypeRef,
GroupInfoTypeRef,
GroupRootTypeRef,
@ -73,7 +76,6 @@ import {
byteLength,
contains,
defer,
downcast,
isNotNull,
isSameTypeRefByAttr,
neverNull,
@ -84,7 +86,7 @@ import {
} from "@tutao/tutanota-utils"
import {BlobFacade} from "./BlobFacade"
import {FileFacade} from "./FileFacade"
import {assertWorkerOrNode, isApp} from "../../common/Env"
import {assertWorkerOrNode, isApp, isDesktop} from "../../common/Env"
import {EntityClient} from "../../common/EntityClient"
import {getEnabledMailAddressesForGroupInfo, getUserGroupMemberships} from "../../common/utils/GroupUtils"
import {containsId, getLetId, isSameId, stringToCustomId} from "../../common/utils/EntityUtils"
@ -106,13 +108,14 @@ import {
sha256Hash,
} from "@tutao/tutanota-crypto"
import {DataFile} from "../../common/DataFile";
import {FileReference} from "../../common/utils/FileUtils";
import {FileReference, isDataFile, isFileReference} from "../../common/utils/FileUtils";
import {CounterService} from "../../entities/monitor/Services"
import {PublicKeyService} from "../../entities/sys/Services.js"
import {IServiceExecutor} from "../../common/ServiceRequest"
import {createWriteCounterData} from "../../entities/monitor/TypeRefs"
import {UserFacade} from "./UserFacade"
import {PartialRecipient, Recipient, RecipientList, RecipientType} from "../../common/recipients/Recipient"
import {NativeFileApp} from "../../../native/common/FileApp"
assertWorkerOrNode()
type Attachments = ReadonlyArray<TutanotaFile | DataFile | FileReference>
@ -159,7 +162,9 @@ export class MailFacade {
private readonly crypto: CryptoFacade,
private readonly serviceExecutor: IServiceExecutor,
private readonly blobFacade: BlobFacade,
) {}
private readonly fileApp: NativeFileApp,
) {
}
async createMailFolder(name: string, parent: IdTuple, ownerGroupId: Id): Promise<void> {
const mailGroupKey = this.userFacade.getGroupKey(ownerGroupId)
@ -348,57 +353,58 @@ export class MailFacade {
mailGroupKey: Aes128Key,
useBlobs: boolean
): Promise<DraftAttachment[]> {
if (providedFiles) {
return promiseMap(providedFiles, async (providedFile) => {
// check if this is a new attachment or an existing one
if (providedFile._type === "DataFile") {
// user added attachment
const fileSessionKey = aes128RandomKey()
const dataFile = downcast<DataFile>(providedFile)
if (useBlobs) {
// return
const referenceTokens = await this.blobFacade.encryptAndUpload(ArchiveDataType.Attachments, dataFile.data, senderMailGroupId, fileSessionKey)
return this.createAndEncryptDraftAttachment(referenceTokens, fileSessionKey, dataFile, mailGroupKey)
} else {
return this.fileFacade.uploadFileData(dataFile, fileSessionKey).then(fileDataId => {
return this.createAndEncryptLegacyDraftAttachment(fileDataId, fileSessionKey, dataFile, mailGroupKey)
})
}
} else if (providedFile._type === "FileReference") {
const fileSessionKey = aes128RandomKey()
const fileRef = downcast<FileReference>(providedFile)
if (useBlobs) {
const referenceTokens = await this.blobFacade.encryptAndUploadNative(ArchiveDataType.Attachments, fileRef.location, senderMailGroupId, fileSessionKey)
return this.createAndEncryptDraftAttachment(referenceTokens, fileSessionKey, fileRef, mailGroupKey)
} else {
return this.fileFacade.uploadFileDataNative(fileRef, fileSessionKey).then(fileDataId => {
return this.createAndEncryptLegacyDraftAttachment(fileDataId, fileSessionKey, fileRef, mailGroupKey)
})
}
} else if (!containsId(existingFileIds, getLetId(providedFile))) {
// forwarded attachment which was not in the draft before
return this.crypto.resolveSessionKeyForInstance(providedFile).then(fileSessionKey => {
const attachment = createDraftAttachment()
attachment.existingFile = getLetId(providedFile)
attachment.ownerEncFileSessionKey = encryptKey(mailGroupKey, neverNull(fileSessionKey))
return attachment
})
} else {
return null
}
}) // disable concurrent file upload to avoid timeout because of missing progress events on Firefox.
.then(attachments => attachments.filter(isNotNull))
.then(it => {
// only delete the temporary files after all attachments have been uploaded
if (isApp()) {
this.fileFacade.clearFileData().catch(e => console.warn("Failed to clear files", e))
}
if (providedFiles == null || providedFiles.length === 0) return []
return it
return promiseMap(providedFiles, async (providedFile) => {
// check if this is a new attachment or an existing one
if (isDataFile(providedFile)) {
// user added attachment
const fileSessionKey = aes128RandomKey()
if (useBlobs) {
let referenceTokens: Array<BlobReferenceTokenWrapper>
if (isApp() || isDesktop()) {
const {location} = await this.fileApp.writeDataFile(providedFile)
referenceTokens = await this.blobFacade.encryptAndUploadNative(ArchiveDataType.Attachments, location, senderMailGroupId, fileSessionKey)
await this.fileApp.deleteFile(location)
} else {
referenceTokens = await this.blobFacade.encryptAndUpload(ArchiveDataType.Attachments, providedFile.data, senderMailGroupId, fileSessionKey)
}
return this.createAndEncryptDraftAttachment(referenceTokens, fileSessionKey, providedFile, mailGroupKey)
} else {
const fileDataId = await this.fileFacade.uploadFileData(providedFile, fileSessionKey)
return this.createAndEncryptLegacyDraftAttachment(fileDataId, fileSessionKey, providedFile, mailGroupKey)
}
} else if (isFileReference(providedFile)) {
const fileSessionKey = aes128RandomKey()
if (useBlobs) {
const referenceTokens = await this.blobFacade.encryptAndUploadNative(ArchiveDataType.Attachments, providedFile.location, senderMailGroupId, fileSessionKey)
return this.createAndEncryptDraftAttachment(referenceTokens, fileSessionKey, providedFile, mailGroupKey)
} else {
const fileDataId = await this.fileFacade.uploadFileDataNative(providedFile, fileSessionKey)
return this.createAndEncryptLegacyDraftAttachment(fileDataId, fileSessionKey, providedFile, mailGroupKey)
}
} else if (!containsId(existingFileIds, getLetId(providedFile))) {
// forwarded attachment which was not in the draft before
return this.crypto.resolveSessionKeyForInstance(providedFile).then(fileSessionKey => {
const attachment = createDraftAttachment()
attachment.existingFile = getLetId(providedFile)
attachment.ownerEncFileSessionKey = encryptKey(mailGroupKey, neverNull(fileSessionKey))
return attachment
})
} else {
return Promise.resolve([])
}
} else {
return null
}
}) // disable concurrent file upload to avoid timeout because of missing progress events on Firefox.
.then(attachments => attachments.filter(isNotNull))
.then(it => {
// only delete the temporary files after all attachments have been uploaded
if (isApp()) {
this.fileFacade.clearFileData().catch(e => console.warn("Failed to clear files", e))
}
return it
})
}
createAndEncryptLegacyDraftAttachment(

View file

@ -9,6 +9,7 @@ import {FileFacade} from "../../../../../src/api/worker/facades/FileFacade.js"
import {EntityClient} from "../../../../../src/api/common/EntityClient.js"
import {BlobFacade} from "../../../../../src/api/worker/facades/BlobFacade.js"
import {UserFacade} from "../../../../../src/api/worker/facades/UserFacade"
import {NativeFileApp} from "../../../../../src/native/common/FileApp.js"
o.spec("MailFacade test", function () {
@ -19,6 +20,7 @@ o.spec("MailFacade test", function () {
let fileFacade: FileFacade
let entity: EntityClient
let blobFacade: BlobFacade
let fileApp: NativeFileApp
o.beforeEach(function () {
userFacade = object()
@ -27,7 +29,8 @@ o.spec("MailFacade test", function () {
entity = object()
cryptoFacade = object()
serviceExecutor = object()
facade = new MailFacade(userFacade, fileFacade, entity, cryptoFacade, serviceExecutor, blobFacade)
fileApp = object()
facade = new MailFacade(userFacade, fileFacade, entity, cryptoFacade, serviceExecutor, blobFacade, fileApp)
})
o.spec("checkMailForPhishing", function () {