diff --git a/.gitignore b/.gitignore index 2f8209ac..8b041cbf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ project.properties /.project /tmp + +# Clojure +.lsp/ +.clj-kondo/ +.cpcache/ +.nrepl-port \ No newline at end of file diff --git a/app/src/debug/assets/fake-fs-database.kdbx b/app/src/debug/assets/fake-fs-database.kdbx deleted file mode 100644 index 22ad723e..00000000 Binary files a/app/src/debug/assets/fake-fs-database.kdbx and /dev/null differ diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/Extensions.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/Extensions.kt new file mode 100644 index 00000000..7dd93912 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/Extensions.kt @@ -0,0 +1,10 @@ +package com.ivanovsky.passnotes.data.repository.file + +import java.text.SimpleDateFormat +import java.util.Locale + +private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US) + +fun parseDate(str: String): Long { + return DATE_FORMAT.parse(str)?.time ?: throw IllegalArgumentException() +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileContentFactory.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileContentFactory.kt new file mode 100644 index 00000000..e716adc3 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileContentFactory.kt @@ -0,0 +1,192 @@ +package com.ivanovsky.passnotes.data.repository.file + +import app.keemobile.kotpass.database.KeePassDatabase +import app.keemobile.kotpass.database.encode +import com.ivanovsky.passnotes.data.repository.file.databaseDsl.EntryEntity +import com.ivanovsky.passnotes.data.repository.file.databaseDsl.GroupEntity +import com.ivanovsky.passnotes.data.repository.file.databaseDsl.KotpassTreeDsl +import java.io.ByteArrayOutputStream + +class FakeFileContentFactory { + + fun createDefaultLocalDatabase(): ByteArray { + return KotpassTreeDsl.tree(ROOT) { + group(GROUP_EMAIL) + group(GROUP_INTERNET) { + group(GROUP_CODING) { + entry(ENTRY_LEETCODE) + entry(ENTRY_NEETCODE) + entry(ENTRY_GITHUB) + } + group(GROUP_GAMING) { + entry(ENTRY_STADIA) + } + group(GROUP_SHOPPING) + group(GROUP_SOCIAL) + + entry(ENTRY_GOOGLE) + entry(ENTRY_APPLE) + entry(ENTRY_MICROSOFT) + } + entry(ENTRY_NAS_LOGIN) + entry(ENTRY_LAPTOP_LOGIN) + entry(ENTRY_LOCAL) + } + .toByteArray() + } + + fun createDefaultRemoteDatabase(): ByteArray { + return KotpassTreeDsl.tree(ROOT) { + group(GROUP_EMAIL) + group(GROUP_INTERNET) { + group(GROUP_CODING) { + entry(ENTRY_LEETCODE) + entry(ENTRY_NEETCODE) + entry(ENTRY_GITLAB) + } + group(GROUP_GAMING) { + entry(ENTRY_STADIA) + } + group(GROUP_SHOPPING) + group(GROUP_SOCIAL) + + entry(ENTRY_GOOGLE) + entry(ENTRY_APPLE) + entry(ENTRY_MICROSOFT) + } + entry(ENTRY_NAS_LOGIN) + entry(ENTRY_LAPTOP_LOGIN) + entry(ENTRY_MAC_BOOK_LOGIN) + entry(ENTRY_REMOTE) + } + .toByteArray() + } + + private fun KeePassDatabase.toByteArray(): ByteArray { + return ByteArrayOutputStream().use { out -> + this.encode(out) + out.toByteArray() + } + } + + companion object { + private val ROOT = GroupEntity(title = "Database") + private val GROUP_INTERNET = GroupEntity(title = "Internet") + private val GROUP_EMAIL = GroupEntity(title = "Email") + + private val GROUP_CODING = GroupEntity(title = "Coding") + private val GROUP_GAMING = GroupEntity(title = "Gaming") + private val GROUP_SHOPPING = GroupEntity(title = "Shopping") + private val GROUP_SOCIAL = GroupEntity(title = "Social") + + private val ENTRY_LOCAL = EntryEntity( + title = "Local", + username = "john.doe", + password = "abc123", + created = parseDate("2020-01-01"), + modified = parseDate("2020-01-01") + ) + + private val ENTRY_REMOTE = EntryEntity( + title = "Remote", + username = "john.doe", + password = "abc123", + created = parseDate("2020-01-01"), + modified = parseDate("2020-01-01") + ) + + private val ENTRY_NAS_LOGIN = EntryEntity( + title = "My NAS Login", + username = "john.doe", + password = "abc123", + created = parseDate("2020-01-01"), + modified = parseDate("2020-01-01") + ) + + private val ENTRY_LAPTOP_LOGIN = EntryEntity( + title = "My Laptop Login", + username = "john.doe", + password = "abc123", + created = parseDate("2020-01-02"), + modified = parseDate("2020-01-02") + ) + + private val ENTRY_MAC_BOOK_LOGIN = EntryEntity( + title = "My Mac Book Login", + username = "john.doe", + password = "abc123", + created = parseDate("2020-02-01"), + modified = parseDate("2020-02-01") + ) + + private val ENTRY_GOOGLE = EntryEntity( + title = "My Google Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://google.com", + created = parseDate("2020-01-03"), + modified = parseDate("2020-01-03") + ) + + private val ENTRY_APPLE = EntryEntity( + title = "My Apple Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://apple.com", + created = parseDate("2020-01-04"), + modified = parseDate("2020-01-04") + ) + + private val ENTRY_MICROSOFT = EntryEntity( + title = "My Microsoft Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://microsoft.com", + created = parseDate("2020-01-05"), + modified = parseDate("2020-01-05") + ) + + private val ENTRY_LEETCODE = EntryEntity( + title = "My LeetCode Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://leetcode.com", + created = parseDate("2020-01-06"), + modified = parseDate("2020-01-06") + ) + + private val ENTRY_NEETCODE = EntryEntity( + title = "My NeetCode Login", + username = "john.doe@example.com", + url = "https://neetcode.io/practice", + created = parseDate("2020-01-07"), + modified = parseDate("2020-01-07") + ) + + private val ENTRY_GITHUB = EntryEntity( + title = "My GitHub Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://github.com", + created = parseDate("2020-01-08"), + modified = parseDate("2020-01-08") + ) + + private val ENTRY_GITLAB = EntryEntity( + title = "My GitLab Login", + username = "john.doe@example.com", + password = "abc123", + url = "https://gitlab.com", + created = parseDate("2020-01-08"), + modified = parseDate("2020-01-08") + ) + + private val ENTRY_STADIA = EntryEntity( + title = "My Stadia Login", + username = "john.doe@example.com", + password = "abc123", + created = parseDate("2020-01-09"), + modified = parseDate("2020-01-09") + ) + } +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileFactory.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileFactory.kt index aaaee805..add0f90b 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileFactory.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileFactory.kt @@ -3,8 +3,6 @@ package com.ivanovsky.passnotes.data.repository.file import com.ivanovsky.passnotes.data.entity.FSAuthority import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.util.FileUtils -import java.text.SimpleDateFormat -import java.util.Locale class FakeFileFactory( private val fsAuthority: FSAuthority @@ -50,10 +48,18 @@ class FakeFileFactory( return create(fsAuthority, FileUid.NOT_FOUND, Time.LOCAL) } + fun createAutoTestsFile(): FileDescriptor { + return create(fsAuthority, FileUid.AUTO_TESTS, Time.NO_CHANGES) + } + + fun createNewFromUid(uid: String): FileDescriptor { + return create(fsAuthority, uid, System.currentTimeMillis()) + } + private fun create( fsAuthority: FSAuthority, uid: String, - modified: Long = System.currentTimeMillis() + modified: Long ): FileDescriptor { val path = pathFromUid(uid) @@ -69,9 +75,11 @@ class FakeFileFactory( } private fun pathFromUid(uid: String): String { - return when (uid) { - FileUid.ROOT -> "/" - else -> "/test-$uid.kdbx" + return when { + uid == FileUid.ROOT -> "/" + uid == FileUid.AUTO_TESTS -> "/automation.kdbx" + uid in FileUid.DEFAULT_UIDS -> "/test-$uid.kdbx" + else -> uid } } @@ -85,19 +93,26 @@ class FakeFileFactory( const val AUTH_ERROR = "auth-error" const val NOT_FOUND = "not-found" const val ERROR = "error" + const val AUTO_TESTS = "auto-tests" + + val DEFAULT_UIDS = listOf( + NO_CHANGES, + ROOT, + REMOTE_CHANGES, + LOCAL_CHANGES, + LOCAL_CHANGES_TIMEOUT, + CONFLICT, + AUTH_ERROR, + NOT_FOUND, + ERROR, + AUTO_TESTS + ) } private object Time { - - private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US) - val ROOT = parseDate("2020-01-01") val NO_CHANGES = parseDate("2020-02-01") val LOCAL = parseDate("2020-03-01") val REMOTE = parseDate("2020-03-02") - - private fun parseDate(str: String): Long { - return DATE_FORMAT.parse(str)?.time ?: throw IllegalArgumentException() - } } } \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileOutputStream.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileOutputStream.kt new file mode 100644 index 00000000..6cf89752 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileOutputStream.kt @@ -0,0 +1,54 @@ +package com.ivanovsky.passnotes.data.repository.file + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import timber.log.Timber + +class FakeFileOutputStream( + private val onFinished: (bytes: ByteArray) -> Unit +) : OutputStream() { + + private val out = ByteArrayOutputStream() + private var isFailed = false + private var isClosed = false + + override fun write(b: Int) { + throwIfInvalidState() + + try { + out.write(b) + } catch (exception: IOException) { + Timber.d(exception) + isFailed = true + throw IOException(exception) + } + } + + override fun flush() { + throwIfInvalidState() + } + + override fun close() { + if (isClosed || isFailed) { + return + } + + val bytes = try { + out.toByteArray() + } catch (exception: IOException) { + Timber.d(exception) + isFailed = true + throw IOException(exception) + } + + onFinished.invoke(bytes) + } + + private fun throwIfInvalidState() { + when { + isFailed -> throw IOException("Invalid state: failed") + isClosed -> throw IOException("Invalid state: closed") + } + } +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileStorage.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileStorage.kt new file mode 100644 index 00000000..04c3dd43 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileStorage.kt @@ -0,0 +1,169 @@ +package com.ivanovsky.passnotes.data.repository.file + +import com.ivanovsky.passnotes.data.entity.FSAuthority +import com.ivanovsky.passnotes.data.entity.FileDescriptor +import com.ivanovsky.passnotes.data.entity.SyncStatus +import com.ivanovsky.passnotes.data.repository.file.FakeFileFactory.FileUid +import com.ivanovsky.passnotes.data.repository.file.entity.StorageDestinationType +import com.ivanovsky.passnotes.data.repository.file.entity.StorageDestinationType.LOCAL +import com.ivanovsky.passnotes.data.repository.file.entity.StorageDestinationType.REMOTE +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import timber.log.Timber + +class FakeFileStorage( + fsAuthority: FSAuthority, + private val defaultStatuses: Map +) { + + private val fileContentFactory = FakeFileContentFactory() + private val fileFactory = FakeFileFactory(fsAuthority) + private val localContent = ConcurrentHashMap() + private val remoteContent = ConcurrentHashMap() + private val statuses = ConcurrentHashMap() + .apply { + putAll(defaultStatuses) + } + private val files = CopyOnWriteArrayList() + .apply { + addAll(createDefaultFiles()) + } + + fun getSyncStatus(uid: String): SyncStatus { + return statuses[uid] ?: SyncStatus.NO_CHANGES + } + + fun putSyncStatus(uid: String, status: SyncStatus) { + statuses[uid] = status + } + + fun put( + uid: String, + destination: StorageDestinationType, + content: ByteArray + ) { + when (destination) { + LOCAL -> localContent[uid] = content + REMOTE -> remoteContent[uid] = content + } + } + + fun get( + uid: String, + destination: StorageDestinationType + ): ByteArray? { + generateAndStoreContentIfNeed(uid) + + return when (destination) { + LOCAL -> localContent[uid] + REMOTE -> remoteContent[uid] + } + } + + fun get( + uid: String, + fsOptions: FSOptions + ): ByteArray? { + val destination = determineDestination(uid, fsOptions) + Timber.d("Get content: uid=$uid, fsOptions=$fsOptions, destination=$destination") + + generateAndStoreContentIfNeed(uid) + + val contentMap = when (destination) { + LOCAL -> localContent + REMOTE -> remoteContent + } + + return contentMap[uid] + } + + fun getFileByUid(uid: String): FileDescriptor? { + return files.firstOrNull { file -> file.uid == uid } + } + + fun getFileByPath(path: String): FileDescriptor? { + return files.firstOrNull { file -> file.path == path } + } + + fun getFiles(): List { + return files + } + + private fun determineDestination( + uid: String, + fsOptions: FSOptions + ): StorageDestinationType { + return if (!fsOptions.isCacheEnabled) { + REMOTE + } else { + LOCAL + } + } + + private fun generateAndStoreContentIfNeed( + uid: String + ) { + if (localContent.containsKey(uid) && remoteContent.containsKey(uid)) { + return + } + + when (uid) { + FileUid.CONFLICT -> { + val local = fileContentFactory.createDefaultLocalDatabase() + val remote = fileContentFactory.createDefaultRemoteDatabase() + + Timber.d( + "Generate content: uid=%s, local.size=%s, remote=%s", + uid, + local.size, + remote.size + ) + + localContent[uid] = local + remoteContent[uid] = remote + } + + else -> { + val content = fileContentFactory.createDefaultLocalDatabase() + + Timber.d("Generate content: uid=$uid, size=${content.size}") + + localContent[uid] = content + remoteContent[uid] = content + + createFileIfNeed(uid) + } + } + } + + private fun createFileIfNeed(uid: String) { + val isExist = files.any { file -> file.uid == uid } + + Timber.d( + "createFileIfNeed: isExist=%s, uid=%s", + isExist, + uid + ) + + if (isExist) { + return + } + + val file = fileFactory.createNewFromUid(uid) + files.add(file) + } + + private fun createDefaultFiles(): List { + return listOf( + fileFactory.createNoChangesFile(), + fileFactory.createRemoteChangesFile(), + fileFactory.createLocalChangesFile(), + fileFactory.createLocalChangesTimeoutFile(), + fileFactory.createConflictLocalFile(), + fileFactory.createAuthErrorFile(), + fileFactory.createNotFoundFile(), + fileFactory.createErrorFile(), + fileFactory.createAutoTestsFile() + ) + } +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemProvider.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemProvider.kt index 1991d231..a528bfa8 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemProvider.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemProvider.kt @@ -1,19 +1,19 @@ package com.ivanovsky.passnotes.data.repository.file -import android.content.Context import com.ivanovsky.passnotes.data.ObserverBus import com.ivanovsky.passnotes.data.entity.FSAuthority import com.ivanovsky.passnotes.data.entity.FSCredentials -import com.ivanovsky.passnotes.data.entity.FSType import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.data.entity.OperationError import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_FAILED_TO_ACCESS_TO_FILE import com.ivanovsky.passnotes.data.entity.OperationError.newFileAccessError import com.ivanovsky.passnotes.data.entity.OperationError.newFileNotFoundError import com.ivanovsky.passnotes.data.entity.OperationResult +import com.ivanovsky.passnotes.data.entity.SyncStatus +import com.ivanovsky.passnotes.data.repository.file.FakeFileFactory.FileUid import com.ivanovsky.passnotes.data.repository.file.delay.ThreadThrottler -import com.ivanovsky.passnotes.data.repository.file.regular.RegularFileSystemProvider -import com.ivanovsky.passnotes.extensions.map +import com.ivanovsky.passnotes.data.repository.file.entity.StorageDestinationType +import java.io.ByteArrayInputStream import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -21,36 +21,34 @@ import java.io.OutputStream import timber.log.Timber class FakeFileSystemProvider( - private val context: Context, throttler: ThreadThrottler, observerBus: ObserverBus, fsAuthority: FSAuthority ) : FileSystemProvider { - private val provider = RegularFileSystemProvider( - context, - FSAuthority( - credentials = null, - type = FSType.EXTERNAL_STORAGE, - isBrowsable = true + private val storage = FakeFileStorage( + fsAuthority = fsAuthority, + defaultStatuses = mapOf( + FileUid.CONFLICT to SyncStatus.CONFLICT, + FileUid.REMOTE_CHANGES to SyncStatus.REMOTE_CHANGES, + FileUid.LOCAL_CHANGES to SyncStatus.LOCAL_CHANGES, + FileUid.LOCAL_CHANGES_TIMEOUT to SyncStatus.LOCAL_CHANGES, + FileUid.ERROR to SyncStatus.ERROR, + FileUid.AUTH_ERROR to SyncStatus.AUTH_ERROR, + FileUid.NOT_FOUND to SyncStatus.FILE_NOT_FOUND ) ) - private val fileFactory = FakeFileFactory(fsAuthority) - private val authenticator = FakeFileSystemAuthenticator(fsAuthority) - private val syncProcessor = FakeFileSystemSyncProcessor(observerBus, throttler, fsAuthority) - - private val allFiles = listOf( - fileFactory.createNoChangesFile(), - fileFactory.createRemoteChangesFile(), - fileFactory.createLocalChangesFile(), - fileFactory.createLocalChangesTimeoutFile(), - fileFactory.createConflictLocalFile(), - fileFactory.createAuthErrorFile(), - fileFactory.createNotFoundFile(), - fileFactory.createErrorFile() + private val syncProcessor = FakeFileSystemSyncProcessor( + storage = storage, + observerBus = observerBus, + throttler = throttler, + fsAuthority = fsAuthority ) + private val authenticator = FakeFileSystemAuthenticator(fsAuthority) + private val fileFactory = FakeFileFactory(fsAuthority) + override fun getAuthenticator(): FileSystemAuthenticator { return authenticator } @@ -68,7 +66,10 @@ class FakeFileSystemProvider( return OperationResult.error(newFileAccessError(MESSAGE_FAILED_TO_ACCESS_TO_FILE)) } - return OperationResult.success(allFiles) + val files = storage.getFiles() + .map { file -> file.substituteFsAuthority() } + + return OperationResult.success(files) } override fun getParent(file: FileDescriptor): OperationResult { @@ -76,8 +77,7 @@ class FakeFileSystemProvider( return newAuthError() } - return provider.getParent(file) - .map { descriptor -> descriptor.substituteFsAuthority() } + return rootFile } override fun getRootFile(): OperationResult { @@ -85,7 +85,8 @@ class FakeFileSystemProvider( return newAuthError() } - return OperationResult.success(fileFactory.createRootFile()) + val root = fileFactory.createRootFile().substituteFsAuthority() + return OperationResult.success(root) } override fun openFileForRead( @@ -93,12 +94,22 @@ class FakeFileSystemProvider( onConflictStrategy: OnConflictStrategy, options: FSOptions ): OperationResult { + Timber.d( + "openFileForRead: uid=%s, options=%s, isAuthenticated=%s", + file.uid, + options, + isAuthenticated() + ) + if (!isAuthenticated()) { return newAuthError() } + val content = storage.get(file.uid, options) + ?: return OperationResult.error(newFileNotFoundError()) + return try { - OperationResult.success(context.assets.open(DB_NAME)) + OperationResult.success(ByteArrayInputStream(content)) } catch (exception: FileNotFoundException) { Timber.w(exception) OperationResult.error(newFileNotFoundError()) @@ -113,11 +124,24 @@ class FakeFileSystemProvider( onConflictStrategy: OnConflictStrategy, options: FSOptions ): OperationResult { + Timber.d( + "openFileForWrite: uid=%s, options=%s, isAuthenticated=%s", + file.uid, + options, + isAuthenticated() + ) + if (!isAuthenticated()) { return newAuthError() } - return provider.openFileForWrite(file, onConflictStrategy, options) + val stream = FakeFileOutputStream( + onFinished = { bytes -> + storage.put(file.uid, StorageDestinationType.LOCAL, bytes) + } + ) + + return OperationResult.success(stream) } override fun exists(file: FileDescriptor): OperationResult { @@ -125,7 +149,7 @@ class FakeFileSystemProvider( return newAuthError() } - val isExist = allFiles.any { it.uid == file.uid } + val isExist = (storage.getFileByPath(file.path) != null) return OperationResult.success(isExist) } @@ -134,7 +158,8 @@ class FakeFileSystemProvider( return newAuthError() } - val file = allFiles.find { it.path == path } + val file = storage.getFileByPath(path) + ?.substituteFsAuthority() return if (file != null) { OperationResult.success(file) @@ -163,6 +188,5 @@ class FakeFileSystemProvider( private const val SERVER_URL = "test://server.com" private const val USERNAME = "user" private const val PASSWORD = "abc123" - private const val DB_NAME = "fake-fs-database.kdbx" } } \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemSyncProcessor.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemSyncProcessor.kt index d042a4d1..d796b891 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemSyncProcessor.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/FakeFileSystemSyncProcessor.kt @@ -5,6 +5,7 @@ import com.ivanovsky.passnotes.data.entity.ConflictResolutionStrategy import com.ivanovsky.passnotes.data.entity.FSAuthority import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_INCORRECT_SYNC_STATUS +import com.ivanovsky.passnotes.data.entity.OperationError.newFileNotFoundError import com.ivanovsky.passnotes.data.entity.OperationError.newGenericError import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.data.entity.SyncConflictInfo @@ -15,16 +16,16 @@ import com.ivanovsky.passnotes.data.repository.file.delay.ThreadThrottler import com.ivanovsky.passnotes.data.repository.file.delay.ThreadThrottler.Type.LONG_DELAY import com.ivanovsky.passnotes.data.repository.file.delay.ThreadThrottler.Type.MEDIUM_DELAY import com.ivanovsky.passnotes.data.repository.file.delay.ThreadThrottler.Type.SHORT_DELAY +import com.ivanovsky.passnotes.data.repository.file.entity.StorageDestinationType class FakeFileSystemSyncProcessor( + private val storage: FakeFileStorage, private val observerBus: ObserverBus, private val throttler: ThreadThrottler, private val fsAuthority: FSAuthority ) : FileSystemSyncProcessor { private val fileFactory = FakeFileFactory(fsAuthority) - - private val statuses = mutableMapOf() private val uidToSyncProgressStatusMap = mutableMapOf() override fun getCachedFile(uid: String): FileDescriptor? { @@ -42,21 +43,7 @@ class FakeFileSystemSyncProcessor( override fun getSyncStatusForFile(uid: String): SyncStatus { throttler.delay(SHORT_DELAY) - val status = statuses[uid] - if (status != null) { - return status - } - - return when (uid) { - FileUid.CONFLICT -> SyncStatus.CONFLICT - FileUid.REMOTE_CHANGES -> SyncStatus.REMOTE_CHANGES - FileUid.LOCAL_CHANGES -> SyncStatus.LOCAL_CHANGES - FileUid.LOCAL_CHANGES_TIMEOUT -> SyncStatus.LOCAL_CHANGES - FileUid.ERROR -> SyncStatus.ERROR - FileUid.AUTH_ERROR -> SyncStatus.AUTH_ERROR - FileUid.NOT_FOUND -> SyncStatus.FILE_NOT_FOUND - else -> SyncStatus.NO_CHANGES - } + return storage.getSyncStatus(uid) } override fun getSyncConflictForFile(uid: String): OperationResult { @@ -77,71 +64,83 @@ class FakeFileSystemSyncProcessor( syncStrategy: SyncStrategy, resolutionStrategy: ConflictResolutionStrategy? ): OperationResult { - return when (file.uid) { - FileUid.LOCAL_CHANGES, FileUid.LOCAL_CHANGES_TIMEOUT -> { - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING - notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) - throttler.delay(MEDIUM_DELAY) - - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.UPLOADING - notifySyncProgressChanges(file.uid, SyncProgressStatus.UPLOADING) - if (file.uid == FileUid.LOCAL_CHANGES_TIMEOUT) { - throttler.delay(50000L) - } else { - throttler.delay(LONG_DELAY) + return file.let { file -> + when (file.uid) { + FileUid.LOCAL_CHANGES, FileUid.LOCAL_CHANGES_TIMEOUT -> { + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING + notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) + throttler.delay(MEDIUM_DELAY) + + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.UPLOADING + notifySyncProgressChanges(file.uid, SyncProgressStatus.UPLOADING) + if (file.uid == FileUid.LOCAL_CHANGES_TIMEOUT) { + throttler.delay(50000L) + } else { + throttler.delay(LONG_DELAY) + } + + uidToSyncProgressStatusMap.remove(file.uid) + storage.putSyncStatus(file.uid, SyncStatus.NO_CHANGES) + notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) + + return@let OperationResult.success(file) } - uidToSyncProgressStatusMap.remove(file.uid) - statuses[file.uid] = SyncStatus.NO_CHANGES - notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) + FileUid.REMOTE_CHANGES -> { + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING + notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) + throttler.delay(MEDIUM_DELAY) - OperationResult.success(file) - } + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.DOWNLOADING + notifySyncProgressChanges(file.uid, SyncProgressStatus.DOWNLOADING) + throttler.delay(LONG_DELAY) - FileUid.REMOTE_CHANGES -> { - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING - notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) - throttler.delay(MEDIUM_DELAY) + uidToSyncProgressStatusMap.remove(file.uid) + storage.putSyncStatus(file.uid, SyncStatus.NO_CHANGES) + notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.DOWNLOADING - notifySyncProgressChanges(file.uid, SyncProgressStatus.DOWNLOADING) - throttler.delay(LONG_DELAY) + return@let OperationResult.success(file) + } - uidToSyncProgressStatusMap.remove(file.uid) - statuses[file.uid] = SyncStatus.NO_CHANGES - notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) + FileUid.CONFLICT -> { + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING + notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) + throttler.delay(MEDIUM_DELAY) - OperationResult.success(file) - } + when (resolutionStrategy) { + ConflictResolutionStrategy.RESOLVE_WITH_LOCAL_FILE -> { + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.UPLOADING + notifySyncProgressChanges(file.uid, SyncProgressStatus.UPLOADING) + throttler.delay(LONG_DELAY) - FileUid.CONFLICT -> { - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.SYNCING - notifySyncProgressChanges(file.uid, SyncProgressStatus.SYNCING) - throttler.delay(MEDIUM_DELAY) + val content = storage.get(file.uid, StorageDestinationType.LOCAL) + ?: return@let OperationResult.error(newFileNotFoundError()) - when (resolutionStrategy) { - ConflictResolutionStrategy.RESOLVE_WITH_LOCAL_FILE -> { - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.UPLOADING - notifySyncProgressChanges(file.uid, SyncProgressStatus.UPLOADING) - throttler.delay(LONG_DELAY) - } + storage.put(file.uid, StorageDestinationType.REMOTE, content) + } - else -> { - uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.DOWNLOADING - notifySyncProgressChanges(file.uid, SyncProgressStatus.DOWNLOADING) - throttler.delay(LONG_DELAY) + else -> { + uidToSyncProgressStatusMap[file.uid] = SyncProgressStatus.DOWNLOADING + notifySyncProgressChanges(file.uid, SyncProgressStatus.DOWNLOADING) + throttler.delay(LONG_DELAY) + + val content = storage.get(file.uid, StorageDestinationType.REMOTE) + ?: return@let OperationResult.error(newFileNotFoundError()) + + storage.put(file.uid, StorageDestinationType.LOCAL, content) + } } - } - uidToSyncProgressStatusMap.remove(file.uid) - statuses[file.uid] = SyncStatus.NO_CHANGES - notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) + uidToSyncProgressStatusMap.remove(file.uid) + storage.putSyncStatus(file.uid, SyncStatus.NO_CHANGES) + notifySyncProgressChanges(file.uid, SyncProgressStatus.IDLE) - OperationResult.success(file) - } + return@let OperationResult.success(file) + } - else -> { - OperationResult.error(newGenericError(MESSAGE_INCORRECT_SYNC_STATUS)) + else -> { + return@let OperationResult.error(newGenericError(MESSAGE_INCORRECT_SYNC_STATUS)) + } } } } diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/EntryEntity.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/EntryEntity.kt new file mode 100644 index 00000000..972e564a --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/EntryEntity.kt @@ -0,0 +1,16 @@ +package com.ivanovsky.passnotes.data.repository.file.databaseDsl + +import com.ivanovsky.passnotes.util.StringUtils.EMPTY +import java.util.UUID + +data class EntryEntity( + val title: String, + val uuid: UUID = UUID(1000L, title.hashCode().toLong()), + val username: String = EMPTY, + val password: String = EMPTY, + val url: String = EMPTY, + val notes: String = EMPTY, + val created: Long = System.currentTimeMillis(), + val modified: Long = System.currentTimeMillis(), + val custom: Map = emptyMap() +) \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/GroupEntity.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/GroupEntity.kt new file mode 100644 index 00000000..5b109559 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/GroupEntity.kt @@ -0,0 +1,8 @@ +package com.ivanovsky.passnotes.data.repository.file.databaseDsl + +import java.util.UUID + +data class GroupEntity( + val title: String, + val uuid: UUID = UUID(1L, title.hashCode().toLong()) +) \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/KotpassTreeDsl.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/KotpassTreeDsl.kt new file mode 100644 index 00000000..dbc9f94e --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/databaseDsl/KotpassTreeDsl.kt @@ -0,0 +1,119 @@ +package com.ivanovsky.passnotes.data.repository.file.databaseDsl + +import app.keemobile.kotpass.constants.BasicField +import app.keemobile.kotpass.cryptography.EncryptedValue +import app.keemobile.kotpass.database.Credentials +import app.keemobile.kotpass.database.KeePassDatabase +import app.keemobile.kotpass.database.modifiers.modifyGroup +import app.keemobile.kotpass.models.DatabaseElement +import app.keemobile.kotpass.models.Entry +import app.keemobile.kotpass.models.EntryFields +import app.keemobile.kotpass.models.EntryValue +import app.keemobile.kotpass.models.Group +import app.keemobile.kotpass.models.Meta +import app.keemobile.kotpass.models.TimeData +import java.time.Instant +import java.util.UUID + +object KotpassTreeDsl { + + private const val DEFAULT_PASSWORD = "abc123" + + fun tree( + root: GroupEntity, + content: (DatabaseElementBuilder.() -> Unit)? = null + ): KeePassDatabase { + val rootGroup = DatabaseElementBuilder( + uuid = root.uuid, + name = root.title + ) + .apply { + content?.invoke(this) + } + .build() as Group + + val db = KeePassDatabase.Ver4x.create( + rootName = root.title, + meta = Meta(recycleBinEnabled = true), + credentials = Credentials.from(DEFAULT_PASSWORD.toByteArray()) + ) + + val realRootUuid = db.content.group.uuid + + val newDb = db.modifyGroup(realRootUuid) { + this.copy( + uuid = rootGroup.uuid, + groups = rootGroup.groups, + entries = rootGroup.entries + ) + } + + return newDb + } + + class DatabaseElementBuilder( + private val uuid: UUID, + private val name: String + ) { + + private val groups = mutableListOf() + private val entries = mutableListOf() + + fun group( + entity: GroupEntity, + content: (DatabaseElementBuilder.() -> Unit)? = null + ) { + val group = DatabaseElementBuilder( + uuid = entity.uuid, + name = entity.title + ) + .apply { + content?.invoke(this) + } + .build() as Group + + groups.add(group) + } + + fun entry( + entity: EntryEntity + ) { + val fields = mutableMapOf( + BasicField.Title.key to EntryValue.Plain(entity.title), + BasicField.UserName.key to EntryValue.Plain(entity.username), + BasicField.Password.key to EntryValue.Encrypted( + EncryptedValue.fromString(entity.password) + ), + BasicField.Url.key to EntryValue.Plain(entity.url), + BasicField.Notes.key to EntryValue.Plain(entity.notes) + ) + + for (customField in entity.custom.entries) { + fields[customField.key] = EntryValue.Plain(customField.value) + } + + entries.add( + Entry( + uuid = entity.uuid, + fields = EntryFields(fields), + times = TimeData( + creationTime = Instant.ofEpochMilli(entity.created), + lastModificationTime = Instant.ofEpochMilli(entity.modified), + lastAccessTime = null, + locationChanged = null, + expiryTime = null + ) + ) + ) + } + + fun build(): DatabaseElement { + return Group( + uuid = uuid, + name = name, + groups = groups, + entries = entries + ) + } + } +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/entity/StorageDestinationType.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/entity/StorageDestinationType.kt new file mode 100644 index 00000000..86c9b516 --- /dev/null +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/data/repository/file/entity/StorageDestinationType.kt @@ -0,0 +1,6 @@ +package com.ivanovsky.passnotes.data.repository.file.entity + +enum class StorageDestinationType { + LOCAL, + REMOTE +} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt index 39fb5152..a44ac8d9 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt @@ -29,7 +29,6 @@ class DebugModuleBuilder( CoreModule.build(loggerInteractor), if (isFakeFileSystemEnabled) { FakeFileSystemProvidersModule.build( - context = context, isExternalStorageAccessEnabled = isExternalStorageAccessEnabled ) } else { diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/modules/FakeFileSystemProvidersModule.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/modules/FakeFileSystemProvidersModule.kt index 70c31d2a..87bdecc2 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/modules/FakeFileSystemProvidersModule.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/modules/FakeFileSystemProvidersModule.kt @@ -1,6 +1,5 @@ package com.ivanovsky.passnotes.injection.modules -import android.content.Context import com.ivanovsky.passnotes.data.ObserverBus import com.ivanovsky.passnotes.data.entity.FSType import com.ivanovsky.passnotes.data.repository.file.FakeFileSystemProvider @@ -12,7 +11,6 @@ import org.koin.dsl.module object FakeFileSystemProvidersModule { fun build( - context: Context, isExternalStorageAccessEnabled: Boolean ) = module { val fsFactories = mapOf( @@ -20,7 +18,7 @@ object FakeFileSystemProvidersModule { val observerBus: ObserverBus = GlobalInjector.get() val throttler = ThreadThrottlerImpl() - FakeFileSystemProvider(context, throttler, observerBus, fsAuthority) + FakeFileSystemProvider(throttler, observerBus, fsAuthority) } ) diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/encdb/EncryptedDatabaseKey.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/encdb/EncryptedDatabaseKey.kt index ba0a5e27..9b71604b 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/encdb/EncryptedDatabaseKey.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/encdb/EncryptedDatabaseKey.kt @@ -1,9 +1,7 @@ package com.ivanovsky.passnotes.data.repository.encdb import com.ivanovsky.passnotes.data.entity.KeyType -import com.ivanovsky.passnotes.data.entity.OperationResult interface EncryptedDatabaseKey { val type: KeyType - fun getKey(): OperationResult } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/DefaultDatabaseKey.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/DefaultDatabaseKey.kt deleted file mode 100644 index 83157a74..00000000 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/DefaultDatabaseKey.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.ivanovsky.passnotes.data.repository.keepass - -import com.ivanovsky.passnotes.data.entity.KeyType -import com.ivanovsky.passnotes.data.entity.OperationResult -import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey - -class DefaultDatabaseKey : EncryptedDatabaseKey { - - override val type: KeyType - get() = throw IllegalStateException() - - override fun getKey(): OperationResult { - throw IllegalStateException() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/FileKeepassKey.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/FileKeepassKey.kt index 0c5311f5..514932bc 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/FileKeepassKey.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/FileKeepassKey.kt @@ -2,59 +2,12 @@ package com.ivanovsky.passnotes.data.repository.keepass import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.data.entity.KeyType -import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_FAILED_TO_READ_KEY_FILE -import com.ivanovsky.passnotes.data.entity.OperationError.newGenericIOError -import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey -import com.ivanovsky.passnotes.data.repository.file.FSOptions -import com.ivanovsky.passnotes.data.repository.file.FileSystemProvider -import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy -import java.lang.Exception -import timber.log.Timber -class FileKeepassKey( +data class FileKeepassKey( val file: FileDescriptor, - val password: String? = null, - private val fileSystemProvider: FileSystemProvider + val password: String? = null ) : EncryptedDatabaseKey { - override val type: KeyType - get() = KeyType.KEY_FILE - - override fun getKey(): OperationResult { - val inputResult = - fileSystemProvider.openFileForRead(file, OnConflictStrategy.CANCEL, FSOptions.READ_ONLY) - if (inputResult.isFailed) { - return inputResult.takeError() - } - - return try { - val input = inputResult.obj - val bytes = input.use { - input.readBytes() - } - OperationResult.success(bytes) - } catch (e: Exception) { - Timber.d(e) - OperationResult.error(newGenericIOError(MESSAGE_FAILED_TO_READ_KEY_FILE, e)) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as FileKeepassKey - - if (file != other.file) return false - if (password != other.password) return false - - return true - } - - override fun hashCode(): Int { - var result = file.hashCode() - result = 31 * result + (password?.hashCode() ?: 0) - return result - } + override val type: KeyType = KeyType.KEY_FILE } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/KeepassDatabaseRepository.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/KeepassDatabaseRepository.kt index 13a3bece..0879d4b2 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/KeepassDatabaseRepository.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/KeepassDatabaseRepository.kt @@ -10,7 +10,6 @@ import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabase import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey import com.ivanovsky.passnotes.data.repository.file.FSOptions import com.ivanovsky.passnotes.data.repository.file.FSOptions.Companion.defaultOptions -import com.ivanovsky.passnotes.data.repository.file.FileSystemProvider import com.ivanovsky.passnotes.data.repository.file.FileSystemResolver import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy import com.ivanovsky.passnotes.data.repository.keepass.kotpass.KotpassDatabase @@ -60,7 +59,7 @@ class KeepassDatabaseRepository( val openResult = openDatabase( type, - fsProvider, + fileSystemResolver, options, file, openFileResult, @@ -106,7 +105,7 @@ class KeepassDatabaseRepository( val openResult = openDatabase( type = type, - fsProvider = fsProvider, + fsResolver = fileSystemResolver, fsOptions = fsOptions, file = file, input = openFileResult, @@ -134,11 +133,9 @@ class KeepassDatabaseRepository( file: FileDescriptor, addTemplates: Boolean ): OperationResult { - val fsProvider = fileSystemResolver.resolveProvider(file.fsAuthority) - return lock.withLock { val dbResult = KotpassDatabase.new( - fsProvider = fsProvider, + fsResolver = fileSystemResolver, fsOptions = defaultOptions(), file = file, key = key, @@ -186,7 +183,7 @@ class KeepassDatabaseRepository( private fun openDatabase( type: KeepassImplementation, - fsProvider: FileSystemProvider, + fsResolver: FileSystemResolver, fsOptions: FSOptions, file: FileDescriptor, input: OperationResult, @@ -194,7 +191,7 @@ class KeepassDatabaseRepository( ): OperationResult { val openResult = when (type) { KeepassImplementation.KOTPASS -> KotpassDatabase.open( - fsProvider, + fsResolver, fsOptions, file, input, diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/PasswordKeepassKey.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/PasswordKeepassKey.kt index 20909fd4..8fe53ddc 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/PasswordKeepassKey.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/PasswordKeepassKey.kt @@ -1,17 +1,11 @@ package com.ivanovsky.passnotes.data.repository.keepass import com.ivanovsky.passnotes.data.entity.KeyType -import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey data class PasswordKeepassKey( val password: String ) : EncryptedDatabaseKey { - override val type: KeyType - get() = KeyType.PASSWORD - - override fun getKey(): OperationResult { - return OperationResult.success(password.toByteArray()) - } + override val type: KeyType = KeyType.PASSWORD } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/EncryptedDatabaseKeyExtensions.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/EncryptedDatabaseKeyExtensions.kt new file mode 100644 index 00000000..6d4c3111 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/EncryptedDatabaseKeyExtensions.kt @@ -0,0 +1,68 @@ +package com.ivanovsky.passnotes.data.repository.keepass.kotpass + +import app.keemobile.kotpass.cryptography.EncryptedValue +import app.keemobile.kotpass.database.Credentials +import com.ivanovsky.passnotes.data.entity.OperationError +import com.ivanovsky.passnotes.data.entity.OperationResult +import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey +import com.ivanovsky.passnotes.data.repository.file.FSOptions +import com.ivanovsky.passnotes.data.repository.file.FileSystemResolver +import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy +import com.ivanovsky.passnotes.data.repository.keepass.FileKeepassKey +import com.ivanovsky.passnotes.data.repository.keepass.PasswordKeepassKey +import java.lang.Exception +import timber.log.Timber + +fun EncryptedDatabaseKey.toCredentials( + fileSystemResolver: FileSystemResolver +): OperationResult { + return when (this) { + is PasswordKeepassKey -> { + val credentials = Credentials.from(password.toByteArray()) + OperationResult.success(credentials) + } + + is FileKeepassKey -> { + val provider = fileSystemResolver.resolveProvider(file.fsAuthority) + + val inputResult = provider.openFileForRead( + file, + OnConflictStrategy.CANCEL, + FSOptions.READ_ONLY + ) + if (inputResult.isFailed) { + return inputResult.takeError() + } + + try { + val input = inputResult.obj + val bytes = input.use { + input.readBytes() + } + + val credentials = if (password == null) { + Credentials.from( + EncryptedValue.fromBinary(bytes) + ) + } else { + Credentials.from( + passphrase = EncryptedValue.fromString(password), + keyData = bytes + ) + } + + OperationResult.success(credentials) + } catch (exception: Exception) { + Timber.d(exception) + OperationResult.error( + OperationError.newGenericIOError( + OperationError.MESSAGE_FAILED_TO_READ_KEY_FILE, + exception + ) + ) + } + } + + else -> throw IllegalStateException() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/KotpassDatabase.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/KotpassDatabase.kt index f80be1f1..ff95cecc 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/KotpassDatabase.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/keepass/kotpass/KotpassDatabase.kt @@ -1,7 +1,5 @@ package com.ivanovsky.passnotes.data.repository.keepass.kotpass -import app.keemobile.kotpass.cryptography.EncryptedValue -import app.keemobile.kotpass.database.Credentials import app.keemobile.kotpass.database.KeePassDatabase import app.keemobile.kotpass.database.decode import app.keemobile.kotpass.database.encode @@ -29,10 +27,8 @@ import com.ivanovsky.passnotes.data.repository.encdb.MutableEncryptedDatabaseCon import com.ivanovsky.passnotes.data.repository.encdb.dao.GroupDao import com.ivanovsky.passnotes.data.repository.encdb.dao.NoteDao import com.ivanovsky.passnotes.data.repository.file.FSOptions -import com.ivanovsky.passnotes.data.repository.file.FileSystemProvider +import com.ivanovsky.passnotes.data.repository.file.FileSystemResolver import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy -import com.ivanovsky.passnotes.data.repository.keepass.FileKeepassKey -import com.ivanovsky.passnotes.data.repository.keepass.PasswordKeepassKey import com.ivanovsky.passnotes.data.repository.keepass.TemplateDaoImpl import com.ivanovsky.passnotes.data.repository.keepass.TemplateFactory import com.ivanovsky.passnotes.data.repository.keepass.kotpass.model.InheritableOptions @@ -49,7 +45,7 @@ import kotlin.concurrent.withLock import timber.log.Timber class KotpassDatabase( - private val fsProvider: FileSystemProvider, + private val fsResolver: FileSystemResolver, private val fsOptions: FSOptions, private val file: FileDescriptor, key: EncryptedDatabaseKey, @@ -65,6 +61,7 @@ class KotpassDatabase( private val noteDao = KotpassNoteDao(this) private val templateDao = TemplateDaoImpl(groupDao, noteDao) private val dbWatcher = DatabaseWatcher() + private val fsProvider = fsResolver.resolveProvider(file.fsAuthority) override fun getWatcher(): DatabaseWatcher = dbWatcher @@ -135,7 +132,7 @@ class KotpassDatabase( ) } - val getCredentialsResult = getCredentials(newKey) + val getCredentialsResult = newKey.toCredentials(fsResolver) if (getCredentialsResult.isFailed) { return getCredentialsResult.mapError() } @@ -392,13 +389,13 @@ class KotpassDatabase( private const val DEFAULT_ROOT_GROUP_NAME = "Database" fun new( - fsProvider: FileSystemProvider, + fsResolver: FileSystemResolver, fsOptions: FSOptions, file: FileDescriptor, key: EncryptedDatabaseKey, isAddTemplates: Boolean ): OperationResult { - val getCredentialsResult = getCredentials(key) + val getCredentialsResult = key.toCredentials(fsResolver) if (getCredentialsResult.isFailed) { return getCredentialsResult.mapError() } @@ -414,7 +411,7 @@ class KotpassDatabase( ) val db = KotpassDatabase( - fsProvider = fsProvider, + fsResolver = fsResolver, fsOptions = fsOptions, file = file, key = key, @@ -442,7 +439,7 @@ class KotpassDatabase( } fun open( - fsProvider: FileSystemProvider, + fsResolver: FileSystemResolver, fsOptions: FSOptions, file: FileDescriptor, content: OperationResult, @@ -453,7 +450,7 @@ class KotpassDatabase( } val contentStream = content.obj - val getCredentialsResult = getCredentials(key) + val getCredentialsResult = key.toCredentials(fsResolver) if (getCredentialsResult.isFailed) { return getCredentialsResult.mapError() } @@ -465,7 +462,7 @@ class KotpassDatabase( return OperationResult.success( KotpassDatabase( - fsProvider = fsProvider, + fsResolver = fsResolver, fsOptions = fsOptions, file = file, key = key, @@ -490,39 +487,5 @@ class KotpassDatabase( InputOutputUtils.close(contentStream) } } - - private fun getCredentials( - key: EncryptedDatabaseKey - ): OperationResult { - return when (key) { - is PasswordKeepassKey -> { - val credentials = Credentials.from(key.getKey().obj) - OperationResult.success(credentials) - } - - is FileKeepassKey -> { - val getBytesResult = key.getKey() - if (getBytesResult.isFailed) { - return getBytesResult.mapError() - } - - val bytes = getBytesResult.obj - val credentials = if (key.password == null) { - Credentials.from( - EncryptedValue.fromBinary(bytes) - ) - } else { - Credentials.from( - passphrase = EncryptedValue.fromString(key.password), - keyData = bytes - ) - } - - OperationResult.success(credentials) - } - - else -> throw IllegalArgumentException() - } - } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/newdb/NewDatabaseViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/newdb/NewDatabaseViewModel.kt index 9b64414b..215c068c 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/newdb/NewDatabaseViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/newdb/NewDatabaseViewModel.kt @@ -22,6 +22,7 @@ import com.ivanovsky.passnotes.presentation.groups.GroupsScreenArgs import com.ivanovsky.passnotes.presentation.storagelist.Action import com.ivanovsky.passnotes.presentation.storagelist.StorageListArgs import com.ivanovsky.passnotes.util.FileUtils +import com.ivanovsky.passnotes.util.FileUtils.createPath import com.ivanovsky.passnotes.util.FileUtils.removeFileExtensionsIfNeed import com.ivanovsky.passnotes.util.StringUtils.EMPTY import java.io.File @@ -185,7 +186,11 @@ class NewDatabaseViewModel( storageType.value = resourceProvider.getString(R.string.public_storage) filename.value = removeFileExtensionsIfNeed(selectedFile.name) } - FSType.UNDEFINED, FSType.FAKE, FSType.GIT -> {} + FSType.FAKE -> { + selectedStorage = SelectedStorage.ParentDir(selectedFile) + storageType.value = resourceProvider.getString(R.string.fake_file_system) + } + FSType.UNDEFINED, FSType.GIT -> {} // TODO: Implement file creation for GIT } storagePath.value = selectedFile.path @@ -198,7 +203,10 @@ class NewDatabaseViewModel( return when (selectedStorage) { is SelectedStorage.ParentDir -> { val name = this.filename.value ?: throw IllegalStateException() - val path = selectedStorage.dir.path + File.separator + name + ".kdbx" + val path = createPath( + parentPath = selectedStorage.dir.path, + name = "$name.kdbx" + ) FileDescriptor( fsAuthority = selectedStorage.dir.fsAuthority, diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt index 18acb914..477778a3 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt @@ -205,10 +205,7 @@ class UnlockViewModel( selectedKeyFile != null -> { FileKeepassKey( file = selectedKeyFile, - password = password.ifEmpty { null }, - fileSystemProvider = fileSystemResolver.resolveProvider( - selectedFile.fsAuthority - ) + password = password.ifEmpty { null } ) } else -> PasswordKeepassKey(password) diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/util/FileUtils.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/util/FileUtils.kt index 980a05c9..48afdc18 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/util/FileUtils.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/util/FileUtils.kt @@ -88,4 +88,13 @@ object FileUtils { val output: OutputStream = BufferedOutputStream(FileOutputStream(destination)) InputOutputUtils.copyOrThrow(input, output, true) } + + @JvmStatic + fun createPath(parentPath: String, name: String): String { + return if (parentPath.endsWith(SEPARATOR)) { + parentPath + name + } else { + parentPath + SEPARATOR + name + } + } } \ No newline at end of file