Skip to content

feat: Implementing encrypted local storage for user sessions with tests #1211

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 12 commits into
base: master
Choose a base branch
from

Conversation

rommansabbir
Copy link
Member

@rommansabbir rommansabbir commented Aug 15, 2024

New Pull Request Checklist

Issue Description

From an existing PR:


Currently the SDK saved cached user session as clear text files.
Feature / Enhancement Description

Encrypting cached user sessions using Jetpack security features to prevent session copy on rooted devices.
Alternatives / Workarounds

No workarounds at the moment.
3rd Party References

I found this [gist](https://github.com/appmattus/layercache/tree/main/testutils/src/main/kotlin/com/appmattus/layercache/keystore) which provides a good way for testing encryption methods on Robolectric.

Closes: #1192

  • Implemented encrypted local storage for user sessions with tests.
  • It was an existing PR.

Approach

  • Added EncryptedFileObjectStore to enable encryption and support CRUD operations.
  • Enabled migration in ParseCorePlugins when getCurrentUserController() is invoked by ParseObjectStoreMigrator.
  • Updated ParseFileUtils to support CRUD operations for encrypted objects.

Testing:

  • Added tests for the new functionality and updates.

TODOs before merging

  • Add tests - gradle clean test
  • Add changes to documentation (guides, repository pages, in-code descriptions)

Summary by CodeRabbit

  • New Features

    • Added secure, encrypted file-based storage for user data using Android Jetpack Security, enhancing data protection.
    • Introduced transparent migration for user data between legacy and encrypted storage formats.
    • Extended file utility support for encrypted file operations, including reading and writing encrypted JSON and text.
  • Build System

    • Enabled Kotlin support and added AndroidX Security dependency to the Android library module.
    • Updated Spotless Gradle plugin version for improved code formatting.
  • Bug Fixes

    • None.
  • Documentation

    • Improved JavaDoc formatting for various methods to enhance code readability.
  • Tests

    • Added comprehensive tests for encrypted file storage, migration logic, and cryptographic providers to ensure reliability and security.

Copy link

parse-github-assistant bot commented Aug 15, 2024

Thanks for opening this pull request!

  • 🎉 We are excited about your hands-on contribution!

@rommansabbir
Copy link
Member Author

@mtrezza

Comment on lines +24 to +49
return store.getAsync()
.continueWithTask(
new Continuation<T, Task<T>>() {
@Override
public Task<T> then(Task<T> task) throws Exception {
if (task.getResult() != null) return task;
return legacy.getAsync()
.continueWithTask(
new Continuation<T, Task<T>>() {
@Override
public Task<T> then(Task<T> task)
throws Exception {
T object = task.getResult();
if (object == null) return task;
return legacy.deleteAsync()
.continueWith(
task1 ->
ParseTaskUtils.wait(
store
.setAsync(
object)))
.onSuccess(task1 -> object);
}
});
}
});
Copy link
Member

Choose a reason for hiding this comment

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

Is there a better way to structure this code?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will look into it.

@@ -46,7 +46,9 @@ public boolean isCancellationRequested() {
}
}

/** @return the token that can be passed to asynchronous method to control cancellation. */
Copy link
Member

@mtrezza mtrezza Oct 5, 2024

Choose a reason for hiding this comment

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

Please revert these unrelated changes in bolts, twitter and wherever you find these comment-only refactors.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay.

@mtrezza
Copy link
Member

mtrezza commented Oct 5, 2024

As this PR is a continuation of #1191, did you consider the PR review comments and discussion feedback there when creating this PR?

@rommansabbir
Copy link
Member Author

As this PR is a continuation of #1191, did you consider the PR review comments and discussion feedback there when creating this PR?

Yes, I did. Did i missed something during the discussion feedback? I will again look into the discussion. Thanks for the attention.

@mtrezza
Copy link
Member

mtrezza commented Oct 6, 2024

Did i missed something during the discussion feedback?

Noting specific, I didn't check the discussion there, only noticed that there has been extensive discussion.

Copy link

coderabbitai bot commented Jun 4, 2025

📝 Walkthrough

Walkthrough

