mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
handle userGroupEncBucketKey for external users in CryptoFacade, see server issue 1313
This commit is contained in:
parent
31d3ef1d9c
commit
7f1344f38a
11 changed files with 429 additions and 352 deletions
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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")
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const modelInfo = {
|
||||
version: 59,
|
||||
version: 60,
|
||||
compatibleSince: 58,
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
25
src/api/worker/offline/migrations/sys-v83.ts
Normal file
25
src/api/worker/offline/migrations/sys-v83.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue