Δημοσίευση πηγής
Κατεβάστε την πηγή ZIP
CH-J Tools
Περιηγηθείτε σε καταλόγους και αρχεία για μια συγκεκριμένη έκδοση εφαρμογών.
CH-J / Server Manager
1704 αρχεία
0.1.2-beta
Προστέθηκε: · Χτισμένο:
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 page3
//! cannot point a signature at another file. No private key or passphrase leaves the Core.4
use super::{text, CommandResult};5
use crate::core::app::{Core, CoreSlot};6
use crate::core::error::CoreError;7
use crate::core::keychain as keys;8
use crate::core::quick::{self, QuickAction, QuickSlot};9
use crate::security::caller::require_quick;10
use serde_json::{json, Value};11
use std::sync::Arc;12
use tauri::{State, Webview};13
use zeroize::Zeroizing;15
fn guard(webview: &Webview, slot: &CoreSlot) -> Result<Arc<Core>, CoreError> {16
require_quick(webview.label(), webview.url().ok())?;17
slot.get()18
}20
fn 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."))22
}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.26
async 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
}30
}32
fn vault_value(core: &Core) -> Value {33
serde_json::to_value(core.vault.status()).unwrap_or_else(|_| json!({}))34
}36
/// Stored OpenPGP identities with at least one component that can sign now (requires an unlocked vault).37
fn 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()57
}59
#[tauri::command]60
pub 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)83
}85
#[tauri::command]86
pub 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!({})))95
}97
#[tauri::command]98
pub 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
.await113
.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 the115
// 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
.await127
.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
found133
}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)142
}144
/// Verifies with a public key file the user picks now in a native dialog (the page never supplies a path). The145
/// key is used for this verification only and is not stored. A cancelled dialog returns `{ "cancelled": true }`.146
#[tauri::command]147
pub 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
.await170
.map_err(|_| CoreError::plain("Verification failed."))??;171
result["keyFile"] = json!(name);172
server_status(&core, &mut result).await;173
Ok(result)174
}176
/// Verifies with the public certificate keys.ch-j.de holds for the fingerprint the user entered (or the one the177
/// signature names). The certificate must contain that fingerprint and is used for this verification only.178
#[tauri::command]179
pub 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
.await195
.map_err(|_| CoreError::plain("Verification failed."))??;196
result["keyserver"] = json!(crate::core::keyserver::ORIGIN);197
server_status(&core, &mut result).await;198
Ok(result)199
}201
#[tauri::command]202
pub 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
.await226
.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)?}))229
}231
/// "Open in CH-J Tools": shows (or creates) the main window; the quick window stays until closed.232
#[tauri::command]233
pub 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}))238
}SHA-256
4093ce904a6b7dfd575a66c6a3fbacf68845ede5729865c8d1576daa9cc2ba4cCH-J Proprietary Software License 1.14 · Η πηγή παρέχεται στο CH-J Proprietary Software License 1.14. Η διαθεσιμότητά του δεν αλλάζει τους όρους της άδειας χρήσης ούτε χορηγεί πρόσθετα δικαιώματα.
Archive SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f