This change introduces encrypted storage for cached user session files using Android Jetpack Security, preventing session data from being easily copied on rooted devices. It adds an EncryptedFileObjectStore, migrates session storage to use encryption, updates file utilities for encrypted files, and provides comprehensive tests and Robolectric-compatible crypto providers.

Changes

Files / Groups Change Summary
parse/src/main/java/com/parse/EncryptedFileObjectStore.java New class for encrypted file-based storage of ParseObject using Jetpack Security.
parse/src/main/java/com/parse/ParseObjectStoreMigrator.java New class to transparently migrate session data from legacy to encrypted store.
parse/src/main/java/com/parse/ParseCorePlugins.java Switched current user controller to use encrypted store with migration from legacy.
parse/src/main/java/com/parse/ParseFileUtils.java Added read/write methods for encrypted files (byte array, string, JSON).
parse/build.gradle Added Kotlin Android plugin and AndroidX Security Crypto dependency.
build.gradle Updated Spotless Gradle plugin version.
parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java Added tests for encrypted object store (save, retrieve, exists, delete).
parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt Added tests for migration logic between legacy and encrypted stores.
parse/src/test/java/com/parse/AlgorithmParameterSpec.kt,
AndroidKeyStoreProvider.kt,
AndroidOpenSSLProvider.kt,
RobolectricKeyStore.kt
Added test utilities and providers for Robolectric-compatible Android KeyStore and crypto operations.
parse/src/main/java/com/parse/ManifestInfo.java,
Parse.java,
ParseClassName.java,
ParseQuery.java,
ParseSession.java,
ParseUser.java,
twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java,
bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java,
Task.java,
TaskCompletionSource.java
Reformatted Javadoc comments from single-line to multi-line style; no logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant ParseCorePlugins
    participant EncryptedFileObjectStore
    participant LegacyStore
    participant ParseObjectStoreMigrator

    App->>ParseCorePlugins: getCurrentUserController()
    ParseCorePlugins->>ParseObjectStoreMigrator: getAsync()
    ParseObjectStoreMigrator->>EncryptedFileObjectStore: getAsync()
    alt Encrypted store has data
        EncryptedFileObjectStore-->>ParseObjectStoreMigrator: Return user/session
    else Encrypted store empty
        ParseObjectStoreMigrator->>LegacyStore: getAsync()
        alt Legacy has data
            LegacyStore-->>ParseObjectStoreMigrator: Return user/session
            ParseObjectStoreMigrator->>EncryptedFileObjectStore: setAsync(user)
            ParseObjectStoreMigrator->>LegacyStore: deleteAsync()
        else Legacy empty
            LegacyStore-->>ParseObjectStoreMigrator: Return null
        end
    end
    ParseObjectStoreMigrator-->>ParseCorePlugins: Return user/session
    ParseCorePlugins-->>App: Return user/session
Loading

Assessment against linked issues

Objective Addressed Explanation
Encrypt cached user sessions using Jetpack Security to prevent session copy on rooted devices (#1192)
Provide migration from legacy unencrypted session files to encrypted storage (#1192)
Add Robolectric-compatible test infrastructure for encrypted storage and AndroidKeyStore (#1192)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes were found. All functional code changes are directly related to encrypted session storage, migration, supporting utilities, and test infrastructure as described in the linked issue. Javadoc reformatting and build file updates are trivial and do not affect functionality.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@parseplatformorg
Copy link

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🔭 Outside diff range comments (1)
parse/src/main/java/com/parse/ParseCorePlugins.java (1)

131-143: ⚠️ Potential issue

Critical: File path conflict between plain and encrypted stores

Both FileObjectStore and EncryptedFileObjectStore are using the same file path, which will cause data corruption. The plain store expects unencrypted data while the encrypted store expects encrypted data in a different format.

Consider using a different file path for the encrypted store:

 File file = new File(Parse.getParseFilesDir(), FILENAME_CURRENT_USER);
 FileObjectStore<ParseUser> fileStore =
         new FileObjectStore<>(ParseUser.class, file, ParseUserCurrentCoder.get());
 ParseObjectStore<ParseUser> store =
         Parse.isLocalDatastoreEnabled()
                 ? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore)
                 : fileStore;
+File encryptedFile = new File(Parse.getParseFilesDir(), FILENAME_CURRENT_USER + ".encrypted");
 EncryptedFileObjectStore<ParseUser> encryptedFileObjectStore =
         new EncryptedFileObjectStore<>(
-                ParseUser.class, file, ParseUserCurrentCoder.get());
+                ParseUser.class, encryptedFile, ParseUserCurrentCoder.get());
 ParseObjectStoreMigrator<ParseUser> storeMigrator =
         new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store);
 ParseCurrentUserController controller = new CachedCurrentUserController(storeMigrator);
