Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Insomnia vault key management UI[INS-4715] #8296

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/insomnia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@getinsomnia/api-client": "0.0.4",
"@getinsomnia/srp-js": "1.0.0-alpha.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this was removed from the project over a year ago, we should discuss the need for this further with @gatzjames

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduce this lib back is for key generation and validation using SRP protocol. I've checked that website is also using this for e2ee with same purpose.

"@sentry/electron": "^5.1.0",
"@stoplight/spectral-core": "^1.18.3",
"@stoplight/spectral-formats": "^1.6.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/account/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ async function _unsetSessionData() {
symmetricKey: {} as JsonWebKey,
publicKey: {} as JsonWebKey,
encPrivateKey: {} as crypt.AESMessage,
vaultSalt: '',
vaultKey: '',
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/insomnia/src/common/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,7 @@ export interface Settings {
useBulkParametersEditor: boolean;
validateAuthSSL: boolean;
validateSSL: boolean;
// vault related settings
saveVaultKeyLocally: boolean;
enableVaultInScripts: boolean;
}
2 changes: 2 additions & 0 deletions packages/insomnia/src/main.development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { backupIfNewerVersionAvailable } from './main/backup';
import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron';
import { registergRPCHandlers } from './main/ipc/grpc';
import { registerMainHandlers } from './main/ipc/main';
import { registerSecretStorageHandlers } from './main/ipc/secret-storage';
import { registerCurlHandlers } from './main/network/curl';
import { registerWebSocketHandlers } from './main/network/websocket';
import { watchProxySettings } from './main/proxy';
Expand Down Expand Up @@ -64,6 +65,7 @@ app.on('ready', async () => {
registergRPCHandlers();
registerWebSocketHandlers();
registerCurlHandlers();
registerSecretStorageHandlers();

/**
* There's no option that prevents Electron from fetching spellcheck dictionaries from Chromium's CDN and passing a non-resolving URL is the only known way to prevent it from fetching.
Expand Down
7 changes: 6 additions & 1 deletion packages/insomnia/src/main/ipc/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export type HandleChannels =
| 'webSocket.open'
| 'webSocket.readyState'
| 'writeFile'
| 'extractJsonFileFromPostmanDataDumpArchive';
| 'extractJsonFileFromPostmanDataDumpArchive'
| 'secretStorage.setSecret'
| 'secretStorage.getSecret'
| 'secretStorage.deleteSecret'
| 'secretStorage.encryptString'
| 'secretStorage.decryptString';

export const ipcMainHandle = (
channel: HandleChannels,
Expand Down
3 changes: 2 additions & 1 deletion packages/insomnia/src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { WebSocketBridgeAPI } from '../network/websocket';
import { ipcMainHandle, ipcMainOn, ipcMainOnce, type RendererOnChannels } from './electron';
import extractPostmanDataDumpHandler from './extractPostmanDataDump';
import type { gRPCBridgeAPI } from './grpc';

import type { secretStorageBridgeAPI } from './secret-storage';
export interface RendererToMainBridgeAPI {
loginStateChange: () => void;
openInBrowser: (url: string) => void;
Expand All @@ -37,6 +37,7 @@ export interface RendererToMainBridgeAPI {
webSocket: WebSocketBridgeAPI;
grpc: gRPCBridgeAPI;
curl: CurlBridgeAPI;
secretStorage: secretStorageBridgeAPI;
trackSegmentEvent: (options: { event: string; properties?: Record<string, unknown> }) => void;
trackPageView: (options: { name: string }) => void;
showContextMenu: (options: { key: string; nunjucksTag?: { template: string; range: MarkerRange } }) => void;
Expand Down
77 changes: 77 additions & 0 deletions packages/insomnia/src/main/ipc/secret-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { safeStorage } from 'electron';

import LocalStorage from '../local-storage';
import { initLocalStorage } from '../window-utils';
import { ipcMainHandle } from './electron';

export interface secretStorageBridgeAPI {
setSecret: typeof setSecret;
getSecret: typeof getSecret;
deleteSecret: typeof deleteSecret;
encryptString: (raw: string) => Promise<string>;
decryptString: (cipherText: string) => Promise<string>;
}

export function registerSecretStorageHandlers() {
ipcMainHandle('secretStorage.setSecret', (_, key, secret) => setSecret(key, secret));
ipcMainHandle('secretStorage.getSecret', (_, key) => getSecret(key));
ipcMainHandle('secretStorage.deleteSecret', (_, key) => deleteSecret(key));
ipcMainHandle('secretStorage.encryptString', (_, raw) => encryptString(raw));
ipcMainHandle('secretStorage.decryptString', (_, raw) => decryptString(raw));
}

let localStorage: LocalStorage | null = null;

const getLocalStorage = () => {
if (!localStorage) {
localStorage = initLocalStorage();
}
return localStorage;
};

const setSecret = async (key: string, secret: string) => {
try {
const secretStorage = getLocalStorage();
const encrypted = encryptString(secret);
secretStorage.setItem(key, encrypted);
} catch (error) {
console.error(`Can not save secret ${error.toString()}`);
return Promise.reject(error);
}
};

const getSecret = async (key: string) => {
try {
const secretStorage = getLocalStorage();
const encrypted = secretStorage.getItem(key, '');
return encrypted === '' ? null : decryptString(encrypted);
} catch (error) {
console.error(`Can not get secret ${error.toString()}`);
return Promise.reject(null);
}
};

const deleteSecret = async (key: string) => {
try {
const secretStorage = getLocalStorage();
secretStorage.deleteItem(key);
} catch (error) {
console.error(`Can not delele secret ${error.toString()}`);
return Promise.reject(error);
}
};
Comment on lines +25 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


const encryptString = (raw: string) => {
if (safeStorage.isEncryptionAvailable()) {
return safeStorage.encryptString(raw).toString('hex');
}
return raw;
};

const decryptString = (cipherText: string) => {
const buffer = Buffer.from(cipherText, 'hex');
if (safeStorage.isEncryptionAvailable()) {
return safeStorage.decryptString(buffer);
}
return cipherText;
};
15 changes: 15 additions & 0 deletions packages/insomnia/src/main/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ class LocalStorage {
}
}

deleteItem(key: string) {
clearTimeout(this._timeouts[key]);
delete this._buffer[key];

const path = this._getKeyPath(key);

try {
fs.unlinkSync(path);
} catch (error) {
if (error.code !== 'ENOENT') {
console.error(`[localstorage] Failed to delete item from LocalStorage: ${error}`);
}
}
}

_flush() {
const keys = Object.keys(this._buffer);

Expand Down
7 changes: 5 additions & 2 deletions packages/insomnia/src/main/window-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 +800,12 @@ export const setZoom = (transformer: (current: number) => number) => () => {
localStorage?.setItem('zoomFactor', actual);
};

function initLocalStorage() {
export function initLocalStorage() {
const localStoragePath = path.join(process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'), 'localStorage');
localStorage = new LocalStorage(localStoragePath);
if (!localStorage) {
localStorage = new LocalStorage(localStoragePath);
}
return localStorage;
}

export function createWindowsAndReturnMain({ firstLaunch }: { firstLaunch?: boolean } = {}) {
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export function init(): BaseSettings {
useBulkParametersEditor: false,
validateAuthSSL: true,
validateSSL: true,
saveVaultKeyLocally: true,
enableVaultInScripts: false,
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/insomnia/src/models/user-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface BaseUserSession {
symmetricKey: JsonWebKey;
publicKey: JsonWebKey;
encPrivateKey: AESMessage;
vaultSalt?: string;
vaultKey?: string;
};

export interface HashedUserSession {
Expand All @@ -34,6 +36,8 @@ export function init(): BaseUserSession {
symmetricKey: {} as JsonWebKey,
publicKey: {} as JsonWebKey,
encPrivateKey: {} as AESMessage,
vaultKey: '',
vaultSalt: '',
};
}

Expand Down
11 changes: 11 additions & 0 deletions packages/insomnia/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { contextBridge, ipcRenderer, webUtils as _webUtils } from 'electron';

import type { gRPCBridgeAPI } from './main/ipc/grpc';
import type { secretStorageBridgeAPI } from './main/ipc/secret-storage';
import type { CurlBridgeAPI } from './main/network/curl';
import type { WebSocketBridgeAPI } from './main/network/websocket';
import { invariant } from './utils/invariant';
Expand Down Expand Up @@ -40,6 +41,15 @@ const grpc: gRPCBridgeAPI = {
loadMethods: options => ipcRenderer.invoke('grpc.loadMethods', options),
loadMethodsFromReflection: options => ipcRenderer.invoke('grpc.loadMethodsFromReflection', options),
};

const secretStorage: secretStorageBridgeAPI = {
setSecret: (key, secret) => ipcRenderer.invoke('secretStorage.setSecret', key, secret),
getSecret: key => ipcRenderer.invoke('secretStorage.getSecret', key),
deleteSecret: key => ipcRenderer.invoke('secretStorage.deleteSecret', key),
encryptString: raw => ipcRenderer.invoke('secretStorage.encryptString', raw),
decryptString: cipherText => ipcRenderer.invoke('secretStorage.decryptString', cipherText),
};

const main: Window['main'] = {
startExecution: options => ipcRenderer.send('startExecution', options),
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
Expand Down Expand Up @@ -67,6 +77,7 @@ const main: Window['main'] = {
webSocket,
grpc,
curl,
secretStorage,
trackSegmentEvent: options => ipcRenderer.send('trackSegmentEvent', options),
trackPageView: options => ipcRenderer.send('trackPageView', options),
showContextMenu: options => ipcRenderer.send('show-context-menu', options),
Expand Down
4 changes: 3 additions & 1 deletion packages/insomnia/src/ui/components/base/copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Button, type ButtonProps } from '../themed-button';

interface Props extends ButtonProps {
confirmMessage?: string;
showConfirmation?: boolean;
content: string;
title?: string;
}

export const CopyButton: FC<Props> = ({
children,
confirmMessage,
showConfirmation: showConfirmationProp = false,
content,
title,
...buttonProps
Expand All @@ -38,7 +40,7 @@ export const CopyButton: FC<Props> = ({
title={title}
onClick={onClick}
>
{showConfirmation ? (
{(showConfirmation || showConfirmationProp) ? (
<span>
{confirm} <i className="fa fa-check-circle-o" />
</span>
Expand Down
Loading
Loading