Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ et [Semantic Versioning 2.0.0](https://semver.org/lang/fr/).
intellectuelle, limitation de responsabilité, juridiction française). Les
options exclusives se désélectionnent automatiquement. Les clauses cochées
s'insèrent dans le PDF avant les CGV légales.
- **Import retour signé client sur les devis** — quand votre client renvoie
un PDF signé (à la main + scan, Adobe Reader, DocuSign…), un nouveau bouton
*Importer signature client* apparaît sur les devis émis. FAKT vérifie
automatiquement que le contenu textuel du PDF importé correspond au devis
original (hash SHA-256 calculé à l'émission). Si tout matche, le devis
bascule en signé et le bouton « Créer une facture » se débloque. Si le
contenu diffère (annotation manuscrite, prix modifié…), un avertissement
explicite affiche les deux empreintes et demande confirmation. Toute
divergence est consignée dans la chaîne d'audit.

### Améliorations

Expand Down
64 changes: 64 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ hmac = "0.12"
# PDF manipulation (PAdES)
lopdf = "0.34"

# Extraction texte PDF pour le hash de vérification d'intégrité (workflow
# « Importer signature client »). pdf-extract est pure Rust (pas de FFI),
# ~200ko compilé.
pdf-extract = "0.7"

# TSA RFC 3161 (blocking sync) + audit client / sidecar healthcheck (async client + JSON).
# `http2` et `charset` permettent au client async par défaut de fonctionner sans enable
# `default-features`, et `json` expose `.json()` sur RequestBuilder/Response.
Expand Down
59 changes: 59 additions & 0 deletions apps/desktop/src-tauri/src/commands/audit_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! Commande Tauri exposant le calcul du `compute_self_hash_hex` d'un
//! `SignatureEvent` au frontend.
//!
//! Utilisé par l'UI « Importer signature client » : avant d'appender un
//! nouvel event au sidecar, le frontend doit calculer le `previousEventHash`
//! du dernier event de la chaîne. Cette logique de hash chaîné vit côté
//! Rust (`crate::crypto::audit`) et ne doit PAS être dupliquée en TS — sinon
//! la moindre divergence casserait la vérification d'intégrité.

use crate::crypto::audit::SignatureEvent;

/// Shape DTO côté frontend : Tauri convertit automatiquement camelCase TS
/// en snake_case Rust à la désérialisation, donc le frontend passe le
/// `SignatureEvent` TS tel quel (pas de conversion explicite côté JS).
#[tauri::command]
pub fn compute_signature_event_self_hash(event: SignatureEvent) -> String {
event.compute_self_hash_hex()
}

#[cfg(test)]
mod tests {
use super::*;

fn sample_event() -> SignatureEvent {
SignatureEvent {
id: "evt-1".into(),
document_type: "quote".into(),
document_id: "quote-1".into(),
signer_name: "Client SARL".into(),
signer_email: "client@example.fr".into(),
ip_address: None,
user_agent: None,
timestamp_iso: "2026-05-03T12:00:00Z".into(),
doc_hash_before: "a".repeat(64),
doc_hash_after: "b".repeat(64),
signature_png_base64: None,
tsa_provider: None,
tsa_response_base64: None,
previous_event_hash: None,
}
}

#[test]
fn hash_is_deterministic() {
let h1 = compute_signature_event_self_hash(sample_event());
let h2 = compute_signature_event_self_hash(sample_event());
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64);
}

#[test]
fn hash_changes_when_field_changes() {
let original = compute_signature_event_self_hash(sample_event());
let mut modified = sample_event();
modified.doc_hash_after = "c".repeat(64);
let after = compute_signature_event_self_hash(modified);
assert_ne!(original, after);
}
}
4 changes: 4 additions & 0 deletions apps/desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
pub mod audit_chain;
pub mod backend;
pub mod backup;
pub mod cycle;
pub mod email;
pub mod files;
pub mod pdf_hash;
pub mod ping;
pub mod signatures;
pub mod state;
pub mod updater;

pub use audit_chain::*;
pub use backend::*;
pub use backup::*;
pub use cycle::*;
pub use email::*;
pub use files::*;
pub use pdf_hash::*;
pub use ping::*;
pub use signatures::*;
pub use state::{AppState, FaktError, FaktResult, NumberingPayload};
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src-tauri/src/commands/pdf_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Commande Tauri exposant `compute_pdf_text_hash`.
//!
//! Appelée :
//! - À l'émission d'un devis pour calculer + persister le hash texte du
//! PDF officiel via le sidecar (`POST /api/quotes/:id/original-text-hash`).
//! - À l'import retour signé pour comparer le hash du PDF reçu à celui
//! stocké à l'émission. Si différent, l'utilisateur peut forcer mais
//! l'écart est consigné dans l'audit trail.

use crate::pdf::text_hash::compute_pdf_text_hash as compute_inner;

/// Calcule le SHA-256 hex du texte normalisé d'un PDF.
///
/// Le frontend appelle `invoke<string>("compute_pdf_text_hash", { pdfBytes })`.
/// Retour : `Ok(hex 64 chars)` ou `Err(message FR)` si extraction texte échoue.
#[tauri::command]
pub fn compute_pdf_text_hash(pdf_bytes: Vec<u8>) -> Result<String, String> {
if pdf_bytes.is_empty() {
return Err("PDF vide".into());
}
compute_inner(&pdf_bytes).map_err(|e| e.to_string())
}
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ fn run_inner() -> Result<(), String> {
commands::backend::get_backend_mode,
commands::backend::set_backend_mode,
pdf::render::render_pdf,
commands::pdf_hash::compute_pdf_text_hash,
commands::audit_chain::compute_signature_event_self_hash,
crypto::generate_cert,
crypto::get_cert_info,
crypto::rotate_cert,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/pdf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
//! stays source-compatible because it only consults the binary path.

pub mod render;
pub mod text_hash;
Loading
Loading