tutanota/test/tests/api/worker/crypto/CryptoFacadeTest.ts
2025-10-17 12:07:41 +02:00

1929 lines
72 KiB
TypeScript

import o from "@tutao/otest"
import { arrayEquals, assertNotNull, hexToUint8Array, KeyVersion, neverNull, utf8Uint8ArrayToString, Versioned } from "@tutao/tutanota-utils"
import { CryptoFacade } from "../../../../../src/common/api/worker/crypto/CryptoFacade.js"
import {
asCryptoProtoocolVersion,
BucketPermissionType,
CryptoProtocolVersion,
EncryptionAuthStatus,
EncryptionKeyVerificationState,
GroupType,
PermissionType,
PresentableKeyVerificationState,
ProcessingState,
PublicKeyIdentifierType,
} from "../../../../../src/common/api/common/TutanotaConstants.js"
import {
createMail,
createMailAddress,
FileTypeRef,
InternalRecipientKeyData,
Mail,
MailAddressTypeRef,
MailDetailsBlobTypeRef,
MailTypeRef,
} from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
import {
BucketKey,
BucketKeyTypeRef,
BucketPermissionTypeRef,
BucketTypeRef,
createBucket,
createBucketKey,
createBucketPermission,
createGroup,
createInstanceSessionKey,
createKeyPair,
createPermission,
createTypeInfo,
CustomerAccountTerminationRequestTypeRef,
Group,
GroupKeysRefTypeRef,
GroupMembershipTypeRef,
GroupTypeRef,
InstanceSessionKey,
InstanceSessionKeyTypeRef,
KeyPair,
KeyPairTypeRef,
PermissionTypeRef,
TypeInfoTypeRef,
UpdatePermissionKeyData,
User,
UserTypeRef,
} from "../../../../../src/common/api/entities/sys/TypeRefs.js"
import { spy } from "@tutao/tutanota-test-utils"
import { RestClient } from "../../../../../src/common/api/worker/rest/RestClient.js"
import { EntityClient } from "../../../../../src/common/api/common/EntityClient.js"
import {
Aes256Key,
aes256RandomKey,
aesDecrypt,
aesEncrypt,
AesKey,
bitArrayToUint8Array,
decryptKey,
encryptKey,
encryptRsaKey,
generateX25519KeyPair,
KeyPairType,
kyberPrivateKeyToBytes,
kyberPublicKeyToBytes,
pqKeyPairsToPublicKeys,
PQPublicKeys,
RsaPublicKey,
rsaPublicKeyToHex,
X25519KeyPair,
X25519PublicKey,
} from "@tutao/tutanota-crypto"
import { ServerModelUntypedInstance, TypeModel, UntypedInstance } from "../../../../../src/common/api/common/EntityTypes.js"
import { IServiceExecutor } from "../../../../../src/common/api/common/ServiceRequest.js"
import { matchers, object, verify, when } from "testdouble"
import { UpdatePermissionKeyService } from "../../../../../src/common/api/entities/sys/Services.js"
import { elementIdPart, getListId, isSameId, listIdPart } from "../../../../../src/common/api/common/utils/EntityUtils.js"
import { HttpMethod, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions.js"
import { UserFacade } from "../../../../../src/common/api/worker/facades/UserFacade.js"
import { SessionKeyNotFoundError } from "../../../../../src/common/api/common/error/SessionKeyNotFoundError.js"
import { OwnerEncSessionKeysUpdateQueue } from "../../../../../src/common/api/worker/crypto/OwnerEncSessionKeysUpdateQueue.js"
import { WASMKyberFacade } from "../../../../../src/common/api/worker/facades/KyberFacade.js"
import { PQFacade } from "../../../../../src/common/api/worker/facades/PQFacade.js"
import { encodePQMessage, PQBucketKeyEncapsulation } from "../../../../../src/common/api/worker/facades/PQMessage.js"
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
import { RSA_TEST_KEYPAIR } from "../facades/RsaPqPerformanceTest.js"
import { DefaultEntityRestCache } from "../../../../../src/common/api/worker/rest/DefaultEntityRestCache.js"
import { loadLibOQSWASM } from "../WASMTestUtils.js"
import { AsymmetricCryptoFacade } from "../../../../../src/common/api/worker/crypto/AsymmetricCryptoFacade.js"
import { VerifiedPublicEncryptionKey } from "../../../../../src/common/api/worker/facades/lazy/KeyVerificationFacade"
import { KeyLoaderFacade, parseKeyVersion } from "../../../../../src/common/api/worker/facades/KeyLoaderFacade.js"
import { PublicEncryptionKeyProvider } from "../../../../../src/common/api/worker/facades/PublicEncryptionKeyProvider.js"
import { KeyRotationFacade } from "../../../../../src/common/api/worker/facades/KeyRotationFacade.js"
import { NotFoundError } from "../../../../../src/common/api/common/error/RestError"
import { AttributeModel } from "../../../../../src/common/api/common/AttributeModel"
import { EntityAdapter } from "../../../../../src/common/api/worker/crypto/EntityAdapter"
import { KeyVerificationMismatchError } from "../../../../../src/common/api/common/error/KeyVerificationMismatchError"
const { captor, anything, argThat } = matchers
const kyberFacade = new WASMKyberFacade(await loadLibOQSWASM())
const pqFacade: PQFacade = new PQFacade(kyberFacade)
let publicEncryptionKeyProvider: PublicEncryptionKeyProvider
/**
* Helper to have all the mocked items available in the test case.
*/
type TestUser = {
user: User
name: string
userGroup: Group
mailGroup: Group
userGroupKey: AesKey
mailGroupKey: AesKey
}
const senderAddress = "hello@tutao.de"
o.spec("CryptoFacadeTest", function () {
let restClient: RestClient
let instancePipeline
let serviceExecutor: IServiceExecutor
let entityClient: EntityClient
let ownerEncSessionKeysUpdateQueue: OwnerEncSessionKeysUpdateQueue
let crypto: CryptoFacade
let userFacade: UserFacade
let keyLoaderFacade: KeyLoaderFacade
let cache: DefaultEntityRestCache
let asymmetricCryptoFacade: AsymmetricCryptoFacade
let keyRotationFacade: KeyRotationFacade
let typeModelResolver: TypeModelResolver
async function prepareBucketKeyInstance(
bucketEncMailSessionKey: Uint8Array,
fileSessionKeys: Array<AesKey>,
bk: AesKey,
pubEncBucketKey: Uint8Array,
recipientUser: TestUser,
mail: Mail,
senderPubEccKey: Versioned<X25519PublicKey> | undefined,
recipientKeyVersion: NumberString,
protocolVersion: CryptoProtocolVersion,
asymmetricCryptoFacade: AsymmetricCryptoFacade,
) {
const MailTypeModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
typeInfo: createTestEntity(TypeInfoTypeRef, {
application: MailTypeModel.app,
typeId: String(MailTypeModel.id),
}),
symEncSessionKey: bucketEncMailSessionKey,
instanceList: "mailListId",
instanceId: "mailId",
})
const FileTypeModel = await typeModelResolver.resolveClientTypeReference(FileTypeRef)
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
return createTestEntity(InstanceSessionKeyTypeRef, {
typeInfo: createTestEntity(TypeInfoTypeRef, {
application: FileTypeModel.app,
typeId: String(FileTypeModel.id),
}),
symEncSessionKey: encryptKey(bk, fileSessionKey),
instanceList: "fileListId",
instanceId: "fileId" + (index + 1),
})
})
bucketEncSessionKeys.push(mailInstanceSessionKey)
const bucketKey = createTestEntity(BucketKeyTypeRef, {
pubEncBucketKey,
keyGroup: recipientUser.userGroup._id,
bucketEncSessionKeys: bucketEncSessionKeys,
recipientKeyVersion,
senderKeyVersion: senderPubEccKey != null ? senderPubEccKey.version.toString() : "0",
protocolVersion,
})
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(bucketKey.keyGroup),
parseKeyVersion(bucketKey.recipientKeyVersion),
asCryptoProtoocolVersion(bucketKey.protocolVersion),
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderPubEccKey?.object ?? null })
mail.bucketKey = bucketKey
}
o.before(function () {
restClient = object()
when(restClient.request(anything(), anything(), anything())).thenResolve(undefined)
userFacade = object()
cache = object()
})
o.beforeEach(function () {
serviceExecutor = object()
entityClient = object()
asymmetricCryptoFacade = object()
ownerEncSessionKeysUpdateQueue = object()
publicEncryptionKeyProvider = object()
keyLoaderFacade = object()
keyRotationFacade = object()
typeModelResolver = clientInitializedTypeModelResolver()
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
crypto = new CryptoFacade(
userFacade,
entityClient,
restClient,
serviceExecutor,
instancePipeline,
ownerEncSessionKeysUpdateQueue,
cache,
keyLoaderFacade,
asymmetricCryptoFacade,
publicEncryptionKeyProvider,
() => keyRotationFacade,
typeModelResolver,
)
})
o("resolve session key: unencrypted instance", async function () {
const customerAccountTerminationRequest = createTestEntity(CustomerAccountTerminationRequestTypeRef)
o(await crypto.resolveSessionKey(customerAccountTerminationRequest)).equals(null)
})
o("resolve session key: _ownerEncSessionKey instance.", async function () {
const recipientUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientUser, userFacade, keyLoaderFacade)
const sk = aes256RandomKey()
const mail = createTestEntity(MailTypeRef, {
_ownerEncSessionKey: recipientUser.mailGroupKey ? encryptKey(recipientUser.mailGroupKey, sk) : null,
_ownerGroup: recipientUser.mailGroup._id,
_ownerKeyVersion: recipientUser.mailGroup.groupKeyVersion,
})
const sessionKey: AesKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
o("resolve session key: _ownerEncSessionKey instance, fetches correct version.", async function () {
const recipientUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientUser, userFacade, keyLoaderFacade)
const sk = aes256RandomKey()
const groupKey_v1 = aes256RandomKey()
when(keyLoaderFacade.loadSymGroupKey(recipientUser.mailGroup._id, 1)).thenResolve(groupKey_v1)
const mail = createTestEntity(MailTypeRef, {
_ownerGroup: recipientUser.mailGroup._id,
_ownerEncSessionKey: encryptKey(groupKey_v1, sk),
_ownerKeyVersion: "1",
})
const sessionKey: AesKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
const protocolVersion = CryptoProtocolVersion.TUTA_CRYPT
o("resolve session key: rsa public key decryption of session key.", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const recipientUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientUser, userFacade, keyLoaderFacade)
let confidential = true
let sk = aes256RandomKey()
let bk = aes256RandomKey()
let privateKey = RSA_TEST_KEYPAIR.privateKey
let publicKey = RSA_TEST_KEYPAIR.publicKey
const keyPair = createTestEntity(KeyPairTypeRef, {
_id: "keyPairId",
symEncPrivRsaKey: encryptRsaKey(recipientUser.userGroupKey, privateKey),
pubRsaKey: hexToUint8Array(rsaPublicKeyToHex(RSA_TEST_KEYPAIR.publicKey)),
})
recipientUser.userGroup.currentKeys = keyPair
const mail = createTestEntity(MailTypeRef, {
confidential,
_ownerGroup: recipientUser.mailGroup._id,
_permissions: "permissionListId",
})
const bucket = createTestEntity(BucketTypeRef, {
bucketPermissions: "bucketPermissionListId",
})
const permission = createTestEntity(PermissionTypeRef, {
_id: ["permissionListId", "permissionId"],
_ownerGroup: recipientUser.userGroup._id,
bucketEncSessionKey: encryptKey(bk, sk),
bucket,
type: PermissionType.Public,
})
const pubEncBucketKey = object<Uint8Array>()
const bucketPermission = createTestEntity(BucketPermissionTypeRef, {
_id: ["bucketPermissionListId", "bucketPermissionId"],
_ownerGroup: recipientUser.userGroup._id,
type: BucketPermissionType.Public,
group: recipientUser.userGroup._id,
pubEncBucketKey,
protocolVersion: protocolVersion,
pubKeyVersion: "0",
})
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(bucketPermission.group),
parseKeyVersion(bucketPermission.pubKeyVersion!),
protocolVersion,
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: null })
when(entityClient.loadAll(BucketPermissionTypeRef, getListId(bucketPermission))).thenResolve([bucketPermission])
when(entityClient.loadAll(PermissionTypeRef, getListId(permission))).thenResolve([permission])
when(
serviceExecutor.post(
UpdatePermissionKeyService,
argThat((p: UpdatePermissionKeyData) => {
return isSameId(p.permission, permission._id) && isSameId(p.bucketPermission, bucketPermission._id)
}),
),
).thenResolve(undefined)
const sessionKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
o("resolve session key: pq public key decryption of session key.", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const recipientTestUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientTestUser, userFacade, keyLoaderFacade)
let pqKeyPairs = await pqFacade.generateKeyPairs()
const senderIdentityKeyPair = generateX25519KeyPair()
// configure test mail
let sk = aes256RandomKey()
let bk = aes256RandomKey()
const mail = createTestEntity(MailTypeRef, {
_permissions: "permissionListId",
_ownerGroup: recipientTestUser.mailGroup._id,
confidential: true,
})
const bucket = createBucket({
bucketPermissions: "bucketPermissionListId",
})
const permission = createPermission({
_format: "",
listElementApplication: null,
listElementTypeId: null,
ops: null,
symEncSessionKey: null,
symKeyVersion: null,
_id: ["permissionListId", "permissionId"],
_ownerGroup: recipientTestUser.mailGroup._id,
bucketEncSessionKey: encryptKey(bk, sk),
bucket,
type: PermissionType.Public,
_ownerEncSessionKey: null,
_ownerKeyVersion: null,
_permissions: "p_id",
group: null,
})
const pubEncBucketKey = await pqFacade.encapsulateAndEncode(
senderIdentityKeyPair,
generateX25519KeyPair(),
pqKeyPairsToPublicKeys(pqKeyPairs),
bitArrayToUint8Array(bk),
)
const protocolVersion = CryptoProtocolVersion.RSA
const bucketPermission = createBucketPermission({
_id: ["bucketPermissionListId", "bucketPermissionId"],
_format: "",
_permissions: "",
_ownerGroup: recipientTestUser.mailGroup._id,
type: BucketPermissionType.Public,
group: recipientTestUser.userGroup._id,
pubEncBucketKey,
senderKeyVersion: "0",
ownerEncBucketKey: null,
ownerKeyVersion: null,
protocolVersion,
pubKeyVersion: "0",
symEncBucketKey: null,
symKeyVersion: null,
})
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(bucketPermission.group),
parseKeyVersion(bucketPermission.pubKeyVersion!),
protocolVersion,
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderIdentityKeyPair.publicKey })
when(userFacade.createAuthHeaders()).thenReturn({})
when(restClient.request(anything(), HttpMethod.PATCH, anything())).thenResolve(undefined)
when(entityClient.loadAll(BucketPermissionTypeRef, getListId(bucketPermission))).thenResolve([bucketPermission])
when(entityClient.loadAll(PermissionTypeRef, getListId(permission))).thenResolve([permission])
const sessionKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
o("resolve session key: pq public key decryption of session key, fetches correct recipient key version", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const recipientTestUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientTestUser, userFacade, keyLoaderFacade)
const pqKeyPairs_v1 = await pqFacade.generateKeyPairs()
const senderIdentityKeyPair = generateX25519KeyPair()
// configure test mail
const sk = aes256RandomKey()
const bk = aes256RandomKey()
const mail = createTestEntity(MailTypeRef, {
_ownerGroup: recipientTestUser.mailGroup._id,
confidential: true,
_permissions: "permissionListId",
})
const bucket = createBucket({
bucketPermissions: "bucketPermissionListId",
})
const permission = createPermission({
_format: "",
listElementApplication: null,
listElementTypeId: null,
ops: null,
symEncSessionKey: null,
symKeyVersion: null,
_id: ["permissionListId", "permissionId"],
_ownerGroup: recipientTestUser.mailGroup._id,
bucketEncSessionKey: encryptKey(bk, sk),
bucket,
type: PermissionType.Public,
_ownerEncSessionKey: null,
_ownerKeyVersion: null,
_permissions: "p_id",
group: null,
})
const pubEncBucketKey = await pqFacade.encapsulateAndEncode(
senderIdentityKeyPair,
generateX25519KeyPair(),
pqKeyPairsToPublicKeys(pqKeyPairs_v1),
bitArrayToUint8Array(bk),
)
const protocolVersion = CryptoProtocolVersion.RSA
const bucketPermission = createBucketPermission({
_id: ["bucketPermissionListId", "bucketPermissionId"],
_format: "",
_permissions: "",
_ownerGroup: recipientTestUser.mailGroup._id,
type: BucketPermissionType.Public,
group: recipientTestUser.userGroup._id,
pubEncBucketKey,
senderKeyVersion: "0",
ownerEncBucketKey: null,
ownerKeyVersion: null,
protocolVersion: "0",
pubKeyVersion: "1",
symEncBucketKey: null,
symKeyVersion: null,
})
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(bucketPermission.group),
parseKeyVersion(bucketPermission.pubKeyVersion!),
protocolVersion,
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderIdentityKeyPair.publicKey })
when(userFacade.createAuthHeaders()).thenReturn({})
when(restClient.request(anything(), HttpMethod.PATCH, anything())).thenResolve(undefined)
when(entityClient.loadAll(BucketPermissionTypeRef, getListId(bucketPermission))).thenResolve([bucketPermission])
when(entityClient.loadAll(PermissionTypeRef, getListId(permission))).thenResolve([permission])
const sessionKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
o("resolve session key: pq public key decryption of session key using bucketKey", async function () {
o.timeout(500) // in CI or with debugging it can take a while
let confidential = true
const recipientTestUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientTestUser, userFacade, keyLoaderFacade)
const pqKeyPairs_v1 = await pqFacade.generateKeyPairs()
const senderIdentityKeyPair = generateX25519KeyPair()
// configure test mail
const sk = aes256RandomKey()
const bk = aes256RandomKey()
let mail = createTestEntity(MailTypeRef, {
_id: ["mailListId", "mailId"],
_ownerGroup: recipientTestUser.mailGroup._id,
confidential,
mailDetails: ["mailDetailsArchiveId", "mailDetailsId"],
sender: createTestEntity(MailAddressTypeRef, {
address: senderAddress,
name: "sender name",
}),
})
const bucketEncMailSessionKey = encryptKey(bk, sk)
const pubEncBucketKey = await pqFacade.encapsulateAndEncode(
senderIdentityKeyPair,
generateX25519KeyPair(),
pqKeyPairsToPublicKeys(pqKeyPairs_v1),
bitArrayToUint8Array(bk),
)
const senderKeyVersion = 1
await prepareBucketKeyInstance(
bucketEncMailSessionKey,
[],
bk,
pubEncBucketKey,
recipientTestUser,
mail,
{
object: senderIdentityKeyPair.publicKey,
version: senderKeyVersion,
},
"1",
protocolVersion,
asymmetricCryptoFacade,
)
when(
asymmetricCryptoFacade.decryptSymKeyWithKeyPair(
{
keyPairType: pqKeyPairs_v1.keyPairType,
x25519KeyPair: pqKeyPairs_v1.x25519KeyPair,
kyberKeyPair: pqKeyPairs_v1.kyberKeyPair,
},
protocolVersion,
pubEncBucketKey,
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderIdentityKeyPair.publicKey })
when(userFacade.createAuthHeaders()).thenReturn({})
when(restClient.request(anything(), HttpMethod.PATCH, anything())).thenResolve(undefined)
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
senderIdentityKeyPair.publicKey,
senderKeyVersion,
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
const sessionKey = neverNull(await crypto.resolveSessionKey(mail))
o(sessionKey).deepEquals(sk)
})
o("enforceSessionKeyUpdateIfNeeded: _ownerEncSessionKey already defined", async function () {
const files = [createTestEntity(FileTypeRef, { _ownerEncSessionKey: new Uint8Array() })]
const mail = createTestEntity(MailTypeRef, { bucketKey: null })
await crypto.enforceSessionKeyUpdateIfNeeded(mail, files)
verify(ownerEncSessionKeysUpdateQueue.postUpdateSessionKeysService(anything()), { times: 0 })
verify(cache.deleteFromCacheIfExists(anything(), anything(), anything()), { times: 0 })
})
o("enforceSessionKeyUpdateIfNeeded: _ownerEncSessionKey missing", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const files = [
createTestEntity(FileTypeRef, { _id: ["listId", "1"], _ownerEncSessionKey: new Uint8Array() }),
createTestEntity(FileTypeRef, { _id: ["listId", "2"], _ownerEncSessionKey: null }),
]
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest()
const bucketKey = assertNotNull(testData.mail.bucketKey)
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
testData.senderIdentityKeyPair.publicKey,
anything(),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
await crypto.enforceSessionKeyUpdateIfNeeded(testData.mail, files)
verify(ownerEncSessionKeysUpdateQueue.postUpdateSessionKeysService(anything()), { times: 1 })
verify(cache.deleteFromCacheIfExists(FileTypeRef, "listId", "2"))
})
o("encryptBucketKeyForInternalRecipient with existing PQKeys for sender and recipient", async function () {
let recipientMailAddress = "bob@tutanota.com"
let senderGroupKey = aes256RandomKey()
let bk = aes256RandomKey()
const recipientKeyPairs = await pqFacade.generateKeyPairs()
const recipientKeyPair = createKeyPair({
_id: "recipientKeyPairId",
pubEccKey: recipientKeyPairs.x25519KeyPair.publicKey,
symEncPrivEccKey: null,
pubKyberKey: kyberPublicKeyToBytes(recipientKeyPairs.kyberKeyPair.publicKey),
symEncPrivKyberKey: null,
pubRsaKey: null,
symEncPrivRsaKey: null,
signature: null,
})
const senderKeyPairs = await pqFacade.generateKeyPairs()
const senderKeyPair = createKeyPair({
_id: "senderKeyPairId",
pubRsaKey: null,
symEncPrivRsaKey: null,
pubEccKey: senderKeyPairs.x25519KeyPair.publicKey,
symEncPrivEccKey: aesEncrypt(senderGroupKey, senderKeyPairs.x25519KeyPair.privateKey),
pubKyberKey: kyberPublicKeyToBytes(senderKeyPairs.kyberKeyPair.publicKey),
symEncPrivKyberKey: aesEncrypt(senderGroupKey, kyberPrivateKeyToBytes(senderKeyPairs.kyberKeyPair.privateKey)),
signature: null,
})
const senderUserGroup = createGroup({
_format: "",
_ownerGroup: "",
_permissions: "",
admin: "admin1",
adminGroupEncGKey: null,
adminGroupKeyVersion: null,
archives: [],
customer: "customer1",
enabled: false,
external: false,
groupInfo: ["", ""],
invitations: "",
members: "",
storageCounter: "counter1",
type: "",
user: "user1",
_id: "userGroupId",
currentKeys: senderKeyPair,
groupKeyVersion: "0",
formerGroupKeys: createTestEntity(GroupKeysRefTypeRef),
pubAdminGroupEncGKey: null,
identityKeyPair: null,
})
const notFoundRecipients = []
const keyVerificationMismatchRecipients = []
const pqEncapsulation: PQBucketKeyEncapsulation = {
kyberCipherText: new Uint8Array([1]),
kekEncBucketKey: new Uint8Array([2]),
}
const encodedPqMessage: Uint8Array = encodePQMessage({
senderIdentityPubKey: senderKeyPair.pubEccKey!,
ephemeralPubKey: senderKeyPair.pubEccKey!,
encapsulation: pqEncapsulation,
})
const recipientPublicKeys: Versioned<PQPublicKeys> = {
version: 0,
object: {
keyPairType: KeyPairType.TUTA_CRYPT,
x25519PublicKey: recipientKeyPair.pubEccKey!,
kyberPublicKey: {
raw: recipientKeyPair.pubKyberKey!,
},
},
}
const loadedPublicKey: VerifiedPublicEncryptionKey = {
publicEncryptionKey: recipientPublicKeys,
verificationState: EncryptionKeyVerificationState.NO_ENTRY,
}
when(publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey(anything())).thenResolve(loadedPublicKey)
when(asymmetricCryptoFacade.asymEncryptSymKey(bk, recipientPublicKeys, senderUserGroup._id)).thenResolve({
recipientKeyVersion: recipientPublicKeys.version,
senderKeyVersion: parseKeyVersion(senderUserGroup.groupKeyVersion),
pubEncSymKeyBytes: encodedPqMessage,
cryptoProtocolVersion: CryptoProtocolVersion.TUTA_CRYPT,
})
const internalRecipientKeyData = (await crypto.encryptBucketKeyForInternalRecipient(
senderUserGroup._id,
bk,
recipientMailAddress,
notFoundRecipients,
keyVerificationMismatchRecipients,
)) as InternalRecipientKeyData
o(internalRecipientKeyData!.recipientKeyVersion).equals("0")
o(internalRecipientKeyData.protocolVersion).equals(CryptoProtocolVersion.TUTA_CRYPT)
o(internalRecipientKeyData!.mailAddress).equals(recipientMailAddress)
o(internalRecipientKeyData!.pubEncBucketKey).deepEquals(encodedPqMessage)
verify(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: recipientMailAddress,
}),
{ times: 1 },
)
})
o("encryptBucketKeyForInternalRecipient with existing PQKeys for sender", async () => {
let recipientMailAddress = "bob@tutanota.com"
let bk = aes256RandomKey()
let senderMailAddress = "alice@tutanota.com"
const senderKeyPair: KeyPair = object()
const senderUserGroup = createGroup({
_id: "userGroupId",
currentKeys: senderKeyPair,
groupKeyVersion: "0",
_permissions: "",
admin: null,
adminGroupEncGKey: null,
adminGroupKeyVersion: null,
archives: [],
customer: null,
enabled: false,
external: false,
groupInfo: ["", ""],
invitations: "",
members: "",
storageCounter: null,
type: "",
user: null,
formerGroupKeys: createTestEntity(GroupKeysRefTypeRef),
pubAdminGroupEncGKey: null,
identityKeyPair: null,
})
const notFoundRecipients = []
const keyVerificationMismatchRecipients = []
const recipientPublicKeys: Versioned<RsaPublicKey> = {
version: 0,
object: object(),
}
const loadedRecipientPublicKey: VerifiedPublicEncryptionKey = {
publicEncryptionKey: recipientPublicKeys,
verificationState: EncryptionKeyVerificationState.NO_ENTRY,
}
when(publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey(anything())).thenResolve(loadedRecipientPublicKey)
const senderPublicKeys: Versioned<PQPublicKeys> = {
version: 0,
object: object(),
}
const loadedSenderPublicKey: VerifiedPublicEncryptionKey = {
publicEncryptionKey: senderPublicKeys,
verificationState: EncryptionKeyVerificationState.NO_ENTRY,
}
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: senderMailAddress,
}),
).thenResolve(loadedSenderPublicKey)
const pubEncBucketKey = object<Uint8Array>()
when(asymmetricCryptoFacade.asymEncryptSymKey(bk, recipientPublicKeys, senderUserGroup._id)).thenResolve({
recipientKeyVersion: recipientPublicKeys.version,
senderKeyVersion: parseKeyVersion(senderUserGroup.groupKeyVersion),
pubEncSymKeyBytes: pubEncBucketKey,
cryptoProtocolVersion: CryptoProtocolVersion.RSA,
})
const internalRecipientKeyData = (await crypto.encryptBucketKeyForInternalRecipient(
senderUserGroup._id,
bk,
recipientMailAddress,
notFoundRecipients,
keyVerificationMismatchRecipients,
)) as InternalRecipientKeyData
o(internalRecipientKeyData!.recipientKeyVersion).equals("0")
o(internalRecipientKeyData!.mailAddress).equals(recipientMailAddress)
o(internalRecipientKeyData.protocolVersion).equals(CryptoProtocolVersion.RSA)
o(internalRecipientKeyData.pubEncBucketKey).deepEquals(pubEncBucketKey)
verify(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: recipientMailAddress,
}),
{ times: 1 },
)
})
o("encryptBucketKeyForInternalRecipient for non-existing recipients", async function () {
let notFoundRecipientMailAddress = "notfound@tutanota.com"
let bk = aes256RandomKey()
const notFoundRecipients: string[] = []
const keyVerificationMismatchRecipients: string[] = []
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: notFoundRecipientMailAddress,
}),
).thenReject(new NotFoundError(""))
await crypto.encryptBucketKeyForInternalRecipient(
"senderGroupId",
bk,
notFoundRecipientMailAddress,
notFoundRecipients,
keyVerificationMismatchRecipients,
)
o(notFoundRecipients).deepEquals(["notfound@tutanota.com"])
o(keyVerificationMismatchRecipients).deepEquals([])
verify(userFacade.getUser(), { times: 0 })
})
o("encryptBucketKeyForInternalRecipient with non-existing recipients", async function () {
let notFoundRecipient1MailAddress = "notfound1@tutanota.com"
let notFoundRecipient2MailAddress = "notfound2@tutanota.com"
const validRecipientMailAddress = "alice@tuta.com"
let bk = aes256RandomKey()
const notFoundRecipients: string[] = []
const mismatchRecipients: string[] = []
const recipientPublicKey: Versioned<PQPublicKeys> = {
version: 0,
object: object(),
}
recipientPublicKey.object.keyPairType = KeyPairType.TUTA_CRYPT
const loadedRecipientPublicKey: VerifiedPublicEncryptionKey = {
publicEncryptionKey: recipientPublicKey,
verificationState: EncryptionKeyVerificationState.NO_ENTRY,
}
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: validRecipientMailAddress,
}),
).thenResolve(loadedRecipientPublicKey)
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: notFoundRecipient1MailAddress,
}),
).thenReject(new NotFoundError(""))
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: notFoundRecipient2MailAddress,
}),
).thenReject(new NotFoundError(""))
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, notFoundRecipient1MailAddress, notFoundRecipients, mismatchRecipients)
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, validRecipientMailAddress, notFoundRecipients, mismatchRecipients)
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, notFoundRecipient2MailAddress, notFoundRecipients, mismatchRecipients)
o(notFoundRecipients).deepEquals([notFoundRecipient1MailAddress, notFoundRecipient2MailAddress])
o(mismatchRecipients).deepEquals([])
verify(userFacade.getUser(), { times: 0 })
})
o("encryptBucketKeyForInternalRecipient for verification-failing recipients", async function () {
let mismatchRecipient1MailAddress = "mismatch1@tutanota.com"
let mismatchRecipient2MailAddress = "mismatch2@tutanota.com"
const validRecipientMailAddress = "alice@tuta.com"
let bk = aes256RandomKey()
const notFoundRecipients: string[] = []
const mismatchRecipients: string[] = []
const recipientPublicKey: Versioned<PQPublicKeys> = {
version: 0,
object: object(),
}
recipientPublicKey.object.keyPairType = KeyPairType.TUTA_CRYPT
const loadedRecipientPublicKey: VerifiedPublicEncryptionKey = {
publicEncryptionKey: recipientPublicKey,
verificationState: EncryptionKeyVerificationState.NO_ENTRY,
}
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: validRecipientMailAddress,
}),
).thenResolve(loadedRecipientPublicKey)
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: mismatchRecipient1MailAddress,
}),
).thenReject(new KeyVerificationMismatchError(""))
when(
publicEncryptionKeyProvider.loadCurrentPublicEncryptionKey({
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
identifier: mismatchRecipient2MailAddress,
}),
).thenReject(new KeyVerificationMismatchError(""))
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, mismatchRecipient1MailAddress, notFoundRecipients, mismatchRecipients)
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, validRecipientMailAddress, notFoundRecipients, mismatchRecipients)
await crypto.encryptBucketKeyForInternalRecipient("senderGroupId", bk, mismatchRecipient2MailAddress, notFoundRecipients, mismatchRecipients)
o(notFoundRecipients).deepEquals([])
o(mismatchRecipients).deepEquals([mismatchRecipient1MailAddress, mismatchRecipient2MailAddress])
verify(userFacade.getUser(), { times: 0 })
})
o("authenticateSender | sender is authenticated for correct SenderIdentityKey", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest()
const senderKeyVersion = "0"
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
testData.senderIdentityKeyPair.publicKey,
parseKeyVersion(senderKeyVersion),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
const sessionKey: AesKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()))
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.mail.bucketKey!.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED)
})
o("authenticateSender | sender is authenticated for correct SenderIdentityKey from system@tutanota.de", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest([], false)
const senderKeyVersion = "0"
const senderIdentifier = "system@tutanota.de"
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderIdentifier,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
testData.senderIdentityKeyPair.publicKey,
parseKeyVersion(senderKeyVersion),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
const sessionKey: AesKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()))
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.mail.bucketKey!.bucketEncSessionKeys.length)
const mailInstanceSessionKey = assertNotNull(
updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, mailInstanceSessionKey.encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED)
})
o("authenticateSender | sender is not authenticated for incorrect SenderIdentityKey", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest()
const senderKeyVersion = "0"
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
testData.senderIdentityKeyPair.publicKey,
parseKeyVersion(senderKeyVersion),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_FAILED, verificationState: PresentableKeyVerificationState.ALERT })
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()))
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.mail.bucketKey!.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_FAILED)
})
o("authenticateSender | no authentication needed for sender with RSAKeypair", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
const sessionKey = assertNotNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()), { times: 1 })
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.mail.bucketKey!.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
)
const actualAuthStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, assertNotNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAuthStatus).deepEquals(EncryptionAuthStatus.RSA_NO_AUTHENTICATION)
})
o("authenticateSender | RSA was used despite recipient having tutacrypt", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
when(keyLoaderFacade.loadCurrentKeyPair(anything())).thenResolve({
version: 1,
object: {
keyPairType: KeyPairType.TUTA_CRYPT,
kyberKeyPair: object(),
x25519KeyPair: object(),
},
})
when(keyRotationFacade.getGroupIdsThatPerformedKeyRotations()).thenResolve([])
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()), { times: 1 })
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.mail.bucketKey!.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.RSA_DESPITE_TUTACRYPT)
})
o("authenticateSender | RSA was used right after a key rotation", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
when(keyLoaderFacade.loadCurrentKeyPair(anything())).thenResolve({
version: 1,
object: {
keyPairType: KeyPairType.TUTA_CRYPT,
kyberKeyPair: object(),
x25519KeyPair: object(),
},
})
when(keyRotationFacade.getGroupIdsThatPerformedKeyRotations()).thenResolve([testData.userGroupId])
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
const bucketKey = assertNotNull(testData.mail.bucketKey)
o(sessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()), { times: 1 })
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(bucketKey.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.mail._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.RSA_NO_AUTHENTICATION)
})
o("authenticateSender | no authentication needed for secure external recipient", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const file1SessionKey = aes256RandomKey()
const file2SessionKey = aes256RandomKey()
const testData = await prepareConfidentialMailToExternalRecipient([file1SessionKey, file2SessionKey])
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.entityAdapter))
o(mailSessionKey).deepEquals(testData.sk)
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()), { times: 1 })
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.bucketKey.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.entityAdapter._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.AES_NO_AUTHENTICATION)
})
o("authenticateSender | no authentication needed for secure external sender", async function () {
//o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareConfidentialReplyFromExternalUser()
const externalUser = testData.externalUser
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.entityAdapter))
o(mailSessionKey).deepEquals(testData.sk)
const mailCaptor = matchers.captor()
const userCaptor = matchers.captor()
verify(keyLoaderFacade.loadSymGroupKey(externalUser.userGroup._id, parseKeyVersion(externalUser.mailGroup.adminGroupKeyVersion!), userCaptor.capture()))
verify(keyLoaderFacade.loadSymGroupKey(externalUser.mailGroup._id, testData.recipientKeyVersion, mailCaptor.capture()))
o(userCaptor.value.version).equals(parseKeyVersion(externalUser.userGroup.groupKeyVersion))
o(mailCaptor.value.version).equals(parseKeyVersion(externalUser.mailGroup.groupKeyVersion))
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()), { times: 1 })
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value as Array<InstanceSessionKey>
o(updatedInstanceSessionKeys.length).equals(testData.bucketKey.bucketEncSessionKeys.length)
const mailInstanceSessionKey = updatedInstanceSessionKeys.find((instanceSessionKey) =>
isSameId([instanceSessionKey.instanceList, instanceSessionKey.instanceId], testData.entityAdapter._id),
)
const actualAutStatus = utf8Uint8ArrayToString(aesDecrypt(testData.sk, neverNull(mailInstanceSessionKey).encryptionAuthStatus!))
o(actualAutStatus).deepEquals(EncryptionAuthStatus.AES_NO_AUTHENTICATION)
})
o("resolve session key: rsa public key decryption of session key using BucketKey aggregated type", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
})
o(
"resolve session key: rsa public key decryption of mail session key using BucketKey aggregated type - already decoded/decrypted Mail referencing MailDetailsDraft",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
// do not use testdouble here because it's hard to not break the function itself and then verify invocations
const decryptAndMapToInstance = (instancePipeline.cryptoMapper.decryptParsedInstance = spy(instancePipeline.cryptoMapper.decryptParsedInstance))
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(decryptAndMapToInstance.invocations.length).equals(0)
o(sessionKey).deepEquals(testData.sk)
},
)
o("resolve session key: rsa public key decryption of session key using BucketKey aggregated type - Mail referencing MailDetailsBlob", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest()
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
})
o(
"resolve session key: rsa public key decryption of session key using BucketKey aggregated type - Mail referencing MailDetailsBlob with attachments",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const file1SessionKey = aes256RandomKey()
const file2SessionKey = aes256RandomKey()
const testData = await prepareRsaPubEncBucketKeyResolveSessionKeyTest([file1SessionKey, file2SessionKey])
const mailSessionKey = assertNotNull(await crypto.resolveSessionKey(testData.mail))
o(mailSessionKey).deepEquals(testData.sk)
const bucketKey = assertNotNull(testData.mail.bucketKey)
o(bucketKey.bucketEncSessionKeys.length).equals(3) //mail, file1, file2
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()))
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value
o(updatedInstanceSessionKeys.length).equals(bucketKey.bucketEncSessionKeys.length)
for (const isk of bucketKey.bucketEncSessionKeys) {
const expectedSessionKey = decryptKey(testData.bk, isk.symEncSessionKey)
o(
updatedInstanceSessionKeys.some((updatedKey) => {
let updatedSessionKey = decryptKey(testData.mailGroupKey, updatedKey.symEncSessionKey)
return (
updatedKey.instanceId === isk.instanceId &&
updatedKey.instanceList === isk.instanceList &&
updatedKey.typeInfo.application === isk.typeInfo.application &&
updatedKey.typeInfo.typeId === isk.typeInfo.typeId &&
arrayEquals(updatedSessionKey, expectedSessionKey)
)
}),
).equals(true)
}
},
)
// ------------
o("resolve session key: pq public key decryption of mail session key using BucketKey aggregated type - Mail", async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest()
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
anything(),
anything(),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(sessionKey).deepEquals(testData.sk)
})
o(
"resolve session key: pq public key decryption of mail session key using BucketKey aggregated type - already decoded/decrypted Mail referencing MailDetailsDraft",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest()
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
anything(),
anything(),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
// do not use testdouble here because it's hard to not break the function itself and then verify invocations
const decryptAndMapToInstance = (instancePipeline.cryptoMapper.decryptParsedInstance = spy(instancePipeline.cryptoMapper.decryptParsedInstance))
const sessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
o(decryptAndMapToInstance.invocations.length).equals(0)
o(sessionKey).deepEquals(testData.sk)
},
)
o(
"resolve session key: pq public key decryption of session key using BucketKey aggregated type - Mail referencing MailDetailsBlob with attachments",
async function () {
o.timeout(500) // in CI or with debugging it can take a while
const file1SessionKey = aes256RandomKey()
const file2SessionKey = aes256RandomKey()
const testData = await preparePqPubEncBucketKeyResolveSessionKeyTest([file1SessionKey, file2SessionKey])
when(
asymmetricCryptoFacade.authenticateSender(
{
identifier: senderAddress,
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
},
anything(),
anything(),
),
).thenResolve({ authStatus: EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED, verificationState: PresentableKeyVerificationState.SECURE })
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.mail))
const bucketKey = assertNotNull(testData.mail.bucketKey)
o(mailSessionKey).deepEquals(testData.sk)
o(bucketKey.bucketEncSessionKeys.length).equals(3) //mail, file1, file2
const updatedInstanceSessionKeysCaptor = captor()
verify(ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatedInstanceSessionKeysCaptor.capture(), anything()))
const updatedInstanceSessionKeys = updatedInstanceSessionKeysCaptor.value
o(updatedInstanceSessionKeys.length).equals(bucketKey.bucketEncSessionKeys.length)
for (const isk of bucketKey.bucketEncSessionKeys) {
const expectedSessionKey = decryptKey(testData.bk, isk.symEncSessionKey)
if (
!updatedInstanceSessionKeys.some((updatedKey) => {
const updatedSessionKey = decryptKey(testData.mailGroupKey, updatedKey.symEncSessionKey)
return (
updatedKey.instanceId === isk.instanceId &&
updatedKey.instanceList === isk.instanceList &&
updatedKey.typeInfo.application === isk.typeInfo.application &&
updatedKey.typeInfo.typeId === isk.typeInfo.typeId &&
arrayEquals(updatedSessionKey, expectedSessionKey)
)
})
) {
console.log("===============================")
updatedInstanceSessionKeys.some((updatedKey) => {
const updatedSessionKey = decryptKey(testData.mailGroupKey, updatedKey.symEncSessionKey)
console.log(">>>>>>>>>>>>>>>>>>>>>>>")
console.log("1 ", updatedKey.instanceId, isk.instanceId)
console.log("2 ", updatedKey.instanceList, isk.instanceList)
console.log("3 ", updatedKey.typeInfo.application, isk.typeInfo.application)
console.log("4 ", updatedKey.typeInfo.typeId, isk.typeInfo.typeId)
console.log("5 ", updatedSessionKey, expectedSessionKey)
})
}
o(
updatedInstanceSessionKeys.some((updatedKey) => {
const updatedSessionKey = decryptKey(testData.mailGroupKey, updatedKey.symEncSessionKey)
return (
updatedKey.instanceId === isk.instanceId &&
updatedKey.instanceList === isk.instanceList &&
updatedKey.typeInfo.application === isk.typeInfo.application &&
updatedKey.typeInfo.typeId === isk.typeInfo.typeId &&
arrayEquals(updatedSessionKey, expectedSessionKey)
)
}),
).equals(true)
}
},
)
o(
"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 = aes256RandomKey()
const file2SessionKey = aes256RandomKey()
const testData = await prepareConfidentialMailToExternalRecipient([file1SessionKey, file2SessionKey])
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.entityAdapter))
o(mailSessionKey).deepEquals(testData.sk)
},
)
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 = aes256RandomKey()
const file2SessionKey = aes256RandomKey()
const testData = await prepareConfidentialMailToExternalRecipient([file1SessionKey, file2SessionKey], true)
const mailSessionKey = neverNull(await crypto.resolveSessionKey(testData.entityAdapter))
o(mailSessionKey).deepEquals(testData.sk)
},
)
o("resolve session key: MailDetailsBlob", async function () {
const gk = aes256RandomKey()
const sk = aes256RandomKey()
const ownerGroup = "mailGroupId"
when(keyLoaderFacade.getCurrentSymGroupKey(ownerGroup)).thenResolve({ object: gk, version: 0 })
when(userFacade.hasGroup(ownerGroup)).thenReturn(true)
when(userFacade.isFullyLoggedIn()).thenReturn(true)
const mailDetailsBlob = createTestEntity(MailDetailsBlobTypeRef, {
_id: ["mailDetailsArchiveId", "mailDetailsId"],
_ownerGroup: ownerGroup,
_ownerEncSessionKey: encryptKey(gk, sk),
})
when(keyLoaderFacade.loadSymGroupKey(ownerGroup, 0)).thenResolve(gk)
const mailDetailsBlobSessionKey = neverNull(await crypto.resolveSessionKey(mailDetailsBlob))
o(mailDetailsBlobSessionKey).deepEquals(sk)
})
o("resolve session key: MailDetailsBlob - session key not found", async function () {
const mailDetailsBlob = createTestEntity(MailDetailsBlobTypeRef, {
_id: ["mailDetailsArchiveId", "mailDetailsId"],
_permissions: "permissionListId",
})
when(entityClient.loadAll(PermissionTypeRef, "permissionListId")).thenResolve([])
try {
await crypto.resolveSessionKey(mailDetailsBlob)
o(true).equals(false) // let the test fails if there is no exception
} catch (error) {
o(error.constructor).equals(SessionKeyNotFoundError)
}
})
/**
* Prepares the environment to test receiving rsa asymmetric encrypted emails that have been sent with the simplified permission system.
* - Creates key pair for the recipient user
* - Creates group, bucket and session keys
* - Creates mail literal and encrypts all encrypted attributes of the mail
* - Create BucketKey object on the mail
*
* @param fileSessionKeys List of session keys for the attachments. When the list is empty there are no attachments
*/
async function prepareRsaPubEncBucketKeyResolveSessionKeyTest(fileSessionKeys: Array<Aes256Key> = []): Promise<{
mail: Mail
sk: Aes256Key
bk: Aes256Key
mailGroupKey: Aes256Key
userGroupId: Id
}> {
// configure test user
const recipientUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientUser, userFacade, keyLoaderFacade)
let privateKey = RSA_TEST_KEYPAIR.privateKey
let publicKey = RSA_TEST_KEYPAIR.publicKey
const keyPair = createTestEntity(KeyPairTypeRef, {
_id: "keyPairId",
symEncPrivRsaKey: encryptRsaKey(recipientUser.userGroupKey, privateKey),
pubRsaKey: hexToUint8Array(rsaPublicKeyToHex(publicKey)),
})
recipientUser.userGroup.currentKeys = keyPair
let sk = aes256RandomKey()
let bk = aes256RandomKey()
const mail = createTestEntity(MailTypeRef, {
_id: ["mailListId", "mailId"],
_permissions: "permissionListId",
_ownerGroup: recipientUser.mailGroup._id,
confidential: true,
subject: "oh no is this a subject",
})
const pubEncBucketKey = new Uint8Array([1, 2, 3, 4])
const bucketEncMailSessionKey = encryptKey(bk, sk)
const mailInstanceSessionKey = createInstanceSessionKey({
typeInfo: createTypeInfo({
application: MailTypeRef.app,
typeId: MailTypeRef.typeId.toString(),
}),
symEncSessionKey: bucketEncMailSessionKey,
instanceList: listIdPart(mail._id),
instanceId: elementIdPart(mail._id),
encryptionAuthStatus: null,
symKeyVersion: "0",
keyVerificationState: null,
})
const FileTypeModel = await typeModelResolver.resolveClientTypeReference(FileTypeRef)
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
return createInstanceSessionKey({
typeInfo: createTypeInfo({
application: FileTypeModel.app,
typeId: String(FileTypeModel.id),
}),
symEncSessionKey: encryptKey(bk, fileSessionKey),
symKeyVersion: "0",
instanceList: "fileListId",
instanceId: "fileId" + (index + 1),
encryptionAuthStatus: null,
keyVerificationState: null,
})
})
bucketEncSessionKeys.push(mailInstanceSessionKey)
const protocolVersion = CryptoProtocolVersion.RSA
const bucketKey = createBucketKey({
pubEncBucketKey,
keyGroup: recipientUser.userGroup._id,
bucketEncSessionKeys: bucketEncSessionKeys,
groupEncBucketKey: null,
protocolVersion,
senderKeyVersion: null,
recipientKeyVersion: "0",
})
when(keyLoaderFacade.loadCurrentKeyPair(recipientUser.userGroup._id)).thenResolve({
object: {
keyPairType: KeyPairType.RSA,
publicKey: RSA_TEST_KEYPAIR.publicKey,
privateKey: RSA_TEST_KEYPAIR.privateKey,
},
version: 0,
})
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(bucketKey.keyGroup),
parseKeyVersion(bucketKey.recipientKeyVersion),
asCryptoProtoocolVersion(bucketKey.protocolVersion),
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: null })
mail.bucketKey = bucketKey
return {
mail,
sk,
bk,
mailGroupKey: recipientUser.mailGroupKey,
userGroupId: recipientUser.userGroup._id,
}
}
/**
* Prepares the environment to test receiving pq asymmetric encrypted emails that have been sent with the simplified permission system.
* - Creates key pair for the recipient user
* - Creates group, bucket and session keys
* - Creates mail literal and encrypts all encrypted attributes of the mail
* - Create BucketKey object on the mail
*
* @param fileSessionKeys List of session keys for the attachments. When the list is empty there are no attachments
*/
async function preparePqPubEncBucketKeyResolveSessionKeyTest(
fileSessionKeys: Array<AesKey> = [],
confidential: boolean = true,
): Promise<{
mail: Mail
sk: AesKey
bk: AesKey
mailGroupKey: AesKey
senderIdentityKeyPair: X25519KeyPair
}> {
// create test user
const recipientUser = createTestUser("Bob", entityClient)
configureLoggedInUser(recipientUser, userFacade, keyLoaderFacade)
let pqKeyPairs = await pqFacade.generateKeyPairs()
const recipientKeyPair = createKeyPair({
_id: "keyPairId",
pubEccKey: pqKeyPairs.x25519KeyPair.publicKey,
symEncPrivEccKey: aesEncrypt(recipientUser.userGroupKey, pqKeyPairs.x25519KeyPair.privateKey),
pubKyberKey: kyberPublicKeyToBytes(pqKeyPairs.kyberKeyPair.publicKey),
symEncPrivKyberKey: aesEncrypt(recipientUser.userGroupKey, kyberPrivateKeyToBytes(pqKeyPairs.kyberKeyPair.privateKey)),
pubRsaKey: null,
symEncPrivRsaKey: null,
signature: null,
})
recipientUser.userGroup.currentKeys = recipientKeyPair
const senderIdentityKeyPair = generateX25519KeyPair()
let sk = aes256RandomKey()
let bk = aes256RandomKey()
const mail = createTestEntity(MailTypeRef, {
confidential,
_ownerGroup: recipientUser.mailGroup._id,
_ownerEncSessionKey: null, // enforce asymmetric crypto to resolve session key
_id: ["mailListId", "mailId"],
_permissions: "permissionListId",
sender: createTestEntity(MailAddressTypeRef, {
address: senderAddress,
name: "sender name",
}),
})
const pubEncBucketKey = await pqFacade.encapsulateAndEncode(
senderIdentityKeyPair,
generateX25519KeyPair(),
pqKeyPairsToPublicKeys(pqKeyPairs),
bitArrayToUint8Array(bk),
)
const bucketEncMailSessionKey = encryptKey(bk, sk)
await prepareBucketKeyInstance(
bucketEncMailSessionKey,
fileSessionKeys,
bk,
pubEncBucketKey,
recipientUser,
mail,
undefined,
"0",
CryptoProtocolVersion.TUTA_CRYPT,
asymmetricCryptoFacade,
)
when(
asymmetricCryptoFacade.decryptSymKeyWithKeyPair(
{
keyPairType: pqKeyPairs.keyPairType,
x25519KeyPair: pqKeyPairs.x25519KeyPair,
kyberKeyPair: pqKeyPairs.kyberKeyPair,
},
CryptoProtocolVersion.TUTA_CRYPT,
pubEncBucketKey,
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderIdentityKeyPair.publicKey })
when(
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
assertNotNull(mail.bucketKey?.keyGroup),
parseKeyVersion(assertNotNull(mail.bucketKey?.recipientKeyVersion)),
asCryptoProtoocolVersion(assertNotNull(mail.bucketKey?.protocolVersion)),
pubEncBucketKey,
anything(),
),
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderIdentityKeyPair.publicKey })
return {
mail,
sk,
bk,
mailGroupKey: recipientUser.mailGroupKey,
senderIdentityKeyPair,
}
}
/**
* Prepares the environment to test receiving symmetric encrypted emails (mails sent from internal to external user) that have been sent with the simplified permission system.
* - Creates group, bucket and session keys
* - Creates mail literal and encrypts all encrypted attributes of the mail
* - Create BucketKey object on the mail
*
* @param fileSessionKeys List of session keys for the attachments. When the list is empty there are no attachments
* @param externalUserGroupEncBucketKey for legacy external user group to encrypt bucket key
*/
async function prepareConfidentialMailToExternalRecipient(
fileSessionKeys: Array<AesKey> = [],
externalUserGroupEncBucketKey = false,
): Promise<{
entityAdapter: EntityAdapter
bucketKey: BucketKey
sk: AesKey
bk: AesKey
MailTypeModel: TypeModel
}> {
// create user
const externalUser = createTestUser("Bob", entityClient)
configureLoggedInUser(externalUser, userFacade, keyLoaderFacade)
// create test mail
let confidential = true
let sk = aes256RandomKey()
let bk = aes256RandomKey()
const mailUntypedInstance = await createUntypedMailInstance(null, sk, confidential, externalUser.mailGroup._id)
const groupKeyToEncryptBucketKey = externalUserGroupEncBucketKey ? externalUser.userGroupKey : externalUser.mailGroupKey
const groupEncBucketKey = encryptKey(groupKeyToEncryptBucketKey, bk)
const bucketEncMailSessionKey = encryptKey(bk, sk)
const MailTypeModel = await typeModelResolver.resolveServerTypeReference(MailTypeRef)
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
typeInfo: createTestEntity(TypeInfoTypeRef, {
application: MailTypeModel.app,
typeId: String(MailTypeModel.id),
}),
symEncSessionKey: bucketEncMailSessionKey,
instanceList: "mailListId",
instanceId: "mailId",
})
const FileTypeModel = await typeModelResolver.resolveServerTypeReference(FileTypeRef)
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
return createTestEntity(InstanceSessionKeyTypeRef, {
typeInfo: createTestEntity(TypeInfoTypeRef, {
application: FileTypeModel.app,
typeId: String(FileTypeModel.id),
}),
symEncSessionKey: encryptKey(bk, fileSessionKey),
instanceList: "fileListId",
instanceId: "fileId" + (index + 1),
})
})
bucketEncSessionKeys.push(mailInstanceSessionKey)
const bucketKey = createTestEntity(BucketKeyTypeRef, {
pubEncBucketKey: null,
keyGroup: externalUserGroupEncBucketKey ? externalUser.userGroup._id : null,
groupEncBucketKey: groupEncBucketKey,
bucketEncSessionKeys: bucketEncSessionKeys,
})
const bucketKeyUntypedInstance: UntypedInstance = await instancePipeline.mapAndEncrypt(BucketKeyTypeRef, bucketKey, null)
mailUntypedInstance[assertNotNull(AttributeModel.getAttributeId(MailTypeModel, "bucketKey"))] = [bucketKeyUntypedInstance]
const mailEncryptedParsedInstance = await instancePipeline.typeMapper.applyJsTypes(MailTypeModel, mailUntypedInstance)
return {
entityAdapter: await EntityAdapter.from(MailTypeModel, mailEncryptedParsedInstance, instancePipeline),
bucketKey,
sk,
bk,
MailTypeModel,
}
}
/**
* Prepares the environment to test receiving symmetric encrypted emails from an external sender(mails sent from external to internal user) that have been sent with the simplified permission system.
* - Creates group, bucket and session keys
* - Creates mail literal and encrypts all encrypted attributes of the mail
* - Create BucketKey object on the mail
*
* @param fileSessionKeys List of session keys for the attachments. When the list is empty there are no attachments
*/
async function prepareConfidentialReplyFromExternalUser(): Promise<{
entityAdapter: EntityAdapter
bucketKey: BucketKey
sk: AesKey
bk: AesKey
MailTypeModel: TypeModel
internalUser: TestUser
externalUser: TestUser
recipientKeyVersion: KeyVersion
}> {
// Setup test users and groups
const internalUser = createTestUser("Alice", entityClient)
const externalUser = createTestUser("Bob", entityClient)
// Setup relationship between internal and external user
externalUser.userGroup.admin = internalUser.userGroup._id
externalUser.userGroup.adminGroupEncGKey = encryptKey(internalUser.userGroupKey, externalUser.userGroupKey)
externalUser.userGroup.adminGroupKeyVersion = "0"
externalUser.mailGroup.admin = externalUser.userGroup._id
externalUser.mailGroup.adminGroupEncGKey = encryptKey(externalUser.userGroupKey, externalUser.mailGroupKey)
externalUser.mailGroup.adminGroupKeyVersion = "4"
const recipientKeyVersion = "5"
externalUser.userGroup.groupKeyVersion = "7"
externalUser.mailGroup.groupKeyVersion = "8"
configureLoggedInUser(internalUser, userFacade, keyLoaderFacade)
when(keyLoaderFacade.loadSymGroupKey(externalUser.mailGroup._id, parseKeyVersion(recipientKeyVersion), anything())).thenResolve(
externalUser.mailGroupKey,
)
when(keyLoaderFacade.loadSymGroupKey(externalUser.userGroup._id, parseKeyVersion(externalUser.mailGroup.adminGroupKeyVersion), anything())).thenResolve(
externalUser.userGroupKey,
)
// setup test mail (confidential reply from external)
let confidential = true
let sk = aes256RandomKey()
let bk = aes256RandomKey()
const untypedMailInstance = await createUntypedMailInstance(null, sk, confidential, internalUser.mailGroup._id)
const keyGroup = externalUser.mailGroup._id
const groupEncBucketKey = encryptKey(externalUser.mailGroupKey, bk)
const bucketEncMailSessionKey = encryptKey(bk, sk)
const MailTypeModel = await typeModelResolver.resolveServerTypeReference(MailTypeRef)
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
typeInfo: createTestEntity(TypeInfoTypeRef, {
application: MailTypeModel.app,
typeId: String(MailTypeModel.id),
}),
symEncSessionKey: bucketEncMailSessionKey,
instanceList: "mailListId",
instanceId: "mailId",
})
const bucketEncSessionKeys = new Array<InstanceSessionKey>()
bucketEncSessionKeys.push(mailInstanceSessionKey)
const bucketKey = createTestEntity(BucketKeyTypeRef, {
pubEncBucketKey: null,
keyGroup: keyGroup,
groupEncBucketKey: groupEncBucketKey,
recipientKeyVersion,
bucketEncSessionKeys: bucketEncSessionKeys,
protocolVersion: CryptoProtocolVersion.SYMMETRIC_ENCRYPTION,
senderKeyVersion: null,
})
const bucketKeyUntypedInstance: UntypedInstance = await instancePipeline.mapAndEncrypt(BucketKeyTypeRef, bucketKey, null)
untypedMailInstance[assertNotNull(AttributeModel.getAttributeId(MailTypeModel, "bucketKey"))] = [bucketKeyUntypedInstance]
const encryptedMailParsedInstance = await instancePipeline.typeMapper.applyJsTypes(MailTypeModel, untypedMailInstance)
const entityAdapter = await EntityAdapter.from(MailTypeModel, encryptedMailParsedInstance, instancePipeline)
return {
entityAdapter: entityAdapter,
bucketKey,
sk,
bk,
MailTypeModel,
internalUser,
externalUser,
recipientKeyVersion: parseKeyVersion(recipientKeyVersion),
}
}
async function createUntypedMailInstance(
ownerGroupKey: AesKey | null,
sessionKey: AesKey,
confidential: boolean,
ownerGroupId: string,
): Promise<ServerModelUntypedInstance> {
const mail = createMail({
_format: "0",
_ownerGroup: ownerGroupId,
_ownerEncSessionKey: ownerGroupKey ? encryptKey(ownerGroupKey, sessionKey) : null,
_permissions: "permissionListId",
_id: ["mailListId", "mailId"],
receivedDate: new Date(1470039025474),
state: "",
unread: true,
subject: "any subject",
replyType: "",
confidential: confidential,
sender: createMailAddress({
address: senderAddress,
name: "any sender",
contact: null,
}),
bucketKey: null,
authStatus: "0",
listUnsubscribe: false,
method: "",
phishingStatus: "0",
recipientCount: "0",
differentEnvelopeSender: null,
movedTime: null,
encryptionAuthStatus: null,
_ownerKeyVersion: null,
attachments: [],
conversationEntry: ["entryListId", "entryId"],
firstRecipient: null,
mailDetails: null,
mailDetailsDraft: null,
sets: [],
keyVerificationState: null,
processingState: ProcessingState.INBOX_RULE_APPLIED,
clientSpamClassifierResult: null,
})
// casting here is fine, since we just want to mimic server response data
return (await instancePipeline.mapAndEncrypt(MailTypeRef, mail, sessionKey)) as unknown as ServerModelUntypedInstance
}
})
export function createTestUser(name: string, entityClient: EntityClient): TestUser {
const userGroupKey = aes256RandomKey()
const mailGroupKey = aes256RandomKey()
const userGroup = createTestEntity(GroupTypeRef, {
_id: "userGroup" + name,
type: GroupType.User,
currentKeys: null,
groupKeyVersion: "0",
})
const mailGroup = createTestEntity(GroupTypeRef, {
_id: "mailGroup" + name,
type: GroupType.Mail,
currentKeys: null,
groupKeyVersion: "0",
})
const userGroupMembership = createTestEntity(GroupMembershipTypeRef, {
group: userGroup._id,
})
const mailGroupMembership = createTestEntity(GroupMembershipTypeRef, {
group: mailGroup._id,
})
const user = createTestEntity(UserTypeRef, {
userGroup: userGroupMembership,
memberships: [mailGroupMembership],
})
when(entityClient.load(GroupTypeRef, userGroup._id)).thenResolve(userGroup)
when(entityClient.load(GroupTypeRef, mailGroup._id)).thenResolve(mailGroup)
return {
user,
userGroup,
mailGroup,
userGroupKey,
mailGroupKey,
name,
}
}
/**
* Helper function to mock the user facade so that the given test user is considered as logged in user.
*/
export function configureLoggedInUser(testUser: TestUser, userFacade: UserFacade, keyLoaderFacade: KeyLoaderFacade) {
when(userFacade.getLoggedInUser()).thenReturn(testUser.user)
when(keyLoaderFacade.getCurrentSymGroupKey(testUser.mailGroup._id)).thenResolve({
object: testUser.mailGroupKey,
version: 0,
})
when(keyLoaderFacade.getCurrentSymGroupKey(testUser.userGroup._id)).thenResolve({
object: testUser.userGroupKey,
version: 0,
})
when(userFacade.hasGroup(testUser.userGroup._id)).thenReturn(true)
when(userFacade.hasGroup(testUser.mailGroup._id)).thenReturn(true)
when(userFacade.getCurrentUserGroupKey()).thenReturn({ object: testUser.userGroupKey, version: 0 })
when(userFacade.isLeader()).thenReturn(true)
when(userFacade.isFullyLoggedIn()).thenReturn(true)
when(keyLoaderFacade.loadSymGroupKey(testUser.mailGroup._id, 0)).thenResolve(testUser.mailGroupKey)
when(keyLoaderFacade.loadSymGroupKey(testUser.userGroup._id, 0)).thenResolve(testUser.userGroupKey)
}