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

View File

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

View File

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

View File

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

View File

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

View File

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