From cfbfde14d2b76d540e4192bbbb0bc7ed139bef57 Mon Sep 17 00:00:00 2001 From: Anatoly Rosencrantz Date: Thu, 2 Nov 2023 13:35:34 +0200 Subject: [PATCH] 1.3.1 --- .gitlab-ci.yml | 14 +- app/build.gradle.kts | 1 + .../android/drive/di/ApplicationModule.kt | 2 - .../ui/test/flow/upload/UploadFlowTest.kt | 6 +- buildSrc/src/main/kotlin/Config.kt | 2 +- drive/base/data-test/build.gradle.kts | 1 + .../me/proton/core/drive/base/data/api/Dto.kt | 2 + .../core/drive/base/data/api/ProtonApiCode.kt | 1 + .../proton/core/drive/base/data/db/Column.kt | 1 + .../block/data/api/entity/UploadBlockDto.kt | 5 +- .../block/data/api/entity/VerifierDto.kt | 29 + .../drive/block/data/extension/UploadBlock.kt | 2 + .../domain/usecase/base/UseSessionKey.kt | 15 +- .../domain/usecase/file/DecryptFiles.kt | 18 + .../domain/usecase/file/EncryptFiles.kt | 3 +- .../27.json | 4460 +++++++++++++++++ .../proton/android/drive/db/DriveDatabase.kt | 4 +- drive/i18n/src/main/res/values/upload.xml | 1 + drive/i18n/src/main/res/values/verifier.xml | 22 + .../key/domain/usecase/BuildContentKey.kt | 16 +- .../linkupload/data/db/dao/UploadBlockDao.kt | 10 + .../data/db/entity/UploadBlockEntity.kt | 3 + .../data/extension/UploadBlockEntity.kt | 1 + .../data/factory/UploadBlockFactoryImpl.kt | 8 +- .../repository/LinkUploadRepositoryImpl.kt | 12 + .../linkupload/domain/entity/UploadBlock.kt | 1 + .../domain/factory/UploadBlockFactory.kt | 2 + .../domain/repository/LinkUploadRepository.kt | 6 + .../domain/usecase/UpdateVerifierToken.kt | 37 + drive/upload/data/build.gradle.kts | 2 + .../drive/upload/data/extension/Throwable.kt | 11 + .../data/extension/UploadCleanupException.kt | 13 +- .../upload/data/worker/BlockUploadWorker.kt | 7 +- .../upload/data/worker/FileUploadFlow.kt | 16 +- .../upload/data/worker/UploadCleanupWorker.kt | 10 + .../upload/data/worker/VerifyBlocksWorker.kt | 118 + .../data/worker/UpdateRevisionWorkerTest.kt | 146 + .../data/worker/VerifyBlocksWorkerTest.kt | 143 + drive/upload/domain/build.gradle.kts | 1 + .../usecase/SplitFileToBlocksAndEncrypt.kt | 2 + .../upload/domain/usecase/VerifyBlocks.kt | 77 + gradle/libs.versions.toml | 3 +- verifier/build.gradle.kts | 27 + verifier/data/build.gradle.kts | 36 + .../drive/verifier/data/api/VerifierApi.kt | 34 + .../data/api/VerifierApiDataSource.kt | 37 + .../response/GetVerificationDataResponse.kt | 38 + .../verifier/data/di/VerifierBindModule.kt | 36 + .../drive/verifier/data/di/VerifierModule.kt | 48 + .../verifier/data/entity/VerifierImpl.kt | 79 + .../verifier/data/extension/ByteArray.kt | 30 + .../drive/verifier/data/extension/File.kt | 31 + .../data/extension/VerifierException.kt | 35 + .../data/factory/VerifierFactoryImpl.kt | 46 + .../data/repository/VerifierRepositoryImpl.kt | 84 + .../verifier/data/entity/VerifierTest.kt | 180 + .../verifier/data/extension/ByteArrayTest.kt | 88 + .../drive/verifier/data/extension/FileTest.kt | 75 + .../data/extension/TemporaryFolder.kt | 33 + .../data/repository/VerifierRepositoryTest.kt | 166 + verifier/domain/build.gradle.kts | 31 + .../domain/entity/VerificationData.kt | 42 + .../drive/verifier/domain/entity/Verifier.kt | 26 + .../domain/exception/VerifierException.kt | 26 + .../domain/factory/VerifierFactory.kt | 31 + .../domain/repository/VerifierRepository.kt | 38 + .../verifier/domain/usecase/BuildVerifier.kt | 57 + .../domain/usecase/CleanupVerifier.kt | 30 + 68 files changed, 6591 insertions(+), 27 deletions(-) create mode 100644 drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt create mode 100644 drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json create mode 100644 drive/i18n/src/main/res/values/verifier.xml create mode 100644 drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt create mode 100644 drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt create mode 100644 drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt create mode 100644 drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt create mode 100644 drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt create mode 100644 verifier/build.gradle.kts create mode 100644 verifier/data/build.gradle.kts create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/ByteArray.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/File.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/VerifierException.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/factory/VerifierFactoryImpl.kt create mode 100644 verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryImpl.kt create mode 100644 verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/entity/VerifierTest.kt create mode 100644 verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/ByteArrayTest.kt create mode 100644 verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/FileTest.kt create mode 100644 verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/TemporaryFolder.kt create mode 100644 verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryTest.kt create mode 100644 verifier/domain/build.gradle.kts create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/VerificationData.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/Verifier.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/exception/VerifierException.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/factory/VerifierFactory.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/repository/VerifierRepository.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/BuildVerifier.kt create mode 100644 verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/CleanupVerifier.kt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a24a679..eaeb9920 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,7 @@ default: image: ${CI_REGISTRY}/android/shared/docker-android:v1.1.1 + tags: + - shared-small variables: # Use fastzip to improve cache times @@ -54,7 +56,7 @@ prepare-build: expire_in: 1 week cache: [] tags: - - large + - shared-large script: - export ARCHIVES_BASE_NAME=$(./gradlew -Dorg.gradle.jvmargs=-Xmx1024m -q getArchivesName | grep "\[ARCHIVES_NAME\]" | sed 's/\[ARCHIVES_NAME\]//') - export ARCHIVES_VERSION=$(./gradlew -Dorg.gradle.jvmargs=-Xmx1024m -q getArchivesVersion | grep "\[ARCHIVES_VERSION\]" | sed 's/\[ARCHIVES_VERSION\]//') @@ -65,6 +67,8 @@ prepare-build: prepare-gradle-build-scan: stage: prepare + tags: + - shared-large script: - echo "BUILD_SCAN_PUBLISH=true" >> build_scan.env artifacts: @@ -77,7 +81,7 @@ prepare-gradle-build-scan: detekt analysis: stage: analyze tags: - - large + - shared-large script: - ./gradlew multiModuleDetekt --configuration-cache-problems=warn allow_failure: true @@ -179,6 +183,8 @@ dev debug unit test: upload to firebase: stage: startReview + tags: + - shared-medium variables: PRODUCT_FLAVOR: "dynamic" script: @@ -202,7 +208,7 @@ upload to firebase: - job: "build dynamic debug" stage: test tags: - - medium + - shared-medium variables: RESULTS_DIR: "$FIREBASE_RESULT_ROOT/$CI_JOB_NAME" PRODUCT_FLAVOR: "dynamic" @@ -357,6 +363,8 @@ startReview: needs: - job: "prepare-build" - job: "build dev debug" + tags: + - shared-medium variables: PRODUCT_FLAVOR: "dev" before_script: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89ff52c5..936d37d3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,7 @@ driveModule( implementation(project(":app-lock")) implementation(project(":app-ui-settings")) implementation(project(":drive")) + implementation(project(":verifier")) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.compose.foundationLayout) diff --git a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt index 7c3218a7..e7c25eed 100644 --- a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt +++ b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt @@ -30,8 +30,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import me.proton.android.drive.BuildConfig -import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl -import me.proton.android.drive.lock.domain.usecase.BuildAppKey import me.proton.android.drive.log.DriveLogger import me.proton.android.drive.notification.AppNotificationBuilderProvider import me.proton.android.drive.notification.AppNotificationEventHandler diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt index d676b4c1..9303b2e8 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt @@ -118,8 +118,8 @@ class UploadFlowTest : BaseTest() { } @Test - fun upload4MBFile() { - val file = externalFilesRule.createFile("4MB.txt", 4 * 1024 * 1024) + fun upload6MBFile() { + val file = externalFilesRule.createFile("6MB.txt", 6 * 1024 * 1024) Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWithFunction { Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(Uri.fromFile(file))) @@ -135,7 +135,7 @@ class UploadFlowTest : BaseTest() { assertStageUploading() assertStageUploadedProgress(0) assertStageUploadedProgress(100) - itemIsDisplayed("4MB.txt") + itemIsDisplayed("6MB.txt") } } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index c27b8580..9f454d0d 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -22,7 +22,7 @@ object Config { const val minSdk = 23 const val targetSdk = 33 const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - const val versionName = "1.2.3" + const val versionName = "1.3.1" const val archivesBaseName = "ProtonDrive-$versionName" val resourceConfigurations = listOf("en") } diff --git a/drive/base/data-test/build.gradle.kts b/drive/base/data-test/build.gradle.kts index ab13ea8f..1730d0f5 100644 --- a/drive/base/data-test/build.gradle.kts +++ b/drive/base/data-test/build.gradle.kts @@ -22,6 +22,7 @@ plugins { driveModule( hilt = true, ) { + api(project(":drive:base:domain")) api(libs.core.domain) } diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt index a6a00d1a..5e15ff05 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt @@ -127,6 +127,8 @@ object Dto { const val URL_PASSWORD_SALT = "UrlPasswordSalt" const val URLS_EXPIRED = "UrlsExpired" const val USED_SPACE = "UsedSpace" + const val VERIFICATION_CODE = "VerificationCode" + const val VERIFIER = "Verifier" const val VOLUME = "Volume" const val VOLUMES = "Volumes" const val VOLUME_ID = "VolumeID" diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt index e5a703c6..e9ee4628 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt @@ -26,6 +26,7 @@ object ProtonApiCode { const val ALREADY_EXISTS = 2500 const val NOT_EXISTS = 2501 const val INSUFFICIENT_QUOTA = 200001 + const val ENCRYPTION_VERIFICATION_FAILED = 200501 val Long.isSuccessful: Boolean get() = this == SUCCESS.toLong() } diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt index d3afdac2..8bc569ce 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt @@ -109,6 +109,7 @@ object Column { const val URL_PASSWORD_SALT = "url_password_salt" const val USED_SPACE = "used_space" const val USER_ID = "user_id" + const val VERIFIER_TOKEN = "verifier_token" const val VOLUME_ID = "volume_id" const val WORK_ID = "work_id" const val WORKER_ID = "worker_id" diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt index a7172a41..61a314e6 100644 --- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt +++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt @@ -23,6 +23,7 @@ import me.proton.core.drive.base.data.api.Dto.ENC_SIGNATURE import me.proton.core.drive.base.data.api.Dto.HASH import me.proton.core.drive.base.data.api.Dto.INDEX import me.proton.core.drive.base.data.api.Dto.SIZE +import me.proton.core.drive.base.data.api.Dto.VERIFIER @Serializable data class UploadBlockDto( @@ -33,5 +34,7 @@ data class UploadBlockDto( @SerialName(ENC_SIGNATURE) val encSignature: String, @SerialName(HASH) - val hash: String + val hash: String, + @SerialName(VERIFIER) + val verifier: VerifierDto? = null, ) diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt new file mode 100644 index 00000000..b24544bb --- /dev/null +++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.block.data.api.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import me.proton.core.drive.base.data.api.Dto.TOKEN + +@Serializable +data class VerifierDto( + @SerialName(TOKEN) + val token: String, +) diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt index a3de861c..c14934b2 100644 --- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt +++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt @@ -18,6 +18,7 @@ package me.proton.core.drive.block.data.extension import me.proton.core.drive.block.data.api.entity.UploadBlockDto +import me.proton.core.drive.block.data.api.entity.VerifierDto import me.proton.core.drive.linkupload.domain.entity.UploadBlock fun UploadBlock.toUploadBlockDto() = @@ -26,4 +27,5 @@ fun UploadBlock.toUploadBlockDto() = size = size.value, hash = hashSha256, encSignature = encSignature, + verifier = verifierToken?.let { token -> VerifierDto(token) }, ) diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt index 43f0078a..af3cd1c5 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt @@ -35,7 +35,7 @@ class UseSessionKey @Inject constructor( ) { suspend operator fun invoke( contentKey: ContentKey, - checkSignature: Boolean = false, + checkSignature: Boolean, coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext, block: suspend (SessionKey) -> T ) = @@ -54,4 +54,17 @@ class UseSessionKey @Inject constructor( } block(sessionKey) } + + suspend operator fun invoke( + contentKey: ContentKey, + coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext, + block: suspend (SessionKey) -> T + ) = + useSessionKey( + decryptKey = contentKey.decryptKey.keyHolder, + encryptedKeyPacket = contentKey.encryptedKeyPacket, + coroutineContext = coroutineContext, + ) { sessionKey -> + block(sessionKey) + } } diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt index 93d326fb..65a50d47 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt @@ -30,6 +30,24 @@ class DecryptFiles @Inject constructor( private val useSessionKey: UseSessionKey, private val decryptFile: DecryptFile ) { + suspend operator fun invoke( + contentKey: ContentKey, + checkSignature: Boolean, + input: List, + output: List, + ): Result> = coRunCatching { + useSessionKey(contentKey = contentKey, checkSignature = checkSignature) { sessionKey -> + input.mapIndexed { index, file -> + DecryptedFile( + file = decryptFile(sessionKey, file, output[index]).getOrThrow(), + status = VerificationStatus.Unknown, + filename = "", + lastModifiedEpochSeconds = -1, + ) + } + }.getOrThrow() + } + suspend operator fun invoke( contentKey: ContentKey, input: List, diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt index edb4cf8a..0e9ffde5 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt @@ -32,11 +32,12 @@ class EncryptFiles @Inject constructor( ) { suspend operator fun invoke( contentKey: ContentKey, + checkSignature: Boolean = false, input: List, output: List, coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext, ): Result> = coRunCatching { - useSessionKey(contentKey = contentKey) { sessionKey -> + useSessionKey(contentKey = contentKey, checkSignature = checkSignature) { sessionKey -> input.mapIndexed { index, file -> encryptFile(sessionKey, file, output[index], coroutineContext).getOrThrow() } diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json new file mode 100644 index 00000000..dd138ce1 --- /dev/null +++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json @@ -0,0 +1,4460 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "443da6c93db22831596a4ae60bb0a948", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "product" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isPrivate", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyList.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addressId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `ignoreKT` INTEGER, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignoreKT", + "columnName": "ignoreKT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.minEpochId", + "columnName": "signedKeyList_minEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.maxEpochId", + "columnName": "signedKeyList_maxEpochId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.expectedMinEpochId", + "columnName": "signedKeyList_expectedMinEpochId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "email" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "email", + "publicKey" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationToken", + "columnName": "verificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "clientId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "invoiceText", + "columnName": "invoiceText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeType", + "columnName": "themeType", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "welcome", + "columnName": "welcome", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags.welcomed", + "columnName": "flags_welcomed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxCalendars", + "columnName": "maxCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedCalendars", + "columnName": "usedCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "config" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "FeatureFlagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "featureId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "featureId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_FeatureFlagEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_FeatureFlagEntity_featureId", + "unique": false, + "columnNames": [ + "featureId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ChallengeFrameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))", + "fields": [ + { + "fieldPath": "challengeFrame", + "columnName": "challengeFrame", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flow", + "columnName": "flow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "focusTime", + "columnName": "focusTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clicks", + "columnName": "clicks", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "copy", + "columnName": "copy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paste", + "columnName": "paste", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keys", + "columnName": "keys", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "challengeFrame" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GooglePurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))", + "fields": [ + { + "fieldPath": "googlePurchaseToken", + "columnName": "googlePurchaseToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "googlePurchaseToken" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_GooglePurchaseEntity_paymentToken", + "unique": true, + "columnNames": [ + "paymentToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ObservabilityEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AddressChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `changeId` TEXT NOT NULL, `counterEncrypted` TEXT NOT NULL, `emailEncrypted` TEXT NOT NULL, `epochIdEncrypted` TEXT NOT NULL, `creationTimestampEncrypted` TEXT NOT NULL, `publicKeysEncrypted` TEXT NOT NULL, `isObsolete` TEXT NOT NULL, PRIMARY KEY(`userId`, `changeId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "changeId", + "columnName": "changeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "counterEncrypted", + "columnName": "counterEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emailEncrypted", + "columnName": "emailEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "epochIdEncrypted", + "columnName": "epochIdEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTimestampEncrypted", + "columnName": "creationTimestampEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKeysEncrypted", + "columnName": "publicKeysEncrypted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isObsolete", + "columnName": "isObsolete", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "changeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SelfAuditResultEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "VolumeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "max_space", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "used_space", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_VolumeEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_VolumeEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_VolumeEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "address_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphraseSignature", + "columnName": "passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_ShareEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_ShareEntity_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareUrlEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creatorEmail", + "columnName": "creatior_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastAccessTime", + "columnName": "last_access_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAccesses", + "columnName": "max_accesses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "urlPasswordSalt", + "columnName": "url_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePasswordSalt", + "columnName": "share_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpVerifier", + "columnName": "srp_verifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpModulusId", + "columnName": "srp_modulus_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedUrlPassword", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePassphraseKeyPacket", + "columnName": "share_passphrase_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicUrl", + "columnName": "public_url", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareUrlEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareUrlEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_ShareUrlEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + } + ], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameSignatureEmail", + "columnName": "name_signature_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashedTime", + "columnName": "trashed_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shared", + "columnName": "is_shared", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareUrlExpirationTime", + "columnName": "share_url_expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "xAttr", + "columnName": "x_attr", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrlShareId", + "columnName": "share_url_share_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shareUrlId", + "columnName": "share_url_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_LinkEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + }, + { + "name": "index_LinkEntity_user_id_id", + "unique": false, + "columnNames": [ + "user_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id_parent_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "parent_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFilePropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "file_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "file_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "file_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeRevisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasThumbnail", + "columnName": "has_thumbnail", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activeRevisionSignatureAddress", + "columnName": "file_signature_address", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFilePropertiesEntity_file_share_id", + "unique": false, + "columnNames": [ + "file_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_link_id", + "unique": false, + "columnNames": [ + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id", + "unique": false, + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFolderPropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "folder_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "folder_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "folder_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeHashKey", + "columnName": "node_hash_key", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFolderPropertiesEntity_folder_share_id", + "unique": false, + "columnNames": [ + "folder_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_link_id", + "unique": false, + "columnNames": [ + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id", + "unique": false, + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkOfflineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkOfflineEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkOfflineEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkOfflineEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkOfflineEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkDownloadStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkDownloadStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "DownloadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id", + "index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DownloadBlockEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DownloadBlockEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DownloadBlockEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DownloadBlockEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkDownloadStateEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ] + } + ] + }, + { + "tableName": "LinkTrashStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkTrashStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkTrashStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkTrashStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkTrashStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkTrashStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashWorkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workId", + "columnName": "work_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TrashWorkEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_TrashWorkEntity_work_id", + "unique": false, + "columnNames": [ + "work_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "MessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_MessageEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UiSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutType", + "columnName": "layout_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeStyle", + "columnName": "theme_style", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DriveLinkRemoteKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevKey", + "columnName": "previous_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextKey", + "columnName": "next_key", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "key", + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "SortingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingBy", + "columnName": "sorting_by", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingDirection", + "columnName": "sorting_direction", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LinkUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, `digests` TEXT DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "mediaResolutionWidth", + "columnName": "media_resolution_width", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "mediaResolutionHeight", + "columnName": "media_resolution_height", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "digests", + "columnName": "digests", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_LinkUploadEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkUploadEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_LinkUploadEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkUploadEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkUploadEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkUploadEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, `verifier_token` TEXT DEFAULT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uploadLinkId", + "columnName": "upload_link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadToken", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rawSize", + "columnName": "raw_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "verifierToken", + "columnName": "verifier_token", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "upload_link_id", + "index" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkUploadEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_link_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "UploadBulkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_UploadBulkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_UploadBulkEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_UploadBulkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_UploadBulkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBulkUriStringEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "upload_bulk_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "upload_bulk_id", + "uri" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UploadBulkUriStringEntity_upload_bulk_id", + "unique": false, + "columnNames": [ + "upload_bulk_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)" + } + ], + "foreignKeys": [ + { + "table": "UploadBulkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_bulk_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FolderMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchChildrenTimestamp", + "columnName": "last_fetch_children_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchTrashTimestamp", + "columnName": "last_fetch_trash_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "NotificationChannelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "type" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationChannelEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "NotificationEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channelType", + "columnName": "channel_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationTag", + "columnName": "notification_tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notification_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationEventId", + "columnName": "notification_event_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationEvent", + "columnName": "notification_event", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id", + "notification_event_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id", + "unique": false, + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)" + } + ], + "foreignKeys": [ + { + "table": "NotificationChannelEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "channel_type" + ], + "referencedColumns": [ + "user_id", + "type" + ] + } + ] + }, + { + "tableName": "LinkSelectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectionId", + "columnName": "selection_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "selection_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkSelectionEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_LinkSelectionEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkSelectionEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkSelectionEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkSelectionEntity_selection_id", + "unique": false, + "columnNames": [ + "selection_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "WorkerRunEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `worker_id` TEXT NOT NULL, `run_at` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "worker_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "runAt", + "columnName": "run_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_WorkerRunEntity_worker_id", + "unique": false, + "columnNames": [ + "worker_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkerRunEntity_worker_id` ON `${TABLE_NAME}` (`worker_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '443da6c93db22831596a4ae60bb0a948')" + ] + } +} \ No newline at end of file diff --git a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt index bfc4d6da..c38c481c 100644 --- a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt +++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt @@ -192,6 +192,7 @@ import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity AutoMigration(from = 18, to = 19), AutoMigration(from = 22, to = 23), AutoMigration(from = 23, to = 24), + AutoMigration(from = 26, to = 27), ], exportSchema = true, ) @@ -250,7 +251,7 @@ abstract class DriveDatabase : WorkerDatabase { companion object { - const val VERSION = 26 + const val VERSION = 27 private val migrations = listOf( DriveDatabaseMigrations.MIGRATION_1_2, @@ -278,6 +279,7 @@ abstract class DriveDatabase : //AutoMigration(from = 23, to = 24) DriveDatabaseMigrations.MIGRATION_24_25, DriveDatabaseMigrations.MIGRATION_25_26, + //AutoMigration(from = 26, to = 27) ) fun buildDatabase(context: Context): DriveDatabase = diff --git a/drive/i18n/src/main/res/values/upload.xml b/drive/i18n/src/main/res/values/upload.xml index 949c608b..c9d6a8dd 100644 --- a/drive/i18n/src/main/res/values/upload.xml +++ b/drive/i18n/src/main/res/values/upload.xml @@ -53,4 +53,5 @@ %1$d item %1$d items + Upload failed: Verification of data failed diff --git a/drive/i18n/src/main/res/values/verifier.xml b/drive/i18n/src/main/res/values/verifier.xml new file mode 100644 index 00000000..216b2ea3 --- /dev/null +++ b/drive/i18n/src/main/res/values/verifier.xml @@ -0,0 +1,22 @@ + + + + Verifier initialization failed + Verifier failed verifing blocks + diff --git a/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt b/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt index 55063c2f..9f6d2a1f 100644 --- a/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt +++ b/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt @@ -54,12 +54,24 @@ class BuildContentKey @Inject constructor( userId: UserId, uploadFile: UploadFileLink, fileKey: Key.Node, + ): Result = invoke( + userId = userId, + contentKeyPacket = uploadFile.contentKeyPacket, + contentKeyPacketSignature = uploadFile.contentKeyPacketSignature, + fileKey = fileKey, + ) + + suspend operator fun invoke( + userId: UserId, + contentKeyPacket: String, + contentKeyPacketSignature: String, + fileKey: Key.Node, ): Result = coRunCatching { contentKeyFactory.createContentKey( decryptKey = fileKey, verifyKey = listOf(fileKey, getAddressKeys(userId, getSignatureAddress(userId))), - contentKeyPacket = uploadFile.contentKeyPacket, - contentKeyPacketSignature = uploadFile.contentKeyPacketSignature + contentKeyPacket = contentKeyPacket, + contentKeyPacketSignature = contentKeyPacketSignature ) } } diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt index 811f1c21..74f5e2d9 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt @@ -56,4 +56,14 @@ abstract class UploadBlockDao : BaseDao() { index: Long, token: String, ) + + @Query(""" + UPDATE UploadBlockEntity SET verifier_token = :verifierToken WHERE + upload_link_id = :uploadLinkId AND `index` = :index + """) + abstract fun updateVerifierToken( + uploadLinkId: Long, + index: Long, + verifierToken: String, + ) } diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt index ca3467a0..77288859 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt @@ -29,6 +29,7 @@ import me.proton.core.drive.base.data.db.Column.SIZE import me.proton.core.drive.base.data.db.Column.TOKEN import me.proton.core.drive.base.data.db.Column.UPLOAD_LINK_ID import me.proton.core.drive.base.data.db.Column.URL +import me.proton.core.drive.base.data.db.Column.VERIFIER_TOKEN @Entity( primaryKeys = [UPLOAD_LINK_ID, INDEX], @@ -58,4 +59,6 @@ data class UploadBlockEntity( val url: String, @ColumnInfo(name = RAW_SIZE, defaultValue = "0") val rawSize: Long, + @ColumnInfo(name = VERIFIER_TOKEN, defaultValue = "NULL") + val verifierToken: String? = null, ) diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt index 64bc0793..54f82d55 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt @@ -30,4 +30,5 @@ fun UploadBlockEntity.toUploadBlock(uploadBlockFactory: UploadBlockFactory) = rawSize = rawSize.bytes, size = size.bytes, token = uploadToken, + verifierToken = verifierToken, ) diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt index cef4e510..83e90aec 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt @@ -33,7 +33,8 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory { encSignature: String, rawSize: Bytes, size: Bytes, - token: String + token: String, + verifierToken: String?, ): UploadBlock = UploadBlock( index = index, @@ -44,6 +45,7 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory { size = size, token = token, file = block, + verifierToken = verifierToken, ) override fun create( @@ -53,7 +55,8 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory { encSignature: String, rawSize: Bytes, size: Bytes, - token: String + token: String, + verifierToken: String?, ): UploadBlock = UploadBlock( index = index, @@ -64,5 +67,6 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory { size = size, token = token, file = File(requireNotNull(Uri.parse(url)?.path)), + verifierToken = verifierToken, ) } diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt index 98a6b620..4f7f5126 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt @@ -17,6 +17,7 @@ */ package me.proton.core.drive.linkupload.data.repository +import android.util.Base64 import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import me.proton.core.domain.entity.UserId @@ -227,6 +228,17 @@ class LinkUploadRepositoryImpl @Inject constructor( token = token, ) + override suspend fun updateUploadBlockVerifierToken( + uploadFileLinkId: Long, + uploadBlockIndex: Long, + verifierToken: ByteArray + ) = + db.uploadBlockDao.updateVerifierToken( + uploadLinkId = uploadFileLinkId, + index = uploadBlockIndex, + verifierToken = Base64.encodeToString(verifierToken, Base64.NO_WRAP), + ) + override suspend fun removeUploadBlocks(uploadFileLink: UploadFileLink) = db.uploadBlockDao.delete(uploadFileLink.id) diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt index 07ad3986..9b4c5e27 100644 --- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt @@ -30,4 +30,5 @@ data class UploadBlock( val size: Bytes, val token: String, val file: File, + val verifierToken: String?, ) : Block diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt index 20de272c..c013ef9b 100644 --- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt @@ -30,6 +30,7 @@ interface UploadBlockFactory { rawSize: Bytes, size: Bytes, token: String, + verifierToken: String?, ): UploadBlock fun create( @@ -40,5 +41,6 @@ interface UploadBlockFactory { rawSize: Bytes, size: Bytes, token: String, + verifierToken: String?, ): UploadBlock } diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt index eceb092b..f654c20f 100644 --- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt @@ -110,6 +110,12 @@ interface LinkUploadRepository { token: String, ) + suspend fun updateUploadBlockVerifierToken( + uploadFileLinkId: Long, + uploadBlockIndex: Long, + verifierToken: ByteArray, + ) + suspend fun removeUploadBlocks(uploadFileLink: UploadFileLink) suspend fun insertUploadBulk(uploadBulk: UploadBulk): UploadBulk diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt new file mode 100644 index 00000000..029b9d41 --- /dev/null +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.linkupload.domain.usecase + +import me.proton.core.drive.linkupload.domain.entity.UploadBlock +import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository +import javax.inject.Inject + +class UpdateVerifierToken @Inject constructor( + private val linkUploadRepository: LinkUploadRepository, + private val getUploadBlockAfterOperation: GetUploadBlockAfterOperation, +) { + + suspend operator fun invoke( + uploadFileLinkId: Long, + index: Long, + verifierToken: ByteArray, + ): Result = getUploadBlockAfterOperation(uploadFileLinkId, index) { + linkUploadRepository.updateUploadBlockVerifierToken(uploadFileLinkId, index, verifierToken) + } +} diff --git a/drive/upload/data/build.gradle.kts b/drive/upload/data/build.gradle.kts index c3f2f517..0ac3186b 100644 --- a/drive/upload/data/build.gradle.kts +++ b/drive/upload/data/build.gradle.kts @@ -33,7 +33,9 @@ driveModule( implementation(project(":drive:link:presentation")) implementation(project(":drive:notification:domain")) implementation(project(":drive:worker:data")) + implementation(project(":verifier:data")) implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.core.crypto) implementation(libs.core.network) + testImplementation(libs.bundles.test.jvm) } diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt index 6f656de3..80fabd4c 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt @@ -17,6 +17,9 @@ */ package me.proton.core.drive.upload.data.extension +import me.proton.android.drive.verifier.data.extension.log +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.core.drive.base.presentation.extension.log import me.proton.core.network.domain.ApiException import me.proton.core.network.domain.ApiResult import me.proton.core.network.domain.isRetryable @@ -30,5 +33,13 @@ internal val Throwable.isRetryable: Boolean else -> this.error.isRetryable() } } + is VerifierException -> this.cause.isRetryable else -> false } + +internal fun Throwable.log(tag: String, message: String? = null): Throwable = this.also { + when (this) { + is VerifierException -> this.log(tag, message.orEmpty()) + else -> this.log(tag, message) + } +} diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt index c2c71aa0..00f3ba15 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt @@ -18,15 +18,24 @@ package me.proton.core.drive.upload.data.extension import android.content.Context +import me.proton.android.drive.verifier.data.extension.log +import me.proton.android.drive.verifier.domain.exception.VerifierException import me.proton.core.drive.base.presentation.extension.getDefaultMessage import me.proton.core.drive.base.presentation.extension.log import me.proton.core.drive.upload.data.exception.UploadCleanupException +import me.proton.core.drive.i18n.R as I18N internal fun UploadCleanupException.getDefaultMessage( context: Context, useExceptionMessage: Boolean, -): String = error.getDefaultMessage(context, useExceptionMessage) +): String = when (error) { + is VerifierException -> context.getString(I18N.string.files_upload_verification_failed) + else -> error.getDefaultMessage(context, useExceptionMessage) +} internal fun UploadCleanupException.log(tag: String, message: String? = null): UploadCleanupException = also { - message?.let { error.log(tag, message) } ?: error.log(tag) + when (error) { + is VerifierException -> message?.let { error.log(tag, message) } ?: error.log(tag) + else -> message?.let { error.log(tag, message) } ?: error.log(tag) + } } diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt index 49d07218..711e2d0d 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt @@ -46,6 +46,7 @@ import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink import me.proton.core.drive.linkupload.domain.usecase.UpdateToken import me.proton.core.drive.upload.data.extension.getSizeData import me.proton.core.drive.upload.data.extension.isRetryable +import me.proton.core.drive.upload.data.extension.retryOrAbort import me.proton.core.drive.upload.data.extension.setSize import me.proton.core.drive.upload.data.worker.WorkerKeys.KEY_BLOCK_INDEX import me.proton.core.drive.upload.data.worker.WorkerKeys.KEY_BLOCK_TOKEN @@ -132,11 +133,7 @@ class BlockUploadWorker @AssistedInject constructor( max retries reached ${!canRetry} """.trimIndent(), ) - return@coroutineScope if (retryable && canRetry) { - Result.retry() - } else { - Result.success(getSizeData(progress.value)) - } + return@coroutineScope retryOrAbort(retryable && canRetry, error, uploadFileLink.name) } } finally { job.cancel() diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt index 16c7d7df..50d34104 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt @@ -46,13 +46,19 @@ internal sealed class FileUploadFlow { uploadFileLinkId = uploadFileLinkId, uriString = uriString, shouldDeleteSource = shouldDeleteSource, - tags = uploadTags + tags = uploadTags, + ) + ).then( + VerifyBlocksWorker.getWorkRequest( + userId = userId, + uploadFileLinkId = uploadFileLinkId, + tags = uploadTags, ) ).then( GetBlocksUploadUrlWorker.getWorkRequest( userId = userId, uploadFileLinkId = uploadFileLinkId, - tags = uploadTags + tags = uploadTags, ) ).enqueue() } @@ -73,6 +79,12 @@ internal sealed class FileUploadFlow { shouldDeleteSource = shouldDeleteSource, tags = uploadTags ) + ).then( + VerifyBlocksWorker.getWorkRequest( + userId = userId, + uploadFileLinkId = uploadFileLinkId, + tags = uploadTags, + ) ).then( GetBlocksUploadUrlWorker.getWorkRequest( userId = userId, diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt index 7f52726e..edb5e4f2 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt @@ -27,6 +27,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi +import me.proton.android.drive.verifier.domain.usecase.CleanupVerifier import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.data.workmanager.addTags import me.proton.core.drive.base.domain.entity.Percentage @@ -60,6 +61,7 @@ class UploadCleanupWorker @AssistedInject constructor( private val getBlockFolder: GetBlockFolder, private val removeUploadFile: RemoveUploadFile, private val announceEvent: AnnounceEvent, + private val cleanupVerifier: CleanupVerifier, configurationProvider: ConfigurationProvider, canRun: CanRun, run: Run, @@ -95,6 +97,14 @@ class UploadCleanupWorker @AssistedInject constructor( percentage = Percentage(0) ) ) + uploadFileLink.linkId?.let { linkId -> + cleanupVerifier( + userId = userId, + shareId = uploadFileLink.shareId.id, + linkId = linkId, + revisionId = uploadFileLink.draftRevisionId, + ) + } } finally { uploadFileLink.deleteOnServer() } diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt new file mode 100644 index 00000000..410a33c3 --- /dev/null +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.upload.data.worker + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.data.workmanager.addTags +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.linkupload.domain.entity.UploadFileLink +import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink +import me.proton.core.drive.upload.data.extension.isRetryable +import me.proton.core.drive.upload.data.extension.log +import me.proton.core.drive.upload.data.extension.logTag +import me.proton.core.drive.upload.data.extension.retryOrAbort +import me.proton.core.drive.upload.domain.usecase.VerifyBlocks +import me.proton.core.drive.worker.domain.usecase.CanRun +import me.proton.core.drive.worker.domain.usecase.Done +import me.proton.core.drive.worker.domain.usecase.Run +import java.util.concurrent.TimeUnit + +@HiltWorker +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("LongParameterList") +class VerifyBlocksWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + workManager: WorkManager, + broadcastMessages: BroadcastMessages, + getUploadFileLink: GetUploadFileLink, + private val verifyBlocks: VerifyBlocks, + configurationProvider: ConfigurationProvider, + canRun: CanRun, + run: Run, + done: Done, +) : UploadCoroutineWorker( + appContext = appContext, + workerParams = workerParams, + workManager = workManager, + broadcastMessages = broadcastMessages, + getUploadFileLink = getUploadFileLink, + configurationProvider = configurationProvider, + canRun = canRun, + run = run, + done = done, +) { + + override suspend fun doLimitedRetryUploadWork(uploadFileLink: UploadFileLink): Result { + verifyBlocks(uploadFileLink) + .onFailure { error -> + val retryable = error.isRetryable + val canRetry = canRetry() + error.log( + tag = uploadFileLink.logTag(), + message = """ + Verify blocks failed with "${error.message}" retryable $retryable, + max retries reached ${!canRetry} + """.trimIndent() + ) + return retryOrAbort(retryable && canRetry, error, uploadFileLink.name) + } + return Result.success() + } + + companion object { + fun getWorkRequest( + userId: UserId, + uploadFileLinkId: Long, + tags: List = emptyList(), + ): OneTimeWorkRequest = + OneTimeWorkRequest.Builder(VerifyBlocksWorker::class.java) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .setInputData( + Data.Builder() + .putString(WorkerKeys.KEY_USER_ID, userId.id) + .putLong(WorkerKeys.KEY_UPLOAD_FILE_LINK_ID, uploadFileLinkId) + .build() + ) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + OneTimeWorkRequest.MIN_BACKOFF_MILLIS, + TimeUnit.MILLISECONDS + ) + .addTags(listOf(userId.id) + tags) + .build() + } +} diff --git a/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt new file mode 100644 index 00000000..8d63fb18 --- /dev/null +++ b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.upload.data.worker + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.Operation +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import androidx.work.testing.TestListenableWorkerBuilder +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.linkupload.domain.entity.UploadFileLink +import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink +import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage +import me.proton.core.drive.upload.domain.usecase.UpdateRevision +import me.proton.core.drive.worker.domain.usecase.CanRun +import me.proton.core.drive.worker.domain.usecase.Done +import me.proton.core.drive.worker.domain.usecase.Run +import me.proton.core.network.domain.ApiException +import me.proton.core.network.domain.ApiResult +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class UpdateRevisionWorkerTest { + private val userId: UserId = UserId("user-id") + private val workManager = mockk() + private val broadcastMessages = mockk() + private val getUploadFileLink = mockk() + private val updateRevision = mockk() + private val configurationProvider = mockk() + private val canRun = mockk() + private val run = mockk() + private val done = mockk() + private val uploadFileLink = mockk() + private val operation = mockk() + + @Before + fun before() { + coEvery { canRun(any(), any()) } returns Result.success(true) + coEvery { getUploadFileLink(any() as Long) } returns DataResult.Success(ResponseSource.Local, uploadFileLink) + coEvery { workManager.enqueue(any() as WorkRequest) } returns operation + coEvery { configurationProvider.useExceptionMessage } returns false + coEvery { broadcastMessages(userId, any(), any(), any()) } returns Unit + } + + @Test + fun `when commit a revision receives error with proton code 200501, upload fails and message is shown`() = runTest { + // Given + val uploadFile = "proton_drive.pdf" + coEvery { uploadFileLink.name } returns uploadFile + val errorFromServer = "Upload failed: Verification of data failed" + coEvery { updateRevision(any()) } returns Result.failure( + ApiException( + ApiResult.Error.Http( + httpCode = 422, + message = "Unprocessable Content", + proton = ApiResult.Error.ProtonData( + code = 200501, + error = errorFromServer, + ) + ) + ) + ) + + // When + val result = updateRevisionWorker(userId).doLimitedRetryWork() + + // Then + assertEquals(ListenableWorker.Result.failure(), result) + verify(exactly = 1) { + broadcastMessages( + userId = userId, + message = "Uploading file $uploadFile failed with reason: $errorFromServer", + type = BroadcastMessage.Type.ERROR, + extra = null, + ) + } + } + + private fun updateRevisionWorker( + userId: UserId, + ): UpdateRevisionWorker { + val context = ApplicationProvider.getApplicationContext() + return TestListenableWorkerBuilder(context) + .setWorkerFactory( + object : WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ) = UpdateRevisionWorker( + appContext = appContext, + workerParams = workerParameters, + workManager = workManager, + broadcastMessages = broadcastMessages, + getUploadFileLink = getUploadFileLink, + updateRevision = updateRevision, + configurationProvider = configurationProvider, + canRun = canRun, + run = run, + done = done, + ) + } + ) + .setInputData( + Data.Builder() + .putString(WorkerKeys.KEY_USER_ID, userId.id) + .build() + ) + .build() + } +} diff --git a/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt new file mode 100644 index 00000000..60283c38 --- /dev/null +++ b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.upload.data.worker + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.Operation +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import androidx.work.testing.TestListenableWorkerBuilder +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.core.crypto.common.pgp.exception.CryptoException +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.linkupload.domain.entity.UploadFileLink +import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink +import me.proton.core.drive.upload.domain.usecase.VerifyBlocks +import me.proton.core.drive.worker.domain.usecase.CanRun +import me.proton.core.drive.worker.domain.usecase.Done +import me.proton.core.drive.worker.domain.usecase.Run +import me.proton.core.util.kotlin.CoreLogger +import me.proton.core.util.kotlin.Logger +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class VerifyBlocksWorkerTest { + private val userId: UserId = UserId("user-id") + private val workManager = mockk() + private val broadcastMessages = mockk() + private val getUploadFileLink = mockk() + private val verifyBlocks = mockk() + private val configurationProvider = mockk() + private val canRun = mockk() + private val run = mockk() + private val done = mockk() + private val uploadFileLink = mockk() + private val operation = mockk() + private val logger = mockk() + + @Before + fun before() { + coEvery { canRun(any(), any()) } returns Result.success(true) + coEvery { getUploadFileLink(any() as Long) } returns DataResult.Success(ResponseSource.Local, uploadFileLink) + coEvery { uploadFileLink.id } returns 123L + coEvery { uploadFileLink.name } returns "secret.jpg" + coEvery { workManager.enqueue(any() as WorkRequest) } returns operation + coEvery { configurationProvider.useExceptionMessage } returns false + coEvery { broadcastMessages(userId, any(), any(), any()) } returns Unit + coEvery { logger.d(any(), any()) } returns Unit + coEvery { logger.e(any(), any(), any()) } returns Unit + } + + @Test + fun `when verify blocks fails with VerifyBlock exception, upload fails and it is logged as error`() = runTest { + // Given + val error = VerifierException.VerifyBlock(CryptoException("Invalid key")) + coEvery { verifyBlocks(uploadFileLink) } returns Result.failure(error) + val uploadFileLinkId = uploadFileLink.id + CoreLogger.set(logger) + + // When + val result = verifyBlocksWorker(userId).doLimitedRetryWork() + + // Then + Assert.assertEquals(ListenableWorker.Result.failure(), result) + verify(exactly = 1) { + logger.e( + tag = "core.drive.upload.$uploadFileLinkId", + e = error, + message = """ + Verify blocks failed with "${error.message}" retryable false, + max retries reached false + """.trimIndent(), + ) + } + } + + private fun verifyBlocksWorker( + userId: UserId, + ): VerifyBlocksWorker { + val context = ApplicationProvider.getApplicationContext() + return TestListenableWorkerBuilder(context) + .setWorkerFactory( + object : WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ) = VerifyBlocksWorker( + appContext = appContext, + workerParams = workerParameters, + workManager = workManager, + broadcastMessages = broadcastMessages, + getUploadFileLink = getUploadFileLink, + verifyBlocks = verifyBlocks, + configurationProvider = configurationProvider, + canRun = canRun, + run = run, + done = done, + ) + } + ) + .setInputData( + Data.Builder() + .putString(WorkerKeys.KEY_USER_ID, userId.id) + .build() + ) + .build() + } +} diff --git a/drive/upload/domain/build.gradle.kts b/drive/upload/domain/build.gradle.kts index fded5d9f..4bd01cc0 100644 --- a/drive/upload/domain/build.gradle.kts +++ b/drive/upload/domain/build.gradle.kts @@ -28,6 +28,7 @@ driveModule( api(project(":drive:link-upload:domain")) api(project(":drive:notification:domain")) api(project(":drive:thumbnail:domain")) + api(project(":verifier:domain")) implementation(project(":drive:crypto:domain")) } diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt index cd988fa9..fd9f267e 100644 --- a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt +++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt @@ -197,6 +197,7 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( rawSize = rawBlock.size, size = encryptedBlock.size, token = "", + verifierToken = null, ).also { rawBlock.delete() } }.getOrThrow() @@ -231,6 +232,7 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( rawSize = thumbnail.size.bytes, size = encryptedUploadThumbnail.size, token = "", + verifierToken = null, ) } diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt new file mode 100644 index 00000000..753f448f --- /dev/null +++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ + +package me.proton.core.drive.upload.domain.usecase + +import me.proton.android.drive.verifier.domain.usecase.BuildVerifier +import me.proton.android.drive.verifier.domain.usecase.CleanupVerifier +import me.proton.core.drive.base.domain.usecase.GetSignatureAddress +import me.proton.core.drive.base.domain.util.coRunCatching +import me.proton.core.drive.key.domain.entity.Key +import me.proton.core.drive.key.domain.usecase.BuildNodeKey +import me.proton.core.drive.key.domain.usecase.GetNodeKey +import me.proton.core.drive.linkupload.domain.entity.UploadFileLink +import me.proton.core.drive.linkupload.domain.usecase.GetUploadBlocks +import me.proton.core.drive.linkupload.domain.usecase.UpdateVerifierToken +import javax.inject.Inject + +class VerifyBlocks @Inject constructor( + private val buildVerifier: BuildVerifier, + private val cleanupVerifier: CleanupVerifier, + private val getUploadBlocks: GetUploadBlocks, + private val getSignatureAddress: GetSignatureAddress, + private val getNodeKey: GetNodeKey, + private val buildNodeKey: BuildNodeKey, + private val updateVerifierToken: UpdateVerifierToken, +) { + suspend operator fun invoke(uploadFileLink: UploadFileLink): Result = coRunCatching { + val verifier = buildVerifier( + userId = uploadFileLink.userId, + shareId = uploadFileLink.shareId.id, + linkId = requireNotNull(uploadFileLink.linkId), + revisionId = uploadFileLink.draftRevisionId, + fileKey = uploadFileLink.buildFileKey(), + ).getOrThrow() + val uploadBlocks = getUploadBlocks(uploadFileLink) + .getOrThrow() + .associateBy { uploadBlock -> uploadBlock.file } + verifier.verifyBlocks(uploadBlocks.keys.toList()) + .getOrThrow() + .map { (file, verifierToken) -> + updateVerifierToken( + uploadFileLinkId = uploadFileLink.id, + index = requireNotNull(uploadBlocks[file]).index, + verifierToken = verifierToken, + ) + } + cleanupVerifier( + userId = uploadFileLink.userId, + shareId = uploadFileLink.shareId.id, + linkId = requireNotNull(uploadFileLink.linkId), + revisionId = uploadFileLink.draftRevisionId, + ) + } + + private suspend fun UploadFileLink.buildFileKey(): Key.Node = + buildNodeKey( + userId = userId, + parentKey = getNodeKey(parentLinkId).getOrThrow(), + uploadFileLink = this, + signatureAddress = getSignatureAddress(userId), + ).getOrThrow() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82fa39d9..6fd49792 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -231,6 +231,7 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espresso-contrib"} androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose" } androidx-compose-ui-test-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" } +androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } junit = { module = "junit:junit", version.ref = "junit" } mockk-jvm = { module = "io.mockk:mockk", version.ref = "mockk" } @@ -243,7 +244,7 @@ accompanist = ["accompanist-insets", "accompanist-navigation-animation", "accomp core = ["core-account", "core-accountManager", "core-auth", "core-challenge", "core-country", "core-crypto", "core-cryptoValidator", "core-data", "core-dataRoom", "core-domain", "core-eventManager", "core-featureFlag", "core-humanVerification", "core-key", "core-keyTransparency", "core-network", "core-observability", "core-payment", "core-payment-iap", "core-plan", "core-report", "core-presentation", "core-presentation-compose", "core-user", "core-userSettings", "core-utilAndroidDagger", "core-utilKotlin"] core-test = ["core-auth-test", "core-humanVerification-test", "core-report-test"] test-android = ["junit", "mockk-android", "coroutines-test", "androidx-test-core-ktx", "androidx-test-runner", "androidx-test-rules", "androidx-compose-ui-test", "androidx-compose-ui-test-junit", "androidx-test-uiautomator", "core-test-android-instrumented"] -test-jvm = ["junit", "mockk-jvm", "coroutines-test", "core-test-kotlin", "core-test-quark", "robolectric"] +test-jvm = ["junit", "mockk-jvm", "coroutines-test", "androidx-test-core-ktx", "androidx-work-testing", "core-test-kotlin", "core-test-quark", "robolectric"] [plugins] proton-detekt = { id = "me.proton.core.gradle-plugins.detekt", version.ref = "proton-detekt-plugin" } diff --git a/verifier/build.gradle.kts b/verifier/build.gradle.kts new file mode 100644 index 00000000..338d8993 --- /dev/null +++ b/verifier/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +plugins { + id("com.android.library") +} + +android { + namespace = "me.proton.android.drive.verifier" +} + +driveModule(includeSubmodules = true) diff --git a/verifier/data/build.gradle.kts b/verifier/data/build.gradle.kts new file mode 100644 index 00000000..b3bb3a7b --- /dev/null +++ b/verifier/data/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021-2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ +plugins { + id("com.android.library") +} + +android { + namespace = "me.proton.android.drive.verifier.data" +} + +driveModule( + hilt = true, + serialization = true, +) { + api(project(":verifier:domain")) + implementation(project(":drive:base:data")) + implementation(project(":drive:crypto:domain")) + implementation(project(":drive:i18n")) + implementation(libs.retrofit) + testImplementation(project(":drive:base:data-test")) +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt new file mode 100644 index 00000000..e9cc0735 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.api + +import me.proton.android.drive.verifier.data.api.response.GetVerificationDataResponse +import me.proton.core.network.data.protonApi.BaseRetrofitApi +import retrofit2.http.GET +import retrofit2.http.Path + +interface VerifierApi : BaseRetrofitApi { + + @GET("drive/shares/@{enc_shareID}/links/@{enc_linkID}/revisions/@{enc_revisionID}/verification") + suspend fun getVerificationData( + @Path("enc_shareID") shareId: String, + @Path("enc_linkID") linkId: String, + @Path("enc_revisionID") revisionId: String, + ): GetVerificationDataResponse +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt new file mode 100644 index 00000000..b279b2a6 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.api + +import me.proton.android.drive.verifier.data.api.response.GetVerificationDataResponse +import me.proton.android.drive.verifier.domain.entity.VerificationData +import me.proton.core.domain.entity.UserId +import me.proton.core.network.data.ApiProvider +import me.proton.core.network.domain.ApiException + +class VerifierApiDataSource(private val apiProvider: ApiProvider) { + + @Throws(ApiException::class) + suspend fun getVerificationData( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + ): GetVerificationDataResponse = + apiProvider.get(userId).invoke { getVerificationData(shareId, linkId, revisionId) }.valueOrThrow +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt new file mode 100644 index 00000000..738754cc --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.api.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import me.proton.core.drive.base.data.api.Dto.CODE +import me.proton.core.drive.base.data.api.Dto.CONTENT_KEY_PACKET +import me.proton.core.drive.base.data.api.Dto.REVISION +import me.proton.core.drive.base.data.api.Dto.SIGNATURE_ADDRESS +import me.proton.core.drive.base.data.api.Dto.STATE +import me.proton.core.drive.base.data.api.Dto.VERIFICATION_CODE + +@Serializable +data class GetVerificationDataResponse( + @SerialName(CODE) + val code: Long, + @SerialName(VERIFICATION_CODE) + val verificationCode: String, + @SerialName(CONTENT_KEY_PACKET) + val contentKeyPacket: String, +) diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt new file mode 100644 index 00000000..d38e5303 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import me.proton.android.drive.verifier.data.repository.VerifierRepositoryImpl +import me.proton.android.drive.verifier.domain.repository.VerifierRepository +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface VerifierBindModule { + + @Binds + @Singleton + fun bindsRepositoryImpl(impl: VerifierRepositoryImpl): VerifierRepository +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt new file mode 100644 index 00000000..83183a8c --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import me.proton.android.drive.verifier.data.api.VerifierApiDataSource +import me.proton.android.drive.verifier.data.factory.VerifierFactoryImpl +import me.proton.android.drive.verifier.domain.factory.VerifierFactory +import me.proton.core.drive.base.domain.usecase.GetCacheTempFolder +import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles +import me.proton.core.network.data.ApiProvider +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object VerifierModule { + @Singleton + @Provides + fun provideVerifierApiDataSource(apiProvider: ApiProvider) = + VerifierApiDataSource(apiProvider) + + @Singleton + @Provides + fun provideVerifierFactory( + decryptFiles: DecryptFiles, + getCacheTempFolder: GetCacheTempFolder, + ): VerifierFactory = + VerifierFactoryImpl(decryptFiles, getCacheTempFolder) +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt new file mode 100644 index 00000000..9810bf20 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.entity + +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.android.drive.verifier.data.extension.head +import me.proton.android.drive.verifier.data.extension.xor +import me.proton.android.drive.verifier.domain.entity.Verifier +import me.proton.core.drive.base.domain.extension.bytes +import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles +import me.proton.core.drive.key.domain.entity.ContentKey +import java.io.File +import java.util.UUID + +internal class VerifierImpl constructor( + private val decryptFiles: DecryptFiles, + private val contentKey: ContentKey, + private val verificationCode: ByteArray, + private val tempFolder: File +) : Verifier { + + init { + require(verificationCode.size == VERIFICATION_CODE_SIZE) { + "Invalid verification code size" + } + } + + override suspend fun verifyBlocks(blocks: List): Result> = + try { + require(blocks.isNotEmpty()) { "Input blocks list is empty" } + require(blocks.all { block -> block.exists() }) { "Input block does not exist" } + verifyByDecryptingBlocks(blocks) + Result.success( + blocks.associateBy( + keySelector = { file -> file } + ) { file -> verificationCode.xor(file.head(len = verificationCode.size.bytes))} + ) + } catch (t: Throwable) { + Result.failure(VerifierException.VerifyBlock(t)) + } + + private suspend fun verifyByDecryptingBlocks(blocks: List) { + val output = blocks.map { block -> + File(block.destinationFolder(), UUID.randomUUID().toString()).apply { + parentFile?.mkdirs() + createNewFile() + } + } + val result = decryptFiles( + contentKey = contentKey, + input = blocks, + output = output, + ) + output.forEach { file -> file.delete() } + result.getOrThrow() + } + + private fun File.destinationFolder() = parent ?: tempFolder.path + + companion object { + private const val VERIFICATION_CODE_SIZE = 32 + } +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/ByteArray.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/ByteArray.kt new file mode 100644 index 00000000..7fef1c85 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/ByteArray.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import kotlin.experimental.xor + +internal fun ByteArray.xor(other: ByteArray): ByteArray { + require(size == other.size) { "Arrays of same size are required" } + val output = ByteArray(size) + for (i in indices) { + output[i] = this[i].xor(other[i]) + } + return output +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/File.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/File.kt new file mode 100644 index 00000000..7e9120ae --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/File.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.domain.extension.MiB +import java.io.File + +internal fun File.head(len: Bytes): ByteArray = inputStream().use { fileInputStream -> + require(len.value in LongRange(1, 1.MiB.value)) + val size = len.value.toInt() + val bytes = ByteArray(size) + fileInputStream.read(bytes, 0, size) + bytes +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/VerifierException.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/VerifierException.kt new file mode 100644 index 00000000..ec7d45ff --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/extension/VerifierException.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import android.content.Context +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.core.util.kotlin.CoreLogger +import me.proton.core.drive.i18n.R as I18N + +fun VerifierException.getDefaultMessage(context: Context): String = when (this) { + is VerifierException.Initialize -> context.getString(I18N.string.verifier_initialize_failed) + is VerifierException.VerifyBlock -> context.getString(I18N.string.verifier_verify_blocks_failed) +} + +fun VerifierException.log(tag: String, message: String = this.message.orEmpty()): VerifierException = also { + val logToSentry = this is VerifierException.VerifyBlock + val log: (String, Throwable, String) -> Unit = if (logToSentry) CoreLogger::e else CoreLogger::d + log(tag, this, message) +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/factory/VerifierFactoryImpl.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/factory/VerifierFactoryImpl.kt new file mode 100644 index 00000000..efc54da5 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/factory/VerifierFactoryImpl.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.factory + +import me.proton.android.drive.verifier.data.entity.VerifierImpl +import me.proton.android.drive.verifier.domain.entity.Verifier +import me.proton.android.drive.verifier.domain.factory.VerifierFactory +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.usecase.GetCacheTempFolder +import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles +import me.proton.core.drive.key.domain.entity.ContentKey +import javax.inject.Inject + +class VerifierFactoryImpl @Inject constructor( + private val decryptFiles: DecryptFiles, + private val getCacheTempFolder: GetCacheTempFolder, +) : VerifierFactory { + + override suspend fun create( + userId: UserId, + contentKey: ContentKey, + verificationCode: ByteArray, + ): Verifier = + VerifierImpl( + decryptFiles = decryptFiles, + contentKey = contentKey, + verificationCode = verificationCode, + tempFolder = getCacheTempFolder(userId), + ) +} diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryImpl.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryImpl.kt new file mode 100644 index 00000000..1b094a86 --- /dev/null +++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryImpl.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.repository + +import android.util.Base64 +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import me.proton.android.drive.verifier.data.api.VerifierApiDataSource +import me.proton.android.drive.verifier.domain.entity.VerificationData +import me.proton.android.drive.verifier.domain.repository.VerifierRepository +import me.proton.core.domain.entity.UserId +import javax.inject.Inject + +class VerifierRepositoryImpl @Inject constructor( + private val api: VerifierApiDataSource, +) : VerifierRepository { + internal val verificationDataCache: MutableMap = mutableMapOf() + private val mutex = Mutex() + + override suspend fun getVerificationData( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + ): VerificationData = getOrFetch(VerificationDataKey(userId, shareId, linkId, revisionId)) + + override suspend fun removeVerificationData( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + ) { + remove( + VerificationDataKey(userId, shareId, linkId, revisionId) + ) + } + + private suspend fun getOrFetch(key: VerificationDataKey): VerificationData = get(key) ?: fetchAndStore(key) + + private suspend fun fetchAndStore(key: VerificationDataKey): VerificationData = + api.getVerificationData(key.userId, key.shareId, key.linkId, key.revisionId).let { response -> + VerificationData( + contentKeyPacket = response.contentKeyPacket, + verificationCode = Base64.decode(response.verificationCode, Base64.NO_WRAP), + ) + }.also { verificationData -> + put(key, verificationData) + } + + internal suspend fun get(key: VerificationDataKey): VerificationData? = mutex.withLock { + verificationDataCache[key] + } + + internal suspend fun put(key: VerificationDataKey, verificationData: VerificationData) = mutex.withLock { + verificationDataCache[key] = verificationData + } + + internal suspend fun remove(key: VerificationDataKey) = mutex.withLock { + verificationDataCache.remove(key) + } + + data class VerificationDataKey( + val userId: UserId, + val shareId: String, + val linkId: String, + val revisionId: String, + ) +} diff --git a/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/entity/VerifierTest.kt b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/entity/VerifierTest.kt new file mode 100644 index 00000000..ad76db90 --- /dev/null +++ b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/entity/VerifierTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.entity + +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.android.drive.verifier.data.extension.createFile +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.core.crypto.common.pgp.exception.CryptoException +import me.proton.core.drive.base.domain.extension.bytes +import me.proton.core.drive.base.domain.extension.toHex +import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles +import me.proton.core.drive.key.domain.entity.ContentKey +import me.proton.core.drive.key.domain.entity.Key +import me.proton.core.drive.key.domain.usecase.BuildContentKey +import me.proton.core.test.kotlin.assertEquals +import me.proton.core.test.kotlin.assertTrue +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +@OptIn(ExperimentalCoroutinesApi::class) +class VerifierTest { + private val buildContentKey = mockk() + private val decryptFiles = mockk() + private val fileKey = mockk() + private val contentKey = mockk() + private val verificationCode = ByteArray(VERIFICATION_CODE_SIZE) { i -> i.toByte() } + private lateinit var verifier: VerifierImpl + private lateinit var file64B: File + private lateinit var file17B: File + + @get: Rule + val temporaryFolder = TemporaryFolder() + + @Before + fun before() { + coEvery { + buildContentKey( + userId = any(), + contentKeyPacket = any(), + contentKeyPacketSignature = any(), + fileKey = fileKey, + ) + } returns Result.success(contentKey) + + coEvery { + decryptFiles( + contentKey = contentKey, + input = any(), + output = any(), + ) + } returns Result.success(emptyList()) + + verifier = VerifierImpl(decryptFiles, contentKey, verificationCode, temporaryFolder.newFolder()) + file64B = temporaryFolder.createFile(64.bytes) + file17B = temporaryFolder.createFile(17.bytes) + } + + @Test(expected = IllegalArgumentException::class) + fun `VerifierImpl throws IllegalArgumentException if verificationCode with invalid size is given`() = runTest { + // Given + val verificationCode = ByteArray(31) { i -> i.toByte() } + + // Then + VerifierImpl(decryptFiles, contentKey, verificationCode, temporaryFolder.newFolder()) + } + + @Test + fun `verifyBlock when given empty block list throws VerifyBlock with cause IllegalArgumentException`() = runTest { + // Given + val blocks = emptyList() + + // When + val exception = verifier.verifyBlocks(blocks).exceptionOrNull() + + // Then + assertNotNull(exception) + assertTrue(exception is VerifierException.VerifyBlock) { + "actual: $exception, expected: VerifyBlock" + } + assertTrue(exception?.cause is IllegalArgumentException) { + "actual: ${exception?.cause}, expected: IllegalArgumentException" + } + } + + @Test + fun `verifyBlock when given non-existing block file(s) throws VerifyBlock with cause IllegalArgumentException`() = runTest { + // Given + val nonExistingFile = File("", "test.txt") + val blocks = listOf(nonExistingFile) + + // When + val exception = verifier.verifyBlocks(blocks).exceptionOrNull() + + // Then + assertNotNull(exception) + assertTrue(exception is VerifierException.VerifyBlock) { + "actual: $exception, expected: VerifyBlock" + } + assertTrue(exception?.cause is IllegalArgumentException) { + "actual: ${exception?.cause}, expected: IllegalArgumentException" + } + } + + @Test + fun `verifyBlock throws VerifyBlock when decrypt blocks fails`() = runTest { + // Given + coEvery { decryptFiles(contentKey, any(), any()) } returns Result.failure(CryptoException()) + + // When + val exception = verifier.verifyBlocks(listOf(file64B)).exceptionOrNull() + + // Then + assertNotNull(exception) + assertTrue(exception is VerifierException.VerifyBlock) { + "actual: $exception, expected: VerifyBlock" + } + assertTrue(exception?.cause is CryptoException) { + "actual: ${exception?.cause}, expected: CryptoException" + } + } + + @Test + fun `successful dual block verification`() = runTest { + // Given + val blocks = listOf(file64B, file17B) + + // When + val verifierTokens = verifier.verifyBlocks(blocks).getOrThrow() + + // Then + assertEquals(blocks.size, verifierTokens.size) { "Verify blocks result size mismatch" } + assertTrue(verifierTokens.containsKey(file64B)) { "File mismatch" } + assertTrue(verifierTokens.containsKey(file17B)) { "File mismatch" } + val verificationCodeXorHeaderOfFile64B = byteArrayOf( + 0x61, 0x60, 0x63, 0x62, 0x65, 0x64, 0x67, 0x66, + 0x69, 0x68, 0x6B, 0x6A, 0x6D, 0x6C, 0x6F, 0x6E, + 0x71, 0x70, 0x73, 0x72, 0x75, 0x74, 0x77, 0x76, + 0x79, 0x78, 0x7B, 0x7A, 0x7D, 0x7C, 0x7F, 0x7E + ) + assertTrue(verifierTokens[file64B].contentEquals(verificationCodeXorHeaderOfFile64B)) { + "Verifier token mismatch actual: ${verifierTokens[file64B]?.toHex()}, expected: ${verificationCodeXorHeaderOfFile64B.toHex()}" + } + val verificationCodeXorHeaderOfFile17B = byteArrayOf( + 0x61, 0x60, 0x63, 0x62, 0x65, 0x64, 0x67, 0x66, + 0x69, 0x68, 0x6B, 0x6A, 0x6D, 0x6C, 0x6F, 0x6E, + 0x71, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + ) + assertTrue(verifierTokens[file17B].contentEquals(verificationCodeXorHeaderOfFile17B)) { + "Verifier token mismatch actual: ${verifierTokens[file17B]?.toHex()}, expected: ${verificationCodeXorHeaderOfFile17B.toHex()}" + } + } + + companion object { + private const val VERIFICATION_CODE_SIZE = 32 // bytes + } +} diff --git a/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/ByteArrayTest.kt b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/ByteArrayTest.kt new file mode 100644 index 00000000..2c5d37a9 --- /dev/null +++ b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/ByteArrayTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import me.proton.core.test.kotlin.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ByteArrayParameterizedTest( + private val first: ByteArray, + private val second: ByteArray, + private val firstXorSecond: ByteArray, +) { + + @Test + fun `xor of two byte arrays`() { + assertTrue( + first.xor(second).contentEquals(firstXorSecond) + ) { "xor failed" } + } + + companion object { + @get:Parameterized.Parameters(name = "{0} xor {1} equals {2}") + @get:JvmStatic + val data = listOf( + // Byte array with size 1 + arrayOf( + byteArrayOf(0x00), + byteArrayOf(0xFF.toByte()), + byteArrayOf(0xFF.toByte()), + ), + arrayOf( + byteArrayOf(0xF0.toByte()), + byteArrayOf(0x0F.toByte()), + byteArrayOf(0xFF.toByte()), + ), + // Byte array with size 2 + arrayOf( + byteArrayOf(0x00, 0xFF.toByte()), + byteArrayOf(0xFF.toByte(), 0x00), + byteArrayOf(0xFF.toByte(), 0xFF.toByte()), + ), + arrayOf( + byteArrayOf(0x00, 0x55.toByte()), + byteArrayOf(0xAA.toByte(), 0x00), + byteArrayOf(0xAA.toByte(), 0x55.toByte()), + ), + // Byte array with size 4 + arrayOf( + byteArrayOf(0x05.toByte(), 0x06.toByte(), 0x07.toByte(), 0x08.toByte()), + byteArrayOf(0xA3.toByte(), 0xA4.toByte(), 0xA5.toByte(), 0xA6.toByte()), + byteArrayOf(0xA6.toByte(), 0xA2.toByte(), 0xA2.toByte(), 0xAE.toByte()), + ), + ) + } +} + +class ByteArrayTest { + @Test(expected = IllegalArgumentException::class) + fun `different size byte arrays cause IllegalArgumentException`() { + byteArrayOf(0x00).xor(byteArrayOf(0x00, 0x00)) + } + + @Test + fun `xor of two empty byte array result in empty byte array`() { + assertTrue( + ByteArray(0).xor(ByteArray(0)).contentEquals(ByteArray(0)) + ) { "xor failed" } + } +} diff --git a/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/FileTest.kt b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/FileTest.kt new file mode 100644 index 00000000..6292d395 --- /dev/null +++ b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/FileTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import me.proton.core.drive.base.domain.extension.bytes +import me.proton.core.drive.base.domain.extension.size +import me.proton.core.test.kotlin.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class FileTest( + private val fileSize: Int, +) { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `head on different file size`() { + // Given + val fileSizeInBytes = fileSize.bytes + val file = temporaryFolder.createFile(fileSizeInBytes) + check(file.exists()) { "File does not exist" } + check(file.size == fileSizeInBytes) { + "File size mismatch expected: $fileSizeInBytes, actual: ${file.size}" + } + + // When + val head = file.head(TARGET_HEAD_SIZE.bytes) + + // Then + assertEquals(TARGET_HEAD_SIZE, head.size) { + "Head size mismatch" + } + for (i in 0 until minOf(fileSize, TARGET_HEAD_SIZE)) { + assertEquals('a'.code, head[i].toInt()) { "Byte mismatch" } + } + for (i in fileSize until TARGET_HEAD_SIZE) { + assertEquals(0, head[i].toInt()) { "Byte mismatch" } + } + } + + companion object { + private const val TARGET_HEAD_SIZE = 32 + + @get:Parameterized.Parameters + @get:JvmStatic + val data = listOf( + arrayOf(0), + arrayOf(1), + arrayOf(17), + arrayOf(33), + ) + } +} diff --git a/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/TemporaryFolder.kt b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/TemporaryFolder.kt new file mode 100644 index 00000000..a27a02db --- /dev/null +++ b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/extension/TemporaryFolder.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.extension + +import me.proton.core.drive.base.domain.entity.Bytes +import org.junit.rules.TemporaryFolder +import java.io.File +import java.util.UUID + +fun TemporaryFolder.createFile(size: Bytes): File = + File(root, "${size.value}_byte(s)_${UUID.randomUUID()}.txt").apply { + if (exists()) { delete() } + createNewFile() + appendText( + (0 until size.value.toInt()).joinToString(separator = "") { "a" } + ) + } diff --git a/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryTest.kt b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryTest.kt new file mode 100644 index 00000000..528b00fb --- /dev/null +++ b/verifier/data/src/test/kotlin/me/proton/android/drive/verifier/data/repository/VerifierRepositoryTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.data.repository + +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.runTest +import me.proton.android.drive.verifier.data.api.VerifierApiDataSource +import me.proton.android.drive.verifier.data.api.response.GetVerificationDataResponse +import me.proton.android.drive.verifier.domain.entity.VerificationData +import me.proton.core.domain.entity.UserId +import me.proton.core.network.domain.ApiException +import me.proton.core.network.domain.ApiResult +import me.proton.core.test.kotlin.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.util.Base64 + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class VerifierRepositoryTest { + private val apiDataSource = mockk() + private val userId = UserId("user-id") + private val shareId = "share-id" + private val linkId = "link-id" + private val revisionId = "revision-id" + private val contentKeyPacket = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + private val verificationCode = Base64.getEncoder().encodeToString(ByteArray(32) { i -> i.toByte() }) + private lateinit var repository: VerifierRepositoryImpl + + @Before + fun before() { + coEvery { + apiDataSource.getVerificationData( + userId = any(), + shareId = any(), + linkId = any(), + revisionId = any(), + ) + } returns GetVerificationDataResponse( + code = 1000, + verificationCode = verificationCode, + contentKeyPacket = contentKeyPacket, + ) + repository = VerifierRepositoryImpl(apiDataSource) + } + + @Test + fun `successful remove of verification data removes it from repository cache`() = runTest { + // When + repository.getVerificationData(userId, shareId, linkId, revisionId) + repository.removeVerificationData(userId, shareId, linkId, revisionId) + + // Then + val key = VerifierRepositoryImpl.VerificationDataKey(userId, shareId, linkId, revisionId) + assertNull(repository.verificationDataCache[key]) + } + + @Test + fun `successful verification data from verifier api data source and repository cache`() = runTest { + // When + val verificationData = repository.getVerificationData(userId, shareId, linkId, revisionId) + + // Then + val expectedVerificationData = VerificationData( + contentKeyPacket = contentKeyPacket, + verificationCode = Base64.getDecoder().decode(verificationCode), + ) + assertEquals(expectedVerificationData, verificationData) { "Verification data mismatch from data source" } + val key = VerifierRepositoryImpl.VerificationDataKey(userId, shareId, linkId, revisionId) + assertEquals(expectedVerificationData, repository.verificationDataCache[key]) { + "Verification data mismatch from cache" + } + } + + @Test + fun `cached verification data is provided when available`() = runTest { + // Given + repository.getVerificationData(userId, shareId, linkId, revisionId) + coEvery { + apiDataSource.getVerificationData( + userId = any(), + shareId = any(), + linkId = any(), + revisionId = any(), + ) + } throws ApiException(ApiResult.Error.Http(httpCode = 500, message = "Internal server error")) + + // When + val verificationData = repository.getVerificationData(userId, shareId, linkId, revisionId) + + // Then + val expectedVerificationData = VerificationData(contentKeyPacket, Base64.getDecoder().decode(verificationCode)) + assertEquals(expectedVerificationData, verificationData) { "Verification data mismatch from data source" } + } + + @Test(expected = ApiException::class) + fun `when network error occurs it is propagated`() = runTest { + // Given + coEvery { + apiDataSource.getVerificationData( + userId = any(), + shareId = any(), + linkId = any(), + revisionId = any(), + ) + } throws ApiException(ApiResult.Error.Http(httpCode = 500, message = "Internal server error")) + + // Then + repository.getVerificationData(userId, shareId, linkId, revisionId) + } + + @Test + fun `many concurrent calls to get same verification data leaves repository cache in consistent state`() = runTest { + // When + (0..99).map { + async { + repository.getVerificationData(userId, shareId, linkId, revisionId) + } + }.awaitAll() + + // Then + val key = VerifierRepositoryImpl.VerificationDataKey(userId, shareId, linkId, revisionId) + assertEquals(1, repository.verificationDataCache.keys.size) { "Invalid cache size" } + val expectedVerificationData = VerificationData(contentKeyPacket, Base64.getDecoder().decode(verificationCode)) + assertEquals(expectedVerificationData, repository.verificationDataCache[key]) { + "Verification data mismatch from cache" + } + } + + @Test + fun `many concurrent calls to get different verification data and then to remove it leaves repository cache in consistent state`() = runTest { + // When + (0..99).map { i -> + async { + repository.getVerificationData(userId, shareId, linkId, "${revisionId}_$i") + repository.removeVerificationData(userId, shareId, linkId, "${revisionId}_$i") + } + }.awaitAll() + + // Then + assertEquals(0, repository.verificationDataCache.keys.size) { "Invalid cache size" } + } +} diff --git a/verifier/domain/build.gradle.kts b/verifier/domain/build.gradle.kts new file mode 100644 index 00000000..a43e2d1d --- /dev/null +++ b/verifier/domain/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021-2023 Proton AG. + * This file is part of Proton Core. + * + * Proton Core is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Core is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Core. If not, see . + */ +plugins { + id("com.android.library") +} + +android { + namespace = "me.proton.android.drive.verifier.domain" +} + +driveModule( + hilt = true, +) { + api(project(":drive:key:domain")) + api(project(":drive:link-upload:domain")) +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/VerificationData.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/VerificationData.kt new file mode 100644 index 00000000..aa8f1c33 --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/VerificationData.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.entity + +data class VerificationData( + val contentKeyPacket: String, + val verificationCode: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VerificationData + + if (contentKeyPacket != other.contentKeyPacket) return false + if (!verificationCode.contentEquals(other.verificationCode)) return false + + return true + } + + override fun hashCode(): Int { + var result = contentKeyPacket.hashCode() + result = 31 * result + verificationCode.contentHashCode() + return result + } +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/Verifier.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/Verifier.kt new file mode 100644 index 00000000..8f10b61c --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/entity/Verifier.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.entity + +import java.io.File + +interface Verifier { + + suspend fun verifyBlocks(blocks: List): Result> +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/exception/VerifierException.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/exception/VerifierException.kt new file mode 100644 index 00000000..94a0b5aa --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/exception/VerifierException.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.exception + +sealed class VerifierException(override val cause: Throwable) : Throwable() { + + data class VerifyBlock(override val cause: Throwable) : VerifierException(cause) + + data class Initialize(override val cause: Throwable) : VerifierException(cause) +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/factory/VerifierFactory.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/factory/VerifierFactory.kt new file mode 100644 index 00000000..116eddef --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/factory/VerifierFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.factory + +import me.proton.android.drive.verifier.domain.entity.Verifier +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.key.domain.entity.ContentKey + +interface VerifierFactory { + suspend fun create( + userId: UserId, + contentKey: ContentKey, + verificationCode: ByteArray, + ): Verifier +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/repository/VerifierRepository.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/repository/VerifierRepository.kt new file mode 100644 index 00000000..4842198f --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/repository/VerifierRepository.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.repository + +import me.proton.android.drive.verifier.domain.entity.VerificationData +import me.proton.core.domain.entity.UserId + +interface VerifierRepository { + suspend fun getVerificationData( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + ): VerificationData + + suspend fun removeVerificationData( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + ) +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/BuildVerifier.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/BuildVerifier.kt new file mode 100644 index 00000000..ee35130d --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/BuildVerifier.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.usecase + +import me.proton.android.drive.verifier.domain.entity.Verifier +import me.proton.android.drive.verifier.domain.exception.VerifierException +import me.proton.android.drive.verifier.domain.factory.VerifierFactory +import me.proton.android.drive.verifier.domain.repository.VerifierRepository +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.key.domain.entity.Key +import me.proton.core.drive.key.domain.usecase.BuildContentKey +import javax.inject.Inject + +class BuildVerifier @Inject constructor( + private val repository: VerifierRepository, + private val buildContentKey: BuildContentKey, + private val factory: VerifierFactory, +) { + suspend operator fun invoke( + userId: UserId, + shareId: String, + linkId: String, + revisionId: String, + fileKey: Key.Node, + ): Result = try { + val verificationData = repository.getVerificationData(userId, shareId, linkId, revisionId) + val contentKey = buildContentKey( + userId = userId, + contentKeyPacket = verificationData.contentKeyPacket, + contentKeyPacketSignature = "", + fileKey = fileKey + ).getOrThrow() + Result.success(factory.create( + userId = userId, + contentKey = contentKey, + verificationCode = verificationData.verificationCode, + )) + } catch (t: Throwable) { + Result.failure(VerifierException.Initialize(t)) + } +} diff --git a/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/CleanupVerifier.kt b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/CleanupVerifier.kt new file mode 100644 index 00000000..cab907fd --- /dev/null +++ b/verifier/domain/src/main/kotlin/me/proton/android/drive/verifier/domain/usecase/CleanupVerifier.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.verifier.domain.usecase + +import me.proton.android.drive.verifier.domain.repository.VerifierRepository +import me.proton.core.domain.entity.UserId +import javax.inject.Inject + +class CleanupVerifier @Inject constructor( + private val repository: VerifierRepository, +) { + suspend operator fun invoke(userId: UserId, shareId: String, linkId: String, revisionId: String) = + repository.removeVerificationData(userId, shareId, linkId, revisionId) +}