CH-J Tools透過 SSH 管理伺服器
選單
已發布的原始碼

CH-J Tools

瀏覽特定應用程式版本的目錄與檔案。

下載原始碼 ZIP
CH-J / Server Manager 1704 個檔案
0.1.2-beta 新增時間: · 建置時間:
quick.rs 新增時間: 未記錄日期 12.7 KB · 238 行
程式碼 下載檔案
1//! Quick-action window commands (verify/sign one file from the system file manager). Only the `quick`
2//! window may call them. The file comes from the request in `QuickSlot`, never from the page, so the page
3//! cannot point a signature at another file. No private key or passphrase leaves the Core.
4use super::{text, CommandResult};
5use crate::core::app::{Core, CoreSlot};
6use crate::core::error::CoreError;
7use crate::core::keychain as keys;
8use crate::core::quick::{self, QuickAction, QuickSlot};
9use crate::security::caller::require_quick;
10use serde_json::{json, Value};
11use std::sync::Arc;
12use tauri::{State, Webview};
13use zeroize::Zeroizing;
15fn guard(webview: &Webview, slot: &CoreSlot) -> Result<Arc<Core>, CoreError> {
16 require_quick(webview.label(), webview.url().ok())?;
17 slot.get()
20fn request(quick: &QuickSlot) -> Result<quick::QuickRequest, CoreError> {
21 quick.get().ok_or_else(|| CoreError::new("QUICK_NO_REQUEST", "No file was handed to the quick action."))
24/// Revocation state on keys.ch-j.de for the verifying key, whatever its source (Keychain: stored if fresh enough,
25/// otherwise checked now; other keys: checked now). Added as `server`; a failed check is reported, not hidden.
26async fn server_status(core: &Core, result: &mut Value) {
27 if let (Some(fingerprint), Ok(transport)) = (result["primaryFingerprint"].as_str().map(str::to_string), crate::core::keysync::Transport::https()) {
28 result["server"] = core.services.keysync.server_status_any(&transport, &fingerprint).await;
29 }
32fn vault_value(core: &Core) -> Value {
33 serde_json::to_value(core.vault.status()).unwrap_or_else(|_| json!({}))
36/// Stored OpenPGP identities with at least one component that can sign now (requires an unlocked vault).
37fn signing_keys(core: &Core) -> Vec<Value> {
38 let Ok(generation) = core.vault.unlocked_generation() else { return Vec::new() };
39 let Ok(data) = core.vault.get_data_at_generation(generation) else { return Vec::new() };
40 data["secrets"]["keychain"]
41 .as_array()
42 .into_iter()
43 .flatten()
44 .filter_map(|entry| {
45 let components = keys::signing_components(entry).ok()?;
46 if components.is_empty() {
47 return None;
48 }
49 let summary = keys::describe(entry);
50 Some(json!({
51 "id": entry["id"], "label": entry["label"], "fingerprint": entry["fingerprint"],
52 "userIds": summary["userIds"], "version": summary["version"], "algorithm": summary["algorithm"],
53 "components": components,
54 }))
55 })
56 .collect()
59#[tauri::command]
60pub async fn quick_state(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>) -> CommandResult {
61 let core = guard(&webview, &slot)?;
62 let request = request(&quick)?;
63 let mut state = json!({
64 "action": request.action.name(),
65 "language": core.config.language(),
66 "vault": vault_value(&core),
67 });
68 match request.action {
69 QuickAction::Verify => {
70 let (data, signature) = quick::locate_pair(&request.path)?;
71 state["file"] = quick::file_summary(&data)?;
72 state["issuers"] = json!(signature.as_ref().and_then(|p| quick::read_signature(p).ok()).map(|b| quick::signature_issuers(&b)).unwrap_or_default());
73 state["signature"] = match signature { Some(p) => quick::file_summary(&p)?, None => Value::Null };
74 }
75 QuickAction::Sign => {
76 state["file"] = quick::file_summary(&request.path)?;
77 let target = quick::signature_target(&request.path);
78 state["target"] = json!({"path": target.to_string_lossy(), "name": target.file_name().map(|n| n.to_string_lossy().to_string()), "exists": target.exists()});
79 state["keys"] = json!(signing_keys(&core));
80 }
81 }
82 Ok(state)
85#[tauri::command]
86pub async fn quick_unlock(webview: Webview, slot: State<'_, CoreSlot>, payload: Option<Value>) -> CommandResult {
87 let core = guard(&webview, &slot)?;
88 let p = super::payload(payload);
89 core.services.biometrics.cancel();
90 let status = core.vault.unlock(text(&p, "password")).await?;
91 core.services.biometrics.disarm_auto_unlock();
92 core.services.lock.activity();
93 core.services.availability.changed();
94 Ok(serde_json::to_value(status).unwrap_or_else(|_| json!({})))
97#[tauri::command]
98pub async fn quick_verify(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>) -> CommandResult {
99 let core = guard(&webview, &slot)?;
100 let request = request(&quick)?;
101 let (data, signature) = quick::locate_pair(&request.path)?;
102 let signature = signature.ok_or_else(|| CoreError::new("QUICK_SIGNATURE_MISSING", "No signature file (.asc, .sig or .gpg) was found next to the file."))?;
103 let bytes = quick::read_signature(&signature)?;
104 let keychain: Vec<Value> = match core.vault.unlocked_generation() {
105 Ok(generation) => core.vault.get_data_at_generation(generation).map(|d| d["secrets"]["keychain"].as_array().cloned().unwrap_or_default()).unwrap_or_default(),
106 Err(_) => Vec::new(),
107 };
108 let keyring = core.paths.in_data("pgp-keys");
109 let unlocked = core.vault.is_unlocked();
110 let (data2, bytes2) = (data.clone(), bytes.clone());
111 let local = tokio::task::spawn_blocking(move || quick::verify(&data, &bytes, &keychain, &keyring))
112 .await
113 .map_err(|_| CoreError::plain("Verification failed."))?;
114 // Unknown signer (not in the Keychain, no saved key): try the public certificate keys.ch-j.de holds for the
115 // fingerprint the signature names. Used for this verification only; a failed lookup keeps the original error.
116 let mut result = match local {
117 Err(error) if error.code == "HASH_PGP_KEY_REQUIRED" => {
118 let issuer = quick::signature_issuers(&bytes2).into_iter().find(|i| i.len() == 40 || i.len() == 64);
119 let certificate = match &issuer {
120 Some(fp) => crate::core::keyserver::fetch_certificate(fp).await.ok().flatten(),
121 None => None,
122 };
123 match (issuer, certificate) {
124 (Some(fp), Some(cert)) => {
125 let mut found = tokio::task::spawn_blocking(move || quick::verify_with_key(&data2, &bytes2, cert.as_bytes(), "keyserver", Some(&fp)))
126 .await
127 .map_err(|_| CoreError::plain("Verification failed."))??;
128 found["keyserver"] = json!(crate::core::keyserver::ORIGIN);
129 // The fingerprint came from the signature itself, not from the user: no "matches" claim.
130 found["expectedMatched"] = json!(false);
131 found["fingerprintMatched"] = json!(false);
132 found
133 }
134 _ => return Err(error.with("keyserverTried", true)),
135 }
136 }
137 other => other?,
138 };
139 result["keychainSearched"] = json!(unlocked);
140 server_status(&core, &mut result).await;
141 Ok(result)
144/// Verifies with a public key file the user picks now in a native dialog (the page never supplies a path). The
145/// key is used for this verification only and is not stored. A cancelled dialog returns `{ "cancelled": true }`.
146#[tauri::command]
147pub async fn quick_verify_key(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>, payload: Option<Value>) -> CommandResult {
148 let core = guard(&webview, &slot)?;
149 let expected = quick::expected_fingerprint(payload.as_ref().and_then(|p| p["fingerprint"].as_str()))?;
150 let request = request(&quick)?;
151 let (data, signature) = quick::locate_pair(&request.path)?;
152 let signature = signature.ok_or_else(|| CoreError::new("QUICK_SIGNATURE_MISSING", "No signature file (.asc, .sig or .gpg) was found next to the file."))?;
153 let bytes = quick::read_signature(&signature)?;
154 let parent = webview.window();
155 let filters = [
156 crate::dialogs::Filter { name: "OpenPGP", extensions: &["asc", "pgp", "gpg", "key", "pub", "txt"] },
157 crate::dialogs::Filter { name: "*", extensions: &["*"] },
158 ];
159 let Some(path) = crate::dialogs::pick_file(&core.app, Some(&parent), None, &filters).await else {
160 return Ok(json!({ "cancelled": true }));
161 };
162 let meta = std::fs::metadata(&path).map_err(|_| CoreError::new("QUICK_KEY_UNREADABLE", "The key file cannot be read."))?;
163 if !meta.is_file() || meta.len() > quick::MAX_PUBLIC_KEY {
164 return Err(CoreError::new("QUICK_KEY_UNREADABLE", "Choose a public key file of at most 1 MiB."));
165 }
166 let key = std::fs::read(&path)?;
167 let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
168 let mut result = tokio::task::spawn_blocking(move || quick::verify_with_key(&data, &bytes, &key, "file", expected.as_deref()))
169 .await
170 .map_err(|_| CoreError::plain("Verification failed."))??;
171 result["keyFile"] = json!(name);
172 server_status(&core, &mut result).await;
173 Ok(result)
176/// Verifies with the public certificate keys.ch-j.de holds for the fingerprint the user entered (or the one the
177/// signature names). The certificate must contain that fingerprint and is used for this verification only.
178#[tauri::command]
179pub async fn quick_verify_keyserver(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>, payload: Option<Value>) -> CommandResult {
180 let core = guard(&webview, &slot)?;
181 let request = request(&quick)?;
182 let (data, signature) = quick::locate_pair(&request.path)?;
183 let signature = signature.ok_or_else(|| CoreError::new("QUICK_SIGNATURE_MISSING", "No signature file (.asc, .sig or .gpg) was found next to the file."))?;
184 let bytes = quick::read_signature(&signature)?;
185 let expected = quick::expected_fingerprint(payload.as_ref().and_then(|p| p["fingerprint"].as_str()))?;
186 let lookup = match &expected {
187 Some(fp) => fp.clone(),
188 None => quick::signature_issuers(&bytes).into_iter().find(|i| i.len() == 40 || i.len() == 64)
189 .ok_or_else(|| CoreError::new("QUICK_FINGERPRINT_REQUIRED", "The signature names no fingerprint. Enter the signer's fingerprint."))?,
190 };
191 let certificate = crate::core::keyserver::fetch_certificate(&lookup).await?
192 .ok_or_else(|| CoreError::new("QUICK_KEYSERVER_NOT_FOUND", "keys.ch-j.de has no public key for this fingerprint."))?;
193 let mut result = tokio::task::spawn_blocking(move || quick::verify_with_key(&data, &bytes, certificate.as_bytes(), "keyserver", Some(&lookup)))
194 .await
195 .map_err(|_| CoreError::plain("Verification failed."))??;
196 result["keyserver"] = json!(crate::core::keyserver::ORIGIN);
197 server_status(&core, &mut result).await;
198 Ok(result)
201#[tauri::command]
202pub async fn quick_sign(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>, payload: Option<Value>) -> CommandResult {
203 let core = guard(&webview, &slot)?;
204 let request = request(&quick)?;
205 if request.action != QuickAction::Sign {
206 return Err(CoreError::plain("This quick action does not sign."));
207 }
208 let p = super::payload(payload);
209 let generation = core.vault.unlocked_generation()?;
210 let target = quick::signature_target(&request.path);
211 if target.exists() && p["overwrite"] != true {
212 return Err(CoreError::new("QUICK_SIGNATURE_EXISTS", "A signature file already exists. Confirm replacing it."));
213 }
214 core.services.biometrics.require_sensitive_action().await?;
215 core.vault.assert_generation(generation)?;
216 let entry = keys::get_at_generation(&core.vault, text(&p, "id"), generation)?;
217 if entry["kind"] != "openpgp" {
218 return Err(CoreError::plain("Only OpenPGP identities can sign files from the system menu."));
219 }
220 let component = Some(text(&p, "component").to_string()).filter(|c| !c.is_empty());
221 let password = Zeroizing::new(text(&p, "passphrase").to_string());
222 let file = request.path.clone();
223 let before = entry.clone();
224 let signature = tokio::task::spawn_blocking(move || keys::sign_with(&entry, &file, &password, component.as_deref()))
225 .await
226 .map_err(|_| CoreError::plain("Signing failed."))??;
227 keys::with_revision(&core.vault, generation, &before, || crate::core::storage::atomic_write_file(&target, signature.as_bytes(), 0o644))?;
228 Ok(json!({"signed": true, "signature": quick::file_summary(&target)?}))
231/// "Open in CH-J Tools": shows (or creates) the main window; the quick window stays until closed.
232#[tauri::command]
233pub async fn quick_open_main(webview: Webview, slot: State<'_, CoreSlot>, quick: State<'_, QuickSlot>) -> CommandResult {
234 let core = guard(&webview, &slot)?;
235 *quick.only_quick.lock() = false;
236 crate::windows::show_or_create_main(&core.app, &core.i18n, &core.config.language()).map_err(|e| CoreError::plain(e.to_string()))?;
237 Ok(json!({"opened": true}))
SHA-2564093ce904a6b7dfd575a66c6a3fbacf68845ede5729865c8d1576daa9cc2ba4c

CH-J Proprietary Software License 1.14 · 原始碼依 CH-J Proprietary Software License 1.14 提供。公開原始碼不會改變授權條款,也不會授予額外的權利。

壓縮檔 SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
檔案