proper secret encrpytion
This commit is contained in:
@@ -171,7 +171,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!settings?.githubSettings.secrets?.accessToken) {
|
if (!settings?.githubAccessToken) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full">
|
||||||
{" "}
|
{" "}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
|||||||
|
|
||||||
const envVarName = PROVIDER_TO_ENV_VAR[provider];
|
const envVarName = PROVIDER_TO_ENV_VAR[provider];
|
||||||
const envApiKey = envVars[envVarName];
|
const envApiKey = envVars[envVarName];
|
||||||
const userApiKey = settings?.providerSettings?.[provider]?.apiKey;
|
const userApiKey = settings?.providerSettings?.[provider]?.apiKey?.value;
|
||||||
|
|
||||||
// --- Configuration Logic --- Updated Priority ---
|
// --- Configuration Logic --- Updated Priority ---
|
||||||
const isValidUserKey =
|
const isValidUserKey =
|
||||||
@@ -100,7 +100,9 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
|||||||
...settings?.providerSettings,
|
...settings?.providerSettings,
|
||||||
[provider]: {
|
[provider]: {
|
||||||
...(settings?.providerSettings?.[provider] || {}),
|
...(settings?.providerSettings?.[provider] || {}),
|
||||||
apiKey: apiKeyInput,
|
apiKey: {
|
||||||
|
value: apiKeyInput,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -124,7 +126,7 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
|||||||
...settings?.providerSettings,
|
...settings?.providerSettings,
|
||||||
[provider]: {
|
[provider]: {
|
||||||
...(settings?.providerSettings?.[provider] || {}),
|
...(settings?.providerSettings?.[provider] || {}),
|
||||||
apiKey: null,
|
apiKey: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export async function getGithubUser(): Promise<GithubUser | null> {
|
|||||||
const email = settings.githubUser?.email;
|
const email = settings.githubUser?.email;
|
||||||
if (email) return { email };
|
if (email) return { email };
|
||||||
try {
|
try {
|
||||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
const accessToken = settings.githubAccessToken?.value;
|
||||||
if (!accessToken) return null;
|
if (!accessToken) return null;
|
||||||
const res = await fetch("https://api.github.com/user/emails", {
|
const res = await fetch("https://api.github.com/user/emails", {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
@@ -116,10 +116,8 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
|
|||||||
message: "Successfully connected!",
|
message: "Successfully connected!",
|
||||||
});
|
});
|
||||||
writeSettings({
|
writeSettings({
|
||||||
githubSettings: {
|
githubAccessToken: {
|
||||||
secrets: {
|
value: data.access_token,
|
||||||
accessToken: data.access_token,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// TODO: Associate token with appId if provided
|
// TODO: Associate token with appId if provided
|
||||||
@@ -324,7 +322,7 @@ async function handleIsRepoAvailable(
|
|||||||
try {
|
try {
|
||||||
// Get access token from settings
|
// Get access token from settings
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
const accessToken = settings.githubAccessToken?.value;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return { available: false, error: "Not authenticated with GitHub." };
|
return { available: false, error: "Not authenticated with GitHub." };
|
||||||
}
|
}
|
||||||
@@ -362,7 +360,7 @@ async function handleCreateRepo(
|
|||||||
try {
|
try {
|
||||||
// Get access token from settings
|
// Get access token from settings
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
const accessToken = settings.githubAccessToken?.value;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return { success: false, error: "Not authenticated with GitHub." };
|
return { success: false, error: "Not authenticated with GitHub." };
|
||||||
}
|
}
|
||||||
@@ -411,7 +409,7 @@ async function handlePushToGithub(
|
|||||||
try {
|
try {
|
||||||
// Get access token from settings
|
// Get access token from settings
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
const accessToken = settings.githubAccessToken?.value;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return { success: false, error: "Not authenticated with GitHub." };
|
return { success: false, error: "Not authenticated with GitHub." };
|
||||||
}
|
}
|
||||||
@@ -437,7 +435,10 @@ async function handlePushToGithub(
|
|||||||
dir: appPath,
|
dir: appPath,
|
||||||
remote: "origin",
|
remote: "origin",
|
||||||
ref: "main",
|
ref: "main",
|
||||||
onAuth: () => ({ username: accessToken, password: "x-oauth-basic" }),
|
onAuth: () => ({
|
||||||
|
username: accessToken,
|
||||||
|
password: "x-oauth-basic",
|
||||||
|
}),
|
||||||
force: false,
|
force: false,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ export function registerSettingsHandlers() {
|
|||||||
) {
|
) {
|
||||||
const providerSetting = settings.providerSettings[providerKey];
|
const providerSetting = settings.providerSettings[providerKey];
|
||||||
// Check if apiKey exists and is a non-empty string before masking
|
// Check if apiKey exists and is a non-empty string before masking
|
||||||
if (
|
if (providerSetting?.apiKey?.value) {
|
||||||
providerSetting?.apiKey &&
|
|
||||||
typeof providerSetting.apiKey === "string" &&
|
|
||||||
providerSetting.apiKey.length > 0
|
|
||||||
) {
|
|
||||||
providerSetting.apiKey = providerSetting.apiKey;
|
providerSetting.apiKey = providerSetting.apiKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function getModelClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey =
|
const apiKey =
|
||||||
settings.providerSettings?.[model.provider]?.apiKey ||
|
settings.providerSettings?.[model.provider]?.apiKey?.value ||
|
||||||
getEnvVar(PROVIDER_TO_ENV_VAR[model.provider]);
|
getEnvVar(PROVIDER_TO_ENV_VAR[model.provider]);
|
||||||
switch (model.provider) {
|
switch (model.provider) {
|
||||||
case "openai": {
|
case "openai": {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SecretSchema = z.object({
|
||||||
|
value: z.string(),
|
||||||
|
encryptionType: z.enum(["electron-safe-storage", "plaintext"]).optional(),
|
||||||
|
});
|
||||||
|
export type Secret = z.infer<typeof SecretSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zod schema for chat summary objects returned by the get-chats IPC
|
* Zod schema for chat summary objects returned by the get-chats IPC
|
||||||
*/
|
*/
|
||||||
@@ -53,7 +59,7 @@ export type LargeLanguageModel = z.infer<typeof LargeLanguageModelSchema>;
|
|||||||
* Zod schema for provider settings
|
* Zod schema for provider settings
|
||||||
*/
|
*/
|
||||||
export const ProviderSettingSchema = z.object({
|
export const ProviderSettingSchema = z.object({
|
||||||
apiKey: z.string().nullable(),
|
apiKey: SecretSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,15 +71,10 @@ export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]);
|
|||||||
export type RuntimeMode = z.infer<typeof RuntimeModeSchema>;
|
export type RuntimeMode = z.infer<typeof RuntimeModeSchema>;
|
||||||
|
|
||||||
export const GitHubSecretsSchema = z.object({
|
export const GitHubSecretsSchema = z.object({
|
||||||
accessToken: z.string().nullable(),
|
accessToken: SecretSchema.nullable(),
|
||||||
});
|
});
|
||||||
export type GitHubSecrets = z.infer<typeof GitHubSecretsSchema>;
|
export type GitHubSecrets = z.infer<typeof GitHubSecretsSchema>;
|
||||||
|
|
||||||
export const GitHubSettingsSchema = z.object({
|
|
||||||
secrets: GitHubSecretsSchema.nullable(),
|
|
||||||
});
|
|
||||||
export type GitHubSettings = z.infer<typeof GitHubSettingsSchema>;
|
|
||||||
|
|
||||||
export const GithubUserSchema = z.object({
|
export const GithubUserSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
});
|
});
|
||||||
@@ -86,8 +87,8 @@ export const UserSettingsSchema = z.object({
|
|||||||
selectedModel: LargeLanguageModelSchema,
|
selectedModel: LargeLanguageModelSchema,
|
||||||
providerSettings: z.record(z.string(), ProviderSettingSchema),
|
providerSettings: z.record(z.string(), ProviderSettingSchema),
|
||||||
runtimeMode: RuntimeModeSchema,
|
runtimeMode: RuntimeModeSchema,
|
||||||
githubSettings: GitHubSettingsSchema,
|
|
||||||
githubUser: GithubUserSchema.optional(),
|
githubUser: GithubUserSchema.optional(),
|
||||||
|
githubAccessToken: SecretSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getUserDataPath } from "../paths/paths";
|
import { getUserDataPath } from "../paths/paths";
|
||||||
import { UserSettingsSchema, type UserSettings } from "../lib/schemas";
|
import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas";
|
||||||
import { safeStorage } from "electron";
|
import { safeStorage } from "electron";
|
||||||
|
|
||||||
|
// IF YOU NEED TO UPDATE THIS, YOU'RE PROBABLY DOING SOMETHING WRONG!
|
||||||
|
// Need to maintain backwards compatibility!
|
||||||
const DEFAULT_SETTINGS: UserSettings = {
|
const DEFAULT_SETTINGS: UserSettings = {
|
||||||
selectedModel: {
|
selectedModel: {
|
||||||
name: "auto",
|
name: "auto",
|
||||||
@@ -11,9 +13,6 @@ const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
},
|
},
|
||||||
providerSettings: {},
|
providerSettings: {},
|
||||||
runtimeMode: "unset",
|
runtimeMode: "unset",
|
||||||
githubSettings: {
|
|
||||||
secrets: null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_FILE = "user-settings.json";
|
const SETTINGS_FILE = "user-settings.json";
|
||||||
@@ -30,18 +29,31 @@ export function readSettings(): UserSettings {
|
|||||||
return DEFAULT_SETTINGS;
|
return DEFAULT_SETTINGS;
|
||||||
}
|
}
|
||||||
const rawSettings = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
const rawSettings = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
// Validate and merge with defaults
|
const combinedSettings: UserSettings = {
|
||||||
const validatedSettings = UserSettingsSchema.parse({
|
|
||||||
...DEFAULT_SETTINGS,
|
...DEFAULT_SETTINGS,
|
||||||
...rawSettings,
|
...rawSettings,
|
||||||
});
|
};
|
||||||
if (validatedSettings.githubSettings?.secrets) {
|
if (combinedSettings.githubAccessToken) {
|
||||||
const accessToken = validatedSettings.githubSettings.secrets.accessToken;
|
const encryptionType = combinedSettings.githubAccessToken.encryptionType;
|
||||||
|
combinedSettings.githubAccessToken = {
|
||||||
validatedSettings.githubSettings.secrets = {
|
value: decrypt(combinedSettings.githubAccessToken),
|
||||||
accessToken: accessToken ? decrypt(accessToken) : null,
|
encryptionType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
for (const provider in combinedSettings.providerSettings) {
|
||||||
|
if (combinedSettings.providerSettings[provider].apiKey) {
|
||||||
|
const encryptionType =
|
||||||
|
combinedSettings.providerSettings[provider].apiKey.encryptionType;
|
||||||
|
combinedSettings.providerSettings[provider].apiKey = {
|
||||||
|
value: decrypt(combinedSettings.providerSettings[provider].apiKey),
|
||||||
|
encryptionType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and merge with defaults
|
||||||
|
const validatedSettings = UserSettingsSchema.parse(combinedSettings);
|
||||||
|
|
||||||
return validatedSettings;
|
return validatedSettings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading settings:", error);
|
console.error("Error reading settings:", error);
|
||||||
@@ -54,30 +66,42 @@ export function writeSettings(settings: Partial<UserSettings>): void {
|
|||||||
const filePath = getSettingsFilePath();
|
const filePath = getSettingsFilePath();
|
||||||
const currentSettings = readSettings();
|
const currentSettings = readSettings();
|
||||||
const newSettings = { ...currentSettings, ...settings };
|
const newSettings = { ...currentSettings, ...settings };
|
||||||
// Validate before writing
|
if (newSettings.githubAccessToken) {
|
||||||
const validatedSettings = UserSettingsSchema.parse(newSettings);
|
newSettings.githubAccessToken = encrypt(
|
||||||
if (validatedSettings.githubSettings?.secrets) {
|
newSettings.githubAccessToken.value
|
||||||
const accessToken = validatedSettings.githubSettings.secrets.accessToken;
|
);
|
||||||
validatedSettings.githubSettings.secrets = {
|
|
||||||
accessToken: accessToken ? encrypt(accessToken) : null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const provider in newSettings.providerSettings) {
|
||||||
|
if (newSettings.providerSettings[provider].apiKey) {
|
||||||
|
newSettings.providerSettings[provider].apiKey = encrypt(
|
||||||
|
newSettings.providerSettings[provider].apiKey.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const validatedSettings = UserSettingsSchema.parse(newSettings);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(validatedSettings, null, 2));
|
fs.writeFileSync(filePath, JSON.stringify(validatedSettings, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error writing settings:", error);
|
console.error("Error writing settings:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encrypt(data: string): string {
|
export function encrypt(data: string): Secret {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
return safeStorage.encryptString(data).toString("base64");
|
return {
|
||||||
|
value: safeStorage.encryptString(data).toString("base64"),
|
||||||
|
encryptionType: "electron-safe-storage",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return data;
|
return {
|
||||||
|
value: data,
|
||||||
|
encryptionType: "plaintext",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(data: string): string {
|
export function decrypt(data: Secret): string {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (data.encryptionType === "electron-safe-storage") {
|
||||||
return safeStorage.decryptString(Buffer.from(data, "base64"));
|
return safeStorage.decryptString(Buffer.from(data.value, "base64"));
|
||||||
}
|
}
|
||||||
return data;
|
return data.value;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user