diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index ef97165764..783376af39 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -824,18 +824,6 @@ Sorts working set by file type ## CMD\_WORKING\_SORT\_TOGGLE\_AUTO Toggles automatic working set sorting -**Kind**: global variable - - -## CMD\_TOGGLE\_SHOW\_WORKING\_SET -Toggles working set visibility - -**Kind**: global variable - - -## CMD\_TOGGLE\_SHOW\_FILE\_TABS -Toggles file tabs visibility - **Kind**: global variable diff --git a/src-node/index.js b/src-node/index.js index f28f4a2c0f..b120bfd127 100644 --- a/src-node/index.js +++ b/src-node/index.js @@ -93,6 +93,7 @@ const PHOENIX_FS_URL = `/PhoenixFS${randomNonce(8)}`; const PHOENIX_STATIC_SERVER_URL = `/Static${randomNonce(8)}`; const PHOENIX_NODE_URL = `/PhoenixNode${randomNonce(8)}`; const PHOENIX_LIVE_PREVIEW_COMM_URL = `/PreviewComm${randomNonce(8)}`; +const PHOENIX_AUTO_AUTH_URL = `/AutoAuth${randomNonce(8)}`; const savedConsoleLog = console.log; @@ -190,7 +191,8 @@ function processCommand(line) { phoenixFSURL: `ws://localhost:${port}${PHOENIX_FS_URL}`, phoenixNodeURL: `ws://localhost:${port}${PHOENIX_NODE_URL}`, staticServerURL: `http://localhost:${port}${PHOENIX_STATIC_SERVER_URL}`, - livePreviewCommURL: `ws://localhost:${port}${PHOENIX_LIVE_PREVIEW_COMM_URL}` + livePreviewCommURL: `ws://localhost:${port}${PHOENIX_LIVE_PREVIEW_COMM_URL}`, + autoAuthURL: `http://localhost:${port}${PHOENIX_AUTO_AUTH_URL}` }, jsonCmd.commandID); }); return; @@ -207,6 +209,67 @@ rl.on('line', (line) => { const localhostOnly = 'localhost'; +const AUTH_CONNECTOR_ID = "ph_auth"; +const EVENT_CONNECTED = "connected"; +let verificationCode = null; +async function setVerificationCode(code) { + verificationCode = code; +} +const nodeConnector = NodeConnector.createNodeConnector(AUTH_CONNECTOR_ID, { + setVerificationCode +}); + +const ALLOWED_ORIGIN = 'https://account.phcode.io'; +function autoAuth(req, res) { + const origin = req.headers.origin; + // localhost dev of loginService is not allowed here to not leak to production. So autoAuth is not available in + // dev builds. + const isAllowedOrigin = !origin || (ALLOWED_ORIGIN === origin); + if(!isAllowedOrigin){ + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden origin'); + return; + } + if (req.method === 'OPTIONS') { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Private-Network', 'true'); + res.writeHead(204); + res.end(); + return; + } + // Remove '/AutoAuth' from the beginning of the URL and construct file path + const url = new URL(req.url, `http://${req.headers.host}`); + const cleanPath = url.pathname.replace(PHOENIX_AUTO_AUTH_URL, ''); + // Check if the request is for the autoVerifyCode endpoint + if (cleanPath === `/autoVerifyCode` && req.method === 'GET') { + origin && res.setHeader('Access-Control-Allow-Origin', origin); + if(!verificationCode) { + res.setHeader('Content-Type', 'text/plain'); + res.writeHead(404); + res.end('Not Found'); + return; + } + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Private-Network', 'true'); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ code: verificationCode })); + verificationCode = null; // verification code is only returned once + } else if (cleanPath === `/appVerified` && req.method === 'GET') { + nodeConnector.triggerPeer(EVENT_CONNECTED, "ok"); + origin && res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Private-Network', 'true'); + res.setHeader('Content-Type', 'application/json'); + res.end("ok"); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } +} + // Create an HTTP server const server = http.createServer((req, res) => { if (req.url.startsWith(PHOENIX_STATIC_SERVER_URL)) { @@ -249,7 +312,9 @@ const server = http.createServer((req, res) => { } }); - } else { + } else if (req.url.startsWith(PHOENIX_AUTO_AUTH_URL)) { + return autoAuth(req, res); + }else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } @@ -269,5 +334,6 @@ server.listen(0, localhostOnly, () => { savedConsoleLog(`Phoenix node tauri FS url is ws://localhost:${port}${PHOENIX_FS_URL}`); savedConsoleLog(`Phoenix node connector url is ws://localhost:${port}${PHOENIX_NODE_URL}`); savedConsoleLog(`Phoenix live preview comm url is ws://localhost:${port}${PHOENIX_LIVE_PREVIEW_COMM_URL}`); + savedConsoleLog(`Phoenix AutoAuth url is ws://localhost:${port}${PHOENIX_AUTO_AUTH_URL}`); serverPortResolve(port); }); diff --git a/src/brackets.js b/src/brackets.js index 75aabed6d2..a21b33aae5 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -139,6 +139,7 @@ define(function (require, exports, module) { require("widgets/InlineMenu"); require("thirdparty/tinycolor"); require("utils/LocalizationUtils"); + require("services/login"); // DEPRECATED: In future we want to remove the global CodeMirror, but for now we // expose our required CodeMirror globally so as to avoid breaking extensions in the diff --git a/src/config.json b/src/config.json index a3c6c88410..96b5475764 100644 --- a/src/config.json +++ b/src/config.json @@ -3,9 +3,10 @@ "app_title": "Phoenix Code", "app_name_about": "Phoenix Code", "about_icon": "styles/images/phoenix-icon.svg", + "account_url": "https://account.phcode.io/", "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "docs_url": "https://docs.phcode.dev/", - "support_url": "https://github.com/phcode-dev/phoenix/discussions", + "support_url": "https://account.phcode.io/?returnUrl=https%3A%2F%2Faccount.phcode.io%2F%23support", "suggest_feature_url": "https://github.com/phcode-dev/phoenix/discussions/categories/ideas", "report_issue_url": "https://github.com/phcode-dev/phoenix/issues/new/choose", "get_involved_url": "https://github.com/phcode-dev/phoenix/discussions/77", @@ -59,4 +60,4 @@ "url": "https://github.com/phcode-dev/phoenix/blob/master/LICENSE" } ] -} \ No newline at end of file +} diff --git a/src/extensionsIntegrated/Phoenix/main.js b/src/extensionsIntegrated/Phoenix/main.js index 5bb8dcacbc..3bf88e91e0 100644 --- a/src/extensionsIntegrated/Phoenix/main.js +++ b/src/extensionsIntegrated/Phoenix/main.js @@ -18,7 +18,6 @@ * */ -/*global Phoenix*/ /*eslint no-console: 0*/ /*eslint strict: ["error", "global"]*/ /* jshint ignore:start */ @@ -32,26 +31,10 @@ define(function (require, exports, module) { Strings = require("strings"), Dialogs = require("widgets/Dialogs"), NotificationUI = require("widgets/NotificationUI"), - DefaultDialogs = require("widgets/DefaultDialogs"), - ProfileMenu = require("./profile-menu"); + DefaultDialogs = require("widgets/DefaultDialogs"); const PERSIST_STORAGE_DIALOG_DELAY_SECS = 60000; - let $icon; - function _addToolbarIcon() { - const helpButtonID = "user-profile-button"; - $icon = $("") - .attr({ - id: helpButtonID, - href: "#", - class: "user", - title: Strings.CMD_USER_PROFILE - }) - .appendTo($("#main-toolbar .bottom-buttons")); - $icon.on('click', ()=>{ - ProfileMenu.init(); - }); - } function _showUnSupportedBrowserDialogue() { if(Phoenix.browser.isMobile || Phoenix.browser.isTablet){ Dialogs.showModalDialog( @@ -109,7 +92,6 @@ define(function (require, exports, module) { if(Phoenix.isSpecRunnerWindow){ return; } - _addToolbarIcon(); serverSync.init(); defaultProjects.init(); newProject.init(); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 681ec04f45..49fc226c2c 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -629,7 +629,7 @@ define({ "CMD_AUTO_UPDATE": "Auto Update", "CMD_HOW_TO_USE_BRACKETS": "How to Use {APP_NAME}", "CMD_SUPPORT": "{APP_NAME} Support", - "CMD_USER_PROFILE": "User Profile", + "CMD_USER_PROFILE": "{APP_NAME} Account", "CMD_DOCS": "Help, Getting Started", "CMD_SUGGEST": "Suggest a Feature", "CMD_REPORT_ISSUE": "Report Issue", @@ -1564,5 +1564,27 @@ define({ "GIT_TOAST_MESSAGE": "Click the Git panel icon to manage your repository. Easily commit, push, pull, and view your project history—all in one place.
Learn more about the Git panel →", // surveys - "SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!" + "SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!", + + // login + "SIGNED_OUT": "You have been signed out.", + "SIGNED_OUT_MESSAGE": "You have been signed out of your {APP_NAME} account. Please sign in again to continue.", + "SIGNED_OUT_MESSAGE_FRIENDLY": "Thank you for using {APP_NAME}. See you soon!", + "SIGNED_IN_OFFLINE_TITLE": "Offline - Cannot Sign In", + "SIGNED_IN_OFFLINE_MESSAGE": "Please connect to the internet to sign in to {APP_NAME}.", + "SIGNED_IN_FAILED_TITLE": "Cannot Sign In", + "SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again.", + "SIGNED_OUT_FAILED_TITLE": "Failed to Sign Out", + "SIGNED_OUT_FAILED_MESSAGE": "Something went wrong while trying to sign out. Please try again.", + "VALIDATION_CODE_TITLE": "Sign In Verification Code", + "VALIDATION_CODE_MESSAGE": "Please use this Verification code to sign in to your {APP_NAME} account:", + "COPY_VALIDATION_CODE": "Copy Code", + "VALIDATION_CODE_COPIED": "Copied", + "OPEN_SIGN_IN_URL": "Open Sign In Page", + "PROFILE_POP_TITLE": "{APP_NAME} Account", + "PROFILE_SIGN_IN": "Sign in to your account", + "CONTACT_SUPPORT": "Contact support", + "SIGN_OUT": "Sign out", + "ACCOUNT_DETAILS": "Account Details", + "AI_QUOTA_USED": "AI quota used" }); diff --git a/src/node-loader.js b/src/node-loader.js index c9a7702000..6721b299af 100644 --- a/src/node-loader.js +++ b/src/node-loader.js @@ -18,9 +18,13 @@ * */ -/*global Phoenix, fs, logger*/ +/*global fs, logger*/ function nodeLoader() { + const KernalModeTrust = window.KernalModeTrust; + if(!KernalModeTrust){ + throw new Error("KernalModeTrust is not defined. Cannot boot nodeLoader without trust ring"); + } const nodeLoadstartTime = Date.now(); const phcodeExecHandlerMap = {}; const nodeConnectorIDMap = {}; @@ -721,6 +725,7 @@ function nodeLoader() { fs.setNodeWSEndpoint(message.phoenixFSURL); fs.forceUseNodeWSEndpoint(true); setNodeWSEndpoint(message.phoenixNodeURL); + KernalModeTrust.localAutoAuthURL = message.autoAuthURL; window.isNodeReady = true; resolve(message); // node is designed such that it is not required at boot time to lower startup time. diff --git a/src/phoenix/trust_ring.js b/src/phoenix/trust_ring.js index 757533c3ee..0cd11fa29f 100644 --- a/src/phoenix/trust_ring.js +++ b/src/phoenix/trust_ring.js @@ -1,3 +1,102 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * KernalModeTrust is a security mechanism in Phoenix that provides a trust base for core components before any + * extensions are loaded. It establishes a secure communication channel between core modules and the Tauri shell, + * preventing unauthorized access to sensitive information by extensions or other potentially malicious code. + * + * ## Purpose + * + * The primary purposes of KernalModeTrust are: + * + * 1. **Secure Boot Process**: Ensures that the application can only boot with a properly initialized trust ring. + * 2. **Secure Communication**: Enables core modules to communicate securely without worrying about interception by + * extensions. + * 3. **API Key Management**: Provides secure storage and retrieval of Phoenix API keys. + * 4. **Security Boundary**: Creates a clear security boundary between trusted core components and potentially untrusted + * extensions. + * + * ## Implementation Details + * + * ### Trust Ring Initialization + * + * The trust ring is initialized at boot time before any extensions are loaded: + * + * 1. Random AES-256 keys and initialization vectors (IV) are generated using the Web Crypto API. + * 2. These cryptographic materials are stored in the `window.KernalModeTrust` object. + * 3. The trust relationship is established with the Tauri backend via the `initTrustRing()` function. + * + * The trust ring has several important security characteristics: + * + * 1. **Memory-Only Storage**: The random AES key-based trust ring is only kept in memory and never persisted to disk. + * 2. **One-Time Use**: The trust ring is designed for one-time use and is discarded after serving its purpose. + * 3. **Session Lifetime**: It is maintained in memory only until the end of the Phoenix session. + * 4. **Tauri Communication**: The trust keys are communicated to the Tauri shell at boot time. + * 5. **API Response Encryption**: Once an AES key is trusted by the Tauri shell, all sensitive API responses will be + * encrypted with this key. This means extensions can still call sensitive APIs but will receive only encrypted + * garbage responses without access to the trust key. + * + * ### Security Model + * + * KernalModeTrust implements a strict security model: + * + * 1. **Boot-time Only Access**: The trust ring is only available to code that loads before any extensions. + * 2. **One-time Trust**: For a given Tauri window, the trust ring can only be set once. + * 3. **Deliberate Removal**: Before extensions are loaded, `window.KernalModeTrust` is deleted to prevent extensions + * from accessing it. + * 4. **Dismantling Before Restart**: The trust ring must be dismantled before restarting the application. This is a + * critical security requirement. If not dismantled, the old trust keys will still be in place when the page reloads, + * but the application will lose access to them (as they were only stored in memory). As a result, the Tauri shell + * will not trust any sensitive API calls from the reloaded page, as these calls will rely on the old keys that the + * new page instance cannot access. This security measure intentionally prevents any page reload from maintaining + * trust without explicitly dismantling the old trust ring first, ensuring that malicious code cannot bypass + * security by simply reloading the window. + * + * ### Cryptographic Implementation + * + * KernalModeTrust uses strong cryptography: + * + * 1. **AES-256 Encryption**: Uses AES-256 in GCM mode for secure encryption/decryption. + * 2. **Random Key Generation**: Cryptographically secure random number generation for keys and IVs. + * 3. **Secure Key Storage**: Keys are stored securely in the Tauri backend(which is stored in OS keychain). + * + * ## Security Considerations + * + * 1. **Extension Isolation**: Extensions should never have access to KernalModeTrust to prevent potential security + * breaches. + * + * 2. **One-time Trust**: The trust ring can only be set once per Tauri window, preventing malicious code from replacing + * it. + * + * 3. **Complete Dismantling**: When dismantling the keyring, it's recommended to reload the page immediately to prevent + * any potential exploitation of the system. + * + * 4. **Test Environment Handling**: Special handling exists for test environments to ensure tests can run properly + * without compromising security. + * + * ## Conclusion + * + * KernalModeTrust is a critical security component in Phoenix that establishes a trust boundary between core components + * and extensions. By providing secure communication channels and API key management, it helps maintain the overall + * security posture of the application. + * */ + // Generate random AES-256 key and GCM nonce/IV function generateRandomKeyAndIV() { // Generate 32 random bytes for AES-256 key @@ -79,17 +178,18 @@ function _selectKeys() { return generateRandomKeyAndIV(); } -const PHCODE_API_KEY = "PHCODE_API_KEY"; +const CRED_KEY_API = "API_KEY"; const { key, iv } = _selectKeys(); // this key is set at boot time as a truct base for all the core components before any extensions are loaded. // just before extensions are loaded, this key is blanked. This can be used by core modules to talk with other // core modules securely without worrying about interception by extensions. // KernalModeTrust should only be available within all code that loads before the first default/any extension. window.KernalModeTrust = { + CRED_KEY_API, aesKeys: { key, iv }, - setPhoenixAPIKey, - getPhoenixAPIKey, - removePhoenixAPIKey, + setCredential, + getCredential, + removeCredential, AESDecryptString, generateRandomKeyAndIV, dismantleKeyring @@ -99,34 +199,43 @@ if(Phoenix.isSpecRunnerWindow){ } // key is 64 hex characters, iv is 24 hex characters -async function setPhoenixAPIKey(apiKey) { +async function setCredential(credKey, secret) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be set in tauri shell!"); } - return window.__TAURI__.tauri.invoke("store_credential", {scopeName: PHCODE_API_KEY, secretVal: apiKey}); + if(!credKey){ + throw new Error("credKey is required to set credential!"); + } + return window.__TAURI__.tauri.invoke("store_credential", {scopeName: credKey, secretVal: secret}); } -async function getPhoenixAPIKey() { +async function getCredential(credKey) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be get in tauri shell!"); } - const encryptedKey = await window.__TAURI__.tauri.invoke("get_credential", {scopeName: PHCODE_API_KEY}); + if(!credKey){ + throw new Error("credKey is required to get credential!"); + } + const encryptedKey = await window.__TAURI__.tauri.invoke("get_credential", {scopeName: credKey}); if(!encryptedKey){ return null; } return AESDecryptString(encryptedKey, key, iv); } -async function removePhoenixAPIKey() { +async function removeCredential(credKey) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be set in tauri shell!"); } - return window.__TAURI__.tauri.invoke("delete_credential", {scopeName: PHCODE_API_KEY}); + if(!credKey){ + throw new Error("credKey is required to remove credential!"); + } + return window.__TAURI__.tauri.invoke("delete_credential", {scopeName: credKey}); } let _dismatled = false; async function dismantleKeyring() { - if(!_dismatled){ + if(_dismatled){ throw new Error("Keyring can only be dismantled once!"); // and once dismantled, the next line should be reload page. this is a strict security posture requirement to // prevent extensions from stealing sensitive info from system key ring as once the trust in invalidated, diff --git a/src/extensionsIntegrated/Phoenix/html/login-dialog.html b/src/services/html/login-popup.html similarity index 74% rename from src/extensionsIntegrated/Phoenix/html/login-dialog.html rename to src/services/html/login-popup.html index 9726584e22..0e0de2569b 100644 --- a/src/extensionsIntegrated/Phoenix/html/login-dialog.html +++ b/src/services/html/login-popup.html @@ -1,16 +1,16 @@
diff --git a/src/services/html/otp-dialog.html b/src/services/html/otp-dialog.html new file mode 100644 index 0000000000..8d207912bd --- /dev/null +++ b/src/services/html/otp-dialog.html @@ -0,0 +1,18 @@ + diff --git a/src/extensionsIntegrated/Phoenix/html/profile-panel.html b/src/services/html/profile-popup.html similarity index 70% rename from src/extensionsIntegrated/Phoenix/html/profile-panel.html rename to src/services/html/profile-popup.html index 1677c3d59b..a2f5d79144 100644 --- a/src/extensionsIntegrated/Phoenix/html/profile-panel.html +++ b/src/services/html/profile-popup.html @@ -1,19 +1,20 @@