implement sign service for web (#182815)

* signing: implement signing service on the web

* update distro
This commit is contained in:
Connor Peet 2023-05-17 16:52:56 -07:00 committed by GitHub
parent 751fdcb904
commit 0c94abc4a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 79 deletions

View file

@ -49,6 +49,5 @@ xterm-addon-webgl/out/**
!@microsoft/applicationinsights-core-js/browser/applicationinsights-core-js.min.js
!@microsoft/applicationinsights-shims/dist/umd/applicationinsights-shims.min.js
vsda/**
!vsda/rust/web/**

File diff suppressed because one or more lines are too long

View file

@ -394,6 +394,13 @@ export function acquireWebNodePaths() {
const root = path.join(__dirname, '..', '..');
const webPackageJSON = path.join(root, '/remote/web', 'package.json');
const webPackages = JSON.parse(fs.readFileSync(webPackageJSON, 'utf8')).dependencies;
const distroWebPackageJson = path.join(root, '.build/distro/npm/remote/web/package.json');
if (fs.existsSync(distroWebPackageJson)) {
const distroWebPackages = JSON.parse(fs.readFileSync(distroWebPackageJson, 'utf8')).dependencies;
Object.assign(webPackages, distroWebPackages);
}
const nodePaths: { [key: string]: string } = {};
for (const key of Object.keys(webPackages)) {
const packageJSON = path.join(root, 'node_modules', key, 'package.json');

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.79.0",
"distro": "4e88da3231fbd0b33666c0d1b550b1ecdb739449",
"distro": "10ec4d08d4a06a1c18addcded03e90c8a0e6ecad",
"author": {
"name": "Microsoft Corporation"
},

View file

@ -114,6 +114,7 @@
loaderConfig.paths = {
'vscode-textmate': `${baseNodeModulesPath}/vscode-textmate/release/main.js`,
'vscode-oniguruma': `${baseNodeModulesPath}/vscode-oniguruma/release/main.js`,
'vsda': `${baseNodeModulesPath}/vsda/index.js`,
'xterm': `${baseNodeModulesPath}/xterm/lib/xterm.js`,
'xterm-addon-canvas': `${baseNodeModulesPath}/xterm-addon-canvas/lib/xterm-addon-canvas.js`,
'xterm-addon-image': `${baseNodeModulesPath}/xterm-addon-image/lib/xterm-addon-image.js`,

View file

@ -3,24 +3,94 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMessage, ISignService } from 'vs/platform/sign/common/sign';
import { IntervalTimer } from 'vs/base/common/async';
import { memoize } from 'vs/base/common/decorators';
import { FileAccess } from 'vs/base/common/network';
import { IProductService } from 'vs/platform/product/common/productService';
import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService';
import { ISignService } from 'vs/platform/sign/common/sign';
export class SignService implements ISignService {
declare module vsdaWeb {
export function sign(salted_message: string): string;
declare readonly _serviceBrand: undefined;
constructor(
private readonly _token: Promise<string> | string | undefined
) { }
async createNewMessage(value: string): Promise<IMessage> {
return { id: '', data: value };
// eslint-disable-next-line @typescript-eslint/naming-convention
export class validator {
free(): void;
constructor();
createNewMessage(original: string): string;
validate(signed_message: string): 'ok' | 'error';
}
async validate(message: IMessage, value: string): Promise<boolean> {
return true;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export function init(module_or_path?: InitInput | Promise<InitInput>): Promise<unknown>;
}
// Initialized if/when vsda is loaded
declare const vsda_web: {
default: typeof vsdaWeb.init;
sign: typeof vsdaWeb.sign;
validator: typeof vsdaWeb.validator;
};
const KEY_SIZE = 32;
const IV_SIZE = 16;
const STEP_SIZE = KEY_SIZE + IV_SIZE;
export class SignService extends AbstractSignService implements ISignService {
constructor(@IProductService private readonly productService: IProductService) {
super();
}
async sign(value: string): Promise<string> {
const token = await Promise.resolve(this._token);
return token || '';
protected override getValidator(): Promise<IVsdaValidator> {
return this.vsda().then(vsda => {
const v = new vsda.validator();
return {
createNewMessage: arg => v.createNewMessage(arg),
validate: arg => v.validate(arg),
dispose: () => v.free(),
};
});
}
protected override signValue(arg: string): Promise<string> {
return this.vsda().then(vsda => vsda.sign(arg));
}
@memoize
private async vsda(): Promise<typeof vsda_web> {
const checkInterval = new IntervalTimer();
let [wasm] = await Promise.all([
this.getWasmBytes(),
new Promise<void>((resolve, reject) => {
require(['vsda'], resolve, reject);
// todo@connor4312: there seems to be a bug(?) in vscode-loader with
// require() not resolving in web once the script loads, so check manually
checkInterval.cancelAndSet(() => {
if (typeof vsda_web !== 'undefined') {
resolve();
}
}, 50);
}).finally(() => checkInterval!.dispose()),
]);
const keyBytes = new TextEncoder().encode(this.productService.serverLicense?.join('\n') || '');
for (let i = 0; i + STEP_SIZE < keyBytes.length; i += STEP_SIZE) {
const key = await crypto.subtle.importKey('raw', keyBytes.slice(i + IV_SIZE, i + IV_SIZE + KEY_SIZE), { name: 'AES-CBC' }, false, ['decrypt']);
wasm = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: keyBytes.slice(i, i + IV_SIZE) }, key, wasm);
}
await vsda_web.default(wasm);
return vsda_web;
}
private async getWasmBytes(): Promise<ArrayBuffer> {
const response = await fetch(FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true));
if (!response.ok) {
throw new Error('error loading vsda');
}
return response.arrayBuffer();
}
}

View file

@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMessage, ISignService } from 'vs/platform/sign/common/sign';
export interface IVsdaSigner {
sign(arg: string): string;
}
export interface IVsdaValidator {
createNewMessage(arg: string): string;
validate(arg: string): 'ok' | 'error';
dispose?(): void;
}
export abstract class AbstractSignService implements ISignService {
declare readonly _serviceBrand: undefined;
private static _nextId = 1;
private readonly validators = new Map<string, IVsdaValidator>();
protected abstract getValidator(): Promise<IVsdaValidator>;
protected abstract signValue(arg: string): Promise<string>;
public async createNewMessage(value: string): Promise<IMessage> {
try {
const validator = await this.getValidator();
if (validator) {
const id = String(AbstractSignService._nextId++);
this.validators.set(id, validator);
return {
id: id,
data: validator.createNewMessage(value)
};
}
} catch (e) {
// ignore errors silently
}
return { id: '', data: value };
}
async validate(message: IMessage, value: string): Promise<boolean> {
if (!message.id) {
return true;
}
const validator = this.validators.get(message.id);
if (!validator) {
return false;
}
this.validators.delete(message.id);
try {
return (validator.validate(value) === 'ok');
} catch (e) {
// ignore errors silently
return false;
} finally {
validator.dispose?.();
}
}
async sign(value: string): Promise<string> {
try {
return await this.signValue(value);
} catch (e) {
// ignore errors silently
}
return value;
}
}

View file

@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMessage, ISignService } from 'vs/platform/sign/common/sign';
import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService';
import { ISignService } from 'vs/platform/sign/common/sign';
declare module vsda {
// the signer is a native module that for historical reasons uses a lower case class name
@ -19,62 +20,15 @@ declare module vsda {
}
}
export class SignService implements ISignService {
declare readonly _serviceBrand: undefined;
private static _nextId = 1;
private readonly validators = new Map<string, vsda.validator>();
export class SignService extends AbstractSignService implements ISignService {
protected override getValidator(): Promise<IVsdaValidator> {
return this.vsda().then(vsda => new vsda.validator());
}
protected override signValue(arg: string): Promise<string> {
return this.vsda().then(vsda => new vsda.signer().sign(arg));
}
private vsda(): Promise<typeof vsda> {
return new Promise((resolve, reject) => require(['vsda'], resolve, reject));
}
async createNewMessage(value: string): Promise<IMessage> {
try {
const vsda = await this.vsda();
const validator = new vsda.validator();
if (validator) {
const id = String(SignService._nextId++);
this.validators.set(id, validator);
return {
id: id,
data: validator.createNewMessage(value)
};
}
} catch (e) {
// ignore errors silently
}
return { id: '', data: value };
}
async validate(message: IMessage, value: string): Promise<boolean> {
if (!message.id) {
return true;
}
const validator = this.validators.get(message.id);
if (!validator) {
return false;
}
this.validators.delete(message.id);
try {
return (validator.validate(value) === 'ok');
} catch (e) {
// ignore errors silently
return false;
}
}
async sign(value: string): Promise<string> {
try {
const vsda = await this.vsda();
const signer = new vsda.signer();
if (signer) {
return signer.sign(value);
}
} catch (e) {
// ignore errors silently
}
return value;
}
}

View file

@ -282,7 +282,7 @@ export class BrowserMain extends Disposable {
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// Signing
const signService = new SignService(connectionToken);
const signService = new SignService(productService);
serviceCollection.set(ISignService, signService);

View file

@ -90,7 +90,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));
});
@ -133,7 +133,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));
const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a'));
@ -156,7 +156,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));