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

Encrypted Current User in browser #1036

Merged
merged 7 commits into from
Dec 24, 2019
Merged
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
26 changes: 26 additions & 0 deletions integration/test/ParseUserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,32 @@ describe('Parse User', () => {
expect(user.get('authData').facebook.id).toBe('test');
});

it('can encrypt user', async () => {
Parse.User.enableUnsafeCurrentUser();
Parse.enableEncryptedUser();
Parse.secret = 'My Secret Key';
const user = new Parse.User();
user.setUsername('usernameENC');
user.setPassword('passwordENC');
await user.signUp();

const path = Parse.Storage.generatePath('currentUser');
const encryptedUser = Parse.Storage.getItem(path);

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

const currentUser = Parse.User.current();
expect(currentUser).toEqual(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);
});

it('fix GHSA-wvh7-5p38-2qfc', async () => {
Parse.User.enableUnsafeCurrentUser();
const user = new Parse.User();
Expand Down
20 changes: 13 additions & 7 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"@babel/runtime": "7.7.7",
"@babel/runtime-corejs3": "7.7.7",
"crypto-js": "3.1.9-1",
"uuid": "3.3.3",
"ws": "7.2.1",
"xmlhttprequest": "1.8.0"
Expand Down
17 changes: 16 additions & 1 deletion src/CoreManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type ConfigController = {
get: () => Promise;
save: (attrs: { [key: string]: any }) => Promise;
};
type CryptoController = {
encrypt: (obj: any, secretKey: string) => string;
decrypt: (encryptedText: string, secretKey: any) => string;
};
type FileController = {
saveFile: (name: string, source: FileSource, options: FullOptions) => Promise;
saveBase64: (name: string, source: FileSource, options: FullOptions) => Promise;
Expand Down Expand Up @@ -176,13 +180,15 @@ const config: Config & { [key: string]: mixed } = {
SERVER_AUTH_TYPE: null,
SERVER_AUTH_TOKEN: null,
LIVEQUERY_SERVER_URL: null,
ENCRYPTED_KEY: null,
VERSION: 'js' + require('../package.json').version,
APPLICATION_ID: null,
JAVASCRIPT_KEY: null,
MASTER_KEY: null,
USE_MASTER_KEY: false,
PERFORM_USER_REWRITE: true,
FORCE_REVOCABLE_SESSION: false
FORCE_REVOCABLE_SESSION: false,
ENCRYPTED_USER: false
};

function requireMethods(name: string, methods: Array<string>, controller: any) {
Expand Down Expand Up @@ -234,6 +240,15 @@ module.exports = {
return config['ConfigController'];
},

setCryptoController(controller: CryptoController) {
requireMethods('CryptoController', ['encrypt', 'decrypt'], controller);
config['CryptoController'] = controller;
},

getCryptoController(): CryptoController {
return config['CryptoController'];
},

setFileController(controller: FileController) {
requireMethods('FileController', ['saveFile', 'saveBase64'], controller);
config['FileController'] = controller;
Expand Down
16 changes: 16 additions & 0 deletions src/CryptoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import AES from 'crypto-js/aes';
import ENC from 'crypto-js/enc-utf8';

const CryptoController = {
encrypt(obj: any, secretKey: string): ?string {
const encrypted = AES.encrypt(JSON.stringify(obj), secretKey);
return encrypted.toString();
},

decrypt(encryptedText: string, secretKey: string): ?string {
const decryptedStr = AES.decrypt(encryptedText, secretKey).toString(ENC);
return decryptedStr;
},
};

module.exports = CryptoController;
50 changes: 50 additions & 0 deletions src/Parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import decode from './decode';
import encode from './encode';
import CoreManager from './CoreManager';
import CryptoController from './CryptoController';
import InstallationController from './InstallationController';
import * as ParseOp from './ParseOp';
import RESTController from './RESTController';
Expand Down Expand Up @@ -169,6 +170,34 @@ Object.defineProperty(Parse, 'liveQueryServerURL', {
CoreManager.set('LIVEQUERY_SERVER_URL', value);
}
});

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

/**
* @member Parse.secret
* @type string
* @static
*/
Object.defineProperty(Parse, 'secret', {
get() {
return CoreManager.get('ENCRYPTED_KEY');
},
set(value) {
CoreManager.set('ENCRYPTED_KEY', value);
}
});
/* End setters */

Parse.ACL = require('./ParseACL').default;
Expand Down Expand Up @@ -255,6 +284,27 @@ Parse.dumpLocalDatastore = function() {
return Parse.LocalDatastore._getAllContents();
}
}

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

/**
* Flag that indicates whether Encrypted User is enabled.
*
* @static
*/
Parse.isEncryptedUserEnabled = function() {
return Parse.encryptedUser;
}

CoreManager.setCryptoController(CryptoController);
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController(RESTController);

Expand Down
15 changes: 14 additions & 1 deletion src/ParseUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -872,8 +872,13 @@ const DefaultController = {
delete json.password;

json.className = '_User';
let userData = JSON.stringify(json);
if (CoreManager.get('ENCRYPTED_USER')) {
const crypto = CoreManager.getCryptoController();
userData = crypto.encrypt(json, CoreManager.get('ENCRYPTED_KEY'))
}
return Storage.setItemAsync(
path, JSON.stringify(json)
path, userData
).then(() => {
return user;
});
Expand Down Expand Up @@ -918,6 +923,10 @@ const DefaultController = {
currentUserCache = null;
return null;
}
if (CoreManager.get('ENCRYPTED_USER')) {
const crypto = CoreManager.getCryptoController();
userData = crypto.decrypt(userData, CoreManager.get('ENCRYPTED_KEY'));
}
userData = JSON.parse(userData);
if (!userData.className) {
userData.className = '_User';
Expand Down Expand Up @@ -954,6 +963,10 @@ const DefaultController = {
currentUserCache = null;
return Promise.resolve(null);
}
if (CoreManager.get('ENCRYPTED_USER')) {
const crypto = CoreManager.getCryptoController();
userData = crypto.decrypt(userData.toString(), CoreManager.get('ENCRYPTED_KEY'));
}
userData = JSON.parse(userData);
if (!userData.className) {
userData.className = '_User';
Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/Parse-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
*/

jest.dontMock('../CoreManager');
jest.dontMock('../CryptoController');
jest.dontMock('../Parse');
jest.dontMock('../LocalDatastore');
jest.dontMock('crypto-js/aes');

const CoreManager = require('../CoreManager');
const Parse = require('../Parse');
Expand Down Expand Up @@ -109,4 +111,19 @@ describe('Parse module', () => {
LDS = await Parse.dumpLocalDatastore();
expect(LDS).toEqual({ key: 'value' });
});

it('can enable encrypter CurrentUser', () => {
jest.spyOn(console, 'log').mockImplementationOnce(() => {});
process.env.PARSE_BUILD = 'browser';
Parse.encryptedUser = false;
Parse.enableEncryptedUser();
expect(Parse.encryptedUser).toBe(true);
expect(Parse.isEncryptedUserEnabled()).toBe(true);
});

it('can set an encrypt token as String', () => {
Parse.secret = 'My Super secret key';
expect(CoreManager.get('ENCRYPTED_KEY')).toBe('My Super secret key');
expect(Parse.secret).toBe('My Super secret key');
});
});
Loading