♻️ Duplicate comments (8)
bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java (4)

544-551: Revert these unrelated Javadoc formatting changes.

As mentioned in the previous review feedback, these comment-only refactors in bolts files should be reverted since they are unrelated to the PR's main objective of implementing encrypted storage.


553-560: Revert these unrelated Javadoc formatting changes.

As mentioned in the previous review feedback, these comment-only refactors in bolts files should be reverted since they are unrelated to the PR's main objective of implementing encrypted storage.


562-569: Revert these unrelated Javadoc formatting changes.

As mentioned in the previous review feedback, these comment-only refactors in bolts files should be reverted since they are unrelated to the PR's main objective of implementing encrypted storage.


571-585: Revert these unrelated Javadoc formatting changes.

As mentioned in the previous review feedback, these comment-only refactors in bolts files should be reverted since they are unrelated to the PR's main objective of implementing encrypted storage.

bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java (1)

49-51: Revert these unrelated Javadoc formatting changes.

As you acknowledged in your previous response, these comment-only refactors in bolts files should be reverted since they are unrelated to the PR's main objective.

parse/src/main/java/com/parse/ManifestInfo.java (1)

153-155: Revert these unrelated Javadoc formatting changes.

Per the previous review feedback requesting to revert comment-only refactors "wherever you find these", this Javadoc formatting change should also be reverted as it's unrelated to the encrypted storage implementation.

parse/src/main/java/com/parse/ParseUser.java (1)

720-722: Revert these unrelated Javadoc formatting changes.

Per the previous review feedback requesting to revert comment-only refactors "wherever you find these", this Javadoc formatting change should also be reverted as it's unrelated to the encrypted storage implementation.

parse/src/main/java/com/parse/ParseObjectStoreMigrator.java (1)

24-49: 🛠️ Refactor suggestion

Simplify deeply nested anonymous classes

The current implementation uses deeply nested anonymous classes that make the code difficult to read and maintain.

Consider using Java 8+ lambdas or extracting methods to improve readability:

 @Override
 public Task<T> getAsync() {
     return store.getAsync()
-            .continueWithTask(
-                    new Continuation<T, Task<T>>() {
-                        @Override
-                        public Task<T> then(Task<T> task) throws Exception {
-                            if (task.getResult() != null) return task;
-                            return legacy.getAsync()
-                                    .continueWithTask(
-                                            new Continuation<T, Task<T>>() {
-                                                @Override
-                                                public Task<T> then(Task<T> task)
-                                                        throws Exception {
-                                                    T object = task.getResult();
-                                                    if (object == null) return task;
-                                                    return legacy.deleteAsync()
-                                                            .continueWith(
-                                                                    task1 ->
-                                                                            ParseTaskUtils.wait(
-                                                                                    store
-                                                                                            .setAsync(
-                                                                                                    object)))
-                                                            .onSuccess(task1 -> object);
-                                                }
-                                            });
-                        }
-                    });
+            .continueWithTask(task -> {
+                if (task.getResult() != null) return task;
+                return migrateFromLegacy();
+            });
+}
+
+private Task<T> migrateFromLegacy() {
+    return legacy.getAsync()
+            .continueWithTask(task -> {
+                T object = task.getResult();
+                if (object == null) return task;
+                return legacy.deleteAsync()
+                        .continueWithTask(ignored -> store.setAsync(object))
+                        .onSuccess(ignored -> object);
+            });
 }
🧹 Nitpick comments (2)
parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java (1)

43-43: Consider renaming getSetup() to follow Java naming conventions.

The method name getSetup() is misleading as it suggests a getter that returns a value, but it appears to perform initialization. Consider renaming it to setup() or initialize().

parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt (1)

165-169: Incomplete RSA key pair storage implementation.

