vault adapter

This commit is contained in:
jackkav
2026-06-01 22:09:13 +02:00
parent 5e88f641a6
commit cb3ac09c83
10 changed files with 90 additions and 69 deletions

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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';

View 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');
});
});

View 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;
}
};

View File

@@ -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();

View 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;

View File

@@ -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();

View File

@@ -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;
}
};