Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
SeedVault 15-5.2

* It is now possible to verify the integrity of file backups as well, partially or fully
* Improve files backup snapshot UI
* Allow changing backup location when USB drive isn't plugged in
* Fix work profile USB backup

Also contains 15-5.1

* It is now possible to verify the integrity of app backups, partially or fully
* The entire WebDAV URL is now shown when in settings
* A launch button is now shown for apps that are force-stopped so that they can be backed up

# -----BEGIN PGP SIGNATURE-----
#
# iQIzBAABCgAdFiEE8LiYseXDVwsHMtSKo5wwJqfeYAEFAmdqwqIACgkQo5wwJqfe
# YAE+pQ//SMo4S3bBdJ4SgKr/mGuwoq0b1QVHlujwTSFUXGugUehJY8lhhhEdd9QX
# dHyJm/fSxwUbjT9VQGBW4jAy590g+CwN/zrl2rPyg5tEzyidF5NOeu6FfBuFHqr0
# IauJ1e9GXd1hZM9vpgIUUXrkMwt6s+LUHPnSu5mfzW8Rd85Yh3o2GkKOHjhklBHb
# wwtVFzIsKICv55DgWZp77AdyfKw2XuddsqYA+i3xuGKt8U6U8t18zArQYAY++hwc
# kzJx5fJG+TE7T81QklhkZPdeO+CLGrs0pNrJWzI7tk9Qu1oN1AmZj83x/+61Y0DH
# 9K29/XQiDd/YWL+iXBIlFxQO5EMe/liSzO9ev1aUReaoOskSEb6ixVCJnoiO8FYv
# iXck5rgM5yQJjU+mbhJlbLcfrHlIbdHiHILryo2ryR/0BplKP3hJXuuDEAjR+Hnm
# 4+bWgAzGOjvqzDGwu+jWTBx0sXztzwb3DmeqExVg430L8TXPUSGNw6iSMKMM88TU
# DtAodOxQoa55dbcRvma56rYmmw8Ld9dqbi0alRIWFH0cFXGYWAs//c5ZaoGkVXCJ
# lYAtGVcca0YGH4TBIH1EcJqEjqP5tMHkww4BxR2QQ9kSOSPQzOLJhyg69GgTf8uO
# zFntjqD0CbNlgqMiOInOCbKKHKdSoFOGBTv5ooCVfjy4aSzQfEM=
# =rhBP
# -----END PGP SIGNATURE-----
# gpg: Signature made Tue 24 Dec 2024 07:48:10 PM IST
# gpg:                using RSA key F0B898B1E5C3570B0732D48AA39C3026A7DE6001
# gpg: Good signature from "Chirayu Desai <chirayudesai1@gmail.com>" [ultimate]
# gpg:                 aka "Chirayu Desai <chirayu@calyxinstitute.org>" [ultimate]
# gpg:                 aka "Chirayu Desai <chirayu@cdesai.in>" [ultimate]

* tag '15-5.2' of https://github.com/seedvault-app/seedvault: (71 commits)
  Bump version to 15-5.2
  Update state tracking to also include file backup check
  Allow changing backup location when flash drive isn't plugged in
  Tweak file backup strings as requested
  Don't allow running file backup and check at the same time
  Show which files are affected by corrupted in files overview view
  Move background color into core module
  Improve files backup snapshot UI
  Show detailed file listing after tapping snapshots in check result
  Store ciphertext size in the DB instead of the plaintext size
  ChunksCacheRepopulater ignores snapshots it can't read
  Make backups self-healing after corrupted chunks were found
  Mark corrupted chunks in the ChunksCache
  Export DB schema for files backup' internal cache DB
  Check file backup chunks with unexpected file size on backend first
  Replace backendGetter lambda with IBackendManager
  Add UI for new Files Backup Checker
  Add Files Backup Checker to storage library
  Move some Sud styles into core
  Remove FIXME since we are no longer updating lastBackupTime a lot
  ...

Change-Id: Icbe272f0613a10959883e7b19198f73c64a0a795
  • Loading branch information
chirayudesai committed Dec 24, 2024
2 parents 13d97fb + 8e819ff commit 0951af8
Show file tree
Hide file tree
Showing 166 changed files with 4,859 additions and 921 deletions.
77 changes: 65 additions & 12 deletions .cirrus.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
task:
name: Build with AOSP
only_if: $CIRRUS_PR_LABELS =~ ".*aosp-build.*"
timeout_in: 70m
container:
image: ubuntu:23.04
cpu: 8
memory: 32G
build_script:
- ./.github/scripts/build_aosp.sh aosp_arm64 ap1a userdebug android-14.0.0_r29
container:
image: ghcr.io/cirruslabs/android-sdk:34
kvm: true
cpu: 8
memory: 16G

instrumentation_tests_task:
name: "Cirrus CI Instrumentation Tests"
start_avd_background_script:
sdkmanager --install "system-images;android-34;default;x86_64" "emulator";
echo no | avdmanager create avd -n seedvault -k "system-images;android-34;default;x86_64";
$ANDROID_HOME/emulator/emulator
-avd seedvault
-no-audio
-no-boot-anim
-gpu swiftshader_indirect
-no-snapshot
-no-window
-writable-system;
provision_avd_background_script:
wget https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz;

adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
sleep 5;
assemble_script:
./gradlew :app:assembleRelease :contacts:assembleRelease assembleAndroidTest
install_app_script:
timeout 180s bash -c 'while [[ -z $(adb shell mount | grep "/system " | grep "(rw,") ]]; do sleep 1; done;';
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';

adb shell mkdir -p /sdcard/seedvault_baseline;
adb push backup.tar.gz /sdcard/seedvault_baseline/backup.tar.gz;
adb shell tar xzf /sdcard/seedvault_baseline/backup.tar.gz --directory=/sdcard/seedvault_baseline;

adb shell mkdir -p /system/priv-app/Seedvault;
adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk;
adb push permissions_com.stevesoltys.seedvault.xml /system/etc/permissions/privapp-permissions-seedvault.xml;
adb push allowlist_com.stevesoltys.seedvault.xml /system/etc/sysconfig/allowlist-seedvault.xml;

adb shell mkdir -p /system/priv-app/ContactsBackup;
adb push contactsbackup/build/outputs/apk/release/contactsbackup-release.apk /system/priv-app/ContactsBackup/contactsbackup.apk;
adb push contactsbackup/default-permissions_org.calyxos.backup.contacts.xml /system/etc/default-permissions/default-permissions_org.calyxos.backup.contacts.xml;

adb shell bmgr enable true;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
run_large_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.size=large :app:connectedAndroidTest
run_other_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest connectedAndroidTest
always:
seedvault_artifacts:
path: Seedvault.apk
pull_screenshots_script:
adb pull /sdcard/seedvault_test_results
screenshots_artifacts:
path: "seedvault_test_results/**/*.mp4"
logcat_artifacts:
path: "seedvault_test_results/**/*.log"
3 changes: 2 additions & 1 deletion .idea/dictionaries/user.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## [15-5.2] - 2024-12-24
* It is now possible to verify the integrity of file backups as well, partially or fully
* Improve files backup snapshot UI
* Allow changing backup location when USB drive isn't plugged in
* Fix work profile USB backup

## [15-5.1] - 2024-11-20
* It is now possible to verify the integrity of app backups, partially or fully
* The entire WebDAV URL is now shown when in settings
* A launch button is now shown for apps that are force-stopped so that they can be backed up

