mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-26 02:27:14 +00:00
feat: implement native OS keyring for site logins
- Replaced plaintext local storage password fields with keyring v3 OS native integration. - Tauri backend securely sets, gets, and deletes credentials via system keychains. - Dynamic password injection occurs seamlessly on download start and metadata parsing.
This commit is contained in:
Generated
+11
@@ -1949,6 +1949,16 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyring"
|
||||||
|
version = "3.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3813,6 +3823,7 @@ dependencies = [
|
|||||||
name = "tauri-app"
|
name = "tauri-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"keyring",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ regex = "1.10"
|
|||||||
reqwest = "0.12"
|
reqwest = "0.12"
|
||||||
tauri-plugin-notification = "2.3.3"
|
tauri-plugin-notification = "2.3.3"
|
||||||
sysinfo = "0.39.3"
|
sysinfo = "0.39.3"
|
||||||
|
keyring = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -902,6 +902,26 @@ fn get_free_space(app_handle: tauri::AppHandle, path: String) -> Result<String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_keychain_password(id: String, password: String) -> Result<(), String> {
|
||||||
|
let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
|
||||||
|
entry.set_password(&password).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_keychain_password(id: String) -> Result<String, String> {
|
||||||
|
let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
|
||||||
|
entry.get_password().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn delete_keychain_password(id: String) -> Result<(), String> {
|
||||||
|
let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
|
||||||
|
let _ = entry.delete_credential(); // Ignore error if it doesn't exist
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -915,7 +935,8 @@ pub fn run() {
|
|||||||
greet, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
greet, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
||||||
start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata,
|
start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata,
|
||||||
update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
|
update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
|
||||||
request_automation_permission, open_automation_settings
|
request_automation_permission, open_automation_settings,
|
||||||
|
set_keychain_password, get_keychain_password, delete_keychain_password
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -326,12 +326,20 @@ export const AddDownloadsModal = () => {
|
|||||||
const { mediaCookieSource } = settingsStore;
|
const { mediaCookieSource } = settingsStore;
|
||||||
const browserArg = mediaCookieSource !== 'none' ? mediaCookieSource : null;
|
const browserArg = mediaCookieSource !== 'none' ? mediaCookieSource : null;
|
||||||
const login = getSiteLogin(url, settingsStore);
|
const login = getSiteLogin(url, settingsStore);
|
||||||
|
let keychainPassword = null;
|
||||||
|
if (login) {
|
||||||
|
try {
|
||||||
|
keychainPassword = await invoke<string>('get_keychain_password', { id: login.id });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not fetch keychain password:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const jsonStr = await invoke<string>('fetch_media_metadata', {
|
const jsonStr = await invoke<string>('fetch_media_metadata', {
|
||||||
url,
|
url,
|
||||||
cookieBrowser: browserArg,
|
cookieBrowser: browserArg,
|
||||||
username: login?.username || null,
|
username: login?.username || null,
|
||||||
password: login?.password || null
|
password: keychainPassword
|
||||||
});
|
});
|
||||||
const mediaData = parseMediaFormats(jsonStr);
|
const mediaData = parseMediaFormats(jsonStr);
|
||||||
if (mediaData && mediaData.formats.length > 0) {
|
if (mediaData && mediaData.formats.length > 0) {
|
||||||
@@ -351,10 +359,18 @@ export const AddDownloadsModal = () => {
|
|||||||
} else {
|
} else {
|
||||||
const settingsStore = useSettingsStore.getState();
|
const settingsStore = useSettingsStore.getState();
|
||||||
const login = getSiteLogin(url, settingsStore);
|
const login = getSiteLogin(url, settingsStore);
|
||||||
|
let keychainPassword = null;
|
||||||
|
if (login) {
|
||||||
|
try {
|
||||||
|
keychainPassword = await invoke<string>('get_keychain_password', { id: login.id });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not fetch keychain password:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
const meta = await invoke<{filename: string, size: string, size_bytes: number}>('fetch_metadata', {
|
const meta = await invoke<{filename: string, size: string, size_bytes: number}>('fetch_metadata', {
|
||||||
url,
|
url,
|
||||||
username: login?.username || null,
|
username: login?.username || null,
|
||||||
password: login?.password || null
|
password: keychainPassword
|
||||||
});
|
});
|
||||||
updatedItems[i] = { url, file: meta.filename, size: meta.size, sizeBytes: meta.size_bytes, status: 'Ready' };
|
updatedItems[i] = { url, file: meta.filename, size: meta.size, sizeBytes: meta.size_bytes, status: 'Ready' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,17 +116,27 @@ export default function SettingsView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddLogin = () => {
|
const handleAddLogin = async () => {
|
||||||
if (!loginPattern.trim() || !loginUser.trim()) {
|
if (!loginPattern.trim() || !loginUser.trim()) {
|
||||||
setLoginError("Please enter a URL pattern and a username.");
|
setLoginError("Please enter a URL pattern and a username.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
if (loginPass) {
|
||||||
|
try {
|
||||||
|
await invoke('set_keychain_password', { id, password: loginPass });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save password to keychain:", e);
|
||||||
|
setLoginError("Failed to save password securely.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
settings.addSiteLogin({
|
settings.addSiteLogin({
|
||||||
id,
|
id,
|
||||||
urlPattern: loginPattern.trim(),
|
urlPattern: loginPattern.trim(),
|
||||||
username: loginUser.trim(),
|
username: loginUser.trim()
|
||||||
password: loginPass
|
|
||||||
});
|
});
|
||||||
setLoginPattern('');
|
setLoginPattern('');
|
||||||
setLoginUser('');
|
setLoginUser('');
|
||||||
@@ -534,7 +544,12 @@ export default function SettingsView() {
|
|||||||
<p className="text-text-secondary text-xs">User: {login.username}</p>
|
<p className="text-text-secondary text-xs">User: {login.username}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await invoke('delete_keychain_password', { id: login.id });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not delete password from keychain:", e);
|
||||||
|
}
|
||||||
settings.removeSiteLogin(login.id);
|
settings.removeSiteLogin(login.id);
|
||||||
showToast("Deleted credential");
|
showToast("Deleted credential");
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -291,6 +291,14 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const settings = useSettingsStore.getState();
|
const settings = useSettingsStore.getState();
|
||||||
const login = getSiteLogin(item.url, settings);
|
const login = getSiteLogin(item.url, settings);
|
||||||
|
let keychainPassword = null;
|
||||||
|
if (login) {
|
||||||
|
try {
|
||||||
|
keychainPassword = await invoke<string>('get_keychain_password', { id: login.id });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not fetch keychain password for login:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const destPath = item.destination ||
|
const destPath = item.destination ||
|
||||||
(settings.downloadDirectories && settings.downloadDirectories[item.category]) ||
|
(settings.downloadDirectories && settings.downloadDirectories[item.category]) ||
|
||||||
@@ -311,7 +319,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
formatSelector: item.mediaFormatSelector || null,
|
formatSelector: item.mediaFormatSelector || null,
|
||||||
speedLimit,
|
speedLimit,
|
||||||
username: item.username || (login ? login.username : null),
|
username: item.username || (login ? login.username : null),
|
||||||
password: item.password || (login ? login.password : null)
|
password: item.password || keychainPassword
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const speedLimit = effectiveSpeedLimit(
|
const speedLimit = effectiveSpeedLimit(
|
||||||
@@ -327,7 +335,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
connections: item.connections || settings.perServerConnections || null,
|
connections: item.connections || settings.perServerConnections || null,
|
||||||
speedLimit,
|
speedLimit,
|
||||||
username: item.username || (login ? login.username : null),
|
username: item.username || (login ? login.username : null),
|
||||||
password: item.password || (login ? login.password : null),
|
password: item.password || keychainPassword,
|
||||||
headers: item.headers || null,
|
headers: item.headers || null,
|
||||||
userAgent: settings.customUserAgent || null,
|
userAgent: settings.customUserAgent || null,
|
||||||
maxTries: settings.maxAutomaticRetries,
|
maxTries: settings.maxAutomaticRetries,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export interface SiteLogin {
|
|||||||
id: string;
|
id: string;
|
||||||
urlPattern: string;
|
urlPattern: string;
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppFontSize = 'small' | 'standard' | 'large';
|
export type AppFontSize = 'small' | 'standard' | 'large';
|
||||||
|
|||||||
Reference in New Issue
Block a user