Skip to content
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

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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.

/**
* @return the token that can be passed to asynchronous method to control cancellation.
*/
public CancellationToken getToken() {
synchronized (lock) {
throwIfClosed();
Expand Down
16 changes: 12 additions & 4 deletions bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -541,28 +541,36 @@ public boolean isCompleted() {
}
}

/** @return {@code true} if the task was cancelled, {@code false} otherwise. */
/**
* @return {@code true} if the task was cancelled, {@code false} otherwise.
*/
public boolean isCancelled() {
synchronized (lock) {
return cancelled;
}
}

/** @return {@code true} if the task has an error, {@code false} otherwise. */
/**
* @return {@code true} if the task has an error, {@code false} otherwise.
*/
public boolean isFaulted() {
synchronized (lock) {
return getError() != null;
}
}

/** @return The result of the task, if set. {@code null} otherwise. */
/**
* @return The result of the task, if set. {@code null} otherwise.
*/
public TResult getResult() {
synchronized (lock) {
return result;
}
}

/** @return The error for the task, if set. {@code null} otherwise. */
/**
* @return The error for the task, if set. {@code null} otherwise.
*/
public Exception getError() {
synchronized (lock) {
if (error != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public TaskCompletionSource() {
task = new Task<>();
}

/** @return the Task associated with this TaskCompletionSource. */
/**
* @return the Task associated with this TaskCompletionSource.
*/
public Task<TResult> getTask() {
return task;
}
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
2 changes: 2 additions & 0 deletions parse/build.gradle
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"
Expand Down Expand Up @@ -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"

testImplementation "org.junit.jupiter:junit-jupiter:$rootProject.ext.jupiterVersion"
testImplementation "org.skyscreamer:jsonassert:1.5.0"
Expand Down
135 changes: 135 additions & 0 deletions parse/src/main/java/com/parse/EncryptedFileObjectStore.java
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());
}
}

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());
}
}
},
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());
}
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");
return null;
},
ParseExecutors.io());
}
}
4 changes: 3 additions & 1 deletion parse/src/main/java/com/parse/ManifestInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ private static ApplicationInfo getApplicationInfo(Context context, int flags) {
}
}

/** @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise null. */
/**
* @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise null.
*/
public static Bundle getApplicationMetadata(Context context) {
ApplicationInfo info = getApplicationInfo(context, PackageManager.GET_META_DATA);
if (info != null) {
Expand Down
4 changes: 3 additions & 1 deletion parse/src/main/java/com/parse/Parse.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ public static void destroy() {
allowCustomObjectId = false;
}

/** @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. */
/**
* @return {@code True} if {@link #initialize} has been called, otherwise {@code false}.
*/
static boolean isInitialized() {
return ParsePlugins.get() != null;
}
Expand Down
4 changes: 3 additions & 1 deletion parse/src/main/java/com/parse/ParseClassName.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
@Inherited
@Documented
public @interface ParseClassName {
/** @return The Parse class name associated with the ParseObject subclass. */
/**
* @return The Parse class name associated with the ParseObject subclass.
*/
String value();
}
7 changes: 6 additions & 1 deletion parse/src/main/java/com/parse/ParseCorePlugins.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ public ParseCurrentUserController getCurrentUserController() {
Parse.isLocalDatastoreEnabled()
? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore)
: fileStore;
ParseCurrentUserController controller = new CachedCurrentUserController(store);
EncryptedFileObjectStore<ParseUser> encryptedFileObjectStore =
new EncryptedFileObjectStore<>(
ParseUser.class, file, ParseUserCurrentCoder.get());
ParseObjectStoreMigrator<ParseUser> storeMigrator =
new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store);
ParseCurrentUserController controller = new CachedCurrentUserController(storeMigrator);
currentUserController.compareAndSet(null, controller);
}
return currentUserController.get();
Expand Down
Loading
Loading