Skip to content

Fix subfolders in encrypted folders - #17608

Open
daniele-verducci wants to merge 5 commits into
masterfrom
fix/internal-105265
Open

daniele-verducci wants to merge 5 commits into
masterfrom
fix/internal-105265

Conversation

@daniele-verducci

@daniele-verducci daniele-verducci commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

How to repro:

  • create encrypted folder
  • create encrypted subfolder inside the one just created
  • app crashes

How to repro 2:

  • enter encrypted folder
  • enter encrypted subfolder inside the previous one
  • app crashes

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI (N/A)

@github-actions

Copy link
Copy Markdown

APK file: https://github.com/nextcloud/android/actions/runs/34505348733/artifacts/10163844801
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@github-actions

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

📱 QA build

Download app-qa-debug.apk (76M)
QR code Open the QR code for this download
Commit 3d34758
Version 17608
Available until 7 days after this build

The QA build installs alongside a released Nextcloud app, so you can keep
using your existing install while testing.

Downloading the file requires a GitHub account, so open this link on the
device you want to test on, or transfer the APK to it.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 7.14286% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../java/com/nextcloud/utils/e2ee/E2EEKeyInspector.kt 0.00% 12 Missing ⚠️
...va/com/owncloud/android/utils/EncryptionUtilsV2.kt 0.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@alperozturk96 alperozturk96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you please share the crash-log and explain your approach?


return try {
EncryptionUtils.decryptStringAsymmetricV2(user.encryptedMetadataKey, privateKey)
EncryptionUtils.decryptStringAsymmetricV2(user.decryptedMetadataKey, privateKey)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why we are trying to decrypt the decrypted metadata key here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see, this doesn't make sense. The result is not used, so I believe this call in the previous code was used to throw an exception if the key isn't valid. Is it a correct interpretation?

In that case, maybe I can get directly the key with retrieveTopMostMetadataKey(). It should throw an exception as well.
But then should I verify the key in any way?

val metadataFile = EncryptionUtils.deserializeJSON(
serializedMetadata,
object : TypeToken<EncryptedFolderMetadataFile>() {}
private fun decryptsMetadataV2(ocFile: OCFile, privateKey: String, client: OwnCloudClient): Boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Previously GetMetadataRemoteOperation was already given the metadata why now we need to retrieveTopMostMetadata and also eventually call decryptFolderMetadataFile ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GetMetadataRemoteOperation retrieves the metadata for the current folder, but I saw with the debugger that the users array is present only if the folder is the root encrypted folder. If we are in a subfolder, it's not. I think this isn't a server bug because it's working in the web version (checked the calls and the answer is the same, just in XML instead of JSON, so they must be keeping the root encrypted folder's key and using it for the subfolders, I believe)

@daniele-verducci

Copy link
Copy Markdown
Contributor Author

Could you please share the crash-log and explain your approach?

Hey Alper, here's the crashlog:


2026-09-14 12:06:21.801  4737-4737  AndroidRuntime          com.nextcloud.client                 E  FATAL EXCEPTION: main
                                                                                                    Process: com.nextcloud.client, PID: 4737
                                                                                                    java.lang.NullPointerException: Attempt to invoke interface method 'java.util.Iterator java.lang.Iterable.iterator()' on a null object reference
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector.decryptsMetadataV2(E2EEKeyInspector.kt:117)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector.access$decryptsMetadataV2(E2EEKeyInspector.kt:33)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector$canDecryptFolderMetadata$2.invokeSuspend(E2EEKeyInspector.kt:105)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector$canDecryptFolderMetadata$2.invoke(E2EEKeyInspector.kt:8)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector$canDecryptFolderMetadata$2.invoke(E2EEKeyInspector.kt:4)
                                                                                                    	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
                                                                                                    	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
                                                                                                    	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
                                                                                                    	at kotlinx.coroutines.BuildersKt.withContext(dex-id-4119e2f2b95ce035797d115b10131393af91e27e:1)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEKeyInspector.canDecryptFolderMetadata(E2EEKeyInspector.kt:86)
                                                                                                    	at com.nextcloud.utils.e2ee.E2EEActionResolver$checkFolderMetadataKey$2.invokeSuspend(E2EEActionResolver.kt:44)
                                                                                                    	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
                                                                                                    	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
                                                                                                    	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
                                                                                                    	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
                                                                                                    	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
                                                                                                    	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
                                                                                                    	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
                                                                                                    	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
                                                                                                    	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@5e01fcc, Dispatchers.Main.immediate]

This happens when opening a subfolder on an encrypted folder. The crash is caused by the users array being empty/null (the user is used to retrieve the key).
I checked on the web watching the requests and I saw the users array being empty there as well, so I figured out they are using the parent encrypted folder's key.
I then replaced the piece of code that obtains the user array from the current folder with one that retrieves the topmost encrypted folder (that always has the users array populated) and uses its key.

I can say it works, but I'm not sure it's the correct fix. What do you think about it?

@daniele-verducci

Copy link
Copy Markdown
Contributor Author

@alperozturk96 Can you check last commit? Could it be a solution?

@github-actions

Copy link
Copy Markdown

blue-Light-Screenshot test failed, but no output was generated. Maybe a preliminary stage failed.

Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>
…etrieval function)

Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>

Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>
Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>
This reverts commit 7bdb3c3.

Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>
Signed-off-by: daniele-verducci <daniele.verducci@nextcloud.com>
return try {
EncryptionUtils.decryptStringAsymmetricV2(user.encryptedMetadataKey, privateKey)
true
val metadataKey = EncryptionUtilsV2().retrieveTopMostMetadataKey(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I saw this comment in the master. @tobiasKaminsky I think using retrieveTopMostMetadataKey directly here is okay. What do you think?

// we are in a subfolder, decrypt information is in top most encrypted folder

@github-actions

Copy link
Copy Markdown

Codacy

SpotBugs

CategoryBaseNew
Bad practice3232
Correctness5858
Dodgy code186186
Experimental11
Malicious code vulnerability22
Multithreaded correctness2626
Performance3737
Security1919
Total361361

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants