-
-
Notifications
You must be signed in to change notification settings - Fork 735
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
Changes from all commits
19a252a
d2135c2
d5c50f4
8d8a00d
c19946b
ec87160
4653c28
4d0494a
6d208d7
6887cfe
eb7142a
ba93bd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix version inconsistency with Spotless plugin. The classpath version is updated to 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
|
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -1,4 +1,5 @@ | ||||||||
apply plugin: "com.android.library" | ||||||||
apply plugin: "kotlin-android" | ||||||||
apply plugin: "maven-publish" | ||||||||
apply plugin: "io.freefair.android-javadoc-jar" | ||||||||
apply plugin: "io.freefair.android-sources-jar" | ||||||||
|
@@ -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" | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify the alpha version dependency for production readiness. Using an alpha version ( 🌐 Web query:
💡 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 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 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||
|
||||||||
testImplementation "org.junit.jupiter:junit-jupiter:$rootProject.ext.jupiterVersion" | ||||||||
testImplementation "org.skyscreamer:jsonassert:1.5.0" | ||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,135 @@ | ||||||||||||||||||||||||||||||
package com.parse; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import android.content.Context; | ||||||||||||||||||||||||||||||
import androidx.security.crypto.EncryptedFile; | ||||||||||||||||||||||||||||||
import androidx.security.crypto.MasterKey; | ||||||||||||||||||||||||||||||
import com.parse.boltsinternal.Task; | ||||||||||||||||||||||||||||||
import java.io.File; | ||||||||||||||||||||||||||||||
import java.io.IOException; | ||||||||||||||||||||||||||||||
import java.security.GeneralSecurityException; | ||||||||||||||||||||||||||||||
import java.util.concurrent.Callable; | ||||||||||||||||||||||||||||||
import org.json.JSONException; | ||||||||||||||||||||||||||||||
import org.json.JSONObject; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||
* a file based {@link ParseObjectStore} using Jetpack's {@link EncryptedFile} class to protect | ||||||||||||||||||||||||||||||
* files from a malicious copy. | ||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||
class EncryptedFileObjectStore<T extends ParseObject> implements ParseObjectStore<T> { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private final String className; | ||||||||||||||||||||||||||||||
private final File file; | ||||||||||||||||||||||||||||||
private final EncryptedFile encryptedFile; | ||||||||||||||||||||||||||||||
private final ParseObjectCurrentCoder coder; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
public EncryptedFileObjectStore(Class<T> clazz, File file, ParseObjectCurrentCoder coder) { | ||||||||||||||||||||||||||||||
this(getSubclassingController().getClassName(clazz), file, coder); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
public EncryptedFileObjectStore(String className, File file, ParseObjectCurrentCoder coder) { | ||||||||||||||||||||||||||||||
this.className = className; | ||||||||||||||||||||||||||||||
this.file = file; | ||||||||||||||||||||||||||||||
this.coder = coder; | ||||||||||||||||||||||||||||||
Context context = ParsePlugins.get().applicationContext(); | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
encryptedFile = | ||||||||||||||||||||||||||||||
new EncryptedFile.Builder( | ||||||||||||||||||||||||||||||
context, | ||||||||||||||||||||||||||||||
file, | ||||||||||||||||||||||||||||||
new MasterKey.Builder(context) | ||||||||||||||||||||||||||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM) | ||||||||||||||||||||||||||||||
.build(), | ||||||||||||||||||||||||||||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB) | ||||||||||||||||||||||||||||||
.build(); | ||||||||||||||||||||||||||||||
} catch (GeneralSecurityException | IOException e) { | ||||||||||||||||||||||||||||||
throw new RuntimeException(e.getMessage()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include the original exception when wrapping. Wrapping exceptions without including the original cause makes debugging difficult. The stack trace and original exception details are lost. Apply this fix: - } catch (GeneralSecurityException | IOException e) {
- throw new RuntimeException(e.getMessage());
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException("Failed to initialize encrypted file", e); 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
private static ParseObjectSubclassingController getSubclassingController() { | ||||||||||||||||||||||||||||||
return ParseCorePlugins.getInstance().getSubclassingController(); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||
* Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. | ||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||
* @param current ParseObject which needs to be saved to disk. | ||||||||||||||||||||||||||||||
* @throws IOException thrown if an error occurred during writing of the file | ||||||||||||||||||||||||||||||
* @throws GeneralSecurityException thrown if there is an error with encryption keys or during | ||||||||||||||||||||||||||||||
* the encryption of the file | ||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||
private void saveToDisk(ParseObject current) throws IOException, GeneralSecurityException { | ||||||||||||||||||||||||||||||
JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get()); | ||||||||||||||||||||||||||||||
ParseFileUtils.writeJSONObjectToFile(encryptedFile, json); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||
* Retrieves a {@code ParseObject} from a file on disk in /2/ format. | ||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||
* @return The {@code ParseObject} that was retrieved. If the file wasn't found, or the contents | ||||||||||||||||||||||||||||||
* of the file is an invalid {@code ParseObject}, returns {@code null}. | ||||||||||||||||||||||||||||||
* @throws GeneralSecurityException thrown if there is an error with encryption keys or during | ||||||||||||||||||||||||||||||
* the encryption of the file | ||||||||||||||||||||||||||||||
* @throws JSONException thrown if an error occurred during the decoding process of the | ||||||||||||||||||||||||||||||
* ParseObject to a JSONObject | ||||||||||||||||||||||||||||||
* @throws IOException thrown if an error occurred during writing of the file | ||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||
private T getFromDisk() throws GeneralSecurityException, JSONException, IOException { | ||||||||||||||||||||||||||||||
return ParseObject.from( | ||||||||||||||||||||||||||||||
coder.decode( | ||||||||||||||||||||||||||||||
ParseObject.State.newBuilder(className), | ||||||||||||||||||||||||||||||
ParseFileUtils.readFileToJSONObject(encryptedFile), | ||||||||||||||||||||||||||||||
ParseDecoder.get()) | ||||||||||||||||||||||||||||||
.isComplete(true) | ||||||||||||||||||||||||||||||
.build()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public Task<T> getAsync() { | ||||||||||||||||||||||||||||||
return Task.call( | ||||||||||||||||||||||||||||||
new Callable<T>() { | ||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public T call() throws Exception { | ||||||||||||||||||||||||||||||
if (!file.exists()) return null; | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
return getFromDisk(); | ||||||||||||||||||||||||||||||
} catch (GeneralSecurityException e) { | ||||||||||||||||||||||||||||||
throw new RuntimeException(e.getMessage()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+96
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include the original exception when wrapping. Same issue as above - include the original exception for better debugging. Apply this fix: - } catch (GeneralSecurityException e) {
- throw new RuntimeException(e.getMessage());
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Failed to decrypt file", e); 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
ParseExecutors.io()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public Task<Void> setAsync(T object) { | ||||||||||||||||||||||||||||||
return Task.call( | ||||||||||||||||||||||||||||||
() -> { | ||||||||||||||||||||||||||||||
if (file.exists() && !ParseFileUtils.deleteQuietly(file)) | ||||||||||||||||||||||||||||||
throw new RuntimeException("Unable to delete"); | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
saveToDisk(object); | ||||||||||||||||||||||||||||||
} catch (GeneralSecurityException e) { | ||||||||||||||||||||||||||||||
throw new RuntimeException(e.getMessage()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
Comment on lines
+108
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
ParseExecutors.io()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public Task<Boolean> existsAsync() { | ||||||||||||||||||||||||||||||
return Task.call(file::exists, ParseExecutors.io()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||
public Task<Void> deleteAsync() { | ||||||||||||||||||||||||||||||
return Task.call( | ||||||||||||||||||||||||||||||
() -> { | ||||||||||||||||||||||||||||||
if (file.exists() && !ParseFileUtils.deleteQuietly(file)) | ||||||||||||||||||||||||||||||
throw new RuntimeException("Unable to delete"); | ||||||||||||||||||||||||||||||
Comment on lines
+129
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
ParseExecutors.io()); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay.