Skip to content

Commit

Permalink
fix decrypt file errors #1657
Browse files Browse the repository at this point in the history
  • Loading branch information
mpfau committed Oct 20, 2023
1 parent 2e5f6f4 commit dd2903f
Show file tree
Hide file tree
Showing 22 changed files with 331 additions and 240 deletions.
22 changes: 7 additions & 15 deletions src/api/common/EntityClient.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { EntityRestInterface } from "../worker/rest/EntityRestClient"
import type { EntityRestInterface, OwnerEncSessionKeyProvider } from "../worker/rest/EntityRestClient"
import { EntityRestClientSetupOptions } from "../worker/rest/EntityRestClient"
import type { RootInstance } from "../entities/sys/TypeRefs.js"
import { RootInstanceTypeRef } from "../entities/sys/TypeRefs.js"
import { CUSTOM_MIN_ID, firstBiggerThanSecond, GENERATED_MIN_ID, getElementId, getLetId, RANGE_ITEM_LIMIT } from "./utils/EntityUtils"
import { Type, ValueType } from "./EntityConstants"
import { last, TypeRef } from "@tutao/tutanota-utils"
import { downcast, last, TypeRef } from "@tutao/tutanota-utils"
import { resolveTypeReference } from "./EntityFunctions"
import type { ElementEntity, ListElementEntity, SomeEntity } from "./EntityTypes"
import { downcast } from "@tutao/tutanota-utils"
import { EntityRestClientSetupOptions } from "../worker/rest/EntityRestClient"

