Skip to content

Export to Bitwarden json #830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@
android:enabled="false"
android:exported="false" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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<AccountModel>) : File?
}
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow can we remove this and add support for older version as well

override fun export(context: Context, listOfAccounts: List<AccountModel>) : 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()
Comment on lines +47 to +57
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File creation logic can also be same for all the exporters.


var folderData : HashMap<String, Any?> = HashMap()
folderData.put("id", uuid.toString())
folderData.put("name", "KeyPass")

var itemsData : MutableList<HashMap<String, Any?>> = mutableListOf()
listOfAccounts.forEach {
var accountData : HashMap<String, Any?> = HashMap()
var loginData : HashMap<String, Any?> = HashMap()
var uriData : HashMap<String, Any?> = 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<String, Any?> = HashMap()
exportData.put("folders", listOf(folderData))
exportData.put("items", itemsData)

val jsonString: String = JSONObject(exportData).toString()
exportFile.writeText(jsonString)

return exportFile
}
}
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This as well

@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)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -131,6 +135,7 @@ fun Dashboard() {
}
}

@RequiresApi(Build.VERSION_CODES.O)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this as well

@Composable
fun CurrentPage() {
val currentScreen by selectState<KeyPassState, ScreenState> { this.currentScreen }
Expand Down Expand Up @@ -166,6 +171,8 @@ fun CurrentPage() {
}

is BackupImporterState -> BackupImporter(state = it)

is ExportPasswordState -> PasswordExporter(state = it)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<string name="credentials_backups_desc">Backup Credentials to external storage</string>
<string name="restore_credentials">Imports Credentials</string>
<string name="restore_credentials_desc">Restore your backups</string>
<string name="export_passwords_title">Export Passwords</string>
<string name="export_passwords_desc">Export your passwords</string>
<string name="send_feedback">Send feedback</string>
<string name="send_feedback_desc">Report technical issues or suggest new features</string>
<string name="share">Share</string>
Expand Down Expand Up @@ -89,6 +91,7 @@
<string name="biometric_error_hw_unavailable">Biometric features are currently unavailable.</string>
<string name="biometric_error_none_enrolled">Setup biometric on your device.</string>
<string name="unlock_with_biometric">Unlock with biometric</string>
<string name="export_passwords">Export Passwords</string>
<string name="password_set_from_settings">Please set password for your device first from phone settings</string>
<string name="authentication_failed">Authentication Failed</string>
<string name="authentication_error">Authentication Error %s</string>
Expand All @@ -99,4 +102,9 @@
<string name="google_backup">Google Backup</string>
<string name="generate_qr_code">Export</string>

<string name="bitwarden_export">Bitwarden</string>
<string name="bitwarden_export_desc">Bitwarden export description</string>
<string name="test_export">Test</string>
<string name="test_export_desc">Test export description</string>

</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<paths>
<files-path path="bitwarden_exports/" name="bitwarden" />
</paths>
4 changes: 2 additions & 2 deletions local.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading