handle userGroupEncBucketKey for external users in CryptoFacade, see server issue 1313

This commit is contained in:
vaf 2023-01-26 16:34:37 +01:00 committed by mpfau
parent 31d3ef1d9c
commit 7f1344f38a
11 changed files with 429 additions and 352 deletions

View file

@ -50,6 +50,21 @@
"info": "AddValue CustomerProperties/usageDataOptedOut/2025."
}
]
},
{
"version": 83,
"changes": [
{
"name": "RenameAttribute",
"sourceType": "BucketKey",
"info": "RenameAttribute BucketKey: pubKeyGroup -> keyGroup."
},
{
"name": "RenameAttribute",
"sourceType": "BucketKey",
"info": "RenameAttribute BucketKey: ownerEncBucketKey -> groupEncBucketKey."
}
]
}
]
}

View file

@ -1,6 +1,6 @@
const modelInfo = {
version: 82,
compatibleSince: 80,
version: 83,
compatibleSince: 83,
}
export default modelInfo

File diff suppressed because it is too large Load diff

View file

@ -456,11 +456,11 @@ export type BucketKey = {
_type: TypeRef<BucketKey>;
_id: Id;
ownerEncBucketKey: null | Uint8Array;
groupEncBucketKey: null | Uint8Array;
pubEncBucketKey: null | Uint8Array;
bucketEncSessionKeys: InstanceSessionKey[];
pubKeyGroup: null | Id;
keyGroup: null | Id;
}
export const BucketPermissionTypeRef: TypeRef<BucketPermission> = new TypeRef("sys", "BucketPermission")

View file

@ -1,5 +1,5 @@
const modelInfo = {
version: 59,
version: 60,
compatibleSince: 58,
}

File diff suppressed because it is too large Load diff

View file

