Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/document/DocumentCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ define(function (require, exports, module) {
NodeUtils = require("utils/NodeUtils"),
_ = require("thirdparty/lodash");

const KernalModeTrust = window.KernalModeTrust;
if(!KernalModeTrust){
throw new Error("KernalModeTrust is not defined. Cannot boot without trust ring");
}
async function _resetTauriTrustRingBeforeRestart() {
// This is needed as if for a given tauri window, the trust ring can only be set once. So reloading the app
// in the same window, tauri will deny setting new keys.
// this is a security measure to prevent a malicious extension from setting its own key.
try {
await KernalModeTrust.dismantleKeyring();
} catch (e) {
console.error("Error while resetting trust ring before restart", e);
}
}

/**
* Handlers for commands related to document handling (opening, saving, etc.)
*/
Expand Down Expand Up @@ -2073,6 +2088,9 @@ define(function (require, exports, module) {
.finally(()=>{
raceAgainstTime(_safeNodeTerminate(), 4000)
.finally(()=>{
_resetTauriTrustRingBeforeRestart();
// we do not wait/raceAgainstTime here purposefully to prevent attacks that will rely
// on this brief window of no trust zone in while the kernal trust key is being reset.
window.location.href = href;
});
});
Expand Down
1 change: 1 addition & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
function _isTestWindow() {
// the test window query param will only be acknowledged if we are embedded in the spec runner.
// and test windows should be embedded within the same host as phcode.dev/tauri for security
// please see trust_ring.js before doing any changes to this check
const isTestPhoenixWindow = window.parent.location.pathname.endsWith("SpecRunner.html") &&
window.parent.location.host === window.location.host &&
!!(new window.URLSearchParams(window.location.search || "")).get("testEnvironment");
Expand Down
3 changes: 3 additions & 0 deletions src/phoenix/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import initVFS from "./init_vfs.js";
import ERR_CODES from "./errno.js";
import { LRUCache } from '../thirdparty/no-minify/lru-cache.js';
import * as Emmet from '../thirdparty/emmet.es.js';
import {initTrustRing} from "./trust_ring.js";

initTrustRing()
.catch(console.error);
initVFS();

// We can only have a maximum of 30 windows that have access to tauri apis
Expand Down
151 changes: 151 additions & 0 deletions src/phoenix/trust_ring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Generate random AES-256 key and GCM nonce/IV
function generateRandomKeyAndIV() {
// Generate 32 random bytes for AES-256 key
const keyBytes = new Uint8Array(32);
crypto.getRandomValues(keyBytes);

// Generate 12 random bytes for AES-GCM nonce/IV
const ivBytes = new Uint8Array(12);
crypto.getRandomValues(ivBytes);

// Convert to hex strings
const key = Array.from(keyBytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');

const iv = Array.from(ivBytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');

return { key, iv };
}

async function AESDecryptString(val, key, iv) {
// Convert hex strings to ArrayBuffers
const encryptedData = new Uint8Array(val.length / 2);
for (let i = 0; i < val.length; i += 2) {
encryptedData[i / 2] = parseInt(val.substr(i, 2), 16);
}

const keyBytes = new Uint8Array(key.length / 2);
for (let i = 0; i < key.length; i += 2) {
keyBytes[i / 2] = parseInt(key.substr(i, 2), 16);
}

const ivBytes = new Uint8Array(iv.length / 2);
for (let i = 0; i < iv.length; i += 2) {
ivBytes[i / 2] = parseInt(iv.substr(i, 2), 16);
}

// Import the AES key
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'AES-GCM' },
false,
['decrypt']
);

// Decrypt the data
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivBytes
},
cryptoKey,
encryptedData
);

// Convert back to string
return new TextDecoder('utf-8').decode(decryptedBuffer);
}

const TEMP_KV_TRUST_FOR_TESTSUITE = "TEMP_KV_TRUST_FOR_TESTSUITE";
function _selectKeys() {
if (Phoenix.isTestWindow) {
// this could be an iframe in a spec runner window or the spec runner window itself.
const kvj = window.top.sessionStorage.getItem(TEMP_KV_TRUST_FOR_TESTSUITE);
if(!kvj) {
const kv = generateRandomKeyAndIV();
window.top.sessionStorage.setItem(TEMP_KV_TRUST_FOR_TESTSUITE, JSON.stringify(kv));
return kv;
}
try{
return JSON.parse(kvj);
} catch (e) {
console.error("Error parsing test suite trust keyring, defaulting to random which may not work!", e);
}
}
return generateRandomKeyAndIV();
}

const PHCODE_API_KEY = "PHCODE_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 = {
aesKeys: { key, iv },
setPhoenixAPIKey,
getPhoenixAPIKey,
removePhoenixAPIKey,
AESDecryptString,
generateRandomKeyAndIV,
dismantleKeyring
};
if(Phoenix.isSpecRunnerWindow){
window.specRunnerTestKernalModeTrust = window.KernalModeTrust;
}
// key is 64 hex characters, iv is 24 hex characters

async function setPhoenixAPIKey(apiKey) {
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});
}

async function getPhoenixAPIKey() {
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(!encryptedKey){
return null;
}
return AESDecryptString(encryptedKey, key, iv);
}

async function removePhoenixAPIKey() {
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});
}

let _dismatled = false;
async function dismantleKeyring() {
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,
// the tauri get_system key ring cred apis will work for anyone who does the first call.
}
_dismatled = true;
if(!key || !iv){
console.error("Invalid kernal keys supplied to shutdown. Ignoring kernal trust reset at shutdown.");
return;
}
if(!window.__TAURI__){
return;
}
return window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", {key, iv});
}

export async function initTrustRing() {
if(!window.__TAURI__){
return;
}
await window.__TAURI__.tauri.invoke("trust_window_aes_key", {key, iv});
}
4 changes: 4 additions & 0 deletions src/utils/ExtensionLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,10 @@ define(function (require, exports, module) {
var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled");
FileSystem.getDirectoryForPath(disabledExtensionPath).create();

// just before extensions are loaded, we need to delete the boot time trust ring keys so that extensions
// won't have keys to enter kernal mode in the app.
delete window.KernalModeTrust;

var promise = Async.doInParallel(paths, function (extPath) {
if(extPath === "default"){
return loadAllDefaultExtensions();
Expand Down
Loading
Loading