mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
server-web: implement secret storage provider (#191538)
Works quite similarly to vscode.dev. The client has a key stored in secret storage. The server has a key stored server-side, and issues an http-only cookie to the client. The client can ask the server to combine its key and the http-only cookie key to a key component, which it combines with its local key to encrypt and decrypt data. This logic kicks in if the web server bits see a `vscode-secret-key-path` cookie set when it loads.
This commit is contained in:
parent
5cd507ba17
commit
8ef6961789
|
@ -10,6 +10,7 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use const_format::concatcp;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Request, Response, Server};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
@ -23,6 +24,7 @@ use crate::constants::VSCODE_CLI_QUALITY;
|
|||
use crate::download_cache::DownloadCache;
|
||||
use crate::log;
|
||||
use crate::options::Quality;
|
||||
use crate::state::{LauncherPaths, PersistedState};
|
||||
use crate::update_service::{
|
||||
unzip_downloaded_release, Platform, Release, TargetKind, UpdateService,
|
||||
};
|
||||
|
@ -48,6 +50,22 @@ const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12;
|
|||
/// How long to cache the "latest" version we get from the update service.
|
||||
const RELEASE_CACHE_SECS: u64 = 60 * 60;
|
||||
|
||||
/// Number of bytes for the secret keys. See workbench.ts for their usage.
|
||||
const SECRET_KEY_BYTES: usize = 32;
|
||||
/// Path to mint the key combining server and client parts.
|
||||
const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key";
|
||||
/// Cookie set to the `SECRET_KEY_MINT_PATH`
|
||||
const PATH_COOKIE_NAME: &str = "vscode-secret-key-path";
|
||||
/// Cookie set to the `SECRET_KEY_MINT_PATH`
|
||||
const PATH_COOKIE_VALUE: &str = concatcp!(
|
||||
PATH_COOKIE_NAME,
|
||||
"=",
|
||||
SECRET_KEY_MINT_PATH,
|
||||
"; SameSite=Strict; Path=/"
|
||||
);
|
||||
/// HTTP-only cookie where the client's secret half is stored.
|
||||
const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half";
|
||||
|
||||
/// Implements the vscode "server of servers". Clients who go to the URI get
|
||||
/// served the latest version of the VS Code server whenever they load the
|
||||
/// page. The VS Code server prefixes all assets and connections it loads with
|
||||
|
@ -69,10 +87,14 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result<i3
|
|||
}
|
||||
|
||||
let cm = ConnectionManager::new(&ctx, platform, args.clone());
|
||||
let key = get_server_key_half(&ctx.paths);
|
||||
let make_svc = move || {
|
||||
let cm = cm.clone();
|
||||
let log = cm.log.clone();
|
||||
let service = service_fn(move |req| handle(cm.clone(), log.clone(), req));
|
||||
let ctx = HandleContext {
|
||||
cm: cm.clone(),
|
||||
log: cm.log.clone(),
|
||||
server_secret_key: key.clone(),
|
||||
};
|
||||
let service = service_fn(move |req| handle(ctx.clone(), req));
|
||||
async move { Ok::<_, Infallible>(service) }
|
||||
};
|
||||
|
||||
|
@ -106,35 +128,82 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result<i3
|
|||
Ok(0)
|
||||
}
|
||||
|
||||
/// Handler function for an inbound request
|
||||
async fn handle(
|
||||
#[derive(Clone)]
|
||||
struct HandleContext {
|
||||
cm: Arc<ConnectionManager>,
|
||||
log: log::Logger,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, Infallible> {
|
||||
let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), cm.platform) {
|
||||
server_secret_key: SecretKeyPart,
|
||||
}
|
||||
|
||||
/// Handler function for an inbound request
|
||||
async fn handle(ctx: HandleContext, req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
let client_key_half = get_client_key_half(&req);
|
||||
let mut res = match req.uri().path() {
|
||||
SECRET_KEY_MINT_PATH => handle_secret_mint(ctx, req),
|
||||
_ => handle_proxied(ctx, req).await,
|
||||
};
|
||||
|
||||
append_secret_headers(&mut res, &client_key_half);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn handle_proxied(ctx: HandleContext, req: Request<Body>) -> Response<Body> {
|
||||
let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) {
|
||||
r
|
||||
} else {
|
||||
match cm.get_latest_release().await {
|
||||
match ctx.cm.get_latest_release().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(log, "error getting latest version: {}", e);
|
||||
return Ok(response::code_err(e));
|
||||
error!(ctx.log, "error getting latest version: {}", e);
|
||||
return response::code_err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(match cm.get_connection(release).await {
|
||||
match ctx.cm.get_connection(release).await {
|
||||
Ok(rw) => {
|
||||
if req.headers().contains_key(hyper::header::UPGRADE) {
|
||||
forward_ws_req_to_server(cm.log.clone(), rw, req).await
|
||||
forward_ws_req_to_server(ctx.log.clone(), rw, req).await
|
||||
} else {
|
||||
forward_http_req_to_server(rw, req).await
|
||||
}
|
||||
}
|
||||
Err(CodeError::ServerNotYetDownloaded) => response::wait_for_download(),
|
||||
Err(e) => response::code_err(e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_secret_mint(ctx: HandleContext, req: Request<Body>) -> Response<Body> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(ctx.server_secret_key.0.as_ref());
|
||||
hasher.update(get_client_key_half(&req).0.as_ref());
|
||||
let hash = hasher.finalize();
|
||||
let hash = hash[..SECRET_KEY_BYTES].to_vec();
|
||||
response::secret_key(hash)
|
||||
}
|
||||
|
||||
/// Appends headers to response to maintain the secret storage of the workbench:
|
||||
/// sets the `PATH_COOKIE_VALUE` so workbench.ts knows about the 'mint' endpoint,
|
||||
/// and maintains the http-only cookie the client will use for cookies.
|
||||
fn append_secret_headers(res: &mut Response<Body>, client_key_half: &SecretKeyPart) {
|
||||
let headers = res.headers_mut();
|
||||
headers.append(
|
||||
hyper::header::SET_COOKIE,
|
||||
PATH_COOKIE_VALUE.parse().unwrap(),
|
||||
);
|
||||
headers.append(
|
||||
hyper::header::SET_COOKIE,
|
||||
format!(
|
||||
"{}={}; SameSite=Strict; HttpOnly; Max-Age=2592000; Path=/",
|
||||
SECRET_KEY_COOKIE_NAME,
|
||||
client_key_half.encode()
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the release info from the VS Code path prefix, which is in the
|
||||
|
@ -258,6 +327,77 @@ fn is_commit_hash(s: &str) -> bool {
|
|||
s.len() == COMMIT_HASH_LEN && s.chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
/// Gets a cookie from the request by name.
|
||||
fn extract_cookie(req: &Request<Body>, name: &str) -> Option<String> {
|
||||
for h in req.headers().get_all(hyper::header::COOKIE) {
|
||||
if let Ok(str) = h.to_str() {
|
||||
for pair in str.split("; ") {
|
||||
let i = match pair.find('=') {
|
||||
Some(i) => i,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if &pair[..i] == name {
|
||||
return Some(pair[i + 1..].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SecretKeyPart(Box<[u8; SECRET_KEY_BYTES]>);
|
||||
|
||||
impl SecretKeyPart {
|
||||
pub fn new() -> Self {
|
||||
let key: [u8; SECRET_KEY_BYTES] = rand::random();
|
||||
Self(Box::new(key))
|
||||
}
|
||||
|
||||
pub fn decode(s: &str) -> Result<Self, base64::DecodeSliceError> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let mut key: [u8; SECRET_KEY_BYTES] = [0; SECRET_KEY_BYTES];
|
||||
let v = general_purpose::URL_SAFE.decode(s)?;
|
||||
if v.len() != SECRET_KEY_BYTES {
|
||||
return Err(base64::DecodeSliceError::OutputSliceTooSmall);
|
||||
}
|
||||
|
||||
key.copy_from_slice(&v);
|
||||
Ok(Self(Box::new(key)))
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> String {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
general_purpose::URL_SAFE.encode(self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the server's half of the secret key.
|
||||
fn get_server_key_half(paths: &LauncherPaths) -> SecretKeyPart {
|
||||
let ps = PersistedState::new(paths.root().join("serve-web-key-half"));
|
||||
let value: String = ps.load();
|
||||
if let Ok(sk) = SecretKeyPart::decode(&value) {
|
||||
return sk;
|
||||
}
|
||||
|
||||
let key = SecretKeyPart::new();
|
||||
let _ = ps.save(key.encode());
|
||||
key
|
||||
}
|
||||
|
||||
/// Gets the client's half of the secret key.
|
||||
fn get_client_key_half(req: &Request<Body>) -> SecretKeyPart {
|
||||
if let Some(c) = extract_cookie(req, SECRET_KEY_COOKIE_NAME) {
|
||||
if let Ok(sk) = SecretKeyPart::decode(&c) {
|
||||
return sk;
|
||||
}
|
||||
}
|
||||
|
||||
SecretKeyPart::new()
|
||||
}
|
||||
|
||||
/// Module holding original responses the CLI's server makes.
|
||||
mod response {
|
||||
use const_format::concatcp;
|
||||
|
@ -287,6 +427,14 @@ mod response {
|
|||
.body(Body::from(concatcp!("The latest version of the ", QUALITYLESS_SERVER_NAME, " is downloading, please wait a moment...<script>setTimeout(()=>location.reload(),1500)</script>", )))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn secret_key(hash: Vec<u8>) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/octet-stream") // todo: get latest
|
||||
.body(Body::from(hash))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle returned when getting a stream to the server, used to refcount
|
||||
|
@ -515,6 +663,7 @@ impl ConnectionManager {
|
|||
let executable = path
|
||||
.join("bin")
|
||||
.join(args.release.quality.server_entrypoint());
|
||||
|
||||
let socket_path = get_socket_name();
|
||||
|
||||
#[cfg(not(windows))]
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,99 +4,221 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isStandalone } from 'vs/base/browser/browser';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { VSBuffer, decodeBase64, encodeBase64 } from 'vs/base/common/buffer';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { ltrim } from 'vs/base/common/strings';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { ISecretStorageProvider } from 'vs/platform/secrets/common/secrets';
|
||||
import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/window/common/window';
|
||||
import { create } from 'vs/workbench/workbench.web.main';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { ltrim } from 'vs/base/common/strings';
|
||||
import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService';
|
||||
import type { IWorkbenchConstructionOptions } from 'vs/workbench/browser/web.api';
|
||||
import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService';
|
||||
import { ISecretStorageProvider } from 'vs/platform/secrets/common/secrets';
|
||||
import { AuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService';
|
||||
import { create } from 'vs/workbench/workbench.web.main';
|
||||
|
||||
class LocalStorageSecretStorageProvider implements ISecretStorageProvider {
|
||||
private static readonly STORAGE_KEY = 'secrets.provider';
|
||||
interface ISecretStorageCrypto {
|
||||
seal(data: string): Promise<string>;
|
||||
unseal(data: string): Promise<string>;
|
||||
}
|
||||
|
||||
private _secrets: Record<string, string> | undefined;
|
||||
class TransparentCrypto implements ISecretStorageCrypto {
|
||||
async seal(data: string): Promise<string> {
|
||||
return data;
|
||||
}
|
||||
|
||||
async unseal(data: string): Promise<string> {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const enum AESConstants {
|
||||
ALGORITHM = 'AES-GCM',
|
||||
KEY_LENGTH = 256,
|
||||
IV_LENGTH = 12,
|
||||
}
|
||||
|
||||
class ServerKeyedAESCrypto implements ISecretStorageCrypto {
|
||||
private _serverKey: Uint8Array | undefined;
|
||||
|
||||
/** Gets whether the algorithm is supported; requires a secure context */
|
||||
public static supported() {
|
||||
return !!crypto.subtle;
|
||||
}
|
||||
|
||||
constructor(private readonly authEndpoint: string) { }
|
||||
|
||||
async seal(data: string): Promise<string> {
|
||||
// Get a new key and IV on every change, to avoid the risk of reusing the same key and IV pair with AES-GCM
|
||||
// (see also: https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams#properties)
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(AESConstants.IV_LENGTH));
|
||||
// crypto.getRandomValues isn't a good-enough PRNG to generate crypto keys, so we need to use crypto.subtle.generateKey and export the key instead
|
||||
const clientKeyObj = await window.crypto.subtle.generateKey(
|
||||
{ name: AESConstants.ALGORITHM as const, length: AESConstants.KEY_LENGTH as const },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
const clientKey = new Uint8Array(await window.crypto.subtle.exportKey('raw', clientKeyObj));
|
||||
const key = await this.getKey(clientKey);
|
||||
const dataUint8Array = new TextEncoder().encode(data);
|
||||
const cipherText: ArrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: AESConstants.ALGORITHM as const, iv },
|
||||
key,
|
||||
dataUint8Array
|
||||
);
|
||||
|
||||
// Base64 encode the result and store the ciphertext, the key, and the IV in localStorage
|
||||
// Note that the clientKey and IV don't need to be secret
|
||||
const result = new Uint8Array([...clientKey, ...iv, ...new Uint8Array(cipherText)]);
|
||||
return encodeBase64(VSBuffer.wrap(result));
|
||||
}
|
||||
|
||||
async unseal(data: string): Promise<string> {
|
||||
// encrypted should contain, in order: the key (32-byte), the IV for AES-GCM (12-byte) and the ciphertext (which has the GCM auth tag at the end)
|
||||
// Minimum length must be 44 (key+IV length) + 16 bytes (1 block encrypted with AES - regardless of key size)
|
||||
const dataUint8Array = decodeBase64(data);
|
||||
|
||||
if (dataUint8Array.byteLength < 60) {
|
||||
throw Error('Invalid length for the value for credentials.crypto');
|
||||
}
|
||||
|
||||
const keyLength = AESConstants.KEY_LENGTH / 8;
|
||||
const clientKey = dataUint8Array.slice(0, keyLength);
|
||||
const iv = dataUint8Array.slice(keyLength, keyLength + AESConstants.IV_LENGTH);
|
||||
const cipherText = dataUint8Array.slice(keyLength + AESConstants.IV_LENGTH);
|
||||
|
||||
// Do the decryption and parse the result as JSON
|
||||
const key = await this.getKey(clientKey.buffer);
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: AESConstants.ALGORITHM as const, iv: iv.buffer },
|
||||
key,
|
||||
cipherText.buffer
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(decrypted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a clientKey, returns the CryptoKey object that is used to encrypt/decrypt the data.
|
||||
* The actual key is (clientKey XOR serverKey)
|
||||
*/
|
||||
private async getKey(clientKey: Uint8Array): Promise<CryptoKey> {
|
||||
if (!clientKey || clientKey.byteLength !== AESConstants.KEY_LENGTH / 8) {
|
||||
throw Error('Invalid length for clientKey');
|
||||
}
|
||||
|
||||
const serverKey = await this.getServerKeyPart();
|
||||
const keyData = new Uint8Array(AESConstants.KEY_LENGTH / 8);
|
||||
|
||||
for (let i = 0; i < keyData.byteLength; i++) {
|
||||
keyData[i] = clientKey[i]! ^ serverKey[i]!;
|
||||
}
|
||||
|
||||
return window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{
|
||||
name: AESConstants.ALGORITHM as const,
|
||||
length: AESConstants.KEY_LENGTH as const,
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
private async getServerKeyPart(): Promise<Uint8Array> {
|
||||
if (this._serverKey) {
|
||||
return this._serverKey;
|
||||
}
|
||||
|
||||
let attempt = 0;
|
||||
let lastError: unknown | undefined;
|
||||
|
||||
while (attempt <= 3) {
|
||||
try {
|
||||
const res = await fetch(this.authEndpoint, { credentials: 'include', method: 'POST' });
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
const serverKey = new Uint8Array(await await res.arrayBuffer());
|
||||
if (serverKey.byteLength !== AESConstants.KEY_LENGTH / 8) {
|
||||
throw Error(`The key retrieved by the server is not ${AESConstants.KEY_LENGTH} bit long.`);
|
||||
}
|
||||
this._serverKey = serverKey;
|
||||
return this._serverKey;
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
attempt++;
|
||||
|
||||
// exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, attempt * attempt * 100));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalStorageSecretStorageProvider implements ISecretStorageProvider {
|
||||
private readonly _storageKey = 'secrets.provider';
|
||||
|
||||
private _secretsPromise: Promise<Record<string, string>> = this.load();
|
||||
|
||||
type: 'in-memory' | 'persisted' | 'unknown' = 'persisted';
|
||||
|
||||
constructor() {
|
||||
let authSessionInfo: (AuthenticationSessionInfo & { scopes: string[][] }) | undefined;
|
||||
const authSessionElement = document.getElementById('vscode-workbench-auth-session');
|
||||
const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined;
|
||||
if (authSessionElementAttribute) {
|
||||
constructor(
|
||||
private readonly crypto: ISecretStorageCrypto,
|
||||
) { }
|
||||
|
||||
private async load(): Promise<Record<string, string>> {
|
||||
// Get the secrets from localStorage
|
||||
const encrypted = window.localStorage.getItem(this._storageKey);
|
||||
if (encrypted) {
|
||||
try {
|
||||
authSessionInfo = JSON.parse(authSessionElementAttribute);
|
||||
} catch (error) { /* Invalid session is passed. Ignore. */ }
|
||||
}
|
||||
|
||||
if (authSessionInfo) {
|
||||
// Settings Sync Entry
|
||||
this.set(`${product.urlProtocol}.loginAccount`, JSON.stringify(authSessionInfo));
|
||||
|
||||
// Auth extension Entry
|
||||
if (authSessionInfo.providerId !== 'github') {
|
||||
console.error(`Unexpected auth provider: ${authSessionInfo.providerId}. Expected 'github'.`);
|
||||
return;
|
||||
return JSON.parse(await this.crypto.unseal(encrypted));
|
||||
} catch (err) {
|
||||
// TODO: send telemetry
|
||||
console.error('Failed to decrypt secrets from localStorage', err);
|
||||
window.localStorage.removeItem(this._storageKey);
|
||||
}
|
||||
const authAccount = JSON.stringify({ extensionId: 'vscode.github-authentication', key: 'github.auth' });
|
||||
this.set(authAccount, JSON.stringify(authSessionInfo.scopes.map(scopes => ({
|
||||
id: authSessionInfo!.id,
|
||||
scopes,
|
||||
accessToken: authSessionInfo!.accessToken
|
||||
}))));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
get(key: string): Promise<string | undefined> {
|
||||
return Promise.resolve(this.secrets[key]);
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
const secrets = await this._secretsPromise;
|
||||
return secrets[key];
|
||||
}
|
||||
set(key: string, value: string): Promise<void> {
|
||||
this.secrets[key] = value;
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
const secrets = await this._secretsPromise;
|
||||
secrets[key] = value;
|
||||
this._secretsPromise = Promise.resolve(secrets);
|
||||
this.save();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
async delete(key: string): Promise<void> {
|
||||
delete this.secrets[key];
|
||||
|
||||
const secrets = await this._secretsPromise;
|
||||
delete secrets[key];
|
||||
this._secretsPromise = Promise.resolve(secrets);
|
||||
this.save();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private get secrets(): Record<string, string> {
|
||||
if (!this._secrets) {
|
||||
try {
|
||||
const serializedCredentials = window.localStorage.getItem(LocalStorageSecretStorageProvider.STORAGE_KEY);
|
||||
if (serializedCredentials) {
|
||||
this._secrets = JSON.parse(serializedCredentials);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!(this._secrets instanceof Object)) {
|
||||
this._secrets = {};
|
||||
}
|
||||
private async save(): Promise<void> {
|
||||
try {
|
||||
const encrypted = await this.crypto.seal(JSON.stringify(await this._secretsPromise));
|
||||
window.localStorage.setItem(this._storageKey, encrypted);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return this._secrets;
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
window.localStorage.setItem(LocalStorageSecretStorageProvider.STORAGE_KEY, JSON.stringify(this.secrets));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LocalStorageURLCallbackProvider extends Disposable implements IURLCallbackProvider {
|
||||
|
||||
private static REQUEST_ID = 0;
|
||||
|
@ -390,6 +512,17 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
|||
}
|
||||
}
|
||||
|
||||
function readCookie(name: string): string | undefined {
|
||||
const cookies = document.cookie.split('; ');
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
(function () {
|
||||
|
||||
// Find config by checking for DOM
|
||||
|
@ -399,6 +532,9 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
|||
throw new Error('Missing web configuration element');
|
||||
}
|
||||
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute);
|
||||
const secretStorageKeyPath = readCookie('vscode-secret-key-path');
|
||||
const secretStorageCrypto = secretStorageKeyPath && ServerKeyedAESCrypto.supported()
|
||||
? new ServerKeyedAESCrypto(secretStorageKeyPath) : new TransparentCrypto();
|
||||
|
||||
// Create workbench
|
||||
create(document.body, {
|
||||
|
@ -407,6 +543,8 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
|||
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
|
||||
workspaceProvider: WorkspaceProvider.create(config),
|
||||
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
|
||||
secretStorageProvider: config.remoteAuthority ? undefined /* with a remote, we don't use a local secret storage provider */ : new LocalStorageSecretStorageProvider()
|
||||
secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath
|
||||
? undefined /* with a remote without embedder-preferred storage, store on the remote */
|
||||
: new LocalStorageSecretStorageProvider(secretStorageCrypto),
|
||||
});
|
||||
})();
|
||||
|
|
Loading…
Reference in a new issue