-
-
Notifications
You must be signed in to change notification settings - Fork 735
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implementing encrypted local storage for user sessions with tests
- Loading branch information
1 parent
4653c28
commit 4d0494a
Showing
11 changed files
with
776 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
parse/src/main/java/com/parse/EncryptedFileObjectStore.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package com.parse; | ||
|
||
import android.content.Context; | ||
|
||
import androidx.security.crypto.EncryptedFile; | ||
import androidx.security.crypto.MasterKey; | ||
import com.parse.boltsinternal.Task; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.security.GeneralSecurityException; | ||
import java.util.concurrent.Callable; | ||
|
||
/** | ||
* 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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
parse/src/main/java/com/parse/ParseObjectStoreMigrator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.parse; | ||
|
||
import com.parse.boltsinternal.Continuation; | ||
import com.parse.boltsinternal.Task; | ||
|
||
import java.util.Arrays; | ||
|
||
/** | ||
* Use this utility class to migrate from one {@link ParseObjectStore} to another | ||
*/ | ||
class ParseObjectStoreMigrator<T extends ParseObject> implements ParseObjectStore<T> { | ||
|
||
private final ParseObjectStore<T> store; | ||
private final ParseObjectStore<T> legacy; | ||
|
||
/** | ||
* @param store the new {@link ParseObjectStore} to migrate to | ||
* @param legacy the old {@link ParseObjectStore} to migrate from | ||
*/ | ||
public ParseObjectStoreMigrator(ParseObjectStore<T> store, ParseObjectStore<T> legacy) { | ||
this.store = store; | ||
this.legacy = legacy; | ||
} | ||
|
||
@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); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public Task<Void> setAsync(T object) { | ||
return store.setAsync(object); | ||
} | ||
|
||
@Override | ||
public Task<Boolean> existsAsync() { | ||
return store.existsAsync().continueWithTask(new Continuation<Boolean, Task<Boolean>>() { | ||
@Override | ||
public Task<Boolean> then(Task<Boolean> task) throws Exception { | ||
if (task.getResult()) return Task.forResult(true); | ||
return legacy.existsAsync(); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public Task<Void> deleteAsync() { | ||
Task<Void> storeTask = store.deleteAsync(); | ||
return Task.whenAll(Arrays.asList(legacy.deleteAsync(), storeTask)).continueWithTask(new Continuation<Void, Task<Void>>() { | ||
@Override | ||
public Task<Void> then(Task<Void> task1) throws Exception { | ||
return storeTask; | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.parse | ||
|
||
import java.security.spec.AlgorithmParameterSpec | ||
|
||
internal val AlgorithmParameterSpec.keystoreAlias: String | ||
get() = this::class.java.getDeclaredMethod("getKeystoreAlias").invoke(this) as String |
Oops, something went wrong.