mirror of
https://github.com/Kong/insomnia.git
synced 2026-06-03 05:35:56 -04:00
add crypto bridges
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user