diff --git a/README.md b/README.md index 7aaf5acc8..4e62e9fe3 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 { + const encryptedJSON = await customEncrypt(json); + return encryptedJSON; + }, + async decrypt(encryptedJSON: string, parseSecret: any): Promise { + 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]. @@ -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 diff --git a/integration/test/ParseDistTest.js b/integration/test/ParseDistTest.js index 090b93187..4697c59d5 100644 --- a/integration/test/ParseDistTest.js +++ b/integration/test/ParseDistTest.js @@ -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.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); + }); }); } diff --git a/integration/test/ParseReactNativeTest.js b/integration/test/ParseReactNativeTest.js index cb2faf1e1..6a1bd632e 100644 --- a/integration/test/ParseReactNativeTest.js +++ b/integration/test/ParseReactNativeTest.js @@ -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'); @@ -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(); @@ -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); }); diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index 4b1db73b4..7a9e9cd0e 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -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'); @@ -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(); @@ -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); }); diff --git a/package-lock.json b/package-lock.json index 92a0104e1..5af18da92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,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" @@ -74,9 +73,6 @@ }, "engines": { "node": "18 || 19 || 20 || 22" - }, - "optionalDependencies": { - "crypto-js": "4.2.0" } }, "node_modules/@ampproject/remapping": { @@ -10914,6 +10910,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "dev": true, "optional": true }, "node_modules/crypto-random-string": { @@ -26528,7 +26525,8 @@ "node_modules/react-native-crypto-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz", - "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==" + "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==", + "dev": true }, "node_modules/react-refresh": { "version": "0.4.3", @@ -39251,6 +39249,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "dev": true, "optional": true }, "crypto-random-string": { @@ -50826,7 +50825,8 @@ "react-native-crypto-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz", - "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==" + "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==", + "dev": true }, "react-refresh": { "version": "0.4.3", diff --git a/package.json b/package.json index e71a23e96..3c8149246 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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", diff --git a/src/CoreManager.ts b/src/CoreManager.ts index 047f2463f..dd2888daf 100644 --- a/src/CoreManager.ts +++ b/src/CoreManager.ts @@ -29,10 +29,17 @@ export interface ConfigController { get: (opts?: RequestOptions) => Promise; save: (attrs: Record, masterKeyOnlyFlags?: Record) => Promise; } -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; + decrypt: (encryptedJSON: string, secretKey: any) => Promise; + }; export interface FileController { saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise; saveBase64: ( @@ -351,7 +358,6 @@ const config: Config & Record = { USE_MASTER_KEY: false, PERFORM_USER_REWRITE: true, FORCE_REVOCABLE_SESSION: false, - ENCRYPTED_USER: false, IDEMPOTENCY: false, ALLOW_CUSTOM_OBJECT_ID: false, PARSE_ERRORS: [], diff --git a/src/CryptoController.ts b/src/CryptoController.ts index 14c1ea2b3..94babc7a7 100644 --- a/src/CryptoController.ts +++ b/src/CryptoController.ts @@ -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 { + 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 { + 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); }, }; diff --git a/src/Parse.ts b/src/Parse.ts index 32e096c97..7ff7d0867 100644 --- a/src/Parse.ts +++ b/src/Parse.ts @@ -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 @@ -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); diff --git a/src/ParseUser.ts b/src/ParseUser.ts index 5f3b39b76..afa28cc68 100644 --- a/src/ParseUser.ts +++ b/src/ParseUser.ts @@ -999,20 +999,19 @@ class ParseUser extends ParseObject { ParseObject.registerSubclass('_User', ParseUser); const DefaultController = { - updateUserOnDisk(user) { + async updateUserOnDisk(user) { const path = Storage.generatePath(CURRENT_USER_KEY); const json = user.toJSON(); delete json.password; json.className = '_User'; let userData = JSON.stringify(json); - if (CoreManager.get('ENCRYPTED_USER')) { + if (CoreManager.get('ENCRYPTED_KEY')) { const crypto = CoreManager.getCryptoController(); - userData = crypto.encrypt(json, CoreManager.get('ENCRYPTED_KEY')); + userData = await crypto.encrypt(json, CoreManager.get('ENCRYPTED_KEY')); } - return Storage.setItemAsync(path, userData).then(() => { - return user; - }); + await Storage.setItemAsync(path, userData); + return user; }, removeUserFromDisk(): Promise { @@ -1042,6 +1041,13 @@ const DefaultController = { 'storage system. Call currentUserAsync() instead.' ); } + const crypto = CoreManager.getCryptoController(); + if (CoreManager.get('ENCRYPTED_KEY') && crypto.async) { + throw new Error( + 'Cannot call currentUser() when using a platform with an async encrypted ' + + 'storage system. Call currentUserAsync() instead.' + ); + } const path = Storage.generatePath(CURRENT_USER_KEY); let userData: any = Storage.getItem(path); currentUserCacheMatchesDisk = true; @@ -1049,8 +1055,7 @@ const DefaultController = { currentUserCache = null; return null; } - if (CoreManager.get('ENCRYPTED_USER')) { - const crypto = CoreManager.getCryptoController(); + if (CoreManager.get('ENCRYPTED_KEY')) { userData = crypto.decrypt(userData, CoreManager.get('ENCRYPTED_KEY')); } userData = JSON.parse(userData); @@ -1073,7 +1078,7 @@ const DefaultController = { return current; }, - currentUserAsync(): Promise { + async currentUserAsync(): Promise { if (currentUserCache) { return Promise.resolve(currentUserCache); } @@ -1081,35 +1086,34 @@ const DefaultController = { return Promise.resolve(null); } const path = Storage.generatePath(CURRENT_USER_KEY); - return Storage.getItemAsync(path).then((userData: any) => { - currentUserCacheMatchesDisk = true; - if (!userData) { - 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'; - } - if (userData._id) { - if (userData.objectId !== userData._id) { - userData.objectId = userData._id; - } - delete userData._id; - } - if (userData._sessionToken) { - userData.sessionToken = userData._sessionToken; - delete userData._sessionToken; + let userData: any = await Storage.getItemAsync(path); + currentUserCacheMatchesDisk = true; + if (!userData) { + currentUserCache = null; + return Promise.resolve(null); + } + if (CoreManager.get('ENCRYPTED_KEY')) { + const crypto = CoreManager.getCryptoController(); + userData = await crypto.decrypt(userData.toString(), CoreManager.get('ENCRYPTED_KEY')); + } + userData = JSON.parse(userData); + if (!userData.className) { + userData.className = '_User'; + } + if (userData._id) { + if (userData.objectId !== userData._id) { + userData.objectId = userData._id; } - const current = ParseObject.fromJSON(userData) as ParseUser; - currentUserCache = current; - current._synchronizeAllAuthData(); - return Promise.resolve(current); - }); + delete userData._id; + } + if (userData._sessionToken) { + userData.sessionToken = userData._sessionToken; + delete userData._sessionToken; + } + const current = ParseObject.fromJSON(userData) as ParseUser; + currentUserCache = current; + current._synchronizeAllAuthData(); + return Promise.resolve(current); }, signUp(user: ParseUser, attrs: Attributes, options?: RequestOptions): Promise { diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index a968de268..8d92fd8f3 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -10,6 +10,9 @@ jest.dontMock('../LocalDatastore'); jest.dontMock('crypto-js/aes'); jest.setMock('../EventuallyQueue', { poll: jest.fn() }); +const { TextEncoder, TextDecoder } = require('util'); +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; global.indexedDB = require('./test_helpers/mockIndexedDB'); const CoreManager = require('../CoreManager').default; const ParseLiveQuery = require('../ParseLiveQuery').default; @@ -168,16 +171,6 @@ describe('Parse module', () => { 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); - process.env.PARSE_BUILD = 'node'; - }); - it('can set an encrypt token as String', () => { Parse.secret = 'My Super secret key'; expect(CoreManager.get('ENCRYPTED_KEY')).toBe('My Super secret key'); @@ -262,6 +255,7 @@ describe('Parse module', () => { it('can get IndexedDB storage', () => { jest.isolateModules(() => { + jest.spyOn(console, 'log').mockImplementationOnce(() => {}); expect(Parse.IndexedDB).toBeUndefined(); process.env.PARSE_BUILD = 'browser'; const ParseInstance = require('../Parse'); diff --git a/src/__tests__/ParseUser-test.js b/src/__tests__/ParseUser-test.js index 2a0a7b6bf..94c5ffb60 100644 --- a/src/__tests__/ParseUser-test.js +++ b/src/__tests__/ParseUser-test.js @@ -1750,7 +1750,6 @@ describe('ParseUser', () => { }); it('can encrypt user', async () => { - CoreManager.set('ENCRYPTED_USER', true); CoreManager.set('ENCRYPTED_KEY', 'hello'); ParseUser.enableUnsafeCurrentUser(); @@ -1778,36 +1777,34 @@ describe('ParseUser', () => { u = await ParseUser.logIn('username', 'password'); // Clear cache to read from disk ParseUser._clearCache(); - + const isCurrent = await u.isCurrentAsync(); expect(u.id).toBe('uid2'); expect(u.getSessionToken()).toBe('123abc'); - expect(u.isCurrent()).toBe(true); + expect(isCurrent).toBe(true); expect(u.authenticated()).toBe(true); - const currentUser = ParseUser.current(); - expect(currentUser.id).toBe('uid2'); + const currentUser = await ParseUser.currentAsync(); + expect(currentUser.id).toBe(u.id); ParseUser._clearCache(); const currentUserAsync = await ParseUser.currentAsync(); - expect(currentUserAsync.id).toEqual('uid2'); + expect(currentUserAsync.id).toEqual(u.id); const path = Storage.generatePath('currentUser'); const encryptedUser = Storage.getItem(path); const crypto = CoreManager.getCryptoController(); - const decryptedUser = crypto.decrypt(encryptedUser, 'hello'); + const decryptedUser = await crypto.decrypt(encryptedUser, 'hello'); expect(JSON.parse(decryptedUser).objectId).toBe(u.id); - CoreManager.set('ENCRYPTED_USER', false); CoreManager.set('ENCRYPTED_KEY', null); Storage._clear(); }); it('can encrypt user with custom CryptoController', async () => { - CoreManager.set('ENCRYPTED_USER', true); CoreManager.set('ENCRYPTED_KEY', 'hello'); const ENCRYPTED_DATA = 'encryptedString'; - + const CryptoController = CoreManager.getCryptoController(); ParseUser.enableUnsafeCurrentUser(); ParseUser._clearCache(); Storage._clear(); @@ -1831,6 +1828,7 @@ describe('ParseUser', () => { ajax() {}, }); const CustomCrypto = { + async: 0, encrypt(_obj, secretKey) { expect(secretKey).toBe('hello'); return ENCRYPTED_DATA; @@ -1855,11 +1853,23 @@ describe('ParseUser', () => { const path = Storage.generatePath('currentUser'); const userStorage = Storage.getItem(path); expect(userStorage).toBe(ENCRYPTED_DATA); - CoreManager.set('ENCRYPTED_USER', false); CoreManager.set('ENCRYPTED_KEY', null); + CoreManager.setCryptoController(CryptoController); Storage._clear(); }); + it('cannot get synchronous current user with encryption enabled', async () => { + CoreManager.set('ENCRYPTED_KEY', 'hello'); + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + expect(() => { + ParseUser.current(); + }).toThrow( + 'Cannot call currentUser() when using a platform with an async encrypted storage system. Call currentUserAsync() instead.' + ); + CoreManager.set('ENCRYPTED_KEY', null); + }); + it('can static signup a user with installationId', async () => { ParseUser.disableUnsafeCurrentUser(); ParseUser._clearCache(); diff --git a/src/__tests__/browser-test.js b/src/__tests__/browser-test.js index c09decc5e..30dce8e44 100644 --- a/src/__tests__/browser-test.js +++ b/src/__tests__/browser-test.js @@ -13,6 +13,9 @@ jest.setMock('../EventuallyQueue', { poll: jest.fn() }); const CoreManager = require('../CoreManager').default; const ParseError = require('../ParseError').default; const EventuallyQueue = require('../EventuallyQueue'); +const { TextEncoder, TextDecoder } = require('util'); +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; class XMLHttpRequest {} class XDomainRequest { diff --git a/src/__tests__/react-native-test.js b/src/__tests__/react-native-test.js index 521b37f06..957dfcb52 100644 --- a/src/__tests__/react-native-test.js +++ b/src/__tests__/react-native-test.js @@ -10,6 +10,7 @@ jest.dontMock('../ParseObject'); jest.dontMock('../Storage'); jest.dontMock('../LocalDatastoreController'); jest.dontMock('../WebSocketController'); +jest.dontMock('crypto'); jest.mock( 'react-native/Libraries/vendor/emitter/EventEmitter', () => { @@ -26,6 +27,17 @@ jest.mock( const mockEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter').default; const CoreManager = require('../CoreManager').default; +const { TextEncoder, TextDecoder } = require('util'); +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; + +const crypto = require('crypto'); +Object.defineProperty(global.self, 'crypto', { + value: { + subtle: crypto.webcrypto.subtle, + getRandomValues: crypto.getRandomValues, + }, +}); describe('React Native', () => { beforeEach(() => { @@ -41,17 +53,11 @@ describe('React Native', () => { expect(EventEmitter).toEqual(mockEmitter); }); - it('load CryptoController', () => { - const CryptoJS = require('react-native-crypto-js'); - jest.spyOn(CryptoJS.AES, 'encrypt').mockImplementation(() => { - return { - toString: () => 'World', - }; - }); + it('load CryptoController', async () => { + jest.spyOn(global.crypto.subtle, 'encrypt'); const CryptoController = require('../CryptoController').default; - const phrase = CryptoController.encrypt({}, 'salt'); - expect(phrase).toBe('World'); - expect(CryptoJS.AES.encrypt).toHaveBeenCalled(); + await CryptoController.encrypt({}, 'salt'); + expect(global.crypto.subtle.encrypt).toHaveBeenCalled(); }); it('load LocalDatastoreController', () => { diff --git a/types/CoreManager.d.ts b/types/CoreManager.d.ts index c01c1d4a9..3407a532e 100644 --- a/types/CoreManager.d.ts +++ b/types/CoreManager.d.ts @@ -28,10 +28,15 @@ export interface ConfigController { get: (opts?: RequestOptions) => Promise; save: (attrs: Record, masterKeyOnlyFlags?: Record) => Promise; } -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; + decrypt: (encryptedJSON: string, secretKey: any) => Promise; +}; export interface FileController { saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise; saveBase64: (name: string, source: FileSource, options?: FileSaveOptions) => Promise<{ diff --git a/types/CryptoController.d.ts b/types/CryptoController.d.ts index 1f6e27162..8a1439062 100644 --- a/types/CryptoController.d.ts +++ b/types/CryptoController.d.ts @@ -1,5 +1,6 @@ declare const CryptoController: { - encrypt(obj: any, secretKey: string): string; - decrypt(encryptedText: string, secretKey: string): string; + async: number; + encrypt(json: any, parseSecret: any): Promise; + decrypt(encryptedJSON: string, parseSecret: any): Promise; }; export default CryptoController; diff --git a/types/Parse.d.ts b/types/Parse.d.ts index 7369fdeed..ac66135ab 100644 --- a/types/Parse.d.ts +++ b/types/Parse.d.ts @@ -50,8 +50,24 @@ declare const Parse: { getCloudController(): import("./CoreManager").CloudController; setConfigController(controller: import("./CoreManager").ConfigController): void; getConfigController(): import("./CoreManager").ConfigController; - setCryptoController(controller: import("./CoreManager").CryptoController): void; - getCryptoController(): import("./CoreManager").CryptoController; + setCryptoController(controller: { + async: 0; + encrypt: (json: any, parseSecret: any) => string; + decrypt: (encryptedJSON: string, secretKey: any) => string; + } | { + async: 1; + encrypt: (json: any, parseSecret: any) => Promise; + decrypt: (encryptedJSON: string, secretKey: any) => Promise; + }): void; + getCryptoController(): { + async: 0; + encrypt: (json: any, parseSecret: any) => string; + decrypt: (encryptedJSON: string, secretKey: any) => string; + } | { + async: 1; + encrypt: (json: any, parseSecret: any) => Promise; + decrypt: (encryptedJSON: string, secretKey: any) => Promise; + }; setEventEmitter(eventEmitter: any): void; getEventEmitter(): any; setFileController(controller: import("./CoreManager").FileController): void; @@ -302,11 +318,6 @@ declare const Parse: { * @static */ liveQueryServerURL: any; - /** - * @member {boolean} Parse.encryptedUser - * @static - */ - encryptedUser: boolean; /** * @member {string} Parse.secret * @static @@ -354,19 +365,5 @@ declare const Parse: { * @returns {object} */ dumpLocalDatastore(): Promise; - /** - * Enable the current user encryption. - * This must be called before login any user. - * - * @static - */ - enableEncryptedUser(): void; - /** - * Flag that indicates whether Encrypted User is enabled. - * - * @static - * @returns {boolean} - */ - isEncryptedUserEnabled(): any; }; export default Parse;