mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-04 03:42:20 -04:00
vault adapter
This commit is contained in:
@@ -39,7 +39,7 @@ import type {
|
||||
import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window';
|
||||
import type { PluginsBridgeAPI } from '../../plugins/bridge-types';
|
||||
import type { RenderedRequest } from '../../templating/types';
|
||||
import { decryptSecretValue,encryptSecretValue } from '../../utils/vault';
|
||||
import { decryptSecretValue, encryptSecretValue } from '../../utils/vault-adapter';
|
||||
import type { AnalyticsEvent } from '../analytics';
|
||||
import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics';
|
||||
import {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { EditorFromTextArea, MarkerRange } from 'codemirror';
|
||||
import { models, services } from 'insomnia-data';
|
||||
|
||||
import { decryptSecretValue } from '~/utils/vault-crypto';
|
||||
|
||||
import { decryptSecretValue } from '~/utils/vault-adapter';
|
||||
import type { NunjucksParsedTag, NunjucksParsedTagArg, RenderPurpose } from '../templating/types';
|
||||
import { decryptVaultKeyFromSession } from '../utils/vault';
|
||||
import { tokenizeArgs } from './tokenize-args';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { checkNestedKeys, ensureKeyIsValid } from '~/utils/environment-utils';
|
||||
|
||||
import { generateId } from '../../../../common/misc';
|
||||
import { base64decode } from '../../../../utils/vault';
|
||||
import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-crypto';
|
||||
import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-adapter';
|
||||
import { PromptButton } from '../../base/prompt-button';
|
||||
import { Icon } from '../../icon';
|
||||
import { showModal } from '../../modals';
|
||||
|
||||
42
packages/insomnia/src/utils/vault-adapter.node.test.ts
Normal file
42
packages/insomnia/src/utils/vault-adapter.node.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { decryptSecretValue, encryptSecretValue } from './vault-adapter.node';
|
||||
|
||||
const TEST_AES_KEY: JsonWebKey = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
key_ops: ['encrypt', 'decrypt'],
|
||||
k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE',
|
||||
};
|
||||
|
||||
describe('encryptSecretValue', () => {
|
||||
it('returns rawValue when symmetricKey is not an object', async () => {
|
||||
expect(await encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
|
||||
});
|
||||
|
||||
it('returns rawValue when symmetricKey is empty object', async () => {
|
||||
expect(await encryptSecretValue('secret', {})).toBe('secret');
|
||||
});
|
||||
|
||||
it('encrypts the value with a valid key', async () => {
|
||||
const encrypted = await encryptSecretValue('my secret', TEST_AES_KEY);
|
||||
expect(typeof encrypted).toBe('string');
|
||||
expect(encrypted).not.toBe('my secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptSecretValue', () => {
|
||||
it('returns encryptedValue when symmetricKey is not an object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted');
|
||||
});
|
||||
|
||||
it('returns encryptedValue when symmetricKey is empty object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', {})).toBe('encrypted');
|
||||
});
|
||||
|
||||
it('round-trips encrypt then decrypt', async () => {
|
||||
const encrypted = await encryptSecretValue('my secret', TEST_AES_KEY);
|
||||
expect(await decryptSecretValue(encrypted, TEST_AES_KEY)).toBe('my secret');
|
||||
});
|
||||
});
|
||||
26
packages/insomnia/src/utils/vault-adapter.node.ts
Normal file
26
packages/insomnia/src/utils/vault-adapter.node.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type AESMessage, decryptAES, encryptAES } from '../account/crypt';
|
||||
import { base64decode, base64encode } from './vault';
|
||||
|
||||
export const encryptSecretValue = async (rawValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
return rawValue;
|
||||
}
|
||||
try {
|
||||
const encryptResult = encryptAES(symmetricKey, rawValue);
|
||||
return base64encode(encryptResult);
|
||||
} catch {
|
||||
return rawValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptSecretValue = async (encryptedValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
return encryptedValue;
|
||||
}
|
||||
try {
|
||||
const jsonWebKey = base64decode(encryptedValue, true) as AESMessage;
|
||||
return decryptAES(symmetricKey, jsonWebKey);
|
||||
} catch {
|
||||
return encryptedValue;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { decryptSecretValue, encryptSecretValue } from './vault-crypto';
|
||||
import { decryptSecretValue, encryptSecretValue } from './vault-adapter.renderer';
|
||||
|
||||
const mockEncrypt = vi.fn();
|
||||
const mockDecrypt = vi.fn();
|
||||
9
packages/insomnia/src/utils/vault-adapter.ts
Normal file
9
packages/insomnia/src/utils/vault-adapter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Runtime adapter selection: renderer delegates to IPC, node/CLI uses direct crypto.
|
||||
// Vite inlines process.type at build time so Rollup tree-shakes the unused branch from each bundle.
|
||||
import type * as AdapterType from './vault-adapter.node';
|
||||
|
||||
const impl = (
|
||||
(process as any).type === 'renderer' ? require('./vault-adapter.renderer') : require('./vault-adapter.node')
|
||||
) as typeof AdapterType;
|
||||
|
||||
export const { encryptSecretValue, decryptSecretValue } = impl;
|
||||
@@ -1,13 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
base64decode,
|
||||
base64encode,
|
||||
decryptSecretValue,
|
||||
decryptVaultKeyFromSession,
|
||||
encryptSecretValue,
|
||||
} from './vault';
|
||||
import { base64decode, base64encode, decryptVaultKeyFromSession } from './vault';
|
||||
|
||||
vi.mock('../models/settings', () => ({
|
||||
getOrCreate: vi.fn(),
|
||||
@@ -32,11 +26,12 @@ const mockSecretStorage = {
|
||||
|
||||
describe('base64encode', () => {
|
||||
it('encodes a string', () => {
|
||||
expect(base64encode('hello world')).toBe(Buffer.from('hello world', 'utf8').toString('base64'));
|
||||
expect(base64encode('hello world')).toBe('aGVsbG8gd29ybGQ=');
|
||||
});
|
||||
|
||||
it('encodes a JsonWebKey object', () => {
|
||||
expect(base64encode(TEST_AES_KEY)).toBe(Buffer.from(JSON.stringify(TEST_AES_KEY), 'utf8').toString('base64'));
|
||||
const encoded = base64encode(TEST_AES_KEY);
|
||||
expect(base64decode(encoded, true)).toEqual(TEST_AES_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,29 +53,6 @@ describe('base64decode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptSecretValue', () => {
|
||||
it('returns rawValue when symmetricKey is not an object', () => {
|
||||
expect(encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
|
||||
});
|
||||
|
||||
it('encrypts the value with a valid key', () => {
|
||||
const encrypted = encryptSecretValue('my secret', TEST_AES_KEY);
|
||||
expect(typeof encrypted).toBe('string');
|
||||
expect(encrypted).not.toBe('my secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptSecretValue', () => {
|
||||
it('returns encryptedValue when symmetricKey is not an object', () => {
|
||||
expect(decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted');
|
||||
});
|
||||
|
||||
it('round-trips encrypt then decrypt', () => {
|
||||
const encrypted = encryptSecretValue('my secret', TEST_AES_KEY);
|
||||
expect(decryptSecretValue(encrypted, TEST_AES_KEY)).toBe('my secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptVaultKeyFromSession', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { services } from 'insomnia-data';
|
||||
|
||||
import { type AESMessage, decryptAES, encryptAES } from '../account/crypt';
|
||||
import { getInsomniaVaultKey, PLAYWRIGHT_TEST } from '../common/constants';
|
||||
|
||||
export const base64encode = (input: string | JsonWebKey) => {
|
||||
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
return Buffer.from(inputStr, 'utf8').toString('base64');
|
||||
const bytes = new TextEncoder().encode(inputStr);
|
||||
let binary = '';
|
||||
bytes.forEach(byte => (binary += String.fromCodePoint(byte)));
|
||||
return btoa(binary);
|
||||
};
|
||||
|
||||
export function base64decode(base64Str: string, toObject: true): object;
|
||||
export function base64decode(base64Str: string, toObject: false): string;
|
||||
export function base64decode(base64Str: string, toObject: boolean): string | object {
|
||||
try {
|
||||
const decodedStr = Buffer.from(base64Str, 'base64').toString('utf8');
|
||||
const decodedStr = new TextDecoder().decode(Uint8Array.from(atob(base64Str), c => c.codePointAt(0) ?? 0));
|
||||
if (toObject) {
|
||||
return JSON.parse(decodedStr);
|
||||
}
|
||||
@@ -61,32 +63,3 @@ export const getVaultKeyFromStorage = async (accountId: string) => {
|
||||
export const deleteVaultKeyFromStorage = async (accountId: string) => {
|
||||
await window.main.secretStorage.deleteSecret(getVaultSecretKey(accountId));
|
||||
};
|
||||
|
||||
export const encryptSecretValue = (rawValue: string, symmetricKey: JsonWebKey) => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
// invalid symmetricKey
|
||||
return rawValue;
|
||||
}
|
||||
try {
|
||||
const encryptResult = encryptAES(symmetricKey, rawValue);
|
||||
const encryptedValue = base64encode(encryptResult);
|
||||
return encryptedValue;
|
||||
} catch {
|
||||
// return original value if encryption fails
|
||||
return rawValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptSecretValue = (encryptedValue: string, symmetricKey: JsonWebKey) => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
// invalid symmetricKey
|
||||
return encryptedValue;
|
||||
}
|
||||
try {
|
||||
const jsonWebKey = base64decode(encryptedValue, true) as AESMessage;
|
||||
return decryptAES(symmetricKey, jsonWebKey);
|
||||
} catch {
|
||||
// return origin value if failed to decrypt
|
||||
return encryptedValue;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user