Skip to content

Commit

Permalink
fix: extension secrets should not be lost when workspace restart (#340)
Browse files Browse the repository at this point in the history
Signed-off-by: vitaliy-guliy <[email protected]>
Co-authored-by: Valerii Svydenko <[email protected]>
  • Loading branch information
vitaliy-guliy and svor authored May 16, 2024
1 parent a67cd09 commit a553e5c
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .rebase/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

The file to keep a list of changed files which will potentionaly help to resolve rebase conflicts.

#### @vitaliy-guliy
https://github.com/che-incubator/che-code/pull/340

- code/src/vs/code/browser/workbench/workbench.ts
---

#### @vitaliy-guliy
https://github.com/che-incubator/che-code/pull/339

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@
{
"from": "return URI.parse(mainWindow.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') });",
"by": "const windowURI = URI.parse(mainWindow.location.href);\\\n\\\t\\\tconst fullPath = windowURI.path.replace(/\\\\/$/, '') + this._callbackRoute;\\\n\\\t\\\treturn windowURI.with({ path: fullPath, query: queryParams.join('\\&') });"
},
{
"from": "private _serverKey: Uint8Array \\| undefined;",
"by": "private _serverKey: Uint8Array \\| undefined = new TextEncoder().encode('{{LOCAL-STORAGE}}/{{SECURE-KEY}}');"
},
{
"from": "const secretStorageKeyPath = readCookie('vscode-secret-key-path');",
"by": "const secretStorageKeyPath = readCookie('vscode-secret-key-path') \\|\\| '/';"
}
]
16 changes: 3 additions & 13 deletions code/src/vs/code/browser/workbench/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const enum AESConstants {
}

