Skip to content

Commit

Permalink
Deprecate RSA encryption for mails
Browse files Browse the repository at this point in the history
We still use the keys, but we show a warning if we detect that TutaCrypt
keys should have been used instead.

In practice, this means that if we have TutaCrypt keys at all, the
sender should have used those. However, in order to prevent unnecessary
warnings when doing a key rotation, we consider it normal to receive RSA
encrypted mails in the same session where we did the key rotation.

There is a corner case when we show a warning even though the RSA
encryption was legitimate, and that is if we decrypt an old RSA mail
after rotating to TutaCrypt keys. We don't expect this to occur very
often.

Co-authored-by: hec <[email protected]>
Co-authored-by: mab <[email protected]>

tutadb#1932
  • Loading branch information
vitoreiji committed Feb 4, 2025
1 parent a5c0e47 commit 4c53ea5
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
locator.keyLoader,
asymmetricCrypto,
locator.publicKeyProvider,
lazyMemoized(() => locator.keyRotation),
)

locator.recoverCode = lazyMemoized(async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/common/api/common/TutanotaConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,8 @@ export enum EncryptionAuthStatus {
AES_NO_AUTHENTICATION = "3",
/** the entity was sent by us encrypted with TutaCrypt, so it is authenticated */
TUTACRYPT_SENDER = "4",
/** the entity was encrypted with RSA although TutaCrypt keys were available */
RSA_DESPITE_TUTACRYPT = "5",
}

export const enum MailReportType {
Expand Down
2 changes: 1 addition & 1 deletion src/common/api/entities/sys/TypeRefs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create, StrippedEntity } from "../../common/utils/EntityUtils.js"
import { create, Stripped, StrippedEntity } from "../../common/utils/EntityUtils.js"
import { TypeRef } from "@tutao/tutanota-utils"
import { typeModels } from "./TypeModels.js"

Expand Down
39 changes: 31 additions & 8 deletions src/common/api/worker/crypto/CryptoFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
downcast,
isSameTypeRef,
isSameTypeRefByAttr,
lazy,
neverNull,
ofClass,
promiseMap,
Expand Down Expand Up @@ -58,7 +59,18 @@ import type { Entity, Instance, SomeEntity, TypeModel } from "../../common/Entit
import { assertWorkerOrNode } from "../../common/Env"
import type { EntityClient } from "../../common/EntityClient"
import { RestClient } from "../rest/RestClient"
import { Aes256Key, aes256RandomKey, aesEncrypt, AesKey, bitArrayToUint8Array, decryptKey, EccPublicKey, encryptKey, sha256Hash } from "@tutao/tutanota-crypto"
import {
Aes256Key,
aes256RandomKey,
aesEncrypt,
AesKey,
bitArrayToUint8Array,
decryptKey,
EccPublicKey,
encryptKey,
isPqKeyPairs,
sha256Hash,
} from "@tutao/tutanota-crypto"
import { RecipientNotResolvedError } from "../../common/error/RecipientNotResolvedError"
import { IServiceExecutor } from "../../common/ServiceRequest"
import { EncryptTutanotaPropertiesService } from "../../entities/tutanota/Services"
Expand All @@ -74,6 +86,7 @@ import { encryptKeyWithVersionedKey, VersionedEncryptedKey, VersionedKey } from
import { AsymmetricCryptoFacade } from "./AsymmetricCryptoFacade.js"
import { PublicKeyProvider, PublicKeys } from "../facades/PublicKeyProvider.js"
import { KeyVersion } from "@tutao/tutanota-utils/dist/Utils.js"
import { KeyRotationFacade } from "../facades/KeyRotationFacade.js"

assertWorkerOrNode()

Expand Down Expand Up @@ -101,6 +114,7 @@ export class CryptoFacade {
private readonly keyLoaderFacade: KeyLoaderFacade,
private readonly asymmetricCryptoFacade: AsymmetricCryptoFacade,
private readonly publicKeyProvider: PublicKeyProvider,
private readonly keyRotationFacade: lazy<KeyRotationFacade>,
) {}

async applyMigrationsForInstance<T>(decryptedInstance: T): Promise<T> {
Expand Down Expand Up @@ -473,6 +487,7 @@ export class CryptoFacade {
resolvedSessionKeyForInstance,
instanceSessionKeyWithOwnerEncSessionKey,
decryptedSessionKey,
bucketKey.keyGroup,
)
}
instanceSessionKeyWithOwnerEncSessionKey.symEncSessionKey = ownerEncSessionKey.key
Expand All @@ -489,26 +504,34 @@ export class CryptoFacade {

private async authenticateMainInstance(
typeModel: TypeModel,
encryptionAuthStatus:
| EncryptionAuthStatus
| null
| EncryptionAuthStatus.RSA_NO_AUTHENTICATION
| EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED
| EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_FAILED
| EncryptionAuthStatus.AES_NO_AUTHENTICATION,
encryptionAuthStatus: EncryptionAuthStatus | null,
pqMessageSenderKey: Uint8Array | null,
pqMessageSenderKeyVersion: KeyVersion | null,
instance: Record<string, any>,
resolvedSessionKeyForInstance: number[],
instanceSessionKeyWithOwnerEncSessionKey: InstanceSessionKey,
decryptedSessionKey: number[],
keyGroup: Id | null,
) {
// we only authenticate mail instances
const isMailInstance = isSameTypeRefByAttr(MailTypeRef, typeModel.app, typeModel.name)
if (isMailInstance) {
if (!encryptionAuthStatus) {
if (!pqMessageSenderKey) {
// This message was encrypted with RSA. We check if TutaCrypt could have been used instead.
const recipientGroup = assertNotNull(
keyGroup,
"trying to authenticate an asymmetrically encrypted message, but we can't determine the recipient's group ID",
)
const currentKeyPair = await this.keyLoaderFacade.loadCurrentKeyPair(recipientGroup)
encryptionAuthStatus = EncryptionAuthStatus.RSA_NO_AUTHENTICATION
if (isPqKeyPairs(currentKeyPair.object)) {
const keyRotationFacade = this.keyRotationFacade()
const rotatedGroups = await keyRotationFacade.getGroupIdsThatPerformedKeyRotations()
if (!rotatedGroups.includes(recipientGroup)) {
encryptionAuthStatus = EncryptionAuthStatus.RSA_DESPITE_TUTACRYPT
}
}
} else {
const mail = this.isLiteralInstance(instance)
? ((await this.instanceMapper.decryptAndMapToInstance(typeModel, instance, resolvedSessionKeyForInstance)) as Mail)
Expand Down
23 changes: 22 additions & 1 deletion src/common/api/worker/facades/KeyRotationFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ export class KeyRotationFacade {
* @VisibleForTesting
*/
pendingKeyRotations: PendingKeyRotation
/**
* Keeps track of which User and Team groups have performed Key Rotation (only for the current session).
* Other group types may be included, but it is not guaranteed.
* @private
*/
private groupIdsThatPerformedKeyRotations: Set<Id>
private readonly facadeInitializedDeferredObject: DeferredObject<void>
private pendingGroupKeyUpdateIds: IdTuple[] // already rotated groups for which we need to update the memberships (GroupKeyUpdateIds all in one list)

Expand All @@ -186,6 +192,7 @@ export class KeyRotationFacade {
}
this.facadeInitializedDeferredObject = defer<void>()
this.pendingGroupKeyUpdateIds = []
this.groupIdsThatPerformedKeyRotations = new Set<Id>()
}

/**
Expand Down Expand Up @@ -310,6 +317,10 @@ export class KeyRotationFacade {
}
await this.serviceExecutor.post(GroupKeyRotationService, serviceData)

for (const groupKeyUpdate of serviceData.groupKeyUpdates) {
this.groupIdsThatPerformedKeyRotations.add(groupKeyUpdate.group)
}

if (!isEmpty(invitationData)) {
const shareFacade = await this.shareFacade()
await promiseMap(invitationData, (preparedInvite) => shareFacade.sendGroupInvitationRequest(preparedInvite))
Expand All @@ -329,7 +340,8 @@ export class KeyRotationFacade {
const currentAdminGroupKey = await this.keyLoaderFacade.getCurrentSymGroupKey(adminGroupMembership.group)
const adminKeyRotationData = await this.prepareKeyRotationForSingleAdmin(keyRotation, user, currentUserGroupKey, currentAdminGroupKey, passphraseKey)

return this.serviceExecutor.post(AdminGroupKeyRotationService, adminKeyRotationData.keyRotationData)
await this.serviceExecutor.post(AdminGroupKeyRotationService, adminKeyRotationData.keyRotationData)
this.groupIdsThatPerformedKeyRotations.add(user.userGroup.group)
}

//We assume that the logged-in user is an admin user and that the key encrypting the group key are already pq secure
Expand Down Expand Up @@ -978,6 +990,7 @@ export class KeyRotationFacade {
userGroupKeyData,
}),
)
this.groupIdsThatPerformedKeyRotations.add(userGroupId)
}

private async handleUserGroupKeyRotationAsUser(
Expand Down Expand Up @@ -1321,6 +1334,7 @@ export class KeyRotationFacade {

// call service
await this.serviceExecutor.post(AdminGroupKeyRotationService, keyRotationData)
this.groupIdsThatPerformedKeyRotations.add(user.userGroup.group)
}

/**
Expand Down Expand Up @@ -1358,6 +1372,13 @@ export class KeyRotationFacade {
return MultiAdminGroupKeyAdminActionPath.IMPOSSIBLE_STATE
}
}

/**
* Gets a list of the groups for which we have rotated keys in the session, so far.
*/
public async getGroupIdsThatPerformedKeyRotations(): Promise<Array<Id>> {
return Array.from(this.groupIdsThatPerformedKeyRotations.values())
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/common/misc/LanguageViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const enum InfoLink {
AppStoreDowngrade = "https://tuta.com/support/#appstore-subscription-downgrade",
PasswordGenerator = "https://tuta.com/faq#passphrase-generator",
HomePageFreeSignup = "https://tuta.com/free-email",
DeprecatedKey = "https://tuta.com/support#deprecated-key-warning",
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/common/misc/TranslationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1856,3 +1856,4 @@ export type TranslationKeyType =
| "yourMessage_label"
| "you_label"
| "emptyString_msg"
| "deprecatedKeyWarning_msg"
31 changes: 20 additions & 11 deletions src/mail-app/mail/view/MailViewerHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
return [
m(
"." + responsiveCardHMargin(),
this.renderPhishingWarning(viewModel) ||
this.renderHardAuthenticationFailWarning(viewModel) ||
this.renderSoftAuthenticationFailWarning(viewModel),
this.renderPhishingWarning(viewModel) ?? viewModel.isWarningDismissed()
? null
: this.renderHardAuthenticationFailWarning(viewModel) ?? this.renderSoftAuthenticationFailWarning(viewModel),
),
m("." + responsiveCardHMargin(), this.renderExternalContentBanner(attrs)),
m("hr.hr.mt-xs." + responsiveCardHMargin()),
Expand Down Expand Up @@ -625,7 +625,8 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
const authFailed =
viewModel.checkMailAuthenticationStatus(MailAuthenticationStatus.HARD_FAIL) ||
viewModel.mail.encryptionAuthStatus === EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_FAILED
if (!viewModel.isWarningDismissed() && authFailed) {

if (authFailed) {
return m(InfoBanner, {
message: "mailAuthFailed_msg",
icon: Icons.Warning,
Expand All @@ -642,7 +643,20 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
}

private renderSoftAuthenticationFailWarning(viewModel: MailViewerViewModel): Children | null {
if (!viewModel.isWarningDismissed() && viewModel.checkMailAuthenticationStatus(MailAuthenticationStatus.SOFT_FAIL)) {
const buttons: ReadonlyArray<BannerButtonAttrs | null> = [
{
label: "close_alt",
click: () => viewModel.setWarningDismissed(true),
},
]
if (viewModel.mail.encryptionAuthStatus === EncryptionAuthStatus.RSA_DESPITE_TUTACRYPT) {
return m(InfoBanner, {
message: () => lang.get("deprecatedKeyWarning_msg"),
icon: Icons.Warning,
helpLink: canSeeTutaLinks(viewModel.logins) ? InfoLink.DeprecatedKey : null,
buttons: buttons,
})
} else if (viewModel.checkMailAuthenticationStatus(MailAuthenticationStatus.SOFT_FAIL)) {
return m(InfoBanner, {
message: () =>
viewModel.mail.differentEnvelopeSender
Expand All @@ -652,12 +666,7 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
: lang.get("mailAuthMissing_label"),
icon: Icons.Warning,
helpLink: canSeeTutaLinks(viewModel.logins) ? InfoLink.MailAuth : null,
buttons: [
{
label: "close_alt",
click: () => viewModel.setWarningDismissed(true),
},
],
buttons: buttons,
})
} else {
return null
Expand Down
12 changes: 1 addition & 11 deletions src/mail-app/mailLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,7 @@ import { SearchViewModel } from "./search/view/SearchViewModel.js"
import { SearchRouter } from "../common/search/view/SearchRouter.js"
import { MailOpenedListener } from "./mail/view/MailViewModel.js"
import { getEnabledMailAddressesWithUser } from "../common/mailFunctionality/SharedMailUtils.js"
import {
CLIENT_ONLY_CALENDARS,
Const,
DEFAULT_CLIENT_ONLY_CALENDAR_COLORS,
FeatureType,
GroupType,
KdfType,
MailSetKind,
} from "../common/api/common/TutanotaConstants.js"
import { CLIENT_ONLY_CALENDARS, Const, DEFAULT_CLIENT_ONLY_CALENDAR_COLORS, FeatureType, GroupType, KdfType } from "../common/api/common/TutanotaConstants.js"
import { ShareableGroupType } from "../common/sharing/GroupUtils.js"
import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js"
import { CalendarViewModel } from "../calendar-app/calendar/view/CalendarViewModel.js"
Expand Down Expand Up @@ -124,7 +116,6 @@ import { getDisplayedSender } from "../common/api/common/CommonMailUtils.js"
import { MailModel } from "./mail/model/MailModel.js"
import { locator } from "../common/api/main/CommonLocator.js"
import { showSnackBar } from "../common/gui/base/SnackBar.js"
import { assertSystemFolderOfType } from "./mail/model/MailUtils.js"
import { WorkerRandomizer } from "../common/api/worker/workerInterfaces.js"
import { SearchCategoryTypes } from "./search/model/SearchUtils.js"
import { WorkerInterface } from "./workerUtils/worker/WorkerImpl.js"
Expand All @@ -137,7 +128,6 @@ import { lang } from "../common/misc/LanguageViewModel.js"
import type { CalendarContactPreviewViewModel } from "../calendar-app/calendar/gui/eventpopup/CalendarContactPreviewViewModel.js"
import { KeyLoaderFacade } from "../common/api/worker/facades/KeyLoaderFacade.js"
import { ContactSuggestion } from "../common/native/common/generatedipc/ContactSuggestion"
import { getElementId } from "../common/api/common/utils/EntityUtils.js"
import { MailImporter } from "./mail/import/MailImporter.js"

assertMainOrNode()
Expand Down
3 changes: 2 additions & 1 deletion src/mail-app/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,7 @@ export default {
"yourCalendars_label": "Deine Kalender",
"yourFolders_action": "DEINE ORDNER",
"yourMessage_label": "Deine Nachricht",
"you_label": "Du"
"you_label": "Du",
"deprecatedKeyWarning_msg": "Für diese Nachricht wurde ein veraltetes Verschlüsselungsprotokoll verwendet und sie stammt möglicherweise nicht von diesem Benutzer",
}
}
3 changes: 2 additions & 1 deletion src/mail-app/translations/de_sie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,7 @@ export default {
"yourCalendars_label": "Deine Kalender",
"yourFolders_action": "Ihre ORDNER",
"yourMessage_label": "Ihre Nachricht",
"you_label": "Sie"
"you_label": "Sie",
"deprecatedKeyWarning_msg": "Für diese Nachricht wurde ein veraltetes Verschlüsselungsprotokoll verwendet und sie stammt möglicherweise nicht von diesem Benutzer"
}
}
3 changes: 2 additions & 1 deletion src/mail-app/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,7 @@ export default {
"yourCalendars_label": "Your calendars",
"yourFolders_action": "YOUR FOLDERS",
"yourMessage_label": "Your message",
"you_label": "You"
"you_label": "You",
"deprecatedKeyWarning_msg": "This message was encrypted with an older key. While it may be legitimate, it's less secure."
}
}
1 change: 1 addition & 0 deletions src/mail-app/workerUtils/worker/WorkerLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
locator.keyLoader,
locator.asymmetricCrypto,
locator.publicKeyProvider,
lazyMemoized(() => locator.keyRotation),
)

locator.recoverCode = lazyMemoized(async () => {
Expand Down
Loading

0 comments on commit 4c53ea5

Please sign in to comment.