@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
163 lines (138 loc) • 6.63 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2021 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/platform/native/electron-main/nativeHostMainService.ts#L679-L771
import { KeyStoreService } from '../common/key-store';
import { injectable } from 'inversify';
import { isWindows } from '../common';
()
export class KeyStoreServiceImpl implements KeyStoreService {
private static readonly MAX_PASSWORD_LENGTH = 2500;
private static readonly PASSWORD_CHUNK_SIZE = KeyStoreServiceImpl.MAX_PASSWORD_LENGTH - 100;
protected keytarImplementation?: typeof import('keytar');
async setPassword(service: string, account: string, password: string): Promise<void> {
const keytar = await this.getKeytar();
if (isWindows && password.length > KeyStoreServiceImpl.MAX_PASSWORD_LENGTH) {
let index = 0;
let chunk = 0;
let hasNextChunk = true;
while (hasNextChunk) {
const passwordChunk = password.substring(index, index + KeyStoreServiceImpl.PASSWORD_CHUNK_SIZE);
index += KeyStoreServiceImpl.PASSWORD_CHUNK_SIZE;
hasNextChunk = password.length - index > 0;
const content: ChunkedPassword = {
content: passwordChunk,
hasNextChunk: hasNextChunk
};
await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
chunk++;
}
} else {
await keytar.setPassword(service, account, password);
}
}
async deletePassword(service: string, account: string): Promise<boolean> {
const keytar = await this.getKeytar();
return keytar.deletePassword(service, account);
}
async getPassword(service: string, account: string): Promise<string | undefined> {
const keytar = await this.getKeytar();
const password = await keytar.getPassword(service, account);
if (password) {
try {
let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password);
if (!content || !hasNextChunk) {
return password;
}
let index = 1;
while (hasNextChunk) {
const nextChunk = await keytar.getPassword(service, `${account}-${index++}`);
const result: ChunkedPassword = JSON.parse(nextChunk!);
content += result.content;
hasNextChunk = result.hasNextChunk;
}
return content;
} catch {
return password;
}
}
}
async findPassword(service: string): Promise<string | undefined> {
const keytar = await this.getKeytar();
const password = await keytar.findPassword(service);
return password ?? undefined;
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
const keytar = await this.getKeytar();
return keytar.findCredentials(service);
}
protected async getKeytar(): Promise<typeof import('keytar')> {
if (this.keytarImplementation) {
return this.keytarImplementation;
}
try {
this.keytarImplementation = await import('keytar');
// Try using keytar to see if it throws or not.
await this.keytarImplementation.findCredentials('test-keytar-loads');
} catch (err) {
this.keytarImplementation = new InMemoryCredentialsProvider();
console.warn('OS level credential store could not be accessed. Using in-memory credentials provider', err);
}
return this.keytarImplementation;
}
}
export class InMemoryCredentialsProvider {
private secretVault: Record<string, Record<string, string> | undefined> = {};
async getPassword(service: string, account: string): Promise<string | null> {
// eslint-disable-next-line no-null/no-null
return this.secretVault[service]?.[account] ?? null;
}
async setPassword(service: string, account: string, password: string): Promise<void> {
this.secretVault[service] = this.secretVault[service] ?? {};
this.secretVault[service]![account] = password;
}
async deletePassword(service: string, account: string): Promise<boolean> {
if (!this.secretVault[service]?.[account]) {
return false;
}
delete this.secretVault[service]![account];
if (Object.keys(this.secretVault[service]!).length === 0) {
delete this.secretVault[service];
}
return true;
}
async findPassword(service: string): Promise<string | null> {
// eslint-disable-next-line no-null/no-null
return JSON.stringify(this.secretVault[service]) ?? null;
}
async findCredentials(service: string): Promise<Array<{ account: string; password: string }>> {
const credentials: { account: string; password: string }[] = [];
for (const account of Object.keys(this.secretVault[service] || {})) {
credentials.push({ account, password: this.secretVault[service]![account] });
}
return credentials;
}
async clear(): Promise<void> {
this.secretVault = {};
}
}
interface ChunkedPassword {
content: string;
hasNextChunk: boolean;
}