class ServerKeyedAESCrypto implements ISecretStorageCrypto {
private _serverKey: Uint8Array | undefined;
private _serverKey: Uint8Array | undefined = new TextEncoder().encode('{{LOCAL-STORAGE}}/{{SECURE-KEY}}');

/** Gets whether the algorithm is supported; requires a secure context */
public static supported() {
Expand Down Expand Up @@ -575,20 +575,10 @@ function readCookie(name: string): string | undefined {

const cheConfig = getCheConfig();
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute);
const secretStorageKeyPath = readCookie('vscode-secret-key-path');
const secretStorageKeyPath = readCookie('vscode-secret-key-path') || '/';
const secretStorageCrypto = secretStorageKeyPath && ServerKeyedAESCrypto.supported()
? new ServerKeyedAESCrypto(secretStorageKeyPath) : new TransparentCrypto();
console.log('Creating workbench with config ', JSON.stringify({
...config,
...cheConfig,
settingsSyncOptions: config.settingsSyncOptions ? {
enabled: config.settingsSyncOptions.enabled,
} : undefined,
workspaceProvider: WorkspaceProvider.create(config),
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
credentialsProvider: config.remoteAuthority ? undefined : new LocalStorageSecretStorageProvider(secretStorageCrypto) // with a remote, we don't use a local secret storage provider
}, undefined, 2));


// Create workbench
create(mainWindow.document.body, {
...config,
Expand Down
2 changes: 2 additions & 0 deletions launcher/src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@

export const FILE_WORKBENCH_WEB_MAIN = 'out/vs/workbench/workbench.web.main.js';

export const FILE_WORKBENCH = 'out/vs/code/browser/workbench/workbench.js';

export const FILE_EXTENSION_HOST_PROCESS = 'out/vs/workbench/api/node/extensionHostProcess.js';
81 changes: 81 additions & 0 deletions launcher/src/local-storage-key-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**********************************************************************
* Copyright (c) 2024 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

import { FILE_WORKBENCH } from './files';
import * as fs from './fs-extra';

const SERVER_KEY_MASK = '{{LOCAL-STORAGE}}/{{SECURE-KEY}}';

const CERTS_DIR = '/etc/ssh';

/**
* Finds a public key in `/etc/ssh` and initializes VS Code with 32 bytes (every fourth character) of the key.
* The key is used to encrypt/decrypt the extension secrets stored in browser local storage.
*/
export class LocalStorageKeyProvider {
async configure(): Promise<void> {
console.log('# Injecting server public key to che-code...');

try {
const publicKeyFile = await this.findPublicKeyFile();
console.log(` > found key file ${publicKeyFile}`);

const secret = await this.getPartOfPublicKey(publicKeyFile);
await this.update(FILE_WORKBENCH, SERVER_KEY_MASK, secret);
} catch (err) {
console.error(err.message);
}
}

async findPublicKeyFile(): Promise<string> {
// Check for public certificates in /public-certs
if (await fs.pathExists(CERTS_DIR)) {
const dir = await fs.readdir(CERTS_DIR);

for (const item of dir) {
const file = `${CERTS_DIR}/${item}`;

if (await fs.isFile(file)) {
// check for it's public part
const publicKey = file + '.pub';
if ((await fs.pathExists(publicKey)) && (await fs.isFile(publicKey))) {
return publicKey;
}
}
}
}

throw new Error(`Public key file is not found in ${CERTS_DIR}`);
}

async getPartOfPublicKey(file: string): Promise<string> {
let content = await fs.readFile(file);
content = content.substring(content.indexOf(' ') + 1);

let secret = '';
for (let i = 0; i < 32; i++) {
secret += content.charAt(i * 4);
}

return secret;
}

async update(file: string, text: string, newText: string): Promise<void> {
const content = await fs.readFile(file);
const newContent = content.replace(text, newText);

if (content === newContent) {
console.log(` > ${file} is not updated`);
} else {
await fs.writeFile(file, newContent);
console.log(` > ${file} has been updated`);
}
}
}
2 changes: 2 additions & 0 deletions launcher/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CodeWorkspace } from './code-workspace';
import { DevWorkspaceId } from './devworkspace-id';
import { NodeExtraCertificate } from './node-extra-certificate';
import { OpenVSIXRegistry } from './openvsix-registry';
import { LocalStorageKeyProvider } from './local-storage-key-provider';
import { TrustedExtensions } from './trusted-extensions';
import { VSCodeLauncher } from './vscode-launcher';
import { WebviewResources } from './webview-resources';
Expand All @@ -28,6 +29,7 @@ export class Main {
await new OpenVSIXRegistry().configure();
await new WebviewResources().configure();
await new NodeExtraCertificate().configure();
await new LocalStorageKeyProvider().configure();
await new TrustedExtensions().configure();

const workspaceFile = await new CodeWorkspace().generate();
Expand Down
73 changes: 73 additions & 0 deletions launcher/tests/local-storage-key-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**********************************************************************
* Copyright (c) 2024 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

import * as fs from '../src/fs-extra';
import { LocalStorageKeyProvider } from '../src/local-storage-key-provider';

const ORIGIN_WORKBENCH_FILE = `
some code, some code, a mask to be replaced {{LOCAL-STORAGE}}/{{SECURE-KEY}}, some code
`;

const NEW_WORKBENCH_FILE = `
some code, some code, a mask to be replaced 1234567890ABCDEFGHIJKLMNOPQRSTUV, some code
`;

describe('Test setting of Local Storage public key to VS Code', () => {
beforeEach(() => {
Object.assign(fs, {
pathExists: jest.fn(),
isFile: jest.fn(),
readdir: jest.fn(),
readFile: jest.fn(),
writeFile: jest.fn(),
});
});

test('should return if env.DEVWORKSPACE_ID is not set', async () => {
const pathExistsMock = jest.fn();
const readdirMock = jest.fn();
const isFileMock = jest.fn();
const readFileMock = jest.fn();
const writeFileMock = jest.fn();
Object.assign(fs, {
pathExists: pathExistsMock,
readdir: readdirMock,
isFile: isFileMock,
readFile: readFileMock,
writeFile: writeFileMock,
});

pathExistsMock.mockImplementation(async (path: string) => {
return '/etc/ssh' === path || '/etc/ssh/first-key.pub' === path;
});

readdirMock.mockImplementation(async (path: string) => {
return ['some-file', 'first-key', 'second-key', 'first-key.pub', 'second-key.pub'];
});

isFileMock.mockImplementation(async (path: string) => {
return '/etc/ssh/first-key' === path || '/etc/ssh/first-key.pub' === path;
});

readFileMock.mockImplementation(async (file: string) => {
switch (file) {
case '/etc/ssh/first-key.pub':
return 'ssh-rsa 1111222233334444555566667777888899990000AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ';
case 'out/vs/code/browser/workbench/workbench.js':
return ORIGIN_WORKBENCH_FILE;
}
});

const localStorageKeyProvider = new LocalStorageKeyProvider();
await localStorageKeyProvider.configure();

expect(writeFileMock).toBeCalledWith('out/vs/code/browser/workbench/workbench.js', NEW_WORKBENCH_FILE);
});
});
8 changes: 8 additions & 0 deletions launcher/tests/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ jest.mock('../src/node-extra-certificate', () => ({
},
}));

const configureLocalStorageKeyProvider = jest.fn();
jest.mock('../src/local-storage-key-provider', () => ({
LocalStorageKeyProvider: function () {
return { configure: configureLocalStorageKeyProvider };
},
}));

const configureTustedExtensions = jest.fn();
jest.mock('../src/trusted-extensions', () => ({
TrustedExtensions: function () {
Expand Down Expand Up @@ -67,6 +74,7 @@ describe('Test main flow:', () => {
expect(configureOpenVSIXRegistryMock).toBeCalled();
expect(configureWebviewResourcesMock).toBeCalled();
expect(configureNodeExtraCertificate).toBeCalled();
expect(configureLocalStorageKeyProvider).toBeCalled();
expect(configureTustedExtensions).toBeCalled();

expect(generateCodeWorkspace).toBeCalled();
Expand Down

0 comments on commit a553e5c

Please sign in to comment.