Skip to content

feat: Replace CryptoJS with Web Crypto #2501

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 6 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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 @@ -82,5 +82,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.#();
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 @@ -42,7 +42,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 @@ -54,7 +53,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 @@ -63,7 +65,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 @@ -1135,7 +1135,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 @@ -1146,7 +1145,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 @@ -1155,7 +1157,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.26.10",
"idb-keyval": "6.2.1",
"react-native-crypto-js": "1.0.0",
"uuid": "10.0.0",
"ws": "8.18.1",
"xmlhttprequest": "1.8.0"
Expand Down Expand Up @@ -94,9 +93,6 @@
"typescript-eslint": "8.27.0",
"vinyl-source-stream": "2.0.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 @@ -351,7 +358,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
31 changes: 0 additions & 31 deletions src/Parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,6 @@ const Parse = {
return CoreManager.get('LIVEQUERY_SERVER_URL');
},

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

/**
* @member {string} Parse.secret
* @static
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