add crypto bridges

This commit is contained in:
jackkav
2026-06-01 22:28:42 +02:00
parent 42c44598e0
commit 4f9498be9f
7 changed files with 127 additions and 39 deletions

View File

@@ -1,10 +1,11 @@
import { getEncryptionKeys, getUserProfile, logout as logoutAPI } from 'insomnia-api';
import type { GitRepository, Project, WorkspaceMeta } from 'insomnia-data';
import { models, services } from 'insomnia-data';
import type { GitRepository, Project, WorkspaceMeta } from '~/insomnia-data';
import type { AESMessage } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { AI_PLUGIN_NAME, LLM_BACKENDS } from '../common/constants';
import { database } from '../common/database';
import * as crypt from './crypt';
export interface SessionData {
accountId: string;
@@ -14,7 +15,7 @@ export interface SessionData {
lastName: string;
symmetricKey: JsonWebKey;
publicKey: JsonWebKey;
encPrivateKey: crypt.AESMessage;
encPrivateKey: AESMessage;
}
/** Creates a session from a sessionId and derived symmetric key. */
@@ -27,7 +28,7 @@ export async function absorbKey(sessionId: string, key: string) {
]);
const { public_key: publicKey, enc_private_key: encPrivateKey, enc_symmetric_key: encSymmetricKey } = keys;
const { email, id: accountId, first_name: firstName, last_name: lastName } = profile;
const symmetricKeyStr = crypt.decryptAES(key, JSON.parse(encSymmetricKey));
const symmetricKeyStr = await window.main.crypt.decryptAES(key, JSON.parse(encSymmetricKey));
// Store the information for later
await setSessionData(
@@ -57,7 +58,7 @@ export async function getPrivateKey() {
throw new Error("Can't get private key: session is missing keys.");
}
const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey);
const privateKeyStr = await window.main.crypt.decryptAES(symmetricKey, encPrivateKey);
return JSON.parse(privateKeyStr) as JsonWebKey;
}
@@ -104,7 +105,7 @@ export async function setSessionData(
email: string,
symmetricKey: JsonWebKey,
publicKey: JsonWebKey,
encPrivateKey: crypt.AESMessage,
encPrivateKey: AESMessage,
) {
const sessionData: SessionData = {
id,

View File

@@ -351,6 +351,26 @@ const main: Window['main'] = {
decryptSecretValue: (encryptedValue, symmetricKey) =>
invokeWithNormalizedError('vault.decryptSecretValue', encryptedValue, symmetricKey),
},
crypt: {
encryptRSAWithJWK: (publicKeyJWK: JsonWebKey, plaintext: string) =>
invokeWithNormalizedError('crypt.encryptRSAWithJWK', publicKeyJWK, plaintext),
decryptRSAWithJWK: (privateJWK: JsonWebKey, encryptedBlob: string) =>
invokeWithNormalizedError('crypt.decryptRSAWithJWK', privateJWK, encryptedBlob),
encryptAESBuffer: (jwkOrKey: string | JsonWebKey, buff: number[], additionalData?: string) =>
invokeWithNormalizedError('crypt.encryptAESBuffer', jwkOrKey, buff, additionalData),
encryptAES: (jwkOrKey: string | JsonWebKey, plaintext: string, additionalData?: string) =>
invokeWithNormalizedError('crypt.encryptAES', jwkOrKey, plaintext, additionalData),
decryptAES: (jwkOrKey: string | JsonWebKey, encryptedResult: object) =>
invokeWithNormalizedError('crypt.decryptAES', jwkOrKey, encryptedResult),
decryptAESToBuffer: (jwkOrKey: string | JsonWebKey, encryptedResult: object) =>
invokeWithNormalizedError('crypt.decryptAESToBuffer', jwkOrKey, encryptedResult),
generateAES256Key: () => invokeWithNormalizedError('crypt.generateAES256Key'),
},
sealedBox: {
keyPair: () => invokeWithNormalizedError('sealedbox.keyPair'),
open: (sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) =>
invokeWithNormalizedError('sealedbox.open', sealedbox, pk, sk),
},
extractJsonFileFromPostmanDataDumpArchive: archivePath =>
invokeWithNormalizedError('extractJsonFileFromPostmanDataDumpArchive', archivePath),
syncNewWorkspaceIfNeeded: options => invokeWithNormalizedError('syncNewWorkspaceIfNeeded', options),

View File

@@ -174,7 +174,16 @@ export type HandleChannels =
| 'deleteRulesetFile'
| 'writeResponseBodyToFile'
| 'vault.encryptSecretValue'
| 'vault.decryptSecretValue';
| 'vault.decryptSecretValue'
| 'crypt.encryptRSAWithJWK'
| 'crypt.decryptRSAWithJWK'
| 'crypt.encryptAESBuffer'
| 'crypt.encryptAES'
| 'crypt.decryptAES'
| 'crypt.decryptAESToBuffer'
| 'crypt.generateAES256Key'
| 'sealedbox.keyPair'
| 'sealedbox.open';
export const ipcMainHandle = (
channel: HandleChannels,

View File

@@ -38,9 +38,11 @@ import type {
ModelConfig,
} from '~/plugins/types';
import * as crypt from '../../account/crypt';
import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window';
import type { PluginsBridgeAPI } from '../../plugins/bridge-types';
import type { RenderedRequest } from '../../templating/types';
import { keyPair as sealedboxKeyPair, open as sealedboxOpen } from '../../utils/sealedbox';
import { decryptSecretValue, encryptSecretValue } from '../../utils/vault-adapter';
import type { AnalyticsEvent } from '../analytics';
import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics';
@@ -298,6 +300,27 @@ export interface RendererToMainBridgeAPI {
encryptSecretValue: (rawValue: string, symmetricKey: JsonWebKey) => Promise<string>;
decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise<string>;
};
crypt: {
encryptRSAWithJWK: (publicKeyJWK: JsonWebKey, plaintext: string) => Promise<string>;
decryptRSAWithJWK: (privateJWK: JsonWebKey, encryptedBlob: string) => Promise<string>;
encryptAESBuffer: (
jwkOrKey: string | JsonWebKey,
buff: number[],
additionalData?: string,
) => Promise<crypt.AESMessage>;
encryptAES: (
jwkOrKey: string | JsonWebKey,
plaintext: string,
additionalData?: string,
) => Promise<crypt.AESMessage>;
decryptAES: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise<string>;
decryptAESToBuffer: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise<number[]>;
generateAES256Key: () => Promise<JsonWebKey>;
};
sealedBox: {
keyPair: () => Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>;
open: (sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => Promise<Uint8Array | null>;
};
timeline: {
getPath: (responseId: string) => Promise<string>;
appendToFile: (options: { timelinePath: string; data: string }) => Promise<void>;
@@ -826,6 +849,38 @@ export function registerMainHandlers() {
return decryptSecretValue(encryptedValue, symmetricKey);
});
ipcMainHandle('crypt.encryptRSAWithJWK', (_, publicKeyJWK: JsonWebKey, plaintext: string) => {
return crypt.encryptRSAWithJWK(publicKeyJWK, plaintext);
});
ipcMainHandle('crypt.decryptRSAWithJWK', (_, privateJWK: JsonWebKey, encryptedBlob: string) => {
return crypt.decryptRSAWithJWK(privateJWK, encryptedBlob);
});
ipcMainHandle(
'crypt.encryptAESBuffer',
(_, jwkOrKey: string | JsonWebKey, buff: number[], additionalData?: string) => {
return crypt.encryptAESBuffer(jwkOrKey, Buffer.from(buff), additionalData);
},
);
ipcMainHandle('crypt.encryptAES', (_, jwkOrKey: string | JsonWebKey, plaintext: string, additionalData?: string) => {
return crypt.encryptAES(jwkOrKey, plaintext, additionalData);
});
ipcMainHandle('crypt.decryptAES', (_, jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => {
return crypt.decryptAES(jwkOrKey, encryptedResult);
});
ipcMainHandle('crypt.decryptAESToBuffer', (_, jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => {
return Array.from(crypt.decryptAESToBuffer(jwkOrKey, encryptedResult));
});
ipcMainHandle('crypt.generateAES256Key', _ => {
return crypt.generateAES256Key();
});
ipcMainHandle('sealedbox.keyPair', _ => {
return sealedboxKeyPair();
});
ipcMainHandle('sealedbox.open', (_, sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => {
return sealedboxOpen(sealedbox, pk, sk);
});
ipcMainHandle('run-tests', async (_, src: string) => {
const { runTests } = await import('insomnia-testing');
const sendRequest = getSendRequestCallback();

View File

@@ -1,24 +1,23 @@
import * as session from '../account/session';
import { getAppWebsiteBaseURL, getInsomniaPublicKey, getInsomniaSecretKey } from '../common/constants';
import { invariant } from '../utils/invariant';
import { keyPair, open } from '../utils/sealedbox';
interface AuthBox {
token: string;
key: string;
}
const sessionKeyPair = keyPair();
encodeBase64(sessionKeyPair.publicKey).then(res => {
const sessionKeyPairPromise = window.main.sealedBox.keyPair();
sessionKeyPairPromise.then(async kp => {
try {
window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res);
const pub = await encodeBase64(kp.publicKey);
window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || pub);
} catch {
console.error('Failed to store public key in localStorage.');
}
});
encodeBase64(sessionKeyPair.secretKey).then(res => {
try {
window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res);
const sec = await encodeBase64(kp.secretKey);
window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || sec);
} catch {
console.error('Failed to store secret key in localStorage.');
}
@@ -68,7 +67,7 @@ export async function submitAuthCode(code: string) {
const rawBox = await decodeBase64(code.trim());
const publicKey = await decodeBase64(window.localStorage.getItem('insomnia.publicKey') || '');
const secretKey = await decodeBase64(window.localStorage.getItem('insomnia.secretKey') || '');
const boxData = open(rawBox, publicKey, secretKey);
const boxData = await window.main.sealedBox.open(rawBox, publicKey, secretKey);
invariant(boxData, 'Invalid authentication code.');
const decoder = new TextDecoder();

View File

@@ -7,7 +7,6 @@ import {
startAddingCollaborators,
} from 'insomnia-api';
import { decryptRSAWithJWK, encryptRSAWithJWK } from '../../../../account/crypt';
import { getCurrentSessionId, getPrivateKey } from '../../../../account/session';
import { invariant } from '../../../../utils/invariant';
@@ -37,21 +36,23 @@ interface Invite {
inviteeId: string;
}
export function buildInviteByInstruction(
export async function buildInviteByInstruction(
instruction: InviteInstruction,
rawProjectKeys: DecryptedProjectKey[],
): Invite {
): Promise<Invite> {
let inviteKeys: InviteKey[] = [];
if (rawProjectKeys?.length) {
const inviteePublicKey = JSON.parse(instruction.inviteePublicKey);
inviteKeys = rawProjectKeys.map(key => {
const reEncryptedSymmetricKey = encryptRSAWithJWK(inviteePublicKey, key.symmetricKey);
return {
projectId: key.projectId,
encSymmetricKey: reEncryptedSymmetricKey,
autoLinked: instruction.inviteeAutoLinked,
};
});
inviteKeys = await Promise.all(
rawProjectKeys.map(async key => {
const reEncryptedSymmetricKey = await window.main.crypt.encryptRSAWithJWK(inviteePublicKey, key.symmetricKey);
return {
projectId: key.projectId,
encSymmetricKey: reEncryptedSymmetricKey,
autoLinked: instruction.inviteeAutoLinked,
};
}),
);
}
return {
inviteeId: instruction.inviteeId,
@@ -60,17 +61,17 @@ export function buildInviteByInstruction(
};
}
function buildMemberProjectKey(
async function buildMemberProjectKey(
accountId: string,
projectId: string,
publicKey: string,
rawProjectKey?: string,
): MemberProjectKey | null {
): Promise<MemberProjectKey | null> {
if (!rawProjectKey) {
return null;
}
const acctPublicKey = JSON.parse(publicKey);
const encSymmetricKey = encryptRSAWithJWK(acctPublicKey, rawProjectKey);
const encSymmetricKey = await window.main.crypt.encryptRSAWithJWK(acctPublicKey, rawProjectKey);
return {
projectId,
accountId,
@@ -86,8 +87,8 @@ async function decryptProjectKeys(
decryptionKey: JsonWebKey,
projectKeys: EncryptedProjectKey[],
): Promise<DecryptedProjectKey[]> {
const promises = projectKeys.map(key => {
const symmetricKey = decryptRSAWithJWK(decryptionKey, key.encKey);
const promises = projectKeys.map(async key => {
const symmetricKey = await window.main.crypt.decryptRSAWithJWK(decryptionKey, key.encKey);
return {
projectId: key.projectId,
symmetricKey,
@@ -139,11 +140,13 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S
}, keyMap);
// This is to reconcile any users in bad standing
memberKeys = myKeysInfo.members
.map((member: ProjectMember) =>
buildMemberProjectKey(member.accountId, member.projectId, member.publicKey, keyMap[member.projectId]),
memberKeys = (
await Promise.all(
myKeysInfo.members.map((member: ProjectMember) =>
buildMemberProjectKey(member.accountId, member.projectId, member.publicKey, keyMap[member.projectId]),
),
)
.filter(Boolean) as MemberProjectKey[];
).filter(Boolean) as MemberProjectKey[];
}
if (memberKeys.length) {
@@ -165,9 +168,9 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S
keys[acctId] = {};
}
projectKeys.forEach(key => {
for (const key of projectKeys) {
const pubKey = instruction[acctId].publicKey;
const newKey = buildMemberProjectKey(acctId, key.projectId, pubKey, key.symmetricKey);
const newKey = await buildMemberProjectKey(acctId, key.projectId, pubKey, key.symmetricKey);
if (newKey) {
keys[acctId][key.projectId] = {
@@ -176,7 +179,7 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S
encKey: newKey.encSymmetricKey,
};
}
});
}
}
}
await finishAddingCollaborators({

View File

@@ -54,6 +54,7 @@ export default defineConfig(({ mode }) => {
// These must appear before the '~' catch-all so the specific path wins.
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
'~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'),
'~/utils/vault-adapter': path.resolve(__dirname, './src/utils/vault-adapter.renderer'),
'~': path.resolve(__dirname, './src'),
// Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname).
'path': path.resolve(__dirname, './src/path-shim.ts'),