diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index 509cffa089..73399ba29a 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -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, diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index d9165ac3a6..15350acbab 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -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), diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 3021d27215..ff49705936 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -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, diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index c2f6e9548b..672b2ac181 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -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; decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise; }; + crypt: { + encryptRSAWithJWK: (publicKeyJWK: JsonWebKey, plaintext: string) => Promise; + decryptRSAWithJWK: (privateJWK: JsonWebKey, encryptedBlob: string) => Promise; + encryptAESBuffer: ( + jwkOrKey: string | JsonWebKey, + buff: number[], + additionalData?: string, + ) => Promise; + encryptAES: ( + jwkOrKey: string | JsonWebKey, + plaintext: string, + additionalData?: string, + ) => Promise; + decryptAES: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise; + decryptAESToBuffer: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise; + generateAES256Key: () => Promise; + }; + sealedBox: { + keyPair: () => Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>; + open: (sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => Promise; + }; timeline: { getPath: (responseId: string) => Promise; appendToFile: (options: { timelinePath: string; data: string }) => Promise; @@ -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(); diff --git a/packages/insomnia/src/ui/auth-session-provider.client.ts b/packages/insomnia/src/ui/auth-session-provider.client.ts index cf33fbfee5..6d0bc7026e 100644 --- a/packages/insomnia/src/ui/auth-session-provider.client.ts +++ b/packages/insomnia/src/ui/auth-session-provider.client.ts @@ -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(); diff --git a/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts b/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts index cbd33a776b..3765f8fda9 100644 --- a/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts +++ b/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts @@ -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 { 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 { 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 { - 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({ diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 9859e7681f..ca02a2cb5e 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -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'),