Skip to content

Commit

Permalink
First pass storing private key (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles committed Oct 15, 2024
1 parent 98b7f4f commit f4adefd
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 20 deletions.
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@
"title": "Deephaven: Discard Connection",
"icon": "$(trash)"
},
{
"command": "vscode-deephaven.generateDHEKeyPair",
"title": "Deephaven: Generate DHE Key Pair",
"icon": "$(key)"
},
{
"command": "vscode-deephaven.openInBrowser",
"title": "Deephaven: Open in Browser",
Expand Down Expand Up @@ -656,6 +661,10 @@
"command": "vscode-deephaven.disconnectFromServer",
"when": "false"
},
{
"command": "vscode-deephaven.generateDHEKeyPair",
"when:": "false"
},
{
"command": "vscode-deephaven.openVariablePanels",
"when": "false"
Expand Down Expand Up @@ -734,6 +743,11 @@
"when": "view == vscode-deephaven.serverConnectionTree && viewItem == isConnection",
"group": "inline@2"
},
{
"command": "vscode-deephaven.generateDHEKeyPair",
"when": "view == vscode-deephaven.serverTree && viewItem == isDHEServerRunning",
"group": "inline"
},
{
"command": "vscode-deephaven.openInBrowser",
"when": "view == vscode-deephaven.serverTree && (viewItem == isManagedServerConnected || viewItem == isServerRunningConnected || viewItem == isServerRunningDisconnected || viewItem == isManagedServerDisconnected || viewItem == isDHEServerRunning)",
Expand Down
45 changes: 29 additions & 16 deletions src/common/commands.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { EXTENSION_ID } from './constants';

export const CONNECT_TO_SERVER_CMD = `${EXTENSION_ID}.connectToServer`;
export const CREATE_NEW_TEXT_DOC_CMD = `${EXTENSION_ID}.createNewTextDoc`;
export const DISCONNECT_EDITOR_CMD = `${EXTENSION_ID}.disconnectEditor`;
export const DISCONNECT_FROM_SERVER_CMD = `${EXTENSION_ID}.disconnectFromServer`;
export const DOWNLOAD_LOGS_CMD = `${EXTENSION_ID}.downloadLogs`;
export const OPEN_IN_BROWSER_CMD = `${EXTENSION_ID}.openInBrowser`;
export const OPEN_VARIABLE_PANELS_CMD = `${EXTENSION_ID}.openVariablePanels`;
export const REFRESH_SERVER_TREE_CMD = `${EXTENSION_ID}.refreshServerTree`;
export const REFRESH_SERVER_CONNECTION_TREE_CMD = `${EXTENSION_ID}.refreshServerConnectionTree`;
export const REFRESH_VARIABLE_PANELS_CMD = `${EXTENSION_ID}.refreshVariablePanels`;
export const REQUEST_DHE_USER_CREDENTIALS_CMD = `${EXTENSION_ID}.requestDheUserCredentials`;
export const RUN_CODE_COMMAND = `${EXTENSION_ID}.runCode`;
export const RUN_SELECTION_COMMAND = `${EXTENSION_ID}.runSelection`;
export const SELECT_CONNECTION_COMMAND = `${EXTENSION_ID}.selectConnection`;
export const START_SERVER_CMD = `${EXTENSION_ID}.startServer`;
export const STOP_SERVER_CMD = `${EXTENSION_ID}.stopServer`;
/**
* Create a command string prefixed with the extension id.
* @param cmd The command string suffix.
*/
function cmd<T extends string>(cmd: T): `${typeof EXTENSION_ID}.${T}` {
return `${EXTENSION_ID}.${cmd}`;
}

export const CONNECT_TO_SERVER_CMD = cmd('connectToServer');
export const CREATE_NEW_TEXT_DOC_CMD = cmd('createNewTextDoc');
export const DISCONNECT_EDITOR_CMD = cmd('disconnectEditor');
export const DISCONNECT_FROM_SERVER_CMD = cmd('disconnectFromServer');
export const DOWNLOAD_LOGS_CMD = cmd('downloadLogs');
export const GENERATE_DHE_KEY_PAIR_CMD = cmd('generateDHEKeyPair');
export const OPEN_IN_BROWSER_CMD = cmd('openInBrowser');
export const OPEN_VARIABLE_PANELS_CMD = cmd('openVariablePanels');
export const REFRESH_SERVER_TREE_CMD = cmd('refreshServerTree');
export const REFRESH_SERVER_CONNECTION_TREE_CMD = cmd(
'refreshServerConnectionTree'
);
export const REFRESH_VARIABLE_PANELS_CMD = cmd('refreshVariablePanels');
export const REQUEST_DHE_USER_CREDENTIALS_CMD = cmd(
'requestDheUserCredentials'
);
export const RUN_CODE_COMMAND = cmd('runCode');
export const RUN_SELECTION_COMMAND = cmd('runSelection');
export const SELECT_CONNECTION_COMMAND = cmd('selectConnection');
export const START_SERVER_CMD = cmd('startServer');
export const STOP_SERVER_CMD = cmd('stopServer');
14 changes: 13 additions & 1 deletion src/controllers/ExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
DheServiceCache,
DhService,
PanelService,
SecretService,
ServerManager,
URLMap,
} from '../services';
Expand Down Expand Up @@ -84,6 +85,7 @@ export class ExtensionController implements Disposable {

this.initializeDiagnostics();
this.initializeConfig();
this.initializeSecrets();
this.initializeCodeLenses();
this.initializeHoverProviders();
this.initializeMessaging();
Expand Down Expand Up @@ -120,6 +122,7 @@ export class ExtensionController implements Disposable {
private _dhcServiceFactory: IDhServiceFactory | null = null;
private _dheJsApiCache: IAsyncCacheService<URL, DheType> | null = null;
private _dheServiceFactory: IDheServiceFactory | null = null;
private _secretService: SecretService | null = null;
private _serverManager: IServerManager | null = null;
private _userLoginController: UserLoginController | null = null;

Expand Down Expand Up @@ -168,6 +171,13 @@ export class ExtensionController implements Disposable {
);
};

/**
* Initialize secrets.
*/
initializeSecrets = (): void => {
this._secretService = new SecretService(this._context.secrets);
};

/**
* Initialize connection controller.
*/
Expand Down Expand Up @@ -224,9 +234,11 @@ export class ExtensionController implements Disposable {
*/
initializeUserLoginController = (): void => {
assertDefined(this._dheCredentialsCache, 'dheCredentialsCache');
assertDefined(this._secretService, 'secretService');

this._userLoginController = new UserLoginController(
this._dheCredentialsCache
this._dheCredentialsCache,
this._secretService
);

this._context.subscriptions.push(this._userLoginController);
Expand Down
57 changes: 54 additions & 3 deletions src/controllers/UserLoginController.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import * as vscode from 'vscode';
import type { URLMap } from '../services';
import type { SecretService, URLMap } from '../services';
import { ControllerBase } from './ControllerBase';
import type { LoginCredentials as DheLoginCredentials } from '@deephaven-enterprise/jsapi-types';
import { REQUEST_DHE_USER_CREDENTIALS_CMD } from '../common';
import {
GENERATE_DHE_KEY_PAIR_CMD,
REQUEST_DHE_USER_CREDENTIALS_CMD,
} from '../common';
import { generateKeyPairForUser, Logger } from '../util';
import type { ServerState } from '../types';

const logger = new Logger('UserLoginController');

/**
* Controller for user login.
*/
export class UserLoginController extends ControllerBase {
constructor(dheCredentialsCache: URLMap<DheLoginCredentials>) {
constructor(
dheCredentialsCache: URLMap<DheLoginCredentials>,
secretService: SecretService
) {
super();
this.dheCredentialsCache = dheCredentialsCache;
this.secretService = secretService;

this.registerCommand(
GENERATE_DHE_KEY_PAIR_CMD,
this.onDidRequestGenerateDheKeyPair
);

this.registerCommand(
REQUEST_DHE_USER_CREDENTIALS_CMD,
Expand All @@ -19,6 +35,41 @@ export class UserLoginController extends ControllerBase {
}

private readonly dheCredentialsCache: URLMap<DheLoginCredentials>;
private readonly secretService: SecretService;

/**
* Handle request for generating a DHE key pair.
* @param serverState The server state to generate the key pair for.
*/
onDidRequestGenerateDheKeyPair = async (
serverState: ServerState
): Promise<void> => {
const serverUrl = serverState.url;
await this.onDidRequestDheUserCredentials(serverUrl);

const credentials = this.dheCredentialsCache.get(serverUrl);
if (credentials?.username == null) {
return;
}

const { publicKey, privateKey } = generateKeyPairForUser(
credentials.username
);

// TODO: Challenge response send public key to server
logger.debug('Public key:', publicKey);

await this.secretService.storePrivateKey(
credentials.username,
serverUrl,
privateKey
);

// Remove credentials from cache since presumably a valid key pair was
// generated and we'll want the user to authenticate with the private key
// instead.
this.dheCredentialsCache.delete(serverUrl);
};

/**
* Handle the request for DHE user credentials. If credentials are provided,
Expand Down
39 changes: 39 additions & 0 deletions src/services/SecretService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { SecretStorage } from 'vscode';
import type { DHPrivateKey } from '../types';

class Key {
static privateKey(userName: string, serverUrl: URL): string {
return `privateKey.${userName}.${serverUrl.toString()}`;
}
}

/**
* Wrapper around `vscode.SecretStorage` for storing and retrieving secrets.
* NOTE: For debugging, the secret store contents can be dumped to devtools
* console via:
* > Developer: Log Storage Database Contents
*/
export class SecretService {
constructor(secrets: SecretStorage) {
this._secrets = secrets;
}

private readonly _secrets: SecretStorage;

getPrivateKey = async (
userName: string,
serverUrl: URL
): Promise<DHPrivateKey | undefined> => {
return this._secrets.get(Key.privateKey(userName, serverUrl)) as Promise<
DHPrivateKey | undefined
>;
};

storePrivateKey = async (
userName: string,
serverUrl: URL,
privateKey: DHPrivateKey
): Promise<void> => {
await this._secrets.store(Key.privateKey(userName, serverUrl), privateKey);
};
}
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './DhcServiceFactory';
export * from './DheService';
export * from './PanelService';
export * from './PollingService';
export * from './SecretService';
export * from './SerializedKeyMap';
export * from './ServerManager';
export * from './URIMap';
Expand Down
7 changes: 7 additions & 0 deletions src/types/commonTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export interface EnterpriseConnectionConfig {
experimentalWorkerConfig?: WorkerConfig;
}

export type DHPrivateKey = Brand<'DHPrivateKey', string>;
export type DHPublicKey = Brand<'DHPublicKey', string>;
export type DHKeyPair = {
privateKey: DHPrivateKey;
publicKey: DHPublicKey;
};

export type ServerConnectionConfig =
| CoreConnectionConfig
| EnterpriseConnectionConfig
Expand Down
45 changes: 45 additions & 0 deletions src/util/authUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { generateKeyPairSync } from 'node:crypto';
import type { DHKeyPair, DHPrivateKey, DHPublicKey } from '../types';

// synonymous with secp256r1
const NAMED_CURVE = 'prime256v1' as const;

/**
* Generate a new keypair in Deephaven format.
* @param userName The userName to generate the keypair for.
*/
export function generateKeyPairForUser<TUser extends string>(
userName: TUser
): DHKeyPair {
const { publicKey: publicKeyBuffer, privateKey: privateKeyBuffer } =
generateKeyPairSync('ec', {
namedCurve: NAMED_CURVE,
publicKeyEncoding: { type: 'spki', format: 'der' },
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
});

const publicBase64Str = toEcBase64String(publicKeyBuffer);
const privateBase64Str = toEcBase64String(privateKeyBuffer);

const publicKey = `${userName} ${publicBase64Str}` as DHPublicKey;
const privateKey = [
`user ${userName}`,
`operateas ${userName}`,
`public ${publicBase64Str}`,
`private ${privateBase64Str}`,
].join('\n') as DHPrivateKey;

return {
publicKey,
privateKey,
};
}

/**
* Prepend 'EC:' to the key and convert to a base64 string.
* @param keyBuffer
*/
export function toEcBase64String(keyBuffer: Buffer): string {
const ecSentinel = Buffer.from('EC:');
return Buffer.concat([ecSentinel, keyBuffer]).toString('base64');
}
1 change: 1 addition & 0 deletions src/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './assertUtil';
export * from './authUtils';
export * from './dataUtils';
export * from './idUtils';
export * from './isInstanceOf';
Expand Down

0 comments on commit f4adefd

Please sign in to comment.