Skip to content

Commit 2a92c2e

Browse files
committed
feat: add framework for KernalModeTrust to secure core modules from extensions
1 parent 0e85f2e commit 2a92c2e

File tree

6 files changed

+273
-57
lines changed

6 files changed

+273
-57
lines changed

src/document/DocumentCommandHandlers.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ define(function (require, exports, module) {
6161
NodeUtils = require("utils/NodeUtils"),
6262
_ = require("thirdparty/lodash");
6363

64+
const KernalModeTrust = window.KernalModeTrust;
65+
if(!KernalModeTrust){
66+
throw new Error("KernalModeTrust is not defined. Cannot boot without trust ring");
67+
}
68+
async function _resetTauriTrustRingBeforeRestart() {
69+
// This is needed as if for a given tauri window, the trust ring can only be set once. So reloading the app
70+
// in the same window, tauri will deny setting new keys.
71+
// this is a security measure to prevent a malicious extension from setting its own key.
72+
try {
73+
await KernalModeTrust.dismantleKeyring();
74+
} catch (e) {
75+
console.error("Error while resetting trust ring before restart", e);
76+
}
77+
}
78+
6479
/**
6580
* Handlers for commands related to document handling (opening, saving, etc.)
6681
*/
@@ -2073,6 +2088,9 @@ define(function (require, exports, module) {
20732088
.finally(()=>{
20742089
raceAgainstTime(_safeNodeTerminate(), 4000)
20752090
.finally(()=>{
2091+
_resetTauriTrustRingBeforeRestart();
2092+
// we do not wait/raceAgainstTime here purposefully to prevent attacks that will rely
2093+
// on this brief window of no trust zone in while the kernal trust key is being reset.
20762094
window.location.href = href;
20772095
});
20782096
});

src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
function _isTestWindow() {
8888
// the test window query param will only be acknowledged if we are embedded in the spec runner.
8989
// and test windows should be embedded within the same host as phcode.dev/tauri for security
90+
// please see trust_ring.js before doing any changes to this check
9091
const isTestPhoenixWindow = window.parent.location.pathname.endsWith("SpecRunner.html") &&
9192
window.parent.location.host === window.location.host &&
9293
!!(new window.URLSearchParams(window.location.search || "")).get("testEnvironment");

src/phoenix/shell.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import initVFS from "./init_vfs.js";
3030
import ERR_CODES from "./errno.js";
3131
import { LRUCache } from '../thirdparty/no-minify/lru-cache.js';
3232
import * as Emmet from '../thirdparty/emmet.es.js';
33+
import {initTrustRing} from "./trust_ring.js";
3334

35+
initTrustRing()
36+
.catch(console.error);
3437
initVFS();
3538

3639
// We can only have a maximum of 30 windows that have access to tauri apis

src/phoenix/trust_ring.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Generate random AES-256 key and GCM nonce/IV
2+
function generateRandomKeyAndIV() {
3+
// Generate 32 random bytes for AES-256 key
4+
const keyBytes = new Uint8Array(32);
5+
crypto.getRandomValues(keyBytes);
6+
7+
// Generate 12 random bytes for AES-GCM nonce/IV
8+
const ivBytes = new Uint8Array(12);
9+
crypto.getRandomValues(ivBytes);
10+
11+
// Convert to hex strings
12+
const key = Array.from(keyBytes)
13+
.map(byte => byte.toString(16).padStart(2, '0'))
14+
.join('');
15+
16+
const iv = Array.from(ivBytes)
17+
.map(byte => byte.toString(16).padStart(2, '0'))
18+
.join('');
19+
20+
return { key, iv };
21+
}
22+
23+
async function AESDecryptString(val, key, iv) {
24+
// Convert hex strings to ArrayBuffers
25+
const encryptedData = new Uint8Array(val.length / 2);
26+
for (let i = 0; i < val.length; i += 2) {
27+
encryptedData[i / 2] = parseInt(val.substr(i, 2), 16);
28+
}
29+
30+
const keyBytes = new Uint8Array(key.length / 2);
31+
for (let i = 0; i < key.length; i += 2) {
32+
keyBytes[i / 2] = parseInt(key.substr(i, 2), 16);
33+
}
34+
35+
const ivBytes = new Uint8Array(iv.length / 2);
36+
for (let i = 0; i < iv.length; i += 2) {
37+
ivBytes[i / 2] = parseInt(iv.substr(i, 2), 16);
38+
}
39+
40+
// Import the AES key
41+
const cryptoKey = await crypto.subtle.importKey(
42+
'raw',
43+
keyBytes,
44+
{ name: 'AES-GCM' },
45+
false,
46+
['decrypt']
47+
);
48+
49+
// Decrypt the data
50+
const decryptedBuffer = await crypto.subtle.decrypt(
51+
{
52+
name: 'AES-GCM',
53+
iv: ivBytes
54+
},
55+
cryptoKey,
56+
encryptedData
57+
);
58+
59+
// Convert back to string
60+
return new TextDecoder('utf-8').decode(decryptedBuffer);
61+
}
62+
63+
const TEMP_KV_TRUST_FOR_TESTSUITE = "TEMP_KV_TRUST_FOR_TESTSUITE";
64+
function _selectKeys() {
65+
if (Phoenix.isTestWindow) {
66+
// this could be an iframe in a spec runner window or the spec runner window itself.
67+
const kvj = window.top.sessionStorage.getItem(TEMP_KV_TRUST_FOR_TESTSUITE);
68+
if(!kvj) {
69+
const kv = generateRandomKeyAndIV();
70+
window.top.sessionStorage.setItem(TEMP_KV_TRUST_FOR_TESTSUITE, JSON.stringify(kv));
71+
return kv;
72+
}
73+
try{
74+
return JSON.parse(kvj);
75+
} catch (e) {
76+
console.error("Error parsing test suite trust keyring, defaulting to random which may not work!", e);
77+
}
78+
}
79+
return generateRandomKeyAndIV();
80+
}
81+
82+
const PHCODE_API_KEY = "PHCODE_API_KEY";
83+
const { key, iv } = _selectKeys();
84+
// this key is set at boot time as a truct base for all the core components before any extensions are loaded.
85+
// just before extensions are loaded, this key is blanked. This can be used by core modules to talk with other
86+
// core modules securely without worrying about interception by extensions.
87+
// KernalModeTrust should only be available within all code that loads before the first default/any extension.
88+
window.KernalModeTrust = {
89+
aesKeys: { key, iv },
90+
setPhoenixAPIKey,
91+
getPhoenixAPIKey,
92+
removePhoenixAPIKey,
93+
AESDecryptString,
94+
generateRandomKeyAndIV,
95+
dismantleKeyring
96+
};
97+
if(Phoenix.isSpecRunnerWindow){
98+
window.specRunnerTestKernalModeTrust = window.KernalModeTrust;
99+
}
100+
// key is 64 hex characters, iv is 24 hex characters
101+
102+
async function setPhoenixAPIKey(apiKey) {
103+
if(!window.__TAURI__){
104+
throw new Error("Phoenix API key can only be set in tauri shell!");
105+
}
106+
return window.__TAURI__.tauri.invoke("store_credential", {scopeName: PHCODE_API_KEY, secretVal: apiKey});
107+
}
108+
109+
async function getPhoenixAPIKey() {
110+
if(!window.__TAURI__){
111+
throw new Error("Phoenix API key can only be get in tauri shell!");
112+
}
113+
const encryptedKey = await window.__TAURI__.tauri.invoke("get_credential", {scopeName: PHCODE_API_KEY});
114+
if(!encryptedKey){
115+
return null;
116+
}
117+
return AESDecryptString(encryptedKey, key, iv);
118+
}
119+
120+
async function removePhoenixAPIKey() {
121+
if(!window.__TAURI__){
122+
throw new Error("Phoenix API key can only be set in tauri shell!");
123+
}
124+
return window.__TAURI__.tauri.invoke("delete_credential", {scopeName: PHCODE_API_KEY});
125+
}
126+
127+
let _dismatled = false;
128+
async function dismantleKeyring() {
129+
if(!_dismatled){
130+
throw new Error("Keyring can only be dismantled once!");
131+
// and once dismantled, the next line should be reload page. this is a strict security posture requirement to
132+
// prevent extensions from stealing sensitive info from system key ring as once the trust in invalidated,
133+
// the tauri get_system key ring cred apis will work for anyone who does the first call.
134+
}
135+
_dismatled = true;
136+
if(!key || !iv){
137+
console.error("Invalid kernal keys supplied to shutdown. Ignoring kernal trust reset at shutdown.");
138+
return;
139+
}
140+
if(!window.__TAURI__){
141+
return;
142+
}
143+
return window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", {key, iv});
144+
}
145+
146+
export async function initTrustRing() {
147+
if(!window.__TAURI__){
148+
return;
149+
}
150+
await window.__TAURI__.tauri.invoke("trust_window_aes_key", {key, iv});
151+
}

src/utils/ExtensionLoader.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,10 @@ define(function (require, exports, module) {
902902
var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled");
903903
FileSystem.getDirectoryForPath(disabledExtensionPath).create();
904904

905+
// just before extensions are loaded, we need to delete the boot time trust ring keys so that extensions
906+
// won't have keys to enter kernal mode in the app.
907+
delete window.KernalModeTrust;
908+
905909
var promise = Async.doInParallel(paths, function (extPath) {
906910
if(extPath === "default"){
907911
return loadAllDefaultExtensions();

0 commit comments

Comments
 (0)