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