mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00

We implement three patch operation types (replace for values and One and ZeroOrOne associations, additem and removeitem for Any associations) on attributes to do PATCH requests instead of doing PUT requests to minimize the request payload by sending only the changed fields. The payload format is specified in the fast-sync design documentation. Co-authored-by: das <das@tutao.de> Co-authored-by: jomapp <17314077+jomapp@users.noreply.github.com> Co-authored-by: abp <abp@tutao.de>
1839 lines
68 KiB
TypeScript
1839 lines
68 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,
|
|
GroupType,
|
|
KeyVerificationState,
|
|
PermissionType,
|
|
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,
|
|
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,
|
|
RsaKeyPair,
|
|
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 { KeyVerificationFacade } from "../../../../../src/common/api/worker/facades/lazy/KeyVerificationFacade"
|
|
import { KeyLoaderFacade, parseKeyVersion } from "../../../../../src/common/api/worker/facades/KeyLoaderFacade.js"
|
|
import { PublicKeyProvider } from "../../../../../src/common/api/worker/facades/PublicKeyProvider.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"
|
|
|
|
const { captor, anything, argThat } = matchers
|
|
|
|
const kyberFacade = new WASMKyberFacade(await loadLibOQSWASM())
|
|
const pqFacade: PQFacade = new PQFacade(kyberFacade)
|
|
let asymmetricCryptoFacade: AsymmetricCryptoFacade
|
|
let publicKeyProvider: PublicKeyProvider
|
|
|
|
/**
|
|
* 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 keyVerificationFacade: KeyVerificationFacade
|
|
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()
|
|
keyVerificationFacade = object()
|
|
publicKeyProvider = object()
|
|
keyLoaderFacade = object()
|
|
keyRotationFacade = object()
|
|
typeModelResolver = clientInitializedTypeModelResolver()
|
|
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
|
|
|
crypto = new CryptoFacade(
|
|
userFacade,
|
|
entityClient,
|
|
restClient,
|
|
serviceExecutor,
|
|
instancePipeline,
|
|
ownerEncSessionKeysUpdateQueue,
|
|
cache,
|
|
keyLoaderFacade,
|
|
asymmetricCryptoFacade,
|
|
async () => keyVerificationFacade,
|
|
publicKeyProvider,
|
|
() => 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(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED)
|
|
|
|
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)
|
|
|
|
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: object(),
|
|
pubAdminGroupEncGKey: null,
|
|
identityKeyPair: null,
|
|
})
|
|
when(keyLoaderFacade.loadCurrentKeyPair(senderUserGroup._id)).thenResolve({ version: 0, object: senderKeyPairs })
|
|
|
|
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!,
|
|
},
|
|
},
|
|
}
|
|
when(
|
|
publicKeyProvider.loadCurrentPubKey({
|
|
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
|
|
identifier: recipientMailAddress,
|
|
}),
|
|
).thenResolve(recipientPublicKeys)
|
|
when(
|
|
publicKeyProvider.loadPubKey(
|
|
{
|
|
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
|
|
identifier: recipientMailAddress,
|
|
},
|
|
0,
|
|
),
|
|
).thenResolve({
|
|
version: 0,
|
|
object: {
|
|
keyPairType: KeyPairType.TUTA_CRYPT,
|
|
x25519PublicKey: senderKeyPair.pubEccKey!,
|
|
kyberPublicKey: { raw: senderKeyPair.pubKyberKey! },
|
|
},
|
|
})
|
|
when(entityClient.load(GroupTypeRef, senderUserGroup._id)).thenResolve(senderUserGroup)
|
|
when(keyLoaderFacade.getCurrentSymGroupKey(senderUserGroup._id)).thenResolve({
|
|
object: senderGroupKey,
|
|
version: 0,
|
|
})
|
|
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(publicKeyProvider, { times: 0 })
|
|
})
|
|
|
|
o("encryptBucketKeyForInternalRecipient with existing PQKeys for sender", async () => {
|
|
let recipientMailAddress = "bob@tutanota.com"
|
|
let senderGroupKey = aes256RandomKey()
|
|
let bk = aes256RandomKey()
|
|
|
|
let senderMailAddress = "alice@tutanota.com"
|
|
|
|
const senderKeyPair: KeyPair = object()
|
|
|
|
const senderAsymmetricKeyPair: Versioned<RsaKeyPair> = object()
|
|
const senderPublicKey: Versioned<RsaPublicKey> = 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: object(),
|
|
pubAdminGroupEncGKey: null,
|
|
identityKeyPair: null,
|
|
})
|
|
|
|
when(keyLoaderFacade.loadCurrentKeyPair(senderUserGroup._id)).thenResolve(senderAsymmetricKeyPair)
|
|
|
|
const notFoundRecipients = []
|
|
const keyVerificationMismatchRecipients = []
|
|
|
|
const recipientPublicKeys: Versioned<RsaPublicKey> = {
|
|
version: 0,
|
|
object: object(),
|
|
}
|
|
when(
|
|
publicKeyProvider.loadCurrentPubKey({
|
|
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
|
|
identifier: recipientMailAddress,
|
|
}),
|
|
).thenResolve(recipientPublicKeys)
|
|
|
|
const senderPublicKeys: Versioned<PQPublicKeys> = {
|
|
version: 0,
|
|
object: object(),
|
|
}
|
|
|
|
when(
|
|
publicKeyProvider.loadCurrentPubKey({
|
|
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
|
|
identifier: senderMailAddress,
|
|
}),
|
|
).thenResolve(senderPublicKeys)
|
|
|
|
when(entityClient.load(GroupTypeRef, senderUserGroup._id)).thenResolve(senderUserGroup)
|
|
when(keyLoaderFacade.getCurrentSymGroupKey(senderUserGroup._id)).thenResolve({
|
|
object: senderGroupKey,
|
|
version: 0,
|
|
})
|
|
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(publicKeyProvider, { times: 0 })
|
|
})
|
|
|
|
o("encryptBucketKeyForInternalRecipient for non-existing recipients", async function () {
|
|
let notFoundRecipientMailAddress = "notfound@tutanota.com"
|
|
let bk = aes256RandomKey()
|
|
|
|
const notFoundRecipients: string[] = []
|
|
const keyVerificationMismatchRecipients: string[] = []
|
|
|
|
const recipientPublicKeys: Versioned<RsaPublicKey> = {
|
|
version: 0,
|
|
object: object(),
|
|
}
|
|
|
|
when(
|
|
publicKeyProvider.loadCurrentPubKey({
|
|
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 for verification-failing recipients", async function () {
|
|
let verificationFailureRecipientMailAddress = "bob@tutanota.com"
|
|
let bk = aes256RandomKey()
|
|
|
|
const notFoundRecipients: string[] = []
|
|
const keyVerificationMismatchRecipients: string[] = []
|
|
|
|
const recipientPublicKeys: Versioned<RsaPublicKey> = {
|
|
version: 0,
|
|
object: object(),
|
|
}
|
|
when(
|
|
publicKeyProvider.loadCurrentPubKey({
|
|
identifierType: PublicKeyIdentifierType.MAIL_ADDRESS,
|
|
identifier: verificationFailureRecipientMailAddress,
|
|
}),
|
|
).thenResolve(recipientPublicKeys)
|
|
|
|
when(keyVerificationFacade.resolveVerificationState(anything(), anything())).thenResolve(KeyVerificationState.MISMATCH)
|
|
|
|
await crypto.encryptBucketKeyForInternalRecipient(
|
|
"senderGroupId",
|
|
bk,
|
|
verificationFailureRecipientMailAddress,
|
|
notFoundRecipients,
|
|
keyVerificationMismatchRecipients,
|
|
)
|
|
|
|
o(notFoundRecipients).deepEquals([])
|
|
o(keyVerificationMismatchRecipients).deepEquals(["bob@tutanota.com"])
|
|
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(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED)
|
|
|
|
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(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_SUCCEEDED)
|
|
|
|
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(EncryptionAuthStatus.TUTACRYPT_AUTHENTICATION_FAILED)
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
// 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])
|
|
|
|
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",
|
|
})
|
|
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,
|
|
})
|
|
})
|
|
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: [],
|
|
})
|
|
|
|
// 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)
|
|
}
|