From a7a1aaf260ef3ac635994343ac8f9389b78d712c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 13 Mar 2025 08:46:53 -0500 Subject: [PATCH 1/4] feat: Replace `crypto-js` with `webcrypto` --- README.md | 34 +++++++++++ integration/test/ParseDistTest.js | 16 +++++ integration/test/ParseReactNativeTest.js | 7 ++- integration/test/ParseUserTest.js | 7 ++- package-lock.json | 12 ++-- package.json | 4 -- src/CoreManager.ts | 16 +++-- src/CryptoController.ts | 72 +++++++++++++++++----- src/Parse.ts | 31 ---------- src/ParseUser.ts | 78 +++++++++++++----------- src/__tests__/Parse-test.js | 14 ++--- src/__tests__/ParseUser-test.js | 21 +++---- src/__tests__/browser-test.js | 3 + src/__tests__/react-native-test.js | 26 +++++--- 14 files changed, 205 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index ec6454279..07fef4581 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) @@ -121,6 +122,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]. @@ -143,7 +174,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 625131bb5..b03d52301 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 dac0d794e..0ff208ce7 100644 --- a/integration/test/ParseReactNativeTest.js +++ b/integration/test/ParseReactNativeTest.js @@ -41,7 +41,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'); @@ -53,7 +52,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(); @@ -62,7 +64,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 89ecfa460..6e0407800 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -1139,7 +1139,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'); @@ -1150,7 +1149,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(); @@ -1159,7 +1161,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 a4866ddd3..2e72725c9 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": { @@ -10927,6 +10923,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": { @@ -26541,7 +26538,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", @@ -39274,6 +39272,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": { @@ -50849,7 +50848,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 ddbb29069..0365b39b2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,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" @@ -92,9 +91,6 @@ "typescript-eslint": "8.26.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 927e28679..fbbf61e29 100644 --- a/src/CoreManager.ts +++ b/src/CoreManager.ts @@ -32,10 +32,17 @@ type ConfigController = { masterKeyOnlyFlags?: { [key: string]: any } ) => Promise; }; -type 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; + }; type FileController = { saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise; saveBase64: ( @@ -358,7 +365,6 @@ const config: Config & { [key: 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: [], diff --git a/src/CryptoController.ts b/src/CryptoController.ts index 211c5d283..ca8a4feff 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 730790f4c..d87679d64 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() { @@ -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 8a4d9c4ea..890508da8 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'); 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 708666b31..df0701c0b 100644 --- a/src/__tests__/ParseUser-test.js +++ b/src/__tests__/ParseUser-test.js @@ -1749,8 +1749,7 @@ describe('ParseUser', () => { }); }); - it('can encrypt user', async () => { - CoreManager.set('ENCRYPTED_USER', true); + fit('can encrypt user', async () => { CoreManager.set('ENCRYPTED_KEY', 'hello'); ParseUser.enableUnsafeCurrentUser(); @@ -1778,33 +1777,31 @@ 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'; @@ -1846,16 +1843,16 @@ describe('ParseUser', () => { // 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); expect(ParseUser.current().id).toBe('uid2'); 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); Storage._clear(); }); diff --git a/src/__tests__/browser-test.js b/src/__tests__/browser-test.js index 175cfec4a..68b47486c 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'); 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 731684760..b3a0d24a5 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'); +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'); - 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', () => { From 3d7ca81ec50d8f0ec514f118c61f4ebfcf3431e8 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 13 Mar 2025 09:48:02 -0500 Subject: [PATCH 2/4] build types --- types/CoreManager.d.ts | 9 +++++-- types/CryptoController.d.ts | 5 ++-- types/Parse.d.ts | 47 +++++++++++++------------------------ 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/types/CoreManager.d.ts b/types/CoreManager.d.ts index 93148d43c..7ec1491c0 100644 --- a/types/CoreManager.d.ts +++ b/types/CoreManager.d.ts @@ -35,8 +35,13 @@ type ConfigController = { }) => Promise; }; type CryptoController = { - encrypt: (obj: any, secretKey: string) => string; - decrypt: (encryptedText: string, secretKey: any) => string; + 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; }; type FileController = { saveFile: (name: string, source: FileSource, options?: FullOptions) => 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 cefbc562f..d47a964e1 100644 --- a/types/Parse.d.ts +++ b/types/Parse.d.ts @@ -84,12 +84,22 @@ declare const Parse: { }) => Promise; }; setCryptoController(controller: { - encrypt: (obj: any, secretKey: string) => string; - decrypt: (encryptedText: string, secretKey: any) => string; + 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(): { - encrypt: (obj: any, secretKey: string) => string; - decrypt: (encryptedText: string, secretKey: any) => string; + 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; @@ -223,10 +233,7 @@ declare const Parse: { purge: (className: string) => Promise; get: (className: string, options?: import("./RESTController").RequestOptions) => Promise; delete: (className: string, options?: import("./RESTController").RequestOptions) => Promise; - create: (className: string, params: any, options? /** - * @member {string} Parse.maintenanceKey - * @static - */: import("./RESTController").RequestOptions) => Promise; + create: (className: string, params: any, options?: import("./RESTController").RequestOptions) => Promise; update: (className: string, params: any, options?: import("./RESTController").RequestOptions) => Promise; send(className: string, method: string, params: any, options?: import("./RESTController").RequestOptions): Promise; }): void; @@ -234,10 +241,7 @@ declare const Parse: { purge: (className: string) => Promise; get: (className: string, options?: import("./RESTController").RequestOptions) => Promise; delete: (className: string, options?: import("./RESTController").RequestOptions) => Promise; - create: (className: string, params: any, options? /** - * @member {string} Parse.maintenanceKey - * @static - */: import("./RESTController").RequestOptions) => Promise; + create: (className: string, params: any, options?: import("./RESTController").RequestOptions) => Promise; update: (className: string, params: any, options?: import("./RESTController").RequestOptions) => Promise; send(className: string, method: string, params: any, options?: import("./RESTController").RequestOptions): Promise; }; @@ -563,11 +567,6 @@ declare const Parse: { * @static */ liveQueryServerURL: any; - /** - * @member {boolean} Parse.encryptedUser - * @static - */ - encryptedUser: boolean; /** * @member {string} Parse.secret * @static @@ -615,19 +614,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; From a445b85903702b0176da5b8e39378c345c00bf90 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 13 Mar 2025 11:19:10 -0500 Subject: [PATCH 3/4] fix coverage --- src/__tests__/ParseUser-test.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/__tests__/ParseUser-test.js b/src/__tests__/ParseUser-test.js index df0701c0b..5f5a42776 100644 --- a/src/__tests__/ParseUser-test.js +++ b/src/__tests__/ParseUser-test.js @@ -1749,7 +1749,7 @@ describe('ParseUser', () => { }); }); - fit('can encrypt user', async () => { + it('can encrypt user', async () => { CoreManager.set('ENCRYPTED_KEY', 'hello'); ParseUser.enableUnsafeCurrentUser(); @@ -1804,7 +1804,7 @@ describe('ParseUser', () => { it('can encrypt user with custom CryptoController', async () => { CoreManager.set('ENCRYPTED_KEY', 'hello'); const ENCRYPTED_DATA = 'encryptedString'; - + const CryptoController = CoreManager.getCryptoController(); ParseUser.enableUnsafeCurrentUser(); ParseUser._clearCache(); Storage._clear(); @@ -1828,6 +1828,7 @@ describe('ParseUser', () => { ajax() {}, }); const CustomCrypto = { + async: 0, encrypt(_obj, secretKey) { expect(secretKey).toBe('hello'); return ENCRYPTED_DATA; @@ -1843,10 +1844,9 @@ describe('ParseUser', () => { // Clear cache to read from disk ParseUser._clearCache(); - const isCurrent = await u.isCurrentAsync(); expect(u.id).toBe('uid2'); expect(u.getSessionToken()).toBe('123abc'); - expect(isCurrent).toBe(true); + expect(u.isCurrent()).toBe(true); expect(u.authenticated()).toBe(true); expect(ParseUser.current().id).toBe('uid2'); @@ -1854,9 +1854,22 @@ describe('ParseUser', () => { const userStorage = Storage.getItem(path); expect(userStorage).toBe(ENCRYPTED_DATA); 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(); From d8c9f55b79910a42d65df9484edc3637979ddc93 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 25 Mar 2025 16:51:38 -0500 Subject: [PATCH 4/4] fix conflict --- src/CoreManager.ts | 15 +++++++++++---- types/CoreManager.d.ts | 13 +++++++++---- types/Parse.d.ts | 20 ++++++++++++++++++-- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/CoreManager.ts b/src/CoreManager.ts index 97434b5c0..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: ( 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/Parse.d.ts b/types/Parse.d.ts index fceb81854..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;