export class EntityClient {
_target: EntityRestInterface
Expand All @@ -16,15 +15,8 @@ export class EntityClient {
this._target = target
}

load<T extends SomeEntity>(
typeRef: TypeRef<T>,
id: PropertyType<T, "_id">,
query?: Dict,
extraHeaders?: Dict,
ownerKey?: Aes128Key,
providedOwnerEncSessionKey?: Uint8Array | null,
): Promise<T> {
return this._target.load(typeRef, id, query, extraHeaders, ownerKey, providedOwnerEncSessionKey)
load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, query?: Dict, extraHeaders?: Dict, ownerKey?: Aes128Key): Promise<T> {
return this._target.load(typeRef, id, query, extraHeaders, ownerKey)
}

async loadAll<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start?: Id): Promise<T[]> {
Expand Down Expand Up @@ -85,9 +77,9 @@ export class EntityClient {
typeRef: TypeRef<T>,
listId: Id | null,
elementIds: Id[],
providedOwnerEncSessionKeys?: Map<Id, Uint8Array>,
ownerEncSessionKeyProvider?: OwnerEncSessionKeyProvider,
): Promise<T[]> {
return this._target.loadMultiple(typeRef, listId, elementIds, providedOwnerEncSessionKeys)
return this._target.loadMultiple(typeRef, listId, elementIds, ownerEncSessionKeyProvider)
}

setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict, options?: EntityRestClientSetupOptions): Promise<Id> {
Expand Down
1 change: 1 addition & 0 deletions src/api/main/MainLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ class MainLocator {
this.eventController,
this.workerFacade,
this.search,
this.mailFacade,
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/worker/WorkerLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
locator.cachingEntityClient = new EntityClient(locator.cache)
locator.indexer = lazyMemoized(async () => {
const { Indexer } = await import("./search/Indexer.js")
return new Indexer(entityRestClient, mainInterface.infoMessageHandler, browserData, locator.cache as DefaultEntityRestCache)
return new Indexer(entityRestClient, mainInterface.infoMessageHandler, browserData, locator.cache as DefaultEntityRestCache, await locator.mail())
})

locator.crypto = new CryptoFacade(
Expand Down
40 changes: 22 additions & 18 deletions src/api/worker/crypto/CryptoFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { RestClient } from "../rest/RestClient"
import {
Aes128Key,
aes128RandomKey,
Aes256Key,
aesEncrypt,
bitArrayToUint8Array,
decryptKey,
Expand Down Expand Up @@ -174,7 +175,7 @@ export class CryptoFacade {
return decryptKey(ownerKey, key)
}

decryptSessionKey(instance: Record<string, any>, ownerEncSessionKey: Uint8Array): Aes128Key {
decryptSessionKey(instance: Record<string, any>, ownerEncSessionKey: Uint8Array): Aes128Key | Aes256Key {
const gk = this.userFacade.getGroupKey(instance._ownerGroup)
return decryptKey(gk, ownerEncSessionKey)
}
Expand Down Expand Up @@ -261,13 +262,27 @@ export class CryptoFacade {
}
}

private async resolveWithBucketKey(bucketKey: BucketKey, instance: Record<string, any>, typeModel: TypeModel): Promise<Aes128Key> {
private async resolveWithBucketKey(bucketKey: BucketKey, instance: Record<string, any>, typeModel: TypeModel): Promise<Aes128Key | Aes256Key> {
const instanceElementId = this.getElementIdFromInstance(instance)
const ownerGroupId = neverNull(instance._ownerGroup)
let decBucketKey = await this.decryptBucketKey(bucketKey, ownerGroupId, typeModel.name)
const { resolvedSessionKeyForInstance, instanceSessionKeys } = this.collectAllInstanceSessionKeys(bucketKey, decBucketKey, instanceElementId, instance)
this.ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(instanceSessionKeys)

if (resolvedSessionKeyForInstance) {
// for symmetrically encrypted instances _ownerEncSessionKey is sent from the server.
// in this case it is not yet and we need to set it because the rest of the app expects it.
instance._ownerEncSessionKey = uint8ArrayToBase64(encryptKey(this.userFacade.getGroupKey(instance._ownerGroup), resolvedSessionKeyForInstance))
return resolvedSessionKeyForInstance
} else {
throw new SessionKeyNotFoundError("no session key for instance " + instance._id)
}
}

let decBucketKey: Aes128Key
public async decryptBucketKey(bucketKey: BucketKey, ownerGroupId: Id, typeName: string): Promise<Aes128Key | Aes256Key> {
if (bucketKey.keyGroup && bucketKey.pubEncBucketKey) {
// bucket key is encrypted with public key for internal recipient
decBucketKey = await this.decryptBucketKeyWithKeyPairOfGroup(bucketKey.keyGroup, bucketKey.pubEncBucketKey)
return await this.decryptBucketKeyWithKeyPairOfGroup(bucketKey.keyGroup, bucketKey.pubEncBucketKey)
} else if (bucketKey.groupEncBucketKey) {
// secure external recipient
let keyGroup
Expand All @@ -277,22 +292,11 @@ export class CryptoFacade {
keyGroup = bucketKey.keyGroup
} else {
// by default, we try to decrypt the bucket key with the ownerGroupKey
keyGroup = neverNull(instance._ownerGroup)
keyGroup = ownerGroupId
}
decBucketKey = decryptKey(this.userFacade.getGroupKey(keyGroup), bucketKey.groupEncBucketKey)
return decryptKey(this.userFacade.getGroupKey(keyGroup), bucketKey.groupEncBucketKey)
} else {
throw new SessionKeyNotFoundError(`encrypted bucket key not set on instance ${typeModel.name}`)
}
const { resolvedSessionKeyForInstance, instanceSessionKeys } = this.collectAllInstanceSessionKeys(bucketKey, decBucketKey, instanceElementId, instance)
this.ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(instanceSessionKeys)

if (resolvedSessionKeyForInstance) {
// for symmetrically encrypted instances _ownerEncSessionKey is sent from the server.
// in this case it is not yet and we need to set it because the rest of the app expects it.
instance._ownerEncSessionKey = uint8ArrayToBase64(encryptKey(this.userFacade.getGroupKey(instance._ownerGroup), resolvedSessionKeyForInstance))
return resolvedSessionKeyForInstance
} else {
throw new SessionKeyNotFoundError("no session key for instance " + instance._id)
throw new SessionKeyNotFoundError(`encrypted bucket key not set on instance ${typeName}`)
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/api/worker/facades/lazy/GroupManagementFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { EntityClient } from "../../../common/EntityClient.js"
import { assertWorkerOrNode } from "../../../common/Env.js"
import { encryptString } from "../../crypto/CryptoFacade.js"
import type { RsaImplementation } from "../../crypto/RsaImplementation.js"
import { aes128RandomKey, decryptKey, encryptKey, encryptRsaKey, publicKeyToHex, RsaKeyPair } from "@tutao/tutanota-crypto"
import { aes128RandomKey, aes256RandomKey, decryptKey, encryptKey, encryptRsaKey, publicKeyToHex, RsaKeyPair } from "@tutao/tutanota-crypto"
import { IServiceExecutor } from "../../../common/ServiceRequest.js"
import {
CalendarService,
Expand Down Expand Up @@ -121,7 +121,7 @@ export class GroupManagementFacade {
const postData = createUserAreaGroupPostData({
groupData,
})
const postGroupData = await this.serviceExecutor.post(CalendarService, postData)
const postGroupData = await this.serviceExecutor.post(CalendarService, postData, { sessionKey: aes256RandomKey() }) // we expect a session key to be defined as the entity is marked encrypted
const group = await this.entityClient.load(GroupTypeRef, postGroupData.group)
const user = await this.reloadUser()

Expand All @@ -133,7 +133,9 @@ export class GroupManagementFacade {
const serviceData = createUserAreaGroupPostData({
groupData: groupData,
})
return this.serviceExecutor.post(TemplateGroupService, serviceData).then((returnValue) => returnValue.group)
return this.serviceExecutor
.post(TemplateGroupService, serviceData, { sessionKey: aes256RandomKey() }) // we expect a session key to be defined as the entity is marked encrypted
.then((returnValue) => returnValue.group)
})
}

Expand All @@ -142,7 +144,7 @@ export class GroupManagementFacade {
const serviceData = createUserAreaGroupPostData({
groupData,
})
const postGroupData = await this.serviceExecutor.post(ContactListGroupService, serviceData)
const postGroupData = await this.serviceExecutor.post(ContactListGroupService, serviceData, { sessionKey: aes256RandomKey() }) // we expect a session key to be defined as the entity is marked encrypted
const group = await this.entityClient.load(GroupTypeRef, postGroupData.group)
await this.reloadUser()

Expand Down
89 changes: 80 additions & 9 deletions src/api/worker/facades/lazy/MailFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {
createSendDraftData,
createUpdateMailFolderData,
FileTypeRef,
MailDetails,
MailDetailsBlobTypeRef,
MailDetailsDraftTypeRef,
MailTypeRef,
TutanotaPropertiesTypeRef,
Expand All @@ -79,6 +81,7 @@ import {
defer,
isNotNull,
isSameTypeRefByAttr,
lazyMemoized,
noOp,
ofClass,
promiseFilter,
Expand All @@ -88,7 +91,7 @@ import { BlobFacade } from "./BlobFacade.js"
import { assertWorkerOrNode, isApp, isDesktop } from "../../../common/Env.js"
import { EntityClient } from "../../../common/EntityClient.js"
import { getEnabledMailAddressesForGroupInfo, getUserGroupMemberships } from "../../../common/utils/GroupUtils.js"
import { containsId, getLetId, isSameId, stringToCustomId } from "../../../common/utils/EntityUtils.js"
import { containsId, elementIdPart, getLetId, isSameId, listIdPart, stringToCustomId } from "../../../common/utils/EntityUtils.js"
import { htmlToText } from "../../search/IndexUtils.js"
import { MailBodyTooLargeError } from "../../../common/error/MailBodyTooLargeError.js"
import { UNCOMPRESSED_MAX_SIZE } from "../../Compression.js"
Expand All @@ -113,9 +116,10 @@ import { createWriteCounterData } from "../../../entities/monitor/TypeRefs.js"
import { UserFacade } from "../UserFacade.js"
import { PartialRecipient, Recipient, RecipientList, RecipientType } from "../../../common/recipients/Recipient.js"
import { NativeFileApp } from "../../../../native/common/FileApp.js"
import { isLegacyMail } from "../../../common/MailWrapper.js"
import { isDetailsDraft, isLegacyMail } from "../../../common/MailWrapper.js"
import { LoginFacade } from "../LoginFacade.js"
import { ProgrammingError } from "../../../common/error/ProgrammingError.js"
import { OwnerEncSessionKeyProvider } from "../../rest/EntityRestClient.js"

assertWorkerOrNode()
type Attachments = ReadonlyArray<TutanotaFile | DataFile | FileReference>
Expand Down Expand Up @@ -503,15 +507,15 @@ export class MailFacade {
if (isLegacyMail(draft)) {
return draft.replyTos
} else {
const mailDetails = await this.entityClient.load(
const ownerEncSessionKeyProvider: OwnerEncSessionKeyProvider = async (instanceElementId: Id) => assertNotNull(draft._ownerEncSessionKey)
const mailDetailsDraftId = assertNotNull(draft.mailDetailsDraft, "draft without mailDetailsDraft")
const mailDetails = await this.entityClient.loadMultiple(
MailDetailsDraftTypeRef,
assertNotNull(draft.mailDetailsDraft, "draft without mailDetailsDraft"),
undefined,
undefined,
undefined,
draft._ownerEncSessionKey,
listIdPart(mailDetailsDraftId),
[elementIdPart(mailDetailsDraftId)],
ownerEncSessionKeyProvider,
)
return mailDetails.details.replyTos
return mailDetails[0].details.replyTos
}
}

Expand Down Expand Up @@ -826,6 +830,73 @@ export class MailFacade {
})
await this.serviceExecutor.post(ListUnsubscribeService, postData)
}

async loadAttachments(mail: Mail): Promise<TutanotaFile[]> {
if (mail.attachments.length === 0) {
return []
}
const attachmentsListId = listIdPart(mail.attachments[0])
const attachmentElementIds = mail.attachments.map(elementIdPart)

const bucketKey = mail.bucketKey
let ownerEncSessionKeyProvider: OwnerEncSessionKeyProvider | undefined
if (bucketKey) {
const mailOwnerGroupId = assertNotNull(mail._ownerGroup)
const decBucketKey = lazyMemoized(() => this.crypto.decryptBucketKey(assertNotNull(mail.bucketKey), mailOwnerGroupId, FileTypeRef.type))
ownerEncSessionKeyProvider = async (instanceElementId: Id) => {
const instanceSessionKey = assertNotNull(
bucketKey.bucketEncSessionKeys.find((instanceSessionKey) => instanceElementId === instanceSessionKey.instanceId),
)
const decryptedSessionKey = decryptKey(await decBucketKey(), instanceSessionKey.symEncSessionKey)
return encryptKey(this.userFacade.getGroupKey(mailOwnerGroupId), decryptedSessionKey)
}
}
return await this.entityClient.loadMultiple(FileTypeRef, attachmentsListId, attachmentElementIds, ownerEncSessionKeyProvider)
}

/**
* @param mail in case it is a mailDetailsBlob
*/
async loadMailDetailsBlob(mail: Mail): Promise<MailDetails> {
if (isLegacyMail(mail) || isDetailsDraft(mail)) {
throw new ProgrammingError("not supported, must be mail details blob")
} else {
const mailDetailsBlobId = assertNotNull(mail.mailDetails)

const mailDetailsBlobs = await this.entityClient.loadMultiple(
MailDetailsBlobTypeRef,
listIdPart(mailDetailsBlobId),
[elementIdPart(mailDetailsBlobId)],
async () => assertNotNull(mail._ownerEncSessionKey),
)
if (mailDetailsBlobs.length === 0) {
throw new NotFoundError(`MailDetailsBlob ${mailDetailsBlobId}`)
}
return mailDetailsBlobs[0].details
}
}

/**
* @param mail in case it is a mailDetailsDraft
*/
async loadMailDetailsDraft(mail: Mail): Promise<MailDetails> {
if (isLegacyMail(mail) || !isDetailsDraft(mail)) {
throw new ProgrammingError("not supported, must be mail details draft")
} else {
const detailsDraftId = assertNotNull(mail.mailDetailsDraft)

const mailDetailsDrafts = await this.entityClient.loadMultiple(
MailDetailsDraftTypeRef,
listIdPart(detailsDraftId),
[elementIdPart(detailsDraftId)],
async () => assertNotNull(mail._ownerEncSessionKey),
)
if (mailDetailsDrafts.length === 0) {
throw new NotFoundError(`MailDetailsDraft ${detailsDraftId}`)
}
return mailDetailsDrafts[0].details
}
}
}

export function phishingMarkerValue(type: ReportedMailFieldType, value: string): string {
Expand Down
15 changes: 7 additions & 8 deletions src/api/worker/rest/DefaultEntityRestCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EntityRestInterface } from "./EntityRestClient"
import type { EntityRestInterface, OwnerEncSessionKeyProvider } from "./EntityRestClient"
import { EntityRestClient, EntityRestClientSetupOptions } from "./EntityRestClient"
import { resolveTypeReference } from "../../common/EntityFunctions"
import { OperationType } from "../../common/TutanotaConstants"
Expand Down Expand Up @@ -223,7 +223,6 @@ export class DefaultEntityRestCache implements EntityRestCache {
queryParameters?: Dict,
extraHeaders?: Dict,
ownerKey?: Aes128Key,
providedOwnerEncSessionKey?: Uint8Array | null,
): Promise<T> {
const { listId, elementId } = expandId(id)

Expand All @@ -232,7 +231,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
queryParameters?.version != null || //if a specific version is requested we have to load again
cachedEntity == null
) {
const entity = await this.entityRestClient.load(typeRef, id, queryParameters, extraHeaders, ownerKey, providedOwnerEncSessionKey)
const entity = await this.entityRestClient.load(typeRef, id, queryParameters, extraHeaders, ownerKey)
if (queryParameters?.version == null && !isIgnoredType(typeRef)) {
await this.storage.put(entity)
}
Expand All @@ -245,13 +244,13 @@ export class DefaultEntityRestCache implements EntityRestCache {
typeRef: TypeRef<T>,
listId: Id | null,
elementIds: Array<Id>,
providedOwnerEncSessionKeys?: Map<Id, Uint8Array>,
ownerEncSessionKeyProvider?: OwnerEncSessionKeyProvider,
): Promise<Array<T>> {
if (isIgnoredType(typeRef)) {
return this.entityRestClient.loadMultiple(typeRef, listId, elementIds, providedOwnerEncSessionKeys)
return this.entityRestClient.loadMultiple(typeRef, listId, elementIds, ownerEncSessionKeyProvider)
}

return this._loadMultiple(typeRef, listId, elementIds, providedOwnerEncSessionKeys)
return this._loadMultiple(typeRef, listId, elementIds, ownerEncSessionKeyProvider)
}

setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict, options?: EntityRestClientSetupOptions): Promise<Id> {
Expand Down Expand Up @@ -324,7 +323,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
typeRef: TypeRef<T>,
listId: Id | null,
ids: Array<Id>,
providedOwnerEncSessionKeys?: Map<Id, Uint8Array>,
ownerEncSessionKeyProvider?: OwnerEncSessionKeyProvider,
): Promise<Array<T>> {
const entitiesInCache: T[] = []
const idsToLoad: Id[] = []
Expand All @@ -338,7 +337,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
}
const entitiesFromServer: T[] = []
if (idsToLoad.length > 0) {
const entities = await this.entityRestClient.loadMultiple(typeRef, listId, idsToLoad, providedOwnerEncSessionKeys)
const entities = await this.entityRestClient.loadMultiple(typeRef, listId, idsToLoad, ownerEncSessionKeyProvider)
for (let entity of entities) {
await this.storage.put(entity)
entitiesFromServer.push(entity)
Expand Down

0 comments on commit dd2903f

Please sign in to comment.