Skip to content

Commit b16eae0

Browse files
mustard-mhroboquat
authored andcommitted
[supervisor-frontend] implement encryption for code
1 parent 4692a05 commit b16eae0

File tree

5 files changed

+87
-19
lines changed

5 files changed

+87
-19
lines changed

components/gitpod-protocol/src/typings/globals.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ interface Window {
1111
loggedUserID?: string;
1212

1313
openDesktopIDE(url: string): void;
14+
15+
encrypt(content: string): string;
16+
decrypt(content: string): string;
17+
isEncryptedData(content: string): boolean;
1418
};
1519
}

components/supervisor/frontend/src/ide/ide-metrics-service-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ export class IDEMetricsServiceClient {
3636
static serviceClient: FrontendDashboardServiceClient;
3737

3838
static get instanceId(): string {
39-
return this.serviceClient.latestStatus?.instanceId ?? "";
39+
return this.serviceClient.latestInfo?.instanceId ?? "";
4040
}
4141
static get userId(): string {
42-
return this.serviceClient.latestStatus?.loggedUserId ?? "";
42+
return this.serviceClient.latestInfo?.loggedUserId ?? "";
4343
}
4444

4545
static async addCounter(

components/supervisor/frontend/src/index.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,16 @@ LoadingFrame.load().then(async (loading) => {
6868
const frontendDashboardServiceClient = loading.frontendDashboardServiceClient;
6969
await frontendDashboardServiceClient.initialize();
7070

71-
if (frontendDashboardServiceClient.latestStatus.workspaceType !== "regular") {
71+
if (frontendDashboardServiceClient.latestInfo.workspaceType !== "regular") {
7272
return;
7373
}
7474

75-
document.title = frontendDashboardServiceClient.latestStatus.workspaceDescription ?? "gitpod";
76-
window.gitpod.loggedUserID = frontendDashboardServiceClient.latestStatus.loggedUserId;
75+
document.title = frontendDashboardServiceClient.latestInfo.workspaceDescription ?? "gitpod";
76+
window.gitpod.loggedUserID = frontendDashboardServiceClient.latestInfo.loggedUserId;
7777
window.gitpod.openDesktopIDE = frontendDashboardServiceClient.openDesktopIDE.bind(frontendDashboardServiceClient);
78+
window.gitpod.decrypt = frontendDashboardServiceClient.decrypt.bind(frontendDashboardServiceClient);
79+
window.gitpod.encrypt = frontendDashboardServiceClient.encrypt.bind(frontendDashboardServiceClient);
80+
window.gitpod.isEncryptedData = frontendDashboardServiceClient.isEncryptedData.bind(frontendDashboardServiceClient);
7881

7982
(async () => {
8083
const supervisorServiceClient = SupervisorServiceClient.get();
@@ -113,7 +116,7 @@ LoadingFrame.load().then(async (loading) => {
113116
let desktopRedirected = false;
114117
let currentInstanceId = "";
115118
const nextFrame = () => {
116-
const { instanceId, ideUrl, statusPhase } = frontendDashboardServiceClient.latestStatus ?? {};
119+
const { instanceId, ideUrl, statusPhase } = frontendDashboardServiceClient.latestInfo ?? {};
117120

118121
if (instanceId) {
119122
// refresh web page when instanceId changed
@@ -209,7 +212,7 @@ LoadingFrame.load().then(async (loading) => {
209212
updateCurrentFrame();
210213
updateLoadingState();
211214
trackIDEStatusRenderedEvent();
212-
frontendDashboardServiceClient.onStatusUpdate(() => updateCurrentFrame());
215+
frontendDashboardServiceClient.onInfoUpdate(() => updateCurrentFrame());
213216
ideService.onDidChange(() => {
214217
updateLoadingState();
215218
updateCurrentFrame();
@@ -228,25 +231,25 @@ LoadingFrame.load().then(async (loading) => {
228231
//#region heart-beat
229232
heartBeat.track(window);
230233
const updateHeartBeat = () => {
231-
if (frontendDashboardServiceClient.latestStatus?.statusPhase === "running") {
234+
if (frontendDashboardServiceClient.latestInfo?.statusPhase === "running") {
232235
heartBeat.schedule(frontendDashboardServiceClient);
233236
} else {
234237
heartBeat.cancel();
235238
}
236239
};
237240
updateHeartBeat();
238-
frontendDashboardServiceClient.onStatusUpdate(() => updateHeartBeat());
241+
frontendDashboardServiceClient.onInfoUpdate(() => updateHeartBeat());
239242
//#endregion
240243
})();
241244

242245
(async () => {
243246
//#region ide lifecycle
244247
function isWorkspaceInstancePhase(phase: WorkspaceInstancePhase): boolean {
245-
return frontendDashboardServiceClient.latestStatus?.statusPhase === phase;
248+
return frontendDashboardServiceClient.latestInfo?.statusPhase === phase;
246249
}
247250
if (!isWorkspaceInstancePhase("running")) {
248251
await new Promise<void>((resolve) => {
249-
frontendDashboardServiceClient.onStatusUpdate((status) => {
252+
frontendDashboardServiceClient.onInfoUpdate((status) => {
250253
if (status.statusPhase === "running") {
251254
resolve();
252255
}
@@ -264,7 +267,7 @@ LoadingFrame.load().then(async (loading) => {
264267
}
265268
toStop.pushAll([
266269
IDEWebSocket.connectWorkspace(),
267-
frontendDashboardServiceClient.onStatusUpdate((status) => {
270+
frontendDashboardServiceClient.onInfoUpdate((status) => {
268271
if (status.statusPhase === "stopping" || status.statusPhase === "stopped") {
269272
toStop.dispose();
270273
}

components/supervisor/frontend/src/shared/frontend-dashboard-service.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7+
import * as crypto from "crypto";
78
import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";
89
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
910
import { Emitter } from "@gitpod/gitpod-protocol/lib/util/event";
1011
import { workspaceUrl, serverUrl } from "./urls";
1112

1213
export class FrontendDashboardServiceClient implements IDEFrontendDashboardService.IClient {
13-
public latestStatus!: IDEFrontendDashboardService.Status;
14+
public latestInfo!: IDEFrontendDashboardService.Info;
15+
private credentialsToken?: Uint8Array;
1416

15-
private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.Status>();
16-
readonly onStatusUpdate = this.onDidChangeEmitter.event;
17+
private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.Info>();
18+
readonly onInfoUpdate = this.onDidChangeEmitter.event;
1719

1820
private readonly onOpenBrowserIDEEmitter = new Emitter<void>();
1921
readonly onOpenBrowserIDE = this.onOpenBrowserIDEEmitter.event;
@@ -28,11 +30,16 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi
2830
if (event.origin !== serverUrl.url.origin) {
2931
return;
3032
}
31-
if (IDEFrontendDashboardService.isStatusUpdateEventData(event.data)) {
33+
if (IDEFrontendDashboardService.isInfoUpdateEventData(event.data)) {
3234
this.version = event.data.version;
33-
this.latestStatus = event.data.status;
35+
this.latestInfo = event.data.info;
36+
if (event.data.info.credentialsToken?.length > 0) {
37+
this.credentialsToken = Uint8Array.from(atob(event.data.info.credentialsToken), (c) =>
38+
c.charCodeAt(0),
39+
);
40+
}
3441
this.resolveInit();
35-
this.onDidChangeEmitter.fire(this.latestStatus);
42+
this.onDidChangeEmitter.fire(this.latestInfo);
3643
}
3744
if (IDEFrontendDashboardService.isRelocateEventData(event.data)) {
3845
window.location.href = event.data.url;
@@ -46,6 +53,50 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi
4653
return this.initPromise;
4754
}
4855

56+
decrypt(str: string): string {
57+
if (!this.credentialsToken) {
58+
throw new Error("no credentials token available");
59+
}
60+
const obj = JSON.parse(str);
61+
if (!isSerializedEncryptedData(obj)) {
62+
throw new Error("incorrect encrypted data");
63+
}
64+
const data = {
65+
...obj,
66+
iv: Buffer.from(obj.iv, "base64"),
67+
tag: Buffer.from(obj.tag, "base64"),
68+
};
69+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.credentialsToken, data.iv);
70+
decipher.setAuthTag(data.tag);
71+
const decrypted = decipher.update(data.encrypted, "hex", "utf8");
72+
return decrypted + decipher.final("utf8");
73+
}
74+
75+
encrypt(content: string): string {
76+
if (!this.credentialsToken) {
77+
throw new Error("no credentials token available");
78+
}
79+
const iv = crypto.randomBytes(12);
80+
const cipher = crypto.createCipheriv("aes-256-gcm", this.credentialsToken, iv);
81+
let encrypted = cipher.update(content, "utf8", "hex");
82+
encrypted += cipher.final("hex");
83+
const tag = cipher.getAuthTag();
84+
return JSON.stringify({
85+
iv: iv.toString("base64"),
86+
tag: tag.toString("base64"),
87+
encrypted,
88+
});
89+
}
90+
91+
isEncryptedData(content: string): boolean {
92+
try {
93+
const obj = JSON.parse(content);
94+
return isSerializedEncryptedData(obj);
95+
} catch (e) {
96+
return false;
97+
}
98+
}
99+
49100
trackEvent(msg: RemoteTrackMessage): void {
50101
const debugWorkspace = workspaceUrl.debugWorkspace;
51102
msg.properties = { ...msg.properties, debugWorkspace };
@@ -78,3 +129,13 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi
78129
);
79130
}
80131
}
132+
133+
function isSerializedEncryptedData(obj: any): obj is { iv: string; encrypted: string; tag: string } {
134+
return (
135+
obj != null &&
136+
typeof obj === "object" &&
137+
typeof obj.iv === "string" &&
138+
typeof obj.encrypted === "string" &&
139+
typeof obj.tag === "string"
140+
);
141+
}

components/supervisor/frontend/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ module.exports = {
5555
fs: "empty",
5656
child_process: "empty",
5757
net: "empty",
58-
crypto: "empty",
58+
crypto: true,
5959
tls: "empty",
6060
},
6161
devtool: "source-map",

0 commit comments

Comments
 (0)