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 @@