Skip to content

Commit a7a1aaf

Browse files
committed
feat: Replace crypto-js with webcrypto
1 parent 478147c commit a7a1aaf

14 files changed

+205
-136
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ A library that gives you access to the powerful Parse Server backend from your J
3030
- [Getting Started](#getting-started)
3131
- [Using Parse on Different Platforms](#using-parse-on-different-platforms)
3232
- [Core Manager](#core-manager)
33+
= [Encrypt Local Storage](#encrypt-local-storage)
3334
- [3rd Party Authentications](#3rd-party-authentications)
3435
- [Experimenting](#experimenting)
3536
- [Contributing](#contributing)
@@ -121,6 +122,36 @@ Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1)
121122
Parse.CoreManager.setRESTController(MyRESTController);
122123
```
123124

125+
#### Encrypt Local Storage
126+
127+
The SDK has a [CryptoController][crypto-controller] that handles encrypting and decrypting local storage data
128+
such as logged in `Parse.User`.
129+
130+
```
131+
// Set your key to enable encryption, this key will be passed to the CryptoController
132+
Parse.secret = 'MY_SECRET_KEY'; // or Parse.CoreManager.set('ENCRYPTED_KEY', 'MY_SECRET_KEY');
133+
```
134+
135+
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.
136+
137+
We recommend creating your own [CryptoController][crypto-controller].
138+
139+
```
140+
const CustomCryptoController = {
141+
async: 1,
142+
async encrypt(json: any, parseSecret: any): Promise<string> {
143+
const encryptedJSON = await customEncrypt(json);
144+
return encryptedJSON;
145+
},
146+
async decrypt(encryptedJSON: string, parseSecret: any): Promise<string> {
147+
const json = await customDecrypt(encryptedJSON);
148+
return JSON.stringify(json);
149+
},
150+
};
151+
// Must be called before Parse.initialize
152+
Parse.CoreManager.setCryptoController(CustomCryptoController);
153+
```
154+
124155
## 3rd Party Authentications
125156

126157
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].
@@ -143,7 +174,10 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
143174
[3rd-party-auth]: http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
144175
[contributing]: https://github.com/parse-community/Parse-SDK-JS/blob/master/CONTRIBUTING.md
145176
[core-manager]: https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/CoreManager.ts
177+
[crypto-controller]: https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/CryptoController.ts
146178
[custom-auth-module]: https://docs.parseplatform.org/js/guide/#custom-authentication-module
147179
[link-with]: https://docs.parseplatform.org/js/guide/#linking-users
148180
[open-collective-link]: https://opencollective.com/parse-server
181+
[react-native-webview-crypto]: https://www.npmjs.com/package/react-native-webview-crypto
149182
[types-parse]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/parse
183+
[webcrypto]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API

integration/test/ParseDistTest.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,21 @@ for (const fileName of ['parse.js', 'parse.min.js']) {
8282
expect(requestsCount).toBe(1);
8383
expect(abortedCount).toBe(1);
8484
});
85+
86+
it('can encrypt a user', async () => {
87+
const user = new Parse.User();
88+
user.setUsername('usernameENC');
89+
user.setPassword('passwordENC');
90+
await user.signUp();
91+
const response = await page.evaluate(async () => {
92+
Parse.secret = 'My Secret Key';
93+
await Parse.User.logIn('usernameENC', 'passwordENC');
94+
const current = await Parse.User.currentAsync();
95+
Parse.secret = undefined;
96+
return current.id;
97+
});
98+
expect(response).toBeDefined();
99+
expect(user.id).toEqual(response);
100+
});
85101
});
86102
}

integration/test/ParseReactNativeTest.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ describe('Parse React Native', () => {
4141
it('can encrypt user', async () => {
4242
// Handle Crypto Controller
4343
Parse.User.enableUnsafeCurrentUser();
44-
Parse.enableEncryptedUser();
4544
Parse.secret = 'My Secret Key';
4645
const user = new Parse.User();
4746
user.setUsername('usernameENC');
@@ -53,7 +52,10 @@ describe('Parse React Native', () => {
5352

5453
const crypto = Parse.CoreManager.getCryptoController();
5554

56-
const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
55+
const decryptedUser = await crypto.decrypt(
56+
encryptedUser,
57+
Parse.CoreManager.get('ENCRYPTED_KEY')
58+
);
5759
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);
5860

5961
const currentUser = Parse.User.current();
@@ -62,7 +64,6 @@ describe('Parse React Native', () => {
6264
const currentUserAsync = await Parse.User.currentAsync();
6365
expect(currentUserAsync).toEqual(user);
6466
await Parse.User.logOut();
65-
Parse.CoreManager.set('ENCRYPTED_USER', false);
6667
Parse.CoreManager.set('ENCRYPTED_KEY', null);
6768
});
6869

integration/test/ParseUserTest.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,7 +1139,6 @@ describe('Parse User', () => {
11391139

11401140
it('can encrypt user', async () => {
11411141
Parse.User.enableUnsafeCurrentUser();
1142-
Parse.enableEncryptedUser();
11431142
Parse.secret = 'My Secret Key';
11441143
const user = new Parse.User();
11451144
user.setUsername('usernameENC');
@@ -1150,7 +1149,10 @@ describe('Parse User', () => {
11501149
const encryptedUser = Parse.Storage.getItem(path);
11511150

11521151
const crypto = Parse.CoreManager.getCryptoController();
1153-
const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
1152+
const decryptedUser = await crypto.decrypt(
1153+
encryptedUser,
1154+
Parse.CoreManager.get('ENCRYPTED_KEY')
1155+
);
11541156
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);
11551157

11561158
const currentUser = Parse.User.current();
@@ -1159,7 +1161,6 @@ describe('Parse User', () => {
11591161
const currentUserAsync = await Parse.User.currentAsync();
11601162
expect(currentUserAsync).toEqual(user);
11611163
await Parse.User.logOut();
1162-
Parse.CoreManager.set('ENCRYPTED_USER', false);
11631164
Parse.CoreManager.set('ENCRYPTED_KEY', null);
11641165
});
11651166

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"dependencies": {
3232
"@babel/runtime-corejs3": "7.26.10",
3333
"idb-keyval": "6.2.1",
34-
"react-native-crypto-js": "1.0.0",
3534
"uuid": "10.0.0",
3635
"ws": "8.18.1",
3736
"xmlhttprequest": "1.8.0"
@@ -92,9 +91,6 @@
9291
"typescript-eslint": "8.26.0",
9392
"vinyl-source-stream": "2.0.0"
9493
},
95-
"optionalDependencies": {
96-
"crypto-js": "4.2.0"
97-
},
9894
"scripts": {
9995
"build": "node build_releases.js",
10096
"build:types": "tsc",

src/CoreManager.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,17 @@ type ConfigController = {
3232
masterKeyOnlyFlags?: { [key: string]: any }
3333
) => Promise<void>;
3434
};
35-
type CryptoController = {
36-
encrypt: (obj: any, secretKey: string) => string;
37-
decrypt: (encryptedText: string, secretKey: any) => string;
38-
};
35+
type CryptoController =
36+
| {
37+
async: 0;
38+
encrypt: (json: any, parseSecret: any) => string;
39+
decrypt: (encryptedJSON: string, secretKey: any) => string;
40+
}
41+
| {
42+
async: 1;
43+
encrypt: (json: any, parseSecret: any) => Promise<string>;
44+
decrypt: (encryptedJSON: string, secretKey: any) => Promise<string>;
45+
};
3946
type FileController = {
4047
saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise<any>;
4148
saveBase64: (
@@ -358,7 +365,6 @@ const config: Config & { [key: string]: any } = {
358365
USE_MASTER_KEY: false,
359366
PERFORM_USER_REWRITE: true,
360367
FORCE_REVOCABLE_SESSION: false,
361-
ENCRYPTED_USER: false,
362368
IDEMPOTENCY: false,
363369
ALLOW_CUSTOM_OBJECT_ID: false,
364370
PARSE_ERRORS: [],

src/CryptoController.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,66 @@
1-
let AES: any;
2-
let ENC: any;
3-
4-
if (process.env.PARSE_BUILD === 'react-native') {
5-
const CryptoJS = require('react-native-crypto-js');
6-
AES = CryptoJS.AES;
7-
ENC = CryptoJS.enc.Utf8;
1+
let webcrypto;
2+
let encoder;
3+
let decoder;
4+
if (typeof window !== 'undefined' && window.crypto && process.env.PARSE_BUILD !== 'node') {
5+
webcrypto = window.crypto;
6+
encoder = new TextEncoder();
7+
decoder = new TextDecoder();
88
} else {
9-
AES = require('crypto-js/aes');
10-
ENC = require('crypto-js/enc-utf8');
9+
const { TextEncoder, TextDecoder } = require('util');
10+
webcrypto = require('crypto').webcrypto;
11+
encoder = new TextEncoder();
12+
decoder = new TextDecoder();
1113
}
1214

15+
const bufferToBase64 = buff =>
16+
btoa(new Uint8Array(buff).reduce((data, byte) => data + String.fromCharCode(byte), ''));
17+
18+
const base64ToBuffer = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(null));
19+
20+
const importKey = async key =>
21+
webcrypto.subtle.importKey('raw', encoder.encode(key), 'PBKDF2', false, ['deriveKey']);
22+
23+
const deriveKey = (key, salt, keyUsage) =>
24+
webcrypto.subtle.deriveKey(
25+
{
26+
salt,
27+
name: 'PBKDF2',
28+
iterations: 250000,
29+
hash: 'SHA-256',
30+
},
31+
key,
32+
{ name: 'AES-GCM', length: 256 },
33+
false,
34+
keyUsage
35+
);
36+
1337
const CryptoController = {
14-
encrypt(obj: any, secretKey: string): string {
15-
const encrypted = AES.encrypt(JSON.stringify(obj), secretKey);
16-
return encrypted.toString();
38+
async: 1,
39+
async encrypt(json: any, parseSecret: any): Promise<string> {
40+
const salt = webcrypto.getRandomValues(new Uint8Array(16));
41+
const iv = webcrypto.getRandomValues(new Uint8Array(12));
42+
const key = await importKey(parseSecret);
43+
const aesKey = await deriveKey(key, salt, ['encrypt']);
44+
const encodedData = encoder.encode(JSON.stringify(json));
45+
const encrypted = await webcrypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encodedData);
46+
const encryptedArray = new Uint8Array(encrypted);
47+
const buffer = new Uint8Array(salt.byteLength + iv.byteLength + encryptedArray.byteLength);
48+
buffer.set(salt, 0);
49+
buffer.set(iv, salt.byteLength);
50+
buffer.set(encryptedArray, salt.byteLength + iv.byteLength);
51+
const base64Buffer = bufferToBase64(buffer);
52+
return base64Buffer;
1753
},
1854

19-
decrypt(encryptedText: string, secretKey: string): string {
20-
const decryptedStr = AES.decrypt(encryptedText, secretKey).toString(ENC);
21-
return decryptedStr;
55+
async decrypt(encryptedJSON: string, parseSecret: any): Promise<string> {
56+
const buffer = base64ToBuffer(encryptedJSON);
57+
const salt = buffer.slice(0, 16);
58+
const iv = buffer.slice(16, 16 + 12);
59+
const data = buffer.slice(16 + 12);
60+
const key = await importKey(parseSecret);
61+
const aesKey = await deriveKey(key, salt, ['decrypt']);
62+
const decrypted = await webcrypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, data);
63+
return decoder.decode(decrypted);
2264
},
2365
};
2466

src/Parse.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -270,17 +270,6 @@ const Parse = {
270270
return CoreManager.get('LIVEQUERY_SERVER_URL');
271271
},
272272

273-
/**
274-
* @member {boolean} Parse.encryptedUser
275-
* @static
276-
*/
277-
set encryptedUser(value: boolean) {
278-
CoreManager.set('ENCRYPTED_USER', value);
279-
},
280-
get encryptedUser() {
281-
return CoreManager.get('ENCRYPTED_USER');
282-
},
283-
284273
/**
285274
* @member {string} Parse.secret
286275
* @static
@@ -381,26 +370,6 @@ const Parse = {
381370
return Parse.LocalDatastore._getAllContents();
382371
}
383372
},
384-
385-
/**
386-
* Enable the current user encryption.
387-
* This must be called before login any user.
388-
*
389-
* @static
390-
*/
391-
enableEncryptedUser() {
392-
this.encryptedUser = true;
393-
},
394-
395-
/**
396-
* Flag that indicates whether Encrypted User is enabled.
397-
*
398-
* @static
399-
* @returns {boolean}
400-
*/
401-
isEncryptedUserEnabled() {
402-
return this.encryptedUser;
403-
},
404373
};
405374

406375
CoreManager.setRESTController(RESTController);

0 commit comments

Comments
 (0)