## [15-5.0] - 2024-10-15
* First Android 15 release
* New backup format using compression and deduplication
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ on developing Seedvault locally.
This project aims to adhere to the
[official Kotlin coding style](https://developer.android.com/kotlin/style-guide).

### Translating
Seedvault is translated using Weblate. It is currently under the [CalyxOS project.](https://hosted.weblate.org/projects/calyxos/)

## Third-party tools

> **⚠ WARNING**: the Seedvault developers make no guarantees about external software projects.
Expand Down
23 changes: 23 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Reporting Security Issues

The Seedvault team and community take security bugs seriously.
We appreciate your efforts to responsibly disclose your findings,
and will make every effort to acknowledge your contributions.

To report a security issue,
please send an email to `security@seedvault.app`
or use the GitHub Security Advisory
["Report a Vulnerability"](https://github.com/seedvault-app/seedvault/security/advisories/new) tab.

The Seedvault team will send a response indicating the next steps in handling your report.
After the initial reply to your report,
we will keep you informed of the progress towards a fix and full announcement,
and may ask for additional information or guidance.

# Older platform branches

Due to API breakage in AOSP versions, we have one branch per major AOSP release,
e.g. `android14` and `android15`.
Note that typically only the latest branch is maintained.
This means that fixes for **security issues do not get backported** to older branches automatically.
Please get in touch if you want to maintain an older branch.
7 changes: 0 additions & 7 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ android {
versionNameSuffix = "-${gitDescribe()}"
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
testInstrumentationRunnerArguments["disableAnalytics"] = "true"

if (project.hasProperty("instrumented_test_size")) {
val testSize = project.property("instrumented_test_size").toString()
println("Instrumented test size: $testSize")

testInstrumentationRunnerArguments["size"] = testSize
}
}

signingConfigs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ class SafBackendTest : BackendTest(), KoinComponent {
fun `test remove create write file`(): Unit = runBlocking {
testRemoveCreateWriteFile()
}

@Test
fun `test free space and create app blob without root folder`(): Unit = runBlocking {
testTestFreeSpaceAndCreateBlob()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import androidx.test.uiautomator.Until
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory
Expand All @@ -21,8 +20,10 @@ import io.mockk.every
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.calyxos.seedvault.core.toHexString
import org.koin.core.component.get
import java.io.ByteArrayOutputStream
import java.security.DigestInputStream
import java.security.MessageDigest
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.fail

Expand Down Expand Up @@ -154,7 +155,8 @@ internal interface LargeBackupTestBase : LargeTestBase {

private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) {
var packageName: String? = null
var dataIntercept = ByteArrayOutputStream()
val messageDigest = MessageDigest.getInstance("SHA-256")
var digestInputStream: DigestInputStream? = null

coEvery {
spyFullBackup.performFullBackup(any(), any(), any())
Expand All @@ -166,20 +168,19 @@ internal interface LargeBackupTestBase : LargeTestBase {
every {
spyInputFactory.getInputStream(any())
} answers {
InputStreamIntercept(
inputStream = callOriginal(),
intercept = dataIntercept
)
digestInputStream = DigestInputStream(callOriginal(), messageDigest)
digestInputStream!!
}

coEvery {
spyFullBackup.finishBackup()
} answers {
val result = callOriginal()
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
val digest = digestInputStream?.messageDigest ?: fail("No digestInputStream")
backupResult.full[packageName!!] = digest.digest().toHexString()

packageName = null
dataIntercept = ByteArrayOutputStream()
digest.reset()
result
}
}
Expand All @@ -192,9 +193,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
every {
spyBackupNotificationManager.onBackupSuccess(any(), any(), any())
} answers {
val success = firstArg<Boolean>()
assert(success) { "Backup failed." }

callOriginal()
completed.set(true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ package com.stevesoltys.seedvault.e2e
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
import com.stevesoltys.seedvault.transport.restore.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory
import io.mockk.Call
import io.mockk.MockKAnswerScope
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
Expand All @@ -22,8 +23,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.calyxos.seedvault.core.toHexString
import org.koin.core.component.get
import java.io.ByteArrayOutputStream
import java.security.DigestOutputStream
import java.security.MessageDigest
import kotlin.test.fail

internal interface LargeRestoreTestBase : LargeTestBase {

Expand Down Expand Up @@ -161,14 +165,26 @@ internal interface LargeRestoreTestBase : LargeTestBase {

clearMocks(spyKVRestore)

coEvery {
spyKVRestore.initializeState(any(), any(), any(), any())
} answers {
packageName = arg<PackageInfo>(3).packageName
fun initializeStateBlock(
packageInfoIndex: Int
): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
packageName = arg<PackageInfo>(packageInfoIndex).packageName
restoreResult.kv[packageName!!] = mutableMapOf()
callOriginal()
}

coEvery {
spyKVRestore.initializeState(any(), any(), any(), any())
} answers initializeStateBlock(1)

coEvery {
spyKVRestore.initializeStateV1(any(), any(), any(), any())
} answers initializeStateBlock(2)

coEvery {
spyKVRestore.initializeStateV0(any(), any())
} answers initializeStateBlock(1)

every {
spyOutputFactory.getBackupDataOutput(any())
} answers {
Expand All @@ -182,47 +198,61 @@ internal interface LargeRestoreTestBase : LargeTestBase {

private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) {
var packageName: String? = null
var dataIntercept = ByteArrayOutputStream()
val messageDigest = MessageDigest.getInstance("SHA-256")
var digestOutputStream: DigestOutputStream? = null

clearMocks(spyFullRestore)

coEvery {
spyFullRestore.initializeState(any(), any(), any())
} answers {
fun initializeStateBlock(
packageInfoIndex: Int
): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
packageName?.let {
restoreResult.full[it] = dataIntercept.toByteArray().sha256()
// sometimes finishRestore() doesn't get called, so get data from last package here
digestOutputStream?.messageDigest?.let { digest ->
restoreResult.full[packageName!!] = digest.digest().toHexString()
}
}

packageName = arg<PackageInfo>(3).packageName
dataIntercept = ByteArrayOutputStream()
packageName = arg<PackageInfo>(packageInfoIndex).packageName

callOriginal()
}

coEvery {
spyFullRestore.initializeState(any(), any(), any())
} answers initializeStateBlock(1)

coEvery {
spyFullRestore.initializeStateV1(any(), any(), any())
} answers initializeStateBlock(2)

coEvery {
spyFullRestore.initializeStateV0(any(), any())
} answers initializeStateBlock(1)

every {
spyOutputFactory.getOutputStream(any())
} answers {
OutputStreamIntercept(
outputStream = callOriginal(),
intercept = dataIntercept
)
digestOutputStream = DigestOutputStream(callOriginal(), messageDigest)
digestOutputStream!!
}

every {
spyFullRestore.abortFullRestore()
} answers {
packageName = null
dataIntercept = ByteArrayOutputStream()
digestOutputStream?.messageDigest?.reset()
callOriginal()
}

every {
spyFullRestore.finishRestore()
} answers {
restoreResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
val digest = digestOutputStream?.messageDigest ?: fail("No digestOutputStream")
restoreResult.full[packageName!!] = digest.digest().toHexString()

packageName = null
dataIntercept = ByteArrayOutputStream()
digest.reset()
callOriginal()
}
}
Expand Down
Loading

0 comments on commit 0951af8

Please sign in to comment.