The RSA key pair is generated but not stored in the AndroidKeyStore. The comment suggests certificate generation might be needed.

Would you like me to help implement the complete RSA key pair storage with certificate generation, or create an issue to track this TODO?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6926f3 and ba93bd7.

📒 Files selected for processing (22)
  • bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java (1 hunks)
  • bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java (1 hunks)
  • bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java (1 hunks)
  • build.gradle (1 hunks)
  • parse/build.gradle (2 hunks)
  • parse/src/main/java/com/parse/EncryptedFileObjectStore.java (1 hunks)
  • parse/src/main/java/com/parse/ManifestInfo.java (1 hunks)
  • parse/src/main/java/com/parse/Parse.java (1 hunks)
  • parse/src/main/java/com/parse/ParseClassName.java (1 hunks)
  • parse/src/main/java/com/parse/ParseCorePlugins.java (1 hunks)
  • parse/src/main/java/com/parse/ParseFileUtils.java (7 hunks)
  • parse/src/main/java/com/parse/ParseObjectStoreMigrator.java (1 hunks)
  • parse/src/main/java/com/parse/ParseQuery.java (1 hunks)
  • parse/src/main/java/com/parse/ParseSession.java (1 hunks)
  • parse/src/main/java/com/parse/ParseUser.java (1 hunks)
  • parse/src/test/java/com/parse/AlgorithmParameterSpec.kt (1 hunks)
  • parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt (1 hunks)
  • parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt (1 hunks)
  • parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java (1 hunks)
  • parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt (1 hunks)
  • parse/src/test/java/com/parse/RobolectricKeyStore.kt (1 hunks)
  • twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt (1)
parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt (1)
  • put (38-177)
parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt (1)
parse/src/main/java/com/parse/ParseObjectStoreMigrator.java (1)
  • ParseObjectStoreMigrator (8-82)
parse/src/main/java/com/parse/ParseObjectStoreMigrator.java (3)
bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java (1)
  • Task (28-993)
parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt (1)
  • store (15-101)
parse/src/main/java/com/parse/ParseTaskUtils.java (1)
  • ParseTaskUtils (17-137)
parse/src/main/java/com/parse/ParseFileUtils.java (1)
parse/src/main/java/com/parse/ParseIOUtils.java (1)
  • ParseIOUtils (27-347)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Gradle Build
🔇 Additional comments (11)
bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java (1)

27-29: Approve JavaDoc formatting update
The new multi-line Javadoc for getTask() is well-formed and consistent with the project’s comment style.

parse/src/main/java/com/parse/Parse.java (1)

291-293: Approve JavaDoc formatting update
The expanded Javadoc for isInitialized() aligns with the repository’s documentation conventions without altering functionality.

parse/src/main/java/com/parse/ParseSession.java (1)

125-127: Approve JavaDoc formatting update
The new multi-line Javadoc for getSessionToken() accurately describes its return behavior and matches the style used across the codebase.

twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java (1)

111-113: Approve JavaDoc formatting update
The adjusted Javadoc for isLinked(ParseUser) is correctly formatted and consistent with the other API docs.

parse/src/main/java/com/parse/ParseClassName.java (1)

24-26: Approve JavaDoc formatting update
The multi-line Javadoc for the value() method is properly structured and aligns with the established annotation documentation style.

parse/src/main/java/com/parse/ParseQuery.java (1)

285-287: JavaDoc formatting standardized
The expanded Javadoc block enhances readability and aligns with the project's JavaDoc conventions. No functional or behavioral changes introduced.

parse/build.gradle (1)

2-2: LGTM: Kotlin plugin added correctly.

The addition of the kotlin-android plugin enables Kotlin support as needed for the new Kotlin test files.

parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt (1)

3-17: Verify license compatibility for Appmattus code

This file contains Appmattus Limited copyright notice. Please ensure that the Apache 2.0 license is compatible with the Parse SDK license and that proper attribution is maintained.

Are you reusing code from an Appmattus project? If so, please ensure you're complying with their license requirements.

parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java (1)

57-85: Well-structured test for encrypted file writing.

The test properly verifies that the ParseUser data is correctly encrypted and saved to disk, with appropriate use of mocks and direct file reading for validation.

parse/src/main/java/com/parse/ParseFileUtils.java (1)

