Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.15.0
20.19.0
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ A library that gives you access to the powerful Parse Server backend from your J
- [Getting Started](#getting-started)
- [Using Parse on Different Platforms](#using-parse-on-different-platforms)
- [Core Manager](#core-manager)
= [Encrypt Local Storage](#encrypt-local-storage)
- [3rd Party Authentications](#3rd-party-authentications)
- [Experimenting](#experimenting)
- [Contributing](#contributing)
Expand Down Expand Up @@ -114,6 +115,36 @@ Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1)
Parse.CoreManager.setRESTController(MyRESTController);
```

#### Encrypt Local Storage

The SDK has a [CryptoController][crypto-controller] that handles encrypting and decrypting local storage data
such as logged in `Parse.User`.

```
// Set your key to enable encryption, this key will be passed to the CryptoController
Parse.secret = 'MY_SECRET_KEY'; // or Parse.CoreManager.set('ENCRYPTED_KEY', 'MY_SECRET_KEY');
```

The SDK has built-in encryption using the [Web Crypto API][webcrypto]. If your platform doesn't have Web Crypto support yet like react-native you will need to [polyfill](react-native-webview-crypto) Web Crypto.

We recommend creating your own [CryptoController][crypto-controller].

```
const CustomCryptoController = {
async: 1,
async encrypt(json: any, parseSecret: any): Promise<string> {
const encryptedJSON = await customEncrypt(json);
return encryptedJSON;
},
async decrypt(encryptedJSON: string, parseSecret: any): Promise<string> {
const json = await customDecrypt(encryptedJSON);
return JSON.stringify(json);
},
};
// Must be called before Parse.initialize
Parse.CoreManager.setCryptoController(CustomCryptoController);
```

## 3rd Party Authentications

Parse Server supports many [3rd Party Authenications][3rd-party-auth]. It is possible to [linkWith][link-with] any 3rd Party Authentication by creating a [custom authentication module][custom-auth-module].
Expand All @@ -136,6 +167,10 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
[3rd-party-auth]: http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
[contributing]: https://github.com/parse-community/Parse-SDK-JS/blob/master/CONTRIBUTING.md
[core-manager]: https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/CoreManager.ts
[crypto-controller]: https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/CryptoController.ts
[custom-auth-module]: https://docs.parseplatform.org/js/guide/#custom-authentication-module
[link-with]: https://docs.parseplatform.org/js/guide/#linking-users
[open-collective-link]: https://opencollective.com/parse-server
[react-native-webview-crypto]: https://www.npmjs.com/package/react-native-webview-crypto
[types-parse]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/parse
[webcrypto]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
16 changes: 16 additions & 0 deletions integration/test/ParseDistTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,21 @@ for (const fileName of ['parse.js', 'parse.min.js']) {
expect(requestsCount).toBe(1);
expect(abortedCount).toBe(1);
});

it('can encrypt a user', async () => {
const user = new Parse.User();
user.setUsername('usernameENC');
user.setPassword('passwordENC');
await user.signUp();
const response = await page.evaluate(async () => {
Parse.secret = 'My Secret Key';
await Parse.User.logIn('usernameENC', 'passwordENC');
const current = await Parse.User.currentAsync();
Parse.secret = undefined;
return current.id;
});
expect(response).toBeDefined();
expect(user.id).toEqual(response);
});
});
}
7 changes: 4 additions & 3 deletions integration/test/ParseReactNativeTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ describe('Parse React Native', () => {
it('can encrypt user', async () => {
// Handle Crypto Controller
Parse.User.enableUnsafeCurrentUser();
Parse.enableEncryptedUser();
Parse.secret = 'My Secret Key';
const user = new Parse.User();
user.setUsername('usernameENC');
Expand All @@ -52,7 +51,10 @@ describe('Parse React Native', () => {

const crypto = Parse.CoreManager.getCryptoController();

const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
const decryptedUser = await crypto.decrypt(
encryptedUser,
Parse.CoreManager.get('ENCRYPTED_KEY')
);
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);

const currentUser = Parse.User.current();
Expand All @@ -61,7 +63,6 @@ describe('Parse React Native', () => {
const currentUserAsync = await Parse.User.currentAsync();
expect(currentUserAsync).toEqual(user);
await Parse.User.logOut();
Parse.CoreManager.set('ENCRYPTED_USER', false);
Parse.CoreManager.set('ENCRYPTED_KEY', null);
});

Expand Down
7 changes: 4 additions & 3 deletions integration/test/ParseUserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,6 @@ describe('Parse User', () => {

it('can encrypt user', async () => {
Parse.User.enableUnsafeCurrentUser();
Parse.enableEncryptedUser();
Parse.secret = 'My Secret Key';
const user = new Parse.User();
user.setUsername('usernameENC');
Expand All @@ -1156,7 +1155,10 @@ describe('Parse User', () => {
const encryptedUser = Parse.Storage.getItem(path);

const crypto = Parse.CoreManager.getCryptoController();
const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
const decryptedUser = await crypto.decrypt(
encryptedUser,
Parse.CoreManager.get('ENCRYPTED_KEY')
);
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);

const currentUser = Parse.User.current();
Expand All @@ -1165,7 +1167,6 @@ describe('Parse User', () => {
const currentUserAsync = await Parse.User.currentAsync();
expect(currentUserAsync).toEqual(user);
await Parse.User.logOut();
Parse.CoreManager.set('ENCRYPTED_USER', false);
Parse.CoreManager.set('ENCRYPTED_KEY', null);
});

Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"dependencies": {
"@babel/runtime-corejs3": "7.28.0",
"idb-keyval": "6.2.2",
"react-native-crypto-js": "1.0.0",
"uuid": "10.0.0",
"ws": "8.18.3"
},
Expand Down Expand Up @@ -88,9 +87,6 @@
"vite-plugin-commonjs": "0.10.4",
"vite-plugin-node-polyfills": "0.24.0"
},
"optionalDependencies": {
"crypto-js": "4.2.0"
},
"scripts": {
"build": "node build_releases.js",
"build:types": "tsc",
Expand Down
16 changes: 11 additions & 5 deletions src/CoreManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ export interface ConfigController {
get: (opts?: RequestOptions) => Promise<ParseConfig>;
save: (attrs: Record<string, any>, masterKeyOnlyFlags?: Record<string, any>) => Promise<void>;
}
export interface CryptoController {
encrypt: (obj: any, secretKey: string) => string;
decrypt: (encryptedText: string, secretKey: any) => string;
}
type CryptoController =
| {
async: 0;
encrypt: (json: any, parseSecret: any) => string;
decrypt: (encryptedJSON: string, secretKey: any) => string;
}
| {
async: 1;
encrypt: (json: any, parseSecret: any) => Promise<string>;
decrypt: (encryptedJSON: string, secretKey: any) => Promise<string>;
};
export interface FileController {
saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise<any>;
saveBase64: (
Expand Down Expand Up @@ -349,7 +356,6 @@ const config: Config & Record<string, any> = {
USE_MASTER_KEY: false,
PERFORM_USER_REWRITE: true,
FORCE_REVOCABLE_SESSION: false,
ENCRYPTED_USER: false,
IDEMPOTENCY: false,
ALLOW_CUSTOM_OBJECT_ID: false,
PARSE_ERRORS: [],
Expand Down
72 changes: 57 additions & 15 deletions src/CryptoController.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,66 @@
let AES: any;
let ENC: any;

if (process.env.PARSE_BUILD === 'react-native') {
const CryptoJS = require('react-native-crypto-js');
AES = CryptoJS.AES;
ENC = CryptoJS.enc.Utf8;
let webcrypto;
let encoder;
let decoder;
if (typeof window !== 'undefined' && window.crypto && process.env.PARSE_BUILD !== 'node') {
webcrypto = window.crypto;
encoder = new TextEncoder();
decoder = new TextDecoder();
} else {
AES = require('crypto-js/aes');
ENC = require('crypto-js/enc-utf8');
const { TextEncoder, TextDecoder } = require('util');
webcrypto = require('crypto').webcrypto;
encoder = new TextEncoder();
decoder = new TextDecoder();
}

const bufferToBase64 = buff =>
btoa(new Uint8Array(buff).reduce((data, byte) => data + String.fromCharCode(byte), ''));

const base64ToBuffer = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(null));

const importKey = async key =>
webcrypto.subtle.importKey('raw', encoder.encode(key), 'PBKDF2', false, ['deriveKey']);

const deriveKey = (key, salt, keyUsage) =>
webcrypto.subtle.deriveKey(
{
salt,
name: 'PBKDF2',
iterations: 250000,
hash: 'SHA-256',
},
key,
{ name: 'AES-GCM', length: 256 },
false,
keyUsage
);

const CryptoController = {
encrypt(obj: any, secretKey: string): string {
const encrypted = AES.encrypt(JSON.stringify(obj), secretKey);
return encrypted.toString();
async: 1,
async encrypt(json: any, parseSecret: any): Promise<string> {
const salt = webcrypto.getRandomValues(new Uint8Array(16));
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const key = await importKey(parseSecret);
const aesKey = await deriveKey(key, salt, ['encrypt']);
const encodedData = encoder.encode(JSON.stringify(json));
const encrypted = await webcrypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encodedData);
const encryptedArray = new Uint8Array(encrypted);
const buffer = new Uint8Array(salt.byteLength + iv.byteLength + encryptedArray.byteLength);
buffer.set(salt, 0);
buffer.set(iv, salt.byteLength);
buffer.set(encryptedArray, salt.byteLength + iv.byteLength);
const base64Buffer = bufferToBase64(buffer);
return base64Buffer;
},

decrypt(encryptedText: string, secretKey: string): string {
const decryptedStr = AES.decrypt(encryptedText, secretKey).toString(ENC);
return decryptedStr;
async decrypt(encryptedJSON: string, parseSecret: any): Promise<string> {
const buffer = base64ToBuffer(encryptedJSON);
const salt = buffer.slice(0, 16);
const iv = buffer.slice(16, 16 + 12);
const data = buffer.slice(16 + 12);
const key = await importKey(parseSecret);
const aesKey = await deriveKey(key, salt, ['decrypt']);
const decrypted = await webcrypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, data);
return decoder.decode(decrypted);
},
};

Expand Down
33 changes: 1 addition & 32 deletions src/Parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,7 @@ const Parse = {
},

/**
* @property {boolean} Parse.encryptedUser
* @static
*/
set encryptedUser(value: boolean) {
CoreManager.set('ENCRYPTED_USER', value);
},
get encryptedUser() {
return CoreManager.get('ENCRYPTED_USER');
},

/**
* @property {string} Parse.secret
* @member {string} Parse.secret
* @static
*/
set secret(value) {
Expand Down Expand Up @@ -381,26 +370,6 @@ const Parse = {
return Parse.LocalDatastore._getAllContents();
}
},

/**
* Enable the current user encryption.
* This must be called before login any user.
*
* @static
*/
enableEncryptedUser() {
this.encryptedUser = true;
},

/**
* Flag that indicates whether Encrypted User is enabled.
*
* @static
* @returns {boolean}
*/
isEncryptedUserEnabled() {
return this.encryptedUser;
},
};

CoreManager.setRESTController(RESTController);
Expand Down
Loading