mirror of
https://github.com/Kong/insomnia.git
synced 2026-06-15 11:30:07 -04:00
fix: address Copilot review comments on PR #9992
- vault-crypto: replace forge-in-renderer with IPC bridge (main process
retains forge; renderer calls window.main.vault.{en,de}cryptSecretValue)
- mime.ts: expand lookup table to 48 entries (webp, wasm, mp4, docx, xlsx,
fonts, audio/video, etc.) and fix remaining mime-types import in send route
- response-viewer: move charset alias map to module level; normalise iconv-lite
alias names (utf8, latin1, win1252, …) to WHATWG labels for TextDecoder
- auth.clear-vault-key: fix typo "all you local" → "all your local"
This commit is contained in:
@@ -1,18 +1,57 @@
|
||||
const extensionToMimeType: Record<string, string> = {
|
||||
// text
|
||||
css: 'text/css',
|
||||
csv: 'text/csv',
|
||||
gif: 'image/gif',
|
||||
htm: 'text/html',
|
||||
html: 'text/html',
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
js: 'application/javascript',
|
||||
json: 'application/json',
|
||||
pdf: 'application/pdf',
|
||||
png: 'image/png',
|
||||
svg: 'image/svg+xml',
|
||||
jsonld: 'application/ld+json',
|
||||
md: 'text/markdown',
|
||||
mjs: 'application/javascript',
|
||||
txt: 'text/plain',
|
||||
xml: 'application/xml',
|
||||
yaml: 'application/yaml',
|
||||
yml: 'application/yaml',
|
||||
// image
|
||||
bmp: 'image/bmp',
|
||||
gif: 'image/gif',
|
||||
ico: 'image/x-icon',
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
svg: 'image/svg+xml',
|
||||
tif: 'image/tiff',
|
||||
tiff: 'image/tiff',
|
||||
webp: 'image/webp',
|
||||
// audio/video
|
||||
aac: 'audio/aac',
|
||||
flac: 'audio/flac',
|
||||
m4a: 'audio/mp4',
|
||||
mp3: 'audio/mpeg',
|
||||
mp4: 'video/mp4',
|
||||
ogg: 'audio/ogg',
|
||||
opus: 'audio/opus',
|
||||
wav: 'audio/wav',
|
||||
webm: 'video/webm',
|
||||
// document / office
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
pdf: 'application/pdf',
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// archive / binary
|
||||
gz: 'application/gzip',
|
||||
tar: 'application/x-tar',
|
||||
wasm: 'application/wasm',
|
||||
zip: 'application/zip',
|
||||
// font
|
||||
otf: 'font/otf',
|
||||
ttf: 'font/ttf',
|
||||
woff: 'font/woff',
|
||||
woff2: 'font/woff2',
|
||||
};
|
||||
|
||||
const mimeTypeToExtension: Record<string, string> = {
|
||||
|
||||
@@ -343,6 +343,12 @@ const main: Window['main'] = {
|
||||
port.postMessage({ ...options, type: 'runPreRequestScript' });
|
||||
}),
|
||||
},
|
||||
vault: {
|
||||
encryptSecretValue: (rawValue, symmetricKey) =>
|
||||
invokeWithNormalizedError('vault.encryptSecretValue', rawValue, symmetricKey),
|
||||
decryptSecretValue: (encryptedValue, symmetricKey) =>
|
||||
invokeWithNormalizedError('vault.decryptSecretValue', encryptedValue, symmetricKey),
|
||||
},
|
||||
extractJsonFileFromPostmanDataDumpArchive: archivePath =>
|
||||
invokeWithNormalizedError('extractJsonFileFromPostmanDataDumpArchive', archivePath),
|
||||
syncNewWorkspaceIfNeeded: options => invokeWithNormalizedError('syncNewWorkspaceIfNeeded', options),
|
||||
|
||||
@@ -170,7 +170,9 @@ export type HandleChannels =
|
||||
| 'timeline.getPath'
|
||||
| 'writeFile'
|
||||
| 'deleteRulesetFile'
|
||||
| 'writeResponseBodyToFile';
|
||||
| 'writeResponseBodyToFile'
|
||||
| 'vault.encryptSecretValue'
|
||||
| 'vault.decryptSecretValue';
|
||||
|
||||
export const ipcMainHandle = (
|
||||
channel: HandleChannels,
|
||||
|
||||
@@ -39,6 +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 type { AnalyticsEvent } from '../analytics';
|
||||
import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics';
|
||||
import {
|
||||
@@ -290,6 +291,10 @@ export interface RendererToMainBridgeAPI {
|
||||
syncNewWorkspaceIfNeeded: typeof syncNewWorkspaceIfNeeded;
|
||||
plugins: PluginsBridgeAPI;
|
||||
notifyPluginPromptResult: (id: string, value: string | null) => void;
|
||||
vault: {
|
||||
encryptSecretValue: (rawValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
};
|
||||
timeline: {
|
||||
getPath: (responseId: string) => Promise<string>;
|
||||
appendToFile: (options: { timelinePath: string; data: string }) => Promise<void>;
|
||||
@@ -808,5 +813,12 @@ export function registerMainHandlers() {
|
||||
ipcMainHandle('timeline.getPath', getTimelinePath);
|
||||
ipcMainHandle('timeline.appendToFile', appendToTimeline);
|
||||
|
||||
ipcMainHandle('vault.encryptSecretValue', (_, rawValue: string, symmetricKey: JsonWebKey) => {
|
||||
return encryptSecretValue(rawValue, symmetricKey);
|
||||
});
|
||||
ipcMainHandle('vault.decryptSecretValue', (_, encryptedValue: string, symmetricKey: JsonWebKey) => {
|
||||
return decryptSecretValue(encryptedValue, symmetricKey);
|
||||
});
|
||||
|
||||
registerPluginIpcHandlers();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
await services.userSession.update({ vaultSalt: newVaultSalt, vaultKey: '' });
|
||||
// show notification
|
||||
showToast({
|
||||
title: 'Your vault key has been reset, all you local secrets have been deleted.',
|
||||
title: 'Your vault key has been reset, all your local secrets have been deleted.',
|
||||
status: 'info',
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import contentDisposition from 'content-disposition';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import { mimeTypeExtension as mimeExtension } from '~/common/mime';
|
||||
import { href, redirect } from 'react-router';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
@@ -158,10 +158,10 @@ export async function maskOrDecryptVaultDataIfNecessary(vaultEnvironmentData: an
|
||||
if (isVaultEnabled && vaultKey) {
|
||||
const symmetricKey = (await decryptVaultKeyFromSession(vaultKey, true)) as JsonWebKey;
|
||||
// decrypt all secret values under vaultEnvironmentPath property in context
|
||||
Object.keys(vaultEnvironmentData).forEach(vaultContextKey => {
|
||||
for (const vaultContextKey of Object.keys(vaultEnvironmentData)) {
|
||||
const encryptedValue = vaultEnvironmentData[vaultContextKey];
|
||||
vaultEnvironmentData[vaultContextKey] = decryptSecretValue(encryptedValue, symmetricKey);
|
||||
});
|
||||
vaultEnvironmentData[vaultContextKey] = await decryptSecretValue(encryptedValue, symmetricKey);
|
||||
}
|
||||
} else if (isVaultEnabled && !vaultKey) {
|
||||
// remove all values under vaultEnvironmentPath if no vault key found
|
||||
vaultEnvironmentData = {};
|
||||
|
||||
@@ -77,8 +77,26 @@ export const EnvironmentKVEditor = ({
|
||||
);
|
||||
const codeModalRef = useRef<CodePromptModalHandle>(null);
|
||||
const [kvPairError, setKvPairError] = useState<{ id: string; error: string }[]>([]);
|
||||
const [decryptedValues, setDecryptedValues] = useState<Record<string, string>>({});
|
||||
const symmetricKey = vaultKey === '' ? {} : base64decode(vaultKey, true);
|
||||
|
||||
useEffect(() => {
|
||||
const secretPairs = kvPairs.filter(p => p.type === EnvironmentKvPairDataType.SECRET);
|
||||
if (secretPairs.length === 0 || Object.keys(symmetricKey).length === 0) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
Promise.all(
|
||||
secretPairs.map(async p => ({ id: p.id, value: await decryptSecretValue(p.value, symmetricKey as JsonWebKey) })),
|
||||
).then(results => {
|
||||
if (!cancelled) {
|
||||
setDecryptedValues(Object.fromEntries(results.map(r => [r.id, r.value])));
|
||||
}
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(kvPairs.filter(p => p.type === EnvironmentKvPairDataType.SECRET).map(p => ({ id: p.id, value: p.value }))), vaultKey]);
|
||||
|
||||
const commonItemTypes = [
|
||||
{
|
||||
id: EnvironmentKvPairDataType.STRING,
|
||||
@@ -152,7 +170,7 @@ export const EnvironmentKVEditor = ({
|
||||
onChange(kvPairs);
|
||||
};
|
||||
|
||||
const handleItemTypeChange = (id: string, newType: EnvironmentKvPairDataType) => {
|
||||
const handleItemTypeChange = async (id: string, newType: EnvironmentKvPairDataType) => {
|
||||
const targetItem = kvPairs.find(pair => pair.id === id);
|
||||
if (targetItem) {
|
||||
const { type: originType, value: originValue } = targetItem;
|
||||
@@ -172,13 +190,13 @@ export const EnvironmentKVEditor = ({
|
||||
if (yes) {
|
||||
handleItemChange(id, 'type', newType);
|
||||
// decrypt and save the value
|
||||
handleItemChange(id, 'value', decryptSecretValue(originValue, symmetricKey));
|
||||
handleItemChange(id, 'value', await decryptSecretValue(originValue, symmetricKey as JsonWebKey));
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (newType === EnvironmentKvPairDataType.SECRET) {
|
||||
// encrypt value if set to secret type
|
||||
handleItemChange(id, 'value', encryptSecretValue(originValue, symmetricKey));
|
||||
handleItemChange(id, 'value', await encryptSecretValue(originValue, symmetricKey as JsonWebKey));
|
||||
handleItemChange(id, 'type', newType);
|
||||
} else {
|
||||
handleItemChange(id, 'type', newType);
|
||||
@@ -310,9 +328,9 @@ export const EnvironmentKVEditor = ({
|
||||
itemId={id}
|
||||
enabled={enabled && !disabled}
|
||||
placeholder="Input Secret"
|
||||
value={decryptSecretValue(value, symmetricKey)}
|
||||
onChange={newValue => {
|
||||
const encryptedValue = encryptSecretValue(newValue, symmetricKey);
|
||||
value={decryptedValues[id] ?? ''}
|
||||
onChange={async newValue => {
|
||||
const encryptedValue = await encryptSecretValue(newValue, symmetricKey as JsonWebKey);
|
||||
handleItemChange(id, 'value', encryptedValue);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,25 @@ import { ResponseMultipartViewer } from './response-multipart-viewer';
|
||||
import { ResponsePDFViewer } from './response-pdf-viewer';
|
||||
import { ResponseWebView } from './response-web-view';
|
||||
|
||||
const CHARSET_ALIASES: Record<string, string> = {
|
||||
utf8: 'utf8',
|
||||
utf16le: 'utf-16le',
|
||||
ucs2: 'utf-16le',
|
||||
'ucs-2': 'utf-16le',
|
||||
latin1: 'iso-8859-1',
|
||||
binary: 'iso-8859-1',
|
||||
ascii: 'ascii',
|
||||
win1250: 'windows-1250',
|
||||
win1251: 'windows-1251',
|
||||
win1252: 'windows-1252',
|
||||
win1253: 'windows-1253',
|
||||
win1254: 'windows-1254',
|
||||
win1255: 'windows-1255',
|
||||
win1256: 'windows-1256',
|
||||
win1257: 'windows-1257',
|
||||
win1258: 'windows-1258',
|
||||
};
|
||||
|
||||
let alwaysShowLargeResponses = false;
|
||||
|
||||
export interface ResponseViewerHandle {
|
||||
@@ -147,9 +166,10 @@ export const ResponseViewer = ({
|
||||
// Show everything else as "source"
|
||||
const match = _getContentType().match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf8';
|
||||
const label = CHARSET_ALIASES[charset.toLowerCase()] ?? charset;
|
||||
// Sometimes decoding fails so fallback to regular buffer
|
||||
try {
|
||||
return new TextDecoder(charset).decode(overSizedBody);
|
||||
return new TextDecoder(label).decode(overSizedBody);
|
||||
} catch (err) {
|
||||
console.warn('[response] Failed to decode body', err);
|
||||
return overSizedBody.toString();
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { decryptSecretValue, encryptSecretValue } from './vault-crypto';
|
||||
|
||||
const TEST_AES_KEY: JsonWebKey = {
|
||||
const mockEncrypt = vi.fn();
|
||||
const mockDecrypt = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
Object.defineProperty(window, 'main', {
|
||||
value: { vault: { encryptSecretValue: mockEncrypt, decryptSecretValue: mockDecrypt } },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
const VALID_KEY: JsonWebKey = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
@@ -12,82 +23,51 @@ const TEST_AES_KEY: JsonWebKey = {
|
||||
};
|
||||
|
||||
describe('encryptSecretValue', () => {
|
||||
it('returns rawValue when symmetricKey is not an object', () => {
|
||||
expect(encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
|
||||
it('returns rawValue when symmetricKey is not an object', async () => {
|
||||
expect(await encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
|
||||
expect(mockEncrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns rawValue when symmetricKey is empty object', () => {
|
||||
expect(encryptSecretValue('secret', {})).toBe('secret');
|
||||
it('returns rawValue when symmetricKey is empty object', async () => {
|
||||
expect(await encryptSecretValue('secret', {})).toBe('secret');
|
||||
expect(mockEncrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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');
|
||||
it('delegates to window.main.vault.encryptSecretValue with a valid key', async () => {
|
||||
mockEncrypt.mockResolvedValue('encrypted-value');
|
||||
const result = await encryptSecretValue('my secret', VALID_KEY);
|
||||
expect(mockEncrypt).toHaveBeenCalledWith('my secret', VALID_KEY);
|
||||
expect(result).toBe('encrypted-value');
|
||||
});
|
||||
|
||||
it('returns original value when encryption fails', () => {
|
||||
// Use an invalid key format
|
||||
const invalidKey = { kty: 'oct', k: 'invalid' };
|
||||
const encrypted = encryptSecretValue('my secret', invalidKey as unknown as JsonWebKey);
|
||||
expect(encrypted).toBe('my secret');
|
||||
it('returns rawValue when IPC call throws', async () => {
|
||||
mockEncrypt.mockRejectedValue(new Error('IPC error'));
|
||||
const result = await encryptSecretValue('my secret', VALID_KEY);
|
||||
expect(result).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('returns encryptedValue when symmetricKey is not an object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted');
|
||||
expect(mockDecrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns encryptedValue when symmetricKey is empty object', () => {
|
||||
expect(decryptSecretValue('encrypted', {})).toBe('encrypted');
|
||||
it('returns encryptedValue when symmetricKey is empty object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', {})).toBe('encrypted');
|
||||
expect(mockDecrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('round-trips encrypt then decrypt', () => {
|
||||
const plaintext = 'my secret value';
|
||||
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
|
||||
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
it('delegates to window.main.vault.decryptSecretValue with a valid key', async () => {
|
||||
mockDecrypt.mockResolvedValue('plaintext');
|
||||
const result = await decryptSecretValue('encrypted-blob', VALID_KEY);
|
||||
expect(mockDecrypt).toHaveBeenCalledWith('encrypted-blob', VALID_KEY);
|
||||
expect(result).toBe('plaintext');
|
||||
});
|
||||
|
||||
it('returns original value when decryption fails', () => {
|
||||
// Use an invalid encrypted value
|
||||
const encrypted = encryptSecretValue('my secret', TEST_AES_KEY);
|
||||
// Try to decrypt with wrong key
|
||||
const wrongKey = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
k: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
};
|
||||
const result = decryptSecretValue(encrypted, wrongKey);
|
||||
expect(result).toBe(encrypted);
|
||||
});
|
||||
|
||||
it('handles special characters in plaintext', () => {
|
||||
const plaintext = 'special chars: !@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
|
||||
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('handles unicode characters in plaintext', () => {
|
||||
const plaintext = 'unicode: 你好世界 🚀 مرحبا العالم';
|
||||
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
|
||||
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
const plaintext = '';
|
||||
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
|
||||
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('handles large plaintext', () => {
|
||||
const plaintext = 'x'.repeat(10_000);
|
||||
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
|
||||
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
it('returns encryptedValue when IPC call throws', async () => {
|
||||
mockDecrypt.mockRejectedValue(new Error('IPC error'));
|
||||
const result = await decryptSecretValue('encrypted-blob', VALID_KEY);
|
||||
expect(result).toBe('encrypted-blob');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,109 +1,21 @@
|
||||
import 'node-forge/lib/util';
|
||||
import 'node-forge/lib/cipher';
|
||||
import 'node-forge/lib/cipherModes';
|
||||
import 'node-forge/lib/aes';
|
||||
|
||||
import forge from 'node-forge/lib/forge';
|
||||
|
||||
import type { AESMessage } from '../account/crypt';
|
||||
|
||||
const base64encode = (input: string | object) => {
|
||||
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
const binary = atob(btoa(unescape(encodeURIComponent(inputStr))));
|
||||
return btoa(binary);
|
||||
};
|
||||
|
||||
const base64decode = (base64Str: string, toObject = false) => {
|
||||
try {
|
||||
const decodedStr = decodeURIComponent(escape(atob(base64Str)));
|
||||
if (toObject) {
|
||||
return JSON.parse(decodedStr);
|
||||
}
|
||||
return decodedStr;
|
||||
} catch {
|
||||
console.error(`failed to base64 decode string ${base64Str}`);
|
||||
}
|
||||
return base64Str;
|
||||
};
|
||||
|
||||
const b64UrlToHex = (value: string) => {
|
||||
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
return forge.util.bytesToHex(atob(base64));
|
||||
};
|
||||
|
||||
const getKeyBytes = (symmetricKey: JsonWebKey) => forge.util.hexToBytes(b64UrlToHex(symmetricKey.k || ''));
|
||||
|
||||
const getRandomIv = () => {
|
||||
const iv = new Uint8Array(12);
|
||||
globalThis.crypto.getRandomValues(iv);
|
||||
return String.fromCodePoint(...iv);
|
||||
};
|
||||
|
||||
// Bind cipher methods to avoid direct pattern detection while preserving call semantics.
|
||||
// The renderer-safe vault-crypto is used only for environment secret encryption and
|
||||
// uses a random IV per encryption, so IV reuse vulnerabilities don't apply here.
|
||||
// Using createCipheriv/createDecipheriv would require IV derivation logic not worth the complexity.
|
||||
const createForgeCipher = forge.cipher.createCipher.bind(forge.cipher);
|
||||
const createForgeDecipher = forge.cipher.createDecipher.bind(forge.cipher);
|
||||
|
||||
const encryptAES = (symmetricKey: JsonWebKey, plaintext: string): AESMessage => {
|
||||
const cipher = createForgeCipher('AES-GCM', getKeyBytes(symmetricKey));
|
||||
const iv = getRandomIv();
|
||||
const encodedPlaintext = encodeURIComponent(plaintext);
|
||||
cipher.start({
|
||||
iv,
|
||||
tagLength: 128,
|
||||
});
|
||||
cipher.update(forge.util.createBuffer(encodedPlaintext));
|
||||
cipher.finish();
|
||||
return {
|
||||
iv: forge.util.bytesToHex(iv),
|
||||
t: forge.util.bytesToHex(cipher.mode.tag.bytes()),
|
||||
ad: '',
|
||||
d: forge.util.bytesToHex(cipher.output.bytes()),
|
||||
};
|
||||
};
|
||||
|
||||
const decryptAES = (symmetricKey: JsonWebKey, encryptedValue: AESMessage) => {
|
||||
const decipher = createForgeDecipher('AES-GCM', getKeyBytes(symmetricKey));
|
||||
decipher.start({
|
||||
iv: forge.util.hexToBytes(encryptedValue.iv),
|
||||
tagLength: encryptedValue.t.length * 4,
|
||||
tag: forge.util.createBuffer(forge.util.hexToBytes(encryptedValue.t)),
|
||||
additionalData: forge.util.hexToBytes(encryptedValue.ad),
|
||||
});
|
||||
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedValue.d)));
|
||||
if (!decipher.finish()) {
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
return decodeURIComponent(decipher.output.toString());
|
||||
};
|
||||
|
||||
export const encryptSecretValue = (rawValue: string, symmetricKey: JsonWebKey) => {
|
||||
export const encryptSecretValue = async (rawValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
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;
|
||||
return await window.main.vault.encryptSecretValue(rawValue, symmetricKey);
|
||||
} catch {
|
||||
// return original value if encryption fails
|
||||
return rawValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptSecretValue = (encryptedValue: string, symmetricKey: JsonWebKey) => {
|
||||
export const decryptSecretValue = async (encryptedValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
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);
|
||||
return await window.main.vault.decryptSecretValue(encryptedValue, symmetricKey);
|
||||
} catch {
|
||||
// return origin value if failed to decrypt
|
||||
return encryptedValue;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user