68-85: Well-implemented encrypted file reading with proper resource management.

The method correctly handles the EncryptedFile API and follows the established pattern of the existing readFileToByteArray method with proper stream cleanup.

parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt (1)

1-17: Verify copyright and licensing compatibility.

This file has a different copyright holder (Appmattus Limited) than the other Parse SDK files. Please ensure:

  1. Proper attribution is maintained
  2. The Apache 2.0 license is compatible with the Parse SDK license
  3. Any modifications are properly documented

@@ -50,6 +51,7 @@ dependencies {
api "androidx.core:core:1.8.0"
api "com.squareup.okhttp3:okhttp:$okhttpVersion"
api project(':bolts-tasks')
implementation "androidx.security:security-crypto:1.1.0-alpha03"
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the alpha version dependency for production readiness.

Using an alpha version (1.1.0-alpha03) of androidx.security:security-crypto in production raises concerns about stability and security. Alpha versions are typically not recommended for production use.


🌐 Web query:

What is the latest stable version of androidx.security:security-crypto and are there any known security vulnerabilities in version 1.1.0-alpha03?

💡 Result:

The latest stable version of androidx.security:security-crypto is 1.0.0, which was released on April 21, 2021[2][4]. Since then, the library has continued with alpha releases in the 1.1.0 series, with the most recent being version 1.1.0-alpha07, released on April 9, 2025[1][2].

Notably, in version 1.1.0-alpha07, all APIs in the library were deprecated in favor of existing platform APIs and direct use of Android Keystore[1]. This deprecation was officially implemented in April 2025, as mentioned in multiple sources[1][7][9].

Regarding version 1.1.0-alpha03 specifically, it does have a known security vulnerability. According to Maven Repository information, this version has a vulnerability inherited from its dependencies, specifically CVE-2022-24329[6]. This vulnerability is related to the kotlin-stdlib dependency that version 1.1.0-alpha03 uses[6].

Additionally, there have been reports of crashes in certain versions of the library. Some users reported that version 1.1.0-alpha01 was the last properly working version before encountering issues[8]. These crashes were particularly problematic in applications that had allowBackUp enabled[8].

If you're currently using this library, you should consider migrating to platform APIs and Android Keystore directly, as recommended by the deprecation notice in the latest version[1]. For those who still need the functionality, some developers have created forks of the library to provide ongoing support post-deprecation[7].

Citations:


Review production readiness of androidx.security:security-crypto:1.1.0-alpha03

Using an alpha release in production introduces stability and security risks. Version 1.1.0-alpha03 is known to inherit CVE-2022-24329 from its Kotlin StdLib dependency and the entire 1.1.0 series has since been deprecated in favor of platform APIs (Android Keystore). The latest stable release is 1.0.0 (Apr 2021).

• File: parse/build.gradle
Line: 54

Suggested changes:

- implementation "androidx.security:security-crypto:1.1.0-alpha03"
+ // Consider migrating to Android Keystore or use the latest stable library
+ implementation "androidx.security:security-crypto:1.0.0"

If you require features beyond 1.0.0, either fork a maintained version or switch to direct platform APIs as recommended in the April 2025 deprecation notice.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation "androidx.security:security-crypto:1.1.0-alpha03"
// Consider migrating to Android Keystore or use the latest stable library
implementation "androidx.security:security-crypto:1.0.0"
🤖 Prompt for AI Agents
In parse/build.gradle at line 54, the dependency on
androidx.security:security-crypto uses the alpha version 1.1.0-alpha03, which
has known security vulnerabilities and is deprecated. Replace this dependency
with the latest stable version 1.0.0 for production use, or alternatively,
remove this dependency and migrate to using direct platform APIs and Android
Keystore as recommended in the April 2025 deprecation notice. If features beyond
1.0.0 are needed, consider using a maintained fork instead of the alpha release.

@@ -11,7 +11,7 @@ buildscript {
classpath "org.jacoco:org.jacoco.core:$jacocoVersion"
classpath "com.dicedmelon.gradle:jacoco-android:0.1.5"
classpath "io.freefair.gradle:android-gradle-plugins:4.2.0-m1"
classpath "com.diffplug.spotless:spotless-plugin-gradle:5.17.1"
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.7.1"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix version inconsistency with Spotless plugin.

The classpath version is updated to 6.7.1, but the plugin declaration on line 20 still uses version 5.17.1. This inconsistency could cause build issues.

Apply this fix to align the plugin version:

plugins {
    id "com.github.ben-manes.versions" version "0.28.0"
-    id "com.diffplug.spotless" version "5.17.1"
+    id "com.diffplug.spotless" version "6.7.1"
}
🤖 Prompt for AI Agents
In build.gradle at line 14 and around line 20, the Spotless plugin version is
inconsistent, with the classpath using 6.7.1 and the plugin declaration using
5.17.1. Update the plugin declaration on line 20 to also use version 6.7.1 to
ensure both references match and prevent build issues.

Comment on lines +5 to +6
internal val AlgorithmParameterSpec.keystoreAlias: String
get() = this::class.java.getDeclaredMethod("getKeystoreAlias").invoke(this) as String
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for reflection-based property access.

The reflection call lacks error handling and could throw NoSuchMethodException, IllegalAccessException, or InvocationTargetException at runtime.

Apply this fix to add proper error handling:

internal val AlgorithmParameterSpec.keystoreAlias: String
-    get() = this::class.java.getDeclaredMethod("getKeystoreAlias").invoke(this) as String
+    get() = try {
+        val method = this::class.java.getDeclaredMethod("getKeystoreAlias")
+        method.isAccessible = true
+        method.invoke(this) as String
+    } catch (e: Exception) {
+        throw IllegalStateException("Failed to access keystoreAlias property", e)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal val AlgorithmParameterSpec.keystoreAlias: String
get() = this::class.java.getDeclaredMethod("getKeystoreAlias").invoke(this) as String
internal val AlgorithmParameterSpec.keystoreAlias: String
get() = try {
val method = this::class.java.getDeclaredMethod("getKeystoreAlias")
method.isAccessible = true
method.invoke(this) as String
} catch (e: Exception) {
throw IllegalStateException("Failed to access keystoreAlias property", e)
}
🤖 Prompt for AI Agents
In parse/src/test/java/com/parse/AlgorithmParameterSpec.kt at lines 5 to 6, the
reflection call to get the "getKeystoreAlias" method lacks error handling and
may throw exceptions like NoSuchMethodException, IllegalAccessException, or
InvocationTargetException. Wrap the reflection invocation in a try-catch block
to catch these exceptions and handle them appropriately, such as by logging the
error or rethrowing a runtime exception with a clear message.

Comment on lines +8 to +16
val setup by lazy {
Security.removeProvider("AndroidKeyStore")
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.removeProvider("AndroidOpenSSL")

Security.addProvider(AndroidKeyStoreProvider())
Security.addProvider(BouncyCastleProvider())
Security.addProvider(AndroidOpenSSLProvider())
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider thread safety and usage documentation for global security provider manipulation.

The lazy initialization manipulates global JVM security providers, which could cause issues in concurrent test execution environments. Consider adding:

  1. Thread safety: Use @Synchronized or other synchronization mechanisms
  2. Usage documentation: Document when and how this setup should be invoked
  3. Test isolation: Ensure this doesn't interfere with other tests
object RobolectricKeyStore {

-    val setup by lazy {
+    @Synchronized
+    fun setupProviders() {
+        if (isSetup) return
+        
         Security.removeProvider("AndroidKeyStore")
         Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
         Security.removeProvider("AndroidOpenSSL")

         Security.addProvider(AndroidKeyStoreProvider())
         Security.addProvider(BouncyCastleProvider())
         Security.addProvider(AndroidOpenSSLProvider())
+        isSetup = true
     }
+    
+    private var isSetup = false
+    
+    /**
+     * Call this in test setup to configure security providers for Robolectric tests
+     * that require AndroidKeyStore simulation.
+     */
+    val setup by lazy { setupProviders() }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val setup by lazy {
Security.removeProvider("AndroidKeyStore")
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.removeProvider("AndroidOpenSSL")
Security.addProvider(AndroidKeyStoreProvider())
Security.addProvider(BouncyCastleProvider())
Security.addProvider(AndroidOpenSSLProvider())
}
@Synchronized
fun setupProviders() {
if (isSetup) return
Security.removeProvider("AndroidKeyStore")
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.removeProvider("AndroidOpenSSL")
Security.addProvider(AndroidKeyStoreProvider())
Security.addProvider(BouncyCastleProvider())
Security.addProvider(AndroidOpenSSLProvider())
isSetup = true
}
private var isSetup = false
/**
* Call this in test setup to configure security providers for Robolectric tests
* that require AndroidKeyStore simulation.
*/
val setup by lazy { setupProviders() }
🤖 Prompt for AI Agents
In parse/src/test/java/com/parse/RobolectricKeyStore.kt around lines 8 to 16,
the lazy initialization block modifies global JVM security providers without
thread safety, risking concurrency issues in tests. To fix this, synchronize the
setup method using @Synchronized or another locking mechanism to ensure only one
thread can modify providers at a time. Additionally, add clear documentation on
when and how to invoke this setup to prevent misuse and interference with other
tests, and consider isolating this setup to avoid impacting concurrent test
executions.

Comment on lines +7 to +15
import org.junit.jupiter.api.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix test framework mismatch between JUnit 4 and JUnit 5

The test uses @RunWith(RobolectricTestRunner::class) which is for JUnit 4, but imports JUnit 5 annotations (org.junit.jupiter.api.Test).

Replace JUnit 5 imports with JUnit 4:

-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertTrue
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
 import org.junit.runner.RunWith

Also update the setup method annotation:

-    @BeforeEach
+    @Before
     fun setUp() {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt around lines 7
to 15, there is a mismatch between JUnit 4 and JUnit 5 usage: the test uses
@RunWith(RobolectricTestRunner::class) from JUnit 4 but imports JUnit 5
annotations like org.junit.jupiter.api.Test. To fix this, replace the JUnit 5
imports with their JUnit 4 equivalents, such as importing org.junit.Test instead
of org.junit.jupiter.api.Test, and update any setup method annotations to use
JUnit 4 annotations like @Before instead of JUnit 5's @BeforeEach.

Comment on lines +108 to +114
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete");
try {
saveToDisk(object);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve error messages and exception handling.

The error messages lack context, and exceptions are wrapped without the original cause.

Apply these improvements:

-                    if (file.exists() && !ParseFileUtils.deleteQuietly(file))
-                        throw new RuntimeException("Unable to delete");
+                    if (file.exists() && !ParseFileUtils.deleteQuietly(file))
+                        throw new RuntimeException("Unable to delete file: " + file.getAbsolutePath());
                     try {
                         saveToDisk(object);
                     } catch (GeneralSecurityException e) {
-                        throw new RuntimeException(e.getMessage());
+                        throw new RuntimeException("Failed to encrypt and save object", e);
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete");
try {
saveToDisk(object);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e.getMessage());
}
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete file: " + file.getAbsolutePath());
try {
saveToDisk(object);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Failed to encrypt and save object", e);
}
🤖 Prompt for AI Agents
In parse/src/main/java/com/parse/EncryptedFileObjectStore.java around lines 108
to 114, improve error handling by enhancing the exception messages with more
context and by wrapping exceptions with their original cause instead of just the
message. Modify the RuntimeException thrown when file deletion fails to include
the file path or name for clarity. When catching GeneralSecurityException, wrap
it in a RuntimeException passing the original exception as the cause to preserve
the stack trace and debugging information.

Comment on lines +129 to +130
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete");
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error message with file context.

Apply this improvement:

-                    if (file.exists() && !ParseFileUtils.deleteQuietly(file))
-                        throw new RuntimeException("Unable to delete");
+                    if (file.exists() && !ParseFileUtils.deleteQuietly(file))
+                        throw new RuntimeException("Unable to delete file: " + file.getAbsolutePath());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete");
if (file.exists() && !ParseFileUtils.deleteQuietly(file))
throw new RuntimeException("Unable to delete file: " + file.getAbsolutePath());
🤖 Prompt for AI Agents
In parse/src/main/java/com/parse/EncryptedFileObjectStore.java around lines 129
to 130, the RuntimeException thrown when file deletion fails has a generic
message. Update the exception message to include the file path or name to
provide context about which file could not be deleted, improving error
traceability.

Comment on lines +641 to +642
* Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already
* exists.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect JavaDoc - methods write strings, not JSONObjects.

The JavaDoc mentions writing a JSONObject but these methods write strings.

Fix the documentation:

-     * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already
+     * Writes a string to an encrypted file, will throw an error if the file already
      * exists.

Also applies to: 657-658

🤖 Prompt for AI Agents
In parse/src/main/java/com/parse/ParseFileUtils.java at lines 641-642 and
657-658, the JavaDoc incorrectly states that the methods write a JSONObject, but
they actually write strings. Update the JavaDoc comments to reflect that these
methods write strings to the encrypted file instead of JSONObject instances.

Comment on lines +139 to +141
override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also {
AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential NullPointerException with force unwrapping.

The lastSpec!! force unwrapping can cause a crash if engineGenerateKey() is called without prior initialization via engineInit(AlgorithmParameterSpec, SecureRandom).

Add a null check:

         override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also {
-            AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it)
+            lastSpec?.let { spec ->
+                AndroidKeyStore.storedKeys[spec.keystoreAlias] = KeyStore.SecretKeyEntry(it)
+            } ?: throw IllegalStateException("Key generator not properly initialized")
         }
🤖 Prompt for AI Agents
In parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt around lines 139 to
141, the code force unwraps lastSpec with !! which can cause a
NullPointerException if engineGenerateKey() is called before lastSpec is
initialized. To fix this, add a null check for lastSpec before accessing its
keystoreAlias, and handle the null case appropriately, such as throwing a clear
exception or returning early to avoid the crash.

Comment on lines +154 to +157
override fun engineInit(keysize: Int, random: SecureRandom?) = Unit
override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also {
AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix empty implementation and potential NPE.

The engineInit(keysize, random) has an empty implementation, and there's the same NPE risk with lastSpec!!.

-        override fun engineInit(keysize: Int, random: SecureRandom?) = Unit
+        override fun engineInit(keysize: Int, random: SecureRandom?) = wrapped.init(keysize, random)
         override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also {
-            AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it)
+            lastSpec?.let { spec ->
+                AndroidKeyStore.storedKeys[spec.keystoreAlias] = KeyStore.SecretKeyEntry(it)
+            } ?: throw IllegalStateException("Key generator not properly initialized")
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun engineInit(keysize: Int, random: SecureRandom?) = Unit
override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also {
AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it)
}
override fun engineInit(keysize: Int, random: SecureRandom?) =
wrapped.init(keysize, random)
override fun engineGenerateKey(): SecretKey =
wrapped.generateKey().also {
lastSpec?.let { spec ->
AndroidKeyStore.storedKeys[spec.keystoreAlias] =
KeyStore.SecretKeyEntry(it)
} ?: throw IllegalStateException("Key generator not properly initialized")
}
🤖 Prompt for AI Agents
In parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt around lines 154 to
157, the engineInit(keysize, random) method has an empty implementation which
should be properly implemented or safely handled, and the usage of lastSpec!! in
engineGenerateKey() risks a NullPointerException. Fix this by providing a
meaningful implementation for engineInit or documenting why it can be empty, and
replace lastSpec!! with a safe null check or throw a clear exception if lastSpec
is null before accessing its keystoreAlias.

@mtrezza
Copy link
Member

mtrezza commented Jun 4, 2025

@rommansabbir could you look through the feedback comments?

@mtrezza
Copy link
Member

mtrezza commented Jun 4, 2025

Just a note, reading through this PR again, I wonder what the purpose of this PR is; isn't a rooted device inherently unsafe? If someone roots a device, then they take all the risks, which go far beyond the Parse Session token.

@rommansabbir
Copy link
Member Author

@mtrezza As far as I remember, I was trying to push an existing PR that was originally created by another developer who may have deleted their account. I took over their changes and created a new PR.

@rommansabbir
Copy link
Member Author

Should we proceed with merging this PR, or would it be better to close it?

@mtrezza
Copy link
Member

mtrezza commented Jun 4, 2025

Do you see any value in it? I assume there was a reason why you picked it up. Just want to confirm it makes sense to merge this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Preventing Session Copy On Rooted Devices By Encrypting Current Session Files
4 participants