@ -300,12 +300,21 @@ export class CryptoFacade {
const instanceElementId = this.getElementIdFromInstance(instance)
let decBucketKey: Aes128Key
if (bucketKey.pubKeyGroup && bucketKey.pubEncBucketKey) {
if (bucketKey.keyGroup && bucketKey.pubEncBucketKey) {
// bucket key is encrypted with public key for internal recipient
decBucketKey = await this.decryptBucketKeyWithKeyPairOfGroup(bucketKey.pubKeyGroup, bucketKey.pubEncBucketKey)
} else if (bucketKey.ownerEncBucketKey) {
decBucketKey = await this.decryptBucketKeyWithKeyPairOfGroup(bucketKey.keyGroup, bucketKey.pubEncBucketKey)
} else if (bucketKey.groupEncBucketKey) {
// secure external recipient
decBucketKey = decryptKey(this.userFacade.getGroupKey(neverNull(instance._ownerGroup)), bucketKey.ownerEncBucketKey)
let keyGroup
if (bucketKey.keyGroup) {
// legacy code path for old external clients that used to encrypt bucket keys with user group keys.
// should be dropped once all old external mailboxes are cleared
keyGroup = bucketKey.keyGroup
} else {
// by default, we try to decrypt the bucket key with the ownerGroupKey
keyGroup = neverNull(instance._ownerGroup)
}
decBucketKey = decryptKey(this.userFacade.getGroupKey(keyGroup), bucketKey.groupEncBucketKey)
} else {
throw new SessionKeyNotFoundError(`encrypted bucket key not set on instance ${typeModel.name}`)
}

View file

@ -14,6 +14,7 @@ import { tutanota58 } from "./migrations/tutanota-v58.js"
import { storage6 } from "./migrations/storage-v6.js"
import { tutanota57 } from "./migrations/tutanota-v57.js"
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
import { sys83 } from "./migrations/sys-v83.js"
export interface OfflineMigration {
readonly app: VersionMetadataBaseKey
@ -39,6 +40,7 @@ export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray<OfflineMigration> = [
tutanota57,
tutanota58,
storage6,
sys83,
]
const CURRENT_OFFLINE_VERSION = 1

View file

@ -27,7 +27,7 @@ export async function migrateAllElements<T extends ElementEntity>(typeRef: TypeR
}
}
type Migration<T extends SomeEntity> = (entity: any) => T
export type Migration<T extends SomeEntity> = (entity: any) => T
export function renameAttribute<T extends SomeEntity>(oldName: string, newName: keyof T): Migration<T> {
return function (entity) {

View file

@ -0,0 +1,25 @@
import { OfflineMigration } from "../OfflineStorageMigrator.js"
import { OfflineStorage } from "../OfflineStorage.js"
import { migrateAllListElements, Migration } from "../StandardMigrations.js"
import { Mail, MailTypeRef } from "../../../entities/tutanota/TypeRefs.js"
export const sys83: OfflineMigration = {
app: "sys",
version: 83,
async migrate(storage: OfflineStorage) {
await migrateAllListElements(MailTypeRef, storage, [migrateBucketKey()])
},
}
function migrateBucketKey(): Migration<Mail> {
return function (mail) {
const bucketKey = mail.bucketKey
if (bucketKey != null) {
bucketKey["keyGroup"] = bucketKey["pubKeyGroup"]
delete bucketKey["pubKeyGroup"]
bucketKey["groupEncBucketKey"] = bucketKey["ownerEncBucketKey"]
delete bucketKey["ownerEncBucketKey"]
}
return mail
}
}

View file

@ -896,7 +896,7 @@ o.spec("crypto facade", function () {
})
o(
"resolve session key: external user key decryption of session key using BucketKey aggregated type - Mail referencing MailDetailsBlob with attachments",
"resolve session key: external user key decryption of session key using BucketKey aggregated type encrypted with MailGroupKey - Mail referencing MailDetailsBlob with attachments",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const file1SessionKey = aes128RandomKey()
@ -914,6 +914,25 @@ o.spec("crypto facade", function () {
},
)
o(
"resolve session key: external user key decryption of session key using BucketKey aggregated type encrypted with UserGroupKey - Mail referencing MailDetailsBlob with attachments",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const file1SessionKey = aes128RandomKey()
const file2SessionKey = aes128RandomKey()
const testData = await prepareSymEncBucketKeyResolveSessionKeyTest([file1SessionKey, file2SessionKey], true)
Object.assign(testData.mailLiteral, { mailDetails: ["mailDetailsArchiveId", "mailDetailsId"] })
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.MailTypeModel, testData.mailLiteral))
o(mailSessionKey).deepEquals(testData.sk)
o(Object.keys(crypto.getSessionKeyCache()).length).equals(3)
o(crypto.getSessionKeyCache()["mailDetailsId"]).deepEquals(testData.sk)
o(crypto.getSessionKeyCache()["fileId1"]).deepEquals(file1SessionKey)
o(crypto.getSessionKeyCache()["fileId2"]).deepEquals(file2SessionKey)
},
)
o("resolve session key from cache: MailDetailsBlob", async function () {
const sk = aes128RandomKey()
crypto.getSessionKeyCache()["mailDetailsId"] = sk
@ -1019,7 +1038,7 @@ o.spec("crypto facade", function () {
const bucketKey = createBucketKey({
pubEncBucketKey: pubEncBucketKey,
pubKeyGroup: userGroup._id,
keyGroup: userGroup._id,
bucketEncSessionKeys: bucketEncSessionKeys,
})
@ -1060,7 +1079,10 @@ o.spec("crypto facade", function () {
*
* @param fileSessionKeys List of session keys for the attachments. When the list is empty there are no attachments
*/
async function prepareSymEncBucketKeyResolveSessionKeyTest(fileSessionKeys: Array<Aes128Key> = []): Promise<{
async function prepareSymEncBucketKeyResolveSessionKeyTest(
fileSessionKeys: Array<Aes128Key> = [],
externalUserGroupEncBucketKey = false,
): Promise<{
mailLiteral: Record<string, any>
sk: Aes128Key
bk: Aes128Key
@ -1073,6 +1095,7 @@ o.spec("crypto facade", function () {
let gk = aes128RandomKey()
let sk = aes128RandomKey()
let bk = aes128RandomKey()
const ugk = aes128RandomKey()
const userGroup = createGroup({
_id: "userGroupId",
@ -1080,9 +1103,11 @@ o.spec("crypto facade", function () {
})
const mailLiteral = createMailLiteral(gk, sk, subject, confidential, senderName, recipientName)
// @ts-ignore
mailLiteral._ownerEncSessionKey = null
const ownerEncBucketKey = encryptKey(gk, bk)
mailLiteral._ownerEncSessionKey = null
const groupKeyToEncryptBucketKey = externalUserGroupEncBucketKey ? ugk : gk
const groupEncBucketKey = encryptKey(groupKeyToEncryptBucketKey, bk)
const bucketEncMailSessionKey = encryptKey(bk, sk)
const MailTypeModel = await resolveTypeReference(MailTypeRef)
@ -1113,8 +1138,8 @@ o.spec("crypto facade", function () {
const bucketKey = createBucketKey({
pubEncBucketKey: null,
pubKeyGroup: null,
ownerEncBucketKey: ownerEncBucketKey,
keyGroup: externalUserGroupEncBucketKey ? userGroup._id : null,
groupEncBucketKey: groupEncBucketKey,
bucketEncSessionKeys: bucketEncSessionKeys,
})
@ -1132,6 +1157,7 @@ o.spec("crypto facade", function () {
when(userFacade.getLoggedInUser()).thenReturn(user)
when(userFacade.getGroupKey("mailGroupId")).thenReturn(gk)
when(userFacade.getGroupKey(userGroup._id)).thenReturn(ugk)
when(userFacade.isLeader()).thenReturn(true)
when(entityClient.load(GroupTypeRef, userGroup._id)).thenResolve(userGroup)