proper secret encrpytion

This commit is contained in:
Will Chen
2025-04-14 23:15:13 -07:00
parent 1c325eccf4
commit 658d4e0bde
7 changed files with 77 additions and 53 deletions

View File

@@ -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">
{" "} {" "}

View File

@@ -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,
}, },
}, },
}); });

View File

@@ -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 };

View File

@@ -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;
} }
} }

View File

@@ -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": {

View File

@@ -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(),
}); });
/** /**

View File

@@ -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;
} }