diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index a6ea08f0c9..65476e9f52 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -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 { diff --git a/packages/insomnia/src/templating/utils.ts b/packages/insomnia/src/templating/utils.ts index e4146ba012..b8d0a8030f 100644 --- a/packages/insomnia/src/templating/utils.ts +++ b/packages/insomnia/src/templating/utils.ts @@ -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'; diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx index 3d89544d67..81f2459e2a 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx @@ -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'; diff --git a/packages/insomnia/src/utils/vault-adapter.node.test.ts b/packages/insomnia/src/utils/vault-adapter.node.test.ts new file mode 100644 index 0000000000..19be47d0e2 --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.node.test.ts @@ -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'); + }); +}); diff --git a/packages/insomnia/src/utils/vault-adapter.node.ts b/packages/insomnia/src/utils/vault-adapter.node.ts new file mode 100644 index 0000000000..b02186c290 --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.node.ts @@ -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 => { + 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 => { + 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; + } +}; diff --git a/packages/insomnia/src/utils/vault-crypto.test.ts b/packages/insomnia/src/utils/vault-adapter.renderer.test.ts similarity index 99% rename from packages/insomnia/src/utils/vault-crypto.test.ts rename to packages/insomnia/src/utils/vault-adapter.renderer.test.ts index 19a96affbb..50e9691038 100644 --- a/packages/insomnia/src/utils/vault-crypto.test.ts +++ b/packages/insomnia/src/utils/vault-adapter.renderer.test.ts @@ -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(); diff --git a/packages/insomnia/src/utils/vault-crypto.ts b/packages/insomnia/src/utils/vault-adapter.renderer.ts similarity index 100% rename from packages/insomnia/src/utils/vault-crypto.ts rename to packages/insomnia/src/utils/vault-adapter.renderer.ts diff --git a/packages/insomnia/src/utils/vault-adapter.ts b/packages/insomnia/src/utils/vault-adapter.ts new file mode 100644 index 0000000000..3e75d3dfdb --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.ts @@ -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; diff --git a/packages/insomnia/src/utils/vault.test.ts b/packages/insomnia/src/utils/vault.test.ts index a8f801f85d..570e5f8aa4 100644 --- a/packages/insomnia/src/utils/vault.test.ts +++ b/packages/insomnia/src/utils/vault.test.ts @@ -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(); diff --git a/packages/insomnia/src/utils/vault.ts b/packages/insomnia/src/utils/vault.ts index 111ced6fc1..60944c8ecb 100644 --- a/packages/insomnia/src/utils/vault.ts +++ b/packages/insomnia/src/utils/vault.ts @@ -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; - } -};