Skip to content

Jules was unable to complete the task in time. Please review the work… #398

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: main
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,31 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material3.Checkbox
import androidx.compose.material3.OutlinedTextField
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.xayah.databackup.R
import com.xayah.databackup.feature.BackupApps
import com.xayah.databackup.feature.backup.apps.AppsViewModel
import com.xayah.databackup.ui.component.SelectableActionButton
import com.xayah.databackup.ui.component.verticalFadingEdges
import com.xayah.databackup.util.navigateSafely
import com.xayah.databackup.util.popBackStackSafely

@Composable
fun BackupPreviewScreen(navController: NavHostController) {
fun BackupPreviewScreen(navController: NavHostController, appsViewModel: AppsViewModel = viewModel()) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier
Expand Down Expand Up @@ -135,6 +142,30 @@ fun BackupPreviewScreen(navController: NavHostController) {
subtitle = "No items selected"
) {}

Spacer(modifier = Modifier.height(16.dp)) // Added spacer for separation

// Encryption Checkbox and Password Field
val encryptBackupState = remember { mutableStateOf(false) }
val passwordState = remember { mutableStateOf("") }

Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = encryptBackupState.value,
onCheckedChange = { encryptBackupState.value = it }
)
Text(text = stringResource(R.string.encrypt_backup))
}

if (encryptBackupState.value) {
OutlinedTextField(
value = passwordState.value,
onValueChange = { passwordState.value = it },
label = { Text(stringResource(R.string.password)) },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
}

Spacer(modifier = Modifier.height(0.dp))
}

Expand All @@ -149,7 +180,13 @@ fun BackupPreviewScreen(navController: NavHostController) {

Button(
modifier = Modifier.wrapContentSize(),
onClick = { }
onClick = {
appsViewModel.setEncryptionEnabled(encryptBackupState.value)
if (encryptBackupState.value) {
appsViewModel.setEncryptionPassword(passwordState.value)
}
navController.navigateSafely(BackupApps)
}
) {
Text(text = stringResource(R.string.next))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

data object UiState

open class AppsViewModel : BaseViewModel() {
private val _uiState = MutableStateFlow(UiState)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

private val _encryptionEnabled = MutableStateFlow(false)
val encryptionEnabled: StateFlow<Boolean> = _encryptionEnabled.asStateFlow()

private val _encryptionPassword = MutableStateFlow("")
val encryptionPassword: StateFlow<String> = _encryptionPassword.asStateFlow()

val apps = combine(
DatabaseHelper.appDao.loadFlowApps(),
App.application.readInt(FilterBackupUser),
Expand Down Expand Up @@ -134,4 +142,16 @@ open class AppsViewModel : BaseViewModel() {
App.application.saveBoolean(key, value)
}
}

fun setEncryptionEnabled(enabled: Boolean) {
viewModelScope.launch {
_encryptionEnabled.value = enabled
}
}

fun setEncryptionPassword(password: String) {
viewModelScope.launch {
_encryptionPassword.value = password
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.xayah.databackup.util

import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom

object EncryptionHelper {

private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA256"
private const val ITERATION_COUNT = 65536
private const val KEY_LENGTH = 256
private const val IV_LENGTH = 16 // AES block size in bytes

fun encrypt(data: ByteArray, password: CharArray): Pair<ByteArray, ByteArray> {
val salt = ByteArray(KEY_LENGTH / 8)
SecureRandom().nextBytes(salt)

val keySpec = PBEKeySpec(password, salt, ITERATION_COUNT, KEY_LENGTH)
val secretKeyFactory = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
val secretKey = secretKeyFactory.generateSecret(keySpec)
val secretKeySpec = SecretKeySpec(secretKey.encoded, ALGORITHM)

val iv = ByteArray(IV_LENGTH)
SecureRandom().nextBytes(iv)
val ivParameterSpec = IvParameterSpec(iv)

val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val encryptedData = cipher.doFinal(data)

return Pair(salt, iv + encryptedData) // Prepend IV to the encrypted data
}

fun decrypt(encryptedDataWithIv: ByteArray, password: CharArray, salt: ByteArray): ByteArray {
val iv = encryptedDataWithIv.copyOfRange(0, IV_LENGTH)
val encryptedData = encryptedDataWithIv.copyOfRange(IV_LENGTH, encryptedDataWithIv.size)

val keySpec = PBEKeySpec(password, salt, ITERATION_COUNT, KEY_LENGTH)
val secretKeyFactory = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
val secretKey = secretKeyFactory.generateSecret(keySpec)
val secretKeySpec = SecretKeySpec(secretKey.encoded, ALGORITHM)

val ivParameterSpec = IvParameterSpec(iv)

val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
return cipher.doFinal(encryptedData)
}
}
2 changes: 2 additions & 0 deletions source-next/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@
<string name="args_current_level">Current level: %1$s</string>
<string name="last_update">Last update</string>
<string name="welcome_screen_app_desc">Enjoy hassle-free backups with DataBackup. It’s simple to use and completely FOSS. Keep your data safe without the stress</string>
<string name="encrypt_backup">Encrypt backup</string>
<string name="password">Password</string>
<string name="get_started">Get started</string>
<string name="file">File</string>
<string name="dismiss">Dismiss</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.xayah.databackup.util

import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
import java.security.SecureRandom
import javax.crypto.AEADBadTagException

class EncryptionHelperTest {

@Test
fun encryptDecrypt_success() {
val data = "Hello, World!".toByteArray()
val password = "testPassword".toCharArray()

val (salt, encryptedDataWithIv) = EncryptionHelper.encrypt(data, password)
val decryptedData = EncryptionHelper.decrypt(encryptedDataWithIv, password, salt)

assertArrayEquals(data, decryptedData)
}

@Test
fun encryptDecrypt_differentData() {
val data1 = "Hello, World!".toByteArray()
val data2 = "Goodbye, World!".toByteArray()
val password = "testPassword".toCharArray()

val (salt, encryptedDataWithIv1) = EncryptionHelper.encrypt(data1, password)
val decryptedData1 = EncryptionHelper.decrypt(encryptedDataWithIv1, password, salt)

val (salt2, encryptedDataWithIv2) = EncryptionHelper.encrypt(data2, password)
val decryptedData2 = EncryptionHelper.decrypt(encryptedDataWithIv2, password, salt2)


assertArrayEquals(data1, decryptedData1)
assertArrayEquals(data2, decryptedData2)
assertNotEquals(String(encryptedDataWithIv1), String(encryptedDataWithIv2))
}

@Test(expected = AEADBadTagException::class)
fun decrypt_wrongPassword_throwsException() {
val data = "Hello, World!".toByteArray()
val correctPassword = "correctPassword".toCharArray()
val wrongPassword = "wrongPassword".toCharArray()

val (salt, encryptedDataWithIv) = EncryptionHelper.encrypt(data, correctPassword)
EncryptionHelper.decrypt(encryptedDataWithIv, wrongPassword, salt)
}

@Test(expected = AEADBadTagException::class)
fun decrypt_wrongSalt_throwsException() {
val data = "Hello, World!".toByteArray()
val password = "testPassword".toCharArray()

val (salt, encryptedDataWithIv) = EncryptionHelper.encrypt(data, password)

val wrongSalt = ByteArray(EncryptionHelper.KEY_LENGTH / 8)
SecureRandom().nextBytes(wrongSalt)

EncryptionHelper.decrypt(encryptedDataWithIv, password, wrongSalt)
}

@Test(expected = AEADBadTagException::class)
fun decrypt_tamperedData_throwsException() {
val data = "Hello, World!".toByteArray()
val password = "testPassword".toCharArray()

val (salt, encryptedDataWithIv) = EncryptionHelper.encrypt(data, password)

// Tamper with the encrypted data
encryptedDataWithIv[encryptedDataWithIv.size / 2] = (encryptedDataWithIv[encryptedDataWithIv.size / 2] + 1).toByte()

EncryptionHelper.decrypt(encryptedDataWithIv, password, salt)
}

@Test
fun encrypt_producesDifferentCiphertextForSameDataWithDifferentSalts() {
val data = "Hello, World!".toByteArray()
val password = "testPassword".toCharArray()

val (salt1, encryptedDataWithIv1) = EncryptionHelper.encrypt(data, password)
val (salt2, encryptedDataWithIv2) = EncryptionHelper.encrypt(data, password) // Encryption will generate a new salt

assertNotEquals(String(salt1), String(salt2))
assertNotEquals(String(encryptedDataWithIv1), String(encryptedDataWithIv2))
}
}
Loading