From 03d2e24cff63471bd6237b9d2f6a0d59b5b11c21 Mon Sep 17 00:00:00 2001 From: Himanshu Choudhary <22himanshu14@gmail.com> Date: Tue, 9 Apr 2024 04:33:11 +0530 Subject: [PATCH] Export to Bitwarden json - Added feature to export to bitwarden json - [IN-PROGRESS] Import from bitwarden json --- app/src/main/AndroidManifest.xml | 10 ++ .../keypass/exporter/AccountsExporter.kt | 14 +++ .../exporter/BitwardenAccountsExporter.kt | 98 +++++++++++++++++++ .../ui/exportPasswords/PasswordExporter.kt | 78 +++++++++++++++ .../ui/nav/DashboardComposeActivity.kt | 7 ++ .../ui/redux/states/ExportPasswordState.kt | 7 ++ .../keypass/ui/settings/MySettingsFragment.kt | 10 ++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/xml/provider_paths.xml | 3 + local.properties | 4 +- 10 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/exporter/AccountsExporter.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/exporter/BitwardenAccountsExporter.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/exportPasswords/PasswordExporter.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ExportPasswordState.kt create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a19320dc3..36b3a7f95 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,16 @@ android:enabled="false" android:exported="false" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/exporter/AccountsExporter.kt b/app/src/main/java/com/yogeshpaliyal/keypass/exporter/AccountsExporter.kt new file mode 100644 index 000000000..fd384bdd6 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/exporter/AccountsExporter.kt @@ -0,0 +1,14 @@ +package com.yogeshpaliyal.keypass.exporter + +import android.content.Context +import androidx.compose.runtime.Composable +import com.yogeshpaliyal.common.data.AccountModel +import java.io.File + +interface AccountsExporter { + fun getExporterTitle(): Int + + fun getExporterDesc(): Int + + fun export(context: Context, listOfAccounts: List) : File? +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/exporter/BitwardenAccountsExporter.kt b/app/src/main/java/com/yogeshpaliyal/keypass/exporter/BitwardenAccountsExporter.kt new file mode 100644 index 000000000..05efee196 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/exporter/BitwardenAccountsExporter.kt @@ -0,0 +1,98 @@ +package com.yogeshpaliyal.keypass.exporter + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.core.content.FileProvider +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.keypass.BuildConfig +import com.yogeshpaliyal.keypass.R +import org.json.JSONObject +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + + +class BitwardenAccountsExporter : AccountsExporter { + override fun getExporterTitle(): Int { + return R.string.bitwarden_export + } + + override fun getExporterDesc(): Int { + return R.string.bitwarden_export_desc + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun export(context: Context, listOfAccounts: List) : File { + val title = context.getString(getExporterTitle()).lowercase() + val path = context.filesDir + val letDirectory = File(path, "${title}_exports") + letDirectory.mkdirs() + + val currentTime = LocalDateTime.now() + val dateTime = currentTime.format(DateTimeFormatter.ofPattern("ddMMyyyyHHmmSS")) + + val exportFile: File = File(letDirectory, "${title}_export_${dateTime}.json") + + val uuid:UUID = UUID.randomUUID() + + var folderData : HashMap = HashMap() + folderData.put("id", uuid.toString()) + folderData.put("name", "KeyPass") + + var itemsData : MutableList> = mutableListOf() + listOfAccounts.forEach { + var accountData : HashMap = HashMap() + var loginData : HashMap = HashMap() + var uriData : HashMap = HashMap() + accountData.put("folderId", uuid.toString()) + accountData.put("type", 1) + accountData.put("organizationId", it.uniqueId) + accountData.put("id", it.id) + accountData.put("name", it.title) + accountData.put("notes", it.notes) + accountData.put("favourite", false) + accountData.put("fields", intArrayOf()) + + loginData.put("username", it.username) + loginData.put("password", it.password) + loginData.put("totp", it.secret) + + uriData.put("uri", it.site) + uriData.put("match", null) + + loginData.put("uris", uriData) + accountData.put("login", loginData) + itemsData.add(accountData) + } + + val exportData: HashMap = HashMap() + exportData.put("folders", listOf(folderData)) + exportData.put("items", itemsData) + + val jsonString: String = JSONObject(exportData).toString() + exportFile.writeText(jsonString) + + return exportFile + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/exportPasswords/PasswordExporter.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/exportPasswords/PasswordExporter.kt new file mode 100644 index 000000000..1bb8707a3 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/exportPasswords/PasswordExporter.kt @@ -0,0 +1,78 @@ +package com.yogeshpaliyal.keypass.ui.exportPasswords + +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.exporter.BitwardenAccountsExporter +import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar +import com.yogeshpaliyal.keypass.ui.commonComponents.PreferenceItem +import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel +import com.yogeshpaliyal.keypass.ui.redux.states.ExportPasswordState +import java.io.BufferedWriter +import java.io.OutputStream +import java.io.OutputStreamWriter + +val listOfExporters = listOf(BitwardenAccountsExporter()) + +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun PasswordExporter(state: ExportPasswordState, mViewModel: DashboardViewModel = hiltViewModel()) { + var fileText : String = "" + val context = LocalContext.current + val listOfAccountsLiveData by mViewModel.mediator.observeAsState() + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("text/plain"), + onResult = { + val outputStream : OutputStream? = context.contentResolver.openOutputStream(it!!) + val bufferedWriter : BufferedWriter = BufferedWriter(OutputStreamWriter(outputStream)) + bufferedWriter.write(fileText) + bufferedWriter.flush() + bufferedWriter.close() + } + ) + + Scaffold(bottomBar = { + DefaultBottomAppBar() + }) { + LazyColumn( + modifier = Modifier + .padding(it) + .fillMaxWidth(1f) + .padding(16.dp) + ) { + item { + PreferenceItem( + title = R.string.export_passwords_title, + isCategory = true, + removeIconSpace = true + ) + } + items(listOfExporters) { + PreferenceItem( + title = it.getExporterTitle(), + summary = it.getExporterDesc(), + removeIconSpace = true + ) { + val exportedFile = it.export(context, listOfAccountsLiveData!!.toList()) + fileText = exportedFile!!.readText() + launcher.launch(exportedFile!!.name) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt index 43f898e82..2827ba145 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt @@ -1,10 +1,12 @@ package com.yogeshpaliyal.keypass.ui.nav +import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -30,6 +32,7 @@ import com.yogeshpaliyal.keypass.ui.backupsImport.BackupImporter import com.yogeshpaliyal.keypass.ui.changeDefaultPasswordLength.ChangeDefaultPasswordLengthScreen import com.yogeshpaliyal.keypass.ui.changePassword.ChangePassword import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage +import com.yogeshpaliyal.keypass.ui.exportPasswords.PasswordExporter import com.yogeshpaliyal.keypass.ui.home.Homepage import com.yogeshpaliyal.keypass.ui.nav.components.DashboardBottomSheet import com.yogeshpaliyal.keypass.ui.nav.components.KeyPassBottomBar @@ -42,6 +45,7 @@ import com.yogeshpaliyal.keypass.ui.redux.states.BackupImporterState import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState import com.yogeshpaliyal.keypass.ui.redux.states.ChangeAppPasswordState import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthState +import com.yogeshpaliyal.keypass.ui.redux.states.ExportPasswordState import com.yogeshpaliyal.keypass.ui.redux.states.HomeState import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState @@ -131,6 +135,7 @@ fun Dashboard() { } } +@RequiresApi(Build.VERSION_CODES.O) @Composable fun CurrentPage() { val currentScreen by selectState { this.currentScreen } @@ -166,6 +171,8 @@ fun CurrentPage() { } is BackupImporterState -> BackupImporter(state = it) + + is ExportPasswordState -> PasswordExporter(state = it) } } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ExportPasswordState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ExportPasswordState.kt new file mode 100644 index 000000000..19a43c230 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ExportPasswordState.kt @@ -0,0 +1,7 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +import com.yogeshpaliyal.keypass.exporter.AccountsExporter + +data class ExportPasswordState( + var selectedExporter : AccountsExporter? = null +) : ScreenState() diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt index 739ef3481..cb4ec006d 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Feedback import androidx.compose.material.icons.rounded.Fingerprint +import androidx.compose.material.icons.rounded.ImportExport import androidx.compose.material.icons.rounded.Password import androidx.compose.material.icons.rounded.Share import androidx.compose.material3.Divider @@ -41,6 +42,7 @@ import com.yogeshpaliyal.keypass.ui.redux.states.BackupImporterState import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState import com.yogeshpaliyal.keypass.ui.redux.states.ChangeAppPasswordState import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthState +import com.yogeshpaliyal.keypass.ui.redux.states.ExportPasswordState import kotlinx.coroutines.launch import org.reduxkotlin.compose.rememberTypedDispatcher @@ -88,6 +90,14 @@ fun MySettingCompose() { dispatchAction(NavigationAction(ChangeDefaultPasswordLengthState())) } + PreferenceItem( + title = R.string.export_passwords, + summary = R.string.export_passwords_desc, + icon = Icons.Rounded.ImportExport + ) { + dispatchAction(NavigationAction(ExportPasswordState())) + } + BiometricsOption() Divider( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b99be6db6..31c6442dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ Backup Credentials to external storage Imports Credentials Restore your backups + Export Passwords + Export your passwords Send feedback Report technical issues or suggest new features Share @@ -89,6 +91,7 @@ Biometric features are currently unavailable. Setup biometric on your device. Unlock with biometric + Export Passwords Please set password for your device first from phone settings Authentication Failed Authentication Error %s @@ -99,4 +102,9 @@ Google Backup Export + Bitwarden + Bitwarden export description + Test + Test export description + diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 000000000..97a593408 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/local.properties b/local.properties index d944e25d7..aeb544602 100644 --- a/local.properties +++ b/local.properties @@ -4,5 +4,5 @@ # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. -#Tue May 09 21:09:59 IST 2023 -sdk.dir=/Users/yogesh.choudhary3/Library/Android/sdk +#Fri Mar 15 15:24:04 IST 2024 +sdk.dir=/Users/himanshu/Library/Android/sdk