Mayo commited on
refactor: move keyring to koharu-runtime
Browse files- Cargo.lock +2 -3
- koharu-app/src/config.rs +25 -5
- koharu-llm/Cargo.toml +0 -11
- koharu-llm/src/providers/mod.rs +0 -3
- koharu-runtime/Cargo.toml +8 -0
- koharu-runtime/src/lib.rs +2 -0
- koharu-llm/src/providers/credentials.rs → koharu-runtime/src/secrets.rs +72 -39
Cargo.lock
CHANGED
|
@@ -4610,14 +4610,12 @@ dependencies = [
|
|
| 4610 |
"anyhow",
|
| 4611 |
"bindgen",
|
| 4612 |
"clap",
|
| 4613 |
-
"dirs",
|
| 4614 |
"encoding_rs",
|
| 4615 |
"enumflags2",
|
| 4616 |
"flate2",
|
| 4617 |
"futures",
|
| 4618 |
"hf-hub",
|
| 4619 |
"image",
|
| 4620 |
-
"keyring",
|
| 4621 |
"koharu-runtime",
|
| 4622 |
"libloading 0.8.9",
|
| 4623 |
"minijinja",
|
|
@@ -4632,7 +4630,6 @@ dependencies = [
|
|
| 4632 |
"strum",
|
| 4633 |
"syn 2.0.117",
|
| 4634 |
"tar",
|
| 4635 |
-
"tempfile",
|
| 4636 |
"thiserror 2.0.18",
|
| 4637 |
"tokio",
|
| 4638 |
"tracing",
|
|
@@ -4746,6 +4743,7 @@ name = "koharu-runtime"
|
|
| 4746 |
version = "0.47.8"
|
| 4747 |
dependencies = [
|
| 4748 |
"anyhow",
|
|
|
|
| 4749 |
"camino",
|
| 4750 |
"dirs",
|
| 4751 |
"flate2",
|
|
@@ -4754,6 +4752,7 @@ dependencies = [
|
|
| 4754 |
"indexmap 2.14.0",
|
| 4755 |
"indicatif",
|
| 4756 |
"inventory",
|
|
|
|
| 4757 |
"koharu-core",
|
| 4758 |
"libloading 0.8.9",
|
| 4759 |
"num_cpus",
|
|
|
|
| 4610 |
"anyhow",
|
| 4611 |
"bindgen",
|
| 4612 |
"clap",
|
|
|
|
| 4613 |
"encoding_rs",
|
| 4614 |
"enumflags2",
|
| 4615 |
"flate2",
|
| 4616 |
"futures",
|
| 4617 |
"hf-hub",
|
| 4618 |
"image",
|
|
|
|
| 4619 |
"koharu-runtime",
|
| 4620 |
"libloading 0.8.9",
|
| 4621 |
"minijinja",
|
|
|
|
| 4630 |
"strum",
|
| 4631 |
"syn 2.0.117",
|
| 4632 |
"tar",
|
|
|
|
| 4633 |
"thiserror 2.0.18",
|
| 4634 |
"tokio",
|
| 4635 |
"tracing",
|
|
|
|
| 4743 |
version = "0.47.8"
|
| 4744 |
dependencies = [
|
| 4745 |
"anyhow",
|
| 4746 |
+
"atomicwrites",
|
| 4747 |
"camino",
|
| 4748 |
"dirs",
|
| 4749 |
"flate2",
|
|
|
|
| 4752 |
"indexmap 2.14.0",
|
| 4753 |
"indicatif",
|
| 4754 |
"inventory",
|
| 4755 |
+
"keyring",
|
| 4756 |
"koharu-core",
|
| 4757 |
"libloading 0.8.9",
|
| 4758 |
"num_cpus",
|
koharu-app/src/config.rs
CHANGED
|
@@ -2,8 +2,7 @@ use std::fs;
|
|
| 2 |
|
| 3 |
use anyhow::{Context, Result};
|
| 4 |
use camino::Utf8PathBuf;
|
| 5 |
-
use
|
| 6 |
-
use koharu_runtime::default_app_data_root;
|
| 7 |
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
| 8 |
use utoipa::ToSchema;
|
| 9 |
|
|
@@ -11,6 +10,8 @@ use crate::pipeline::{Artifact, Registry};
|
|
| 11 |
|
| 12 |
const CONFIG_FILE: &str = "config.toml";
|
| 13 |
const REDACTED: &str = "[REDACTED]";
|
|
|
|
|
|
|
| 14 |
|
| 15 |
// ---------------------------------------------------------------------------
|
| 16 |
// RedactedSecret
|
|
@@ -163,8 +164,9 @@ pub fn load() -> Result<AppConfig> {
|
|
| 163 |
}
|
| 164 |
|
| 165 |
// Populate api_key from credential storage for every known provider.
|
|
|
|
| 166 |
for provider in &mut config.providers {
|
| 167 |
-
if let Ok(Some(key)) =
|
| 168 |
&& !key.trim().is_empty()
|
| 169 |
{
|
| 170 |
provider.api_key = Some(RedactedSecret::new(key));
|
|
@@ -355,13 +357,19 @@ fn validate_engine_name(
|
|
| 355 |
/// - `None` → clear from credential storage
|
| 356 |
/// - `Some(RedactedSecret)` with value == "[REDACTED]" → unchanged
|
| 357 |
pub fn sync_secrets(config: &AppConfig) -> Result<()> {
|
|
|
|
| 358 |
for provider in &config.providers {
|
| 359 |
match &provider.api_key {
|
| 360 |
Some(secret) if secret.expose() != REDACTED => {
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
}
|
| 363 |
None => {
|
| 364 |
-
|
| 365 |
}
|
| 366 |
_ => {} // "[REDACTED]" means unchanged
|
| 367 |
}
|
|
@@ -369,6 +377,10 @@ pub fn sync_secrets(config: &AppConfig) -> Result<()> {
|
|
| 369 |
Ok(())
|
| 370 |
}
|
| 371 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
#[cfg(test)]
|
| 373 |
mod tests {
|
| 374 |
use super::*;
|
|
@@ -413,6 +425,14 @@ mod tests {
|
|
| 413 |
assert!(path.as_str().contains("Koharu"));
|
| 414 |
}
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
#[test]
|
| 417 |
fn invalid_pipeline_engines_reset_to_defaults() {
|
| 418 |
let mut config = AppConfig::default();
|
|
|
|
| 2 |
|
| 3 |
use anyhow::{Context, Result};
|
| 4 |
use camino::Utf8PathBuf;
|
| 5 |
+
use koharu_runtime::{SecretStore, default_app_data_root};
|
|
|
|
| 6 |
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
| 7 |
use utoipa::ToSchema;
|
| 8 |
|
|
|
|
| 10 |
|
| 11 |
const CONFIG_FILE: &str = "config.toml";
|
| 12 |
const REDACTED: &str = "[REDACTED]";
|
| 13 |
+
const SECRET_SERVICE: &str = "koharu";
|
| 14 |
+
const PROVIDER_API_KEY_SECRET_PREFIX: &str = "llm_provider_api_key_";
|
| 15 |
|
| 16 |
// ---------------------------------------------------------------------------
|
| 17 |
// RedactedSecret
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
// Populate api_key from credential storage for every known provider.
|
| 167 |
+
let secrets = SecretStore::new(SECRET_SERVICE);
|
| 168 |
for provider in &mut config.providers {
|
| 169 |
+
if let Ok(Some(key)) = secrets.get(&provider_api_key_secret_key(&provider.id))
|
| 170 |
&& !key.trim().is_empty()
|
| 171 |
{
|
| 172 |
provider.api_key = Some(RedactedSecret::new(key));
|
|
|
|
| 357 |
/// - `None` → clear from credential storage
|
| 358 |
/// - `Some(RedactedSecret)` with value == "[REDACTED]" → unchanged
|
| 359 |
pub fn sync_secrets(config: &AppConfig) -> Result<()> {
|
| 360 |
+
let secrets = SecretStore::new(SECRET_SERVICE);
|
| 361 |
for provider in &config.providers {
|
| 362 |
match &provider.api_key {
|
| 363 |
Some(secret) if secret.expose() != REDACTED => {
|
| 364 |
+
let key = provider_api_key_secret_key(&provider.id);
|
| 365 |
+
if secret.expose().trim().is_empty() {
|
| 366 |
+
secrets.delete(&key)?;
|
| 367 |
+
} else {
|
| 368 |
+
secrets.set(&key, secret.expose())?;
|
| 369 |
+
}
|
| 370 |
}
|
| 371 |
None => {
|
| 372 |
+
secrets.delete(&provider_api_key_secret_key(&provider.id))?;
|
| 373 |
}
|
| 374 |
_ => {} // "[REDACTED]" means unchanged
|
| 375 |
}
|
|
|
|
| 377 |
Ok(())
|
| 378 |
}
|
| 379 |
|
| 380 |
+
fn provider_api_key_secret_key(provider_id: &str) -> String {
|
| 381 |
+
format!("{PROVIDER_API_KEY_SECRET_PREFIX}{provider_id}")
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
#[cfg(test)]
|
| 385 |
mod tests {
|
| 386 |
use super::*;
|
|
|
|
| 425 |
assert!(path.as_str().contains("Koharu"));
|
| 426 |
}
|
| 427 |
|
| 428 |
+
#[test]
|
| 429 |
+
fn provider_api_key_secret_key_preserves_legacy_keyring_user() {
|
| 430 |
+
assert_eq!(
|
| 431 |
+
provider_api_key_secret_key("openai"),
|
| 432 |
+
"llm_provider_api_key_openai"
|
| 433 |
+
);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
#[test]
|
| 437 |
fn invalid_pipeline_engines_reset_to_defaults() {
|
| 438 |
let mut config = AppConfig::default();
|
koharu-llm/Cargo.toml
CHANGED
|
@@ -29,7 +29,6 @@ path = "bin/paddleocr-vl.rs"
|
|
| 29 |
[dependencies]
|
| 30 |
anyhow = { workspace = true }
|
| 31 |
clap = { workspace = true, features = ["derive"] }
|
| 32 |
-
dirs = { workspace = true }
|
| 33 |
flate2 = { workspace = true }
|
| 34 |
futures = { workspace = true }
|
| 35 |
hf-hub = { workspace = true }
|
|
@@ -54,13 +53,6 @@ tracing-core = "0.1"
|
|
| 54 |
zip = { workspace = true }
|
| 55 |
encoding_rs = "0.8"
|
| 56 |
enumflags2 = "0.7.12"
|
| 57 |
-
keyring = { workspace = true }
|
| 58 |
-
|
| 59 |
-
[target.'cfg(windows)'.dependencies]
|
| 60 |
-
keyring = { workspace = true, features = ["windows-native"] }
|
| 61 |
-
|
| 62 |
-
[target.'cfg(target_os = "macos")'.dependencies]
|
| 63 |
-
keyring = { workspace = true, features = ["apple-native"] }
|
| 64 |
|
| 65 |
[build-dependencies]
|
| 66 |
anyhow = { workspace = true }
|
|
@@ -71,6 +63,3 @@ quote = "1.0"
|
|
| 71 |
reqwest = { workspace = true }
|
| 72 |
syn = { version = "2.0", features = ["full"] }
|
| 73 |
tar = "0.4"
|
| 74 |
-
|
| 75 |
-
[dev-dependencies]
|
| 76 |
-
tempfile = { workspace = true }
|
|
|
|
| 29 |
[dependencies]
|
| 30 |
anyhow = { workspace = true }
|
| 31 |
clap = { workspace = true, features = ["derive"] }
|
|
|
|
| 32 |
flate2 = { workspace = true }
|
| 33 |
futures = { workspace = true }
|
| 34 |
hf-hub = { workspace = true }
|
|
|
|
| 53 |
zip = { workspace = true }
|
| 54 |
encoding_rs = "0.8"
|
| 55 |
enumflags2 = "0.7.12"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
[build-dependencies]
|
| 58 |
anyhow = { workspace = true }
|
|
|
|
| 63 |
reqwest = { workspace = true }
|
| 64 |
syn = { version = "2.0", features = ["full"] }
|
| 65 |
tar = "0.4"
|
|
|
|
|
|
|
|
|
koharu-llm/src/providers/mod.rs
CHANGED
|
@@ -19,7 +19,6 @@ pub(crate) fn resolve_system_prompt(custom: Option<&str>, target_language: Langu
|
|
| 19 |
pub mod caiyun;
|
| 20 |
mod chat_completions;
|
| 21 |
pub mod claude;
|
| 22 |
-
mod credentials;
|
| 23 |
pub mod deepl;
|
| 24 |
pub mod deepseek;
|
| 25 |
pub mod gemini;
|
|
@@ -27,8 +26,6 @@ pub mod google_translate;
|
|
| 27 |
pub mod openai;
|
| 28 |
pub mod openai_compatible;
|
| 29 |
|
| 30 |
-
pub use credentials::{get_saved_api_key, set_saved_api_key};
|
| 31 |
-
|
| 32 |
#[derive(Debug, Clone, Copy)]
|
| 33 |
pub struct ProviderModelDescriptor {
|
| 34 |
pub id: &'static str,
|
|
|
|
| 19 |
pub mod caiyun;
|
| 20 |
mod chat_completions;
|
| 21 |
pub mod claude;
|
|
|
|
| 22 |
pub mod deepl;
|
| 23 |
pub mod deepseek;
|
| 24 |
pub mod gemini;
|
|
|
|
| 26 |
pub mod openai;
|
| 27 |
pub mod openai_compatible;
|
| 28 |
|
|
|
|
|
|
|
| 29 |
#[derive(Debug, Clone, Copy)]
|
| 30 |
pub struct ProviderModelDescriptor {
|
| 31 |
pub id: &'static str,
|
koharu-runtime/Cargo.toml
CHANGED
|
@@ -13,6 +13,7 @@ publish.workspace = true
|
|
| 13 |
|
| 14 |
[dependencies]
|
| 15 |
anyhow = { workspace = true }
|
|
|
|
| 16 |
camino = { workspace = true }
|
| 17 |
koharu-core = { workspace = true }
|
| 18 |
dirs = { workspace = true }
|
|
@@ -21,6 +22,7 @@ hf-hub = { workspace = true }
|
|
| 21 |
indexmap = { workspace = true }
|
| 22 |
indicatif = { workspace = true }
|
| 23 |
inventory = { workspace = true }
|
|
|
|
| 24 |
reqwest = { workspace = true }
|
| 25 |
reqwest-middleware = { workspace = true }
|
| 26 |
reqwest-retry = { workspace = true }
|
|
@@ -38,5 +40,11 @@ tar = "0.4"
|
|
| 38 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 39 |
windows-sys = { workspace = true }
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
[dev-dependencies]
|
| 42 |
tempfile = { workspace = true }
|
|
|
|
| 13 |
|
| 14 |
[dependencies]
|
| 15 |
anyhow = { workspace = true }
|
| 16 |
+
atomicwrites = { workspace = true }
|
| 17 |
camino = { workspace = true }
|
| 18 |
koharu-core = { workspace = true }
|
| 19 |
dirs = { workspace = true }
|
|
|
|
| 22 |
indexmap = { workspace = true }
|
| 23 |
indicatif = { workspace = true }
|
| 24 |
inventory = { workspace = true }
|
| 25 |
+
keyring = { workspace = true }
|
| 26 |
reqwest = { workspace = true }
|
| 27 |
reqwest-middleware = { workspace = true }
|
| 28 |
reqwest-retry = { workspace = true }
|
|
|
|
| 40 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 41 |
windows-sys = { workspace = true }
|
| 42 |
|
| 43 |
+
[target.'cfg(windows)'.dependencies]
|
| 44 |
+
keyring = { workspace = true, features = ["windows-native"] }
|
| 45 |
+
|
| 46 |
+
[target.'cfg(target_os = "macos")'.dependencies]
|
| 47 |
+
keyring = { workspace = true, features = ["apple-native"] }
|
| 48 |
+
|
| 49 |
[dev-dependencies]
|
| 50 |
tempfile = { workspace = true }
|
koharu-runtime/src/lib.rs
CHANGED
|
@@ -6,6 +6,7 @@ mod llama;
|
|
| 6 |
mod loader;
|
| 7 |
pub mod packages;
|
| 8 |
mod runtime;
|
|
|
|
| 9 |
mod zluda;
|
| 10 |
|
| 11 |
pub use cuda::{
|
|
@@ -19,3 +20,4 @@ pub use packages::{PackageCatalog as Catalog, PackageFuture, PackageKind, Packag
|
|
| 19 |
pub use runtime::{
|
| 20 |
ComputePolicy, Runtime, RuntimeHttpConfig, RuntimeManager, default_app_data_root,
|
| 21 |
};
|
|
|
|
|
|
| 6 |
mod loader;
|
| 7 |
pub mod packages;
|
| 8 |
mod runtime;
|
| 9 |
+
mod secrets;
|
| 10 |
mod zluda;
|
| 11 |
|
| 12 |
pub use cuda::{
|
|
|
|
| 20 |
pub use runtime::{
|
| 21 |
ComputePolicy, Runtime, RuntimeHttpConfig, RuntimeManager, default_app_data_root,
|
| 22 |
};
|
| 23 |
+
pub use secrets::{SecretStore, delete_secret, get_secret, set_secret};
|
koharu-llm/src/providers/credentials.rs → koharu-runtime/src/secrets.rs
RENAMED
|
@@ -2,12 +2,39 @@ use std::sync::Once;
|
|
| 2 |
|
| 3 |
use keyring::Entry;
|
| 4 |
|
| 5 |
-
const API_KEY_SERVICE: &str = "koharu";
|
| 6 |
-
|
| 7 |
static INIT_CREDENTIAL_STORE: Once = Once::new();
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
match entry.get_password() {
|
| 12 |
Ok(value) => Ok(Some(value)),
|
| 13 |
Err(keyring::Error::NoEntry) => Ok(None),
|
|
@@ -15,29 +42,26 @@ pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
|
|
| 15 |
}
|
| 16 |
}
|
| 17 |
|
| 18 |
-
pub fn
|
| 19 |
-
|
| 20 |
-
if api_key.trim().is_empty() {
|
| 21 |
-
return match entry.delete_credential() {
|
| 22 |
-
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
| 23 |
-
Err(err) => Err(err.into()),
|
| 24 |
-
};
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
entry.set_password(api_key)?;
|
| 28 |
Ok(())
|
| 29 |
}
|
| 30 |
|
| 31 |
-
fn
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
#[cfg(target_os = "linux")]
|
| 39 |
fn configure_platform_store() {
|
| 40 |
-
let root =
|
| 41 |
.as_std_path()
|
| 42 |
.join("secrets")
|
| 43 |
.join("keyring");
|
|
@@ -51,14 +75,14 @@ fn configure_platform_store() {}
|
|
| 51 |
mod filesystem {
|
| 52 |
use std::fmt::Write as _;
|
| 53 |
use std::fs;
|
|
|
|
|
|
|
| 54 |
use std::path::{Path, PathBuf};
|
| 55 |
-
use std::sync::atomic::{AtomicU64, Ordering};
|
| 56 |
|
|
|
|
| 57 |
use keyring::credential::{Credential, CredentialApi, CredentialBuilderApi};
|
| 58 |
use keyring::{Error, Result};
|
| 59 |
|
| 60 |
-
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
| 61 |
-
|
| 62 |
#[derive(Debug)]
|
| 63 |
pub(super) struct Builder {
|
| 64 |
root: PathBuf,
|
|
@@ -98,15 +122,6 @@ mod filesystem {
|
|
| 98 |
fn path(&self) -> PathBuf {
|
| 99 |
self.root.join(&self.name)
|
| 100 |
}
|
| 101 |
-
|
| 102 |
-
fn temp_path(&self) -> PathBuf {
|
| 103 |
-
self.root.join(format!(
|
| 104 |
-
"{}.tmp-{}-{}",
|
| 105 |
-
self.name,
|
| 106 |
-
std::process::id(),
|
| 107 |
-
TEMP_ID.fetch_add(1, Ordering::Relaxed)
|
| 108 |
-
))
|
| 109 |
-
}
|
| 110 |
}
|
| 111 |
|
| 112 |
impl CredentialApi for FileCredential {
|
|
@@ -115,14 +130,12 @@ mod filesystem {
|
|
| 115 |
set_mode(&self.root, 0o700)?;
|
| 116 |
|
| 117 |
let path = self.path();
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
storage_error(err)
|
| 125 |
-
})?;
|
| 126 |
set_mode(&path, 0o600)
|
| 127 |
}
|
| 128 |
|
|
@@ -157,6 +170,26 @@ mod filesystem {
|
|
| 157 |
Ok(())
|
| 158 |
}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
fn storage_error(err: std::io::Error) -> Error {
|
| 161 |
Error::NoStorageAccess(Box::new(err))
|
| 162 |
}
|
|
|
|
| 2 |
|
| 3 |
use keyring::Entry;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
static INIT_CREDENTIAL_STORE: Once = Once::new();
|
| 6 |
|
| 7 |
+
/// Service-scoped access to Koharu's platform-backed string secret storage.
|
| 8 |
+
#[derive(Debug, Clone)]
|
| 9 |
+
pub struct SecretStore {
|
| 10 |
+
service: String,
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
impl SecretStore {
|
| 14 |
+
pub fn new(service: impl Into<String>) -> Self {
|
| 15 |
+
Self {
|
| 16 |
+
service: service.into(),
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/// Load a secret by key, returning `None` when no credential exists.
|
| 21 |
+
pub fn get(&self, key: &str) -> anyhow::Result<Option<String>> {
|
| 22 |
+
get_secret(&self.service, key)
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/// Store a secret by key. Use `delete` to clear an existing credential.
|
| 26 |
+
pub fn set(&self, key: &str, secret: &str) -> anyhow::Result<()> {
|
| 27 |
+
set_secret(&self.service, key, secret)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/// Clear a secret by key. Missing credentials are treated as success.
|
| 31 |
+
pub fn delete(&self, key: &str) -> anyhow::Result<()> {
|
| 32 |
+
delete_secret(&self.service, key)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
pub fn get_secret(service: &str, key: &str) -> anyhow::Result<Option<String>> {
|
| 37 |
+
let entry = secret_entry(service, key)?;
|
| 38 |
match entry.get_password() {
|
| 39 |
Ok(value) => Ok(Some(value)),
|
| 40 |
Err(keyring::Error::NoEntry) => Ok(None),
|
|
|
|
| 42 |
}
|
| 43 |
}
|
| 44 |
|
| 45 |
+
pub fn set_secret(service: &str, key: &str, secret: &str) -> anyhow::Result<()> {
|
| 46 |
+
secret_entry(service, key)?.set_password(secret)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
Ok(())
|
| 48 |
}
|
| 49 |
|
| 50 |
+
pub fn delete_secret(service: &str, key: &str) -> anyhow::Result<()> {
|
| 51 |
+
match secret_entry(service, key)?.delete_credential() {
|
| 52 |
+
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
| 53 |
+
Err(err) => Err(err.into()),
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
|
| 57 |
+
fn secret_entry(service: &str, key: &str) -> anyhow::Result<Entry> {
|
| 58 |
+
INIT_CREDENTIAL_STORE.call_once(configure_platform_store);
|
| 59 |
+
Ok(Entry::new(service, key)?)
|
| 60 |
}
|
| 61 |
|
| 62 |
#[cfg(target_os = "linux")]
|
| 63 |
fn configure_platform_store() {
|
| 64 |
+
let root = crate::runtime::default_app_data_root()
|
| 65 |
.as_std_path()
|
| 66 |
.join("secrets")
|
| 67 |
.join("keyring");
|
|
|
|
| 75 |
mod filesystem {
|
| 76 |
use std::fmt::Write as _;
|
| 77 |
use std::fs;
|
| 78 |
+
use std::io;
|
| 79 |
+
use std::io::Write as _;
|
| 80 |
use std::path::{Path, PathBuf};
|
|
|
|
| 81 |
|
| 82 |
+
use atomicwrites::{AtomicFile, OverwriteBehavior};
|
| 83 |
use keyring::credential::{Credential, CredentialApi, CredentialBuilderApi};
|
| 84 |
use keyring::{Error, Result};
|
| 85 |
|
|
|
|
|
|
|
| 86 |
#[derive(Debug)]
|
| 87 |
pub(super) struct Builder {
|
| 88 |
root: PathBuf,
|
|
|
|
| 122 |
fn path(&self) -> PathBuf {
|
| 123 |
self.root.join(&self.name)
|
| 124 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
impl CredentialApi for FileCredential {
|
|
|
|
| 130 |
set_mode(&self.root, 0o700)?;
|
| 131 |
|
| 132 |
let path = self.path();
|
| 133 |
+
AtomicFile::new(&path, OverwriteBehavior::AllowOverwrite)
|
| 134 |
+
.write(|file| -> io::Result<()> {
|
| 135 |
+
set_file_mode(file, 0o600)?;
|
| 136 |
+
file.write_all(secret)
|
| 137 |
+
})
|
| 138 |
+
.map_err(atomic_write_error)?;
|
|
|
|
|
|
|
| 139 |
set_mode(&path, 0o600)
|
| 140 |
}
|
| 141 |
|
|
|
|
| 170 |
Ok(())
|
| 171 |
}
|
| 172 |
|
| 173 |
+
#[cfg(unix)]
|
| 174 |
+
fn set_file_mode(file: &fs::File, mode: u32) -> io::Result<()> {
|
| 175 |
+
use std::os::unix::fs::PermissionsExt;
|
| 176 |
+
|
| 177 |
+
file.set_permissions(fs::Permissions::from_mode(mode))
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
#[cfg(not(unix))]
|
| 181 |
+
fn set_file_mode(_file: &fs::File, _mode: u32) -> io::Result<()> {
|
| 182 |
+
Ok(())
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
fn atomic_write_error(err: atomicwrites::Error<io::Error>) -> Error {
|
| 186 |
+
match err {
|
| 187 |
+
atomicwrites::Error::Internal(err) | atomicwrites::Error::User(err) => {
|
| 188 |
+
storage_error(err)
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
fn storage_error(err: std::io::Error) -> Error {
|
| 194 |
Error::NoStorageAccess(Box::new(err))
|
| 195 |
}
|