commit 3c9f50676f0698377b380b7529e02b2750c93ece Author: Dom Date: Wed Mar 18 19:55:37 2026 +0100 feat: outil de chiffrement/déchiffrement CSV pour Amadea AES-256-GCM, gestion de clés (aléatoire ou mot de passe Argon2id), détection automatique des colonnes Symbolic, cross-compile Windows. 100k lignes × 3 champs en ~0.14s. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..492ceb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +cryptage.key +*.exe diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cbc8906 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,595 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cryptage" +version = "1.0.0" +dependencies = [ + "aes-gcm", + "argon2", + "base64", + "clap", + "rand", + "rpassword", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0c7263d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cryptage" +version = "1.0.0" +edition = "2021" +description = "Chiffrement/déchiffrement de fichiers CSV pour Amadea" + +[dependencies] +aes-gcm = "0.10" +argon2 = "0.5" +base64 = "0.22" +clap = { version = "4", features = ["derive"] } +rand = "0.8" +rpassword = "7" + +[profile.release] +opt-level = 3 +lto = true +strip = true +codegen-units = 1 diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..edec85b --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,126 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, Nonce, +}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use rand::{rngs::OsRng, RngCore}; +use std::fs; +use std::path::Path; + +const KEY_SIZE: usize = 32; +const NONCE_SIZE: usize = 12; +const ARGON2_SALT: &[u8; 16] = b"cryptage_v1_2026"; + +/// Génère une clé aléatoire et l'écrit en base64 +pub fn generate_random_key(path: &Path) -> Result<(), Box> { + let mut key = [0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key); + fs::write(path, BASE64.encode(key))?; + Ok(()) +} + +/// Génère une clé dérivée d'un mot de passe (Argon2id) et l'écrit en base64 +pub fn generate_key_from_password(path: &Path) -> Result<(), Box> { + let password = rpassword::prompt_password("Mot de passe : ")?; + let confirm = rpassword::prompt_password("Confirmer : ")?; + if password != confirm { + return Err("Les mots de passe ne correspondent pas".into()); + } + + let mut key = [0u8; KEY_SIZE]; + argon2::Argon2::default() + .hash_password_into(password.as_bytes(), ARGON2_SALT, &mut key) + .map_err(|e| format!("Erreur Argon2 : {e}"))?; + + fs::write(path, BASE64.encode(key))?; + Ok(()) +} + +/// Charge une clé depuis un fichier base64 +pub fn load_key(path: &Path) -> Result<[u8; KEY_SIZE], Box> { + let content = fs::read_to_string(path)?; + let decoded = BASE64.decode(content.trim())?; + if decoded.len() != KEY_SIZE { + return Err(format!( + "Clé invalide : {} bytes (attendu {KEY_SIZE})", + decoded.len() + ) + .into()); + } + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&decoded); + Ok(key) +} + +/// Chiffre une valeur en AES-256-GCM, retourne base64(nonce || ciphertext || tag) +pub fn encrypt(plaintext: &str, key: &[u8; KEY_SIZE]) -> Result> { + let cipher = Aes256Gcm::new(Key::::from_slice(key)); + + let mut nonce_bytes = [0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(|e| format!("Chiffrement échoué : {e}"))?; + + let mut out = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + + Ok(BASE64.encode(out)) +} + +/// Déchiffre une valeur base64(nonce || ciphertext || tag) en AES-256-GCM +pub fn decrypt(encoded: &str, key: &[u8; KEY_SIZE]) -> Result> { + let data = BASE64.decode(encoded.trim())?; + if data.len() < NONCE_SIZE + 16 { + return Err("Données chiffrées trop courtes".into()); + } + + let cipher = Aes256Gcm::new(Key::::from_slice(key)); + let nonce = Nonce::from_slice(&data[..NONCE_SIZE]); + + let plaintext = cipher + .decrypt(nonce, &data[NONCE_SIZE..]) + .map_err(|_| "Déchiffrement échoué (clé incorrecte ou données corrompues)")?; + + Ok(String::from_utf8(plaintext)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_roundtrip() { + let mut key = [0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key); + + let original = "879856498"; + let encrypted = encrypt(original, &key).unwrap(); + let decrypted = decrypt(&encrypted, &key).unwrap(); + assert_eq!(original, decrypted); + } + + #[test] + fn test_different_ciphertexts() { + let mut key = [0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key); + + let enc1 = encrypt("test", &key).unwrap(); + let enc2 = encrypt("test", &key).unwrap(); + assert_ne!(enc1, enc2); // IV aléatoire → sorties différentes + } + + #[test] + fn test_wrong_key_fails() { + let mut key1 = [0u8; KEY_SIZE]; + let mut key2 = [0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key1); + OsRng.fill_bytes(&mut key2); + + let encrypted = encrypt("secret", &key1).unwrap(); + assert!(decrypt(&encrypted, &key2).is_err()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..63e487f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,113 @@ +use clap::Parser; +use std::path::PathBuf; +use std::process; +use std::time::Instant; + +mod crypto; +mod processor; + +/// Chiffrement/déchiffrement de fichiers CSV pour Amadea +#[derive(Parser)] +#[command(name = "cryptage", version)] +struct Cli { + /// Mode déchiffrement + #[arg(short = 'd', long = "decrypt")] + decrypt: bool, + + /// Générer une nouvelle clé + #[arg(long = "generate-key")] + generate_key: bool, + + /// Dériver la clé d'un mot de passe (avec --generate-key) + #[arg(short = 'p', long = "password")] + from_password: bool, + + /// Chemin vers le fichier de clé + #[arg(short = 'k', long = "key")] + key_path: Option, + + /// Fichier d'entrée CSV + input: Option, + + /// Fichier de sortie CSV + output: Option, +} + +fn default_key_path() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")) + .join("cryptage.key") +} + +fn main() { + let cli = Cli::parse(); + + // Mode génération de clé + if cli.generate_key { + let key_path = cli.key_path.unwrap_or_else(default_key_path); + let result = if cli.from_password { + crypto::generate_key_from_password(&key_path) + } else { + crypto::generate_random_key(&key_path) + }; + match result { + Ok(()) => println!("Clé générée : {}", key_path.display()), + Err(e) => { + eprintln!("Erreur : {e}"); + process::exit(1); + } + } + return; + } + + // Mode chiffrement/déchiffrement — fichiers requis + let input = match cli.input { + Some(p) => p, + None => { + eprintln!("Usage : cryptage [-d] "); + eprintln!(" cryptage --generate-key [-p] [-k chemin]"); + process::exit(1); + } + }; + let output = match cli.output { + Some(p) => p, + None => { + eprintln!("Erreur : fichier de sortie requis"); + process::exit(1); + } + }; + + // Charger la clé + let key_path = cli.key_path.unwrap_or_else(default_key_path); + let key = match crypto::load_key(&key_path) { + Ok(k) => k, + Err(e) => { + eprintln!("Erreur clé '{}' : {e}", key_path.display()); + process::exit(1); + } + }; + + // Traitement + let start = Instant::now(); + let mode = if cli.decrypt { + "Déchiffrement" + } else { + "Chiffrement" + }; + + match processor::process_csv(&input, &output, &key, cli.decrypt) { + Ok(count) => { + let elapsed = start.elapsed(); + println!( + "{mode} terminé : {count} lignes en {:.2}s", + elapsed.as_secs_f64() + ); + } + Err(e) => { + eprintln!("Erreur : {e}"); + process::exit(1); + } + } +} diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..8199702 --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,61 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::Path; + +use crate::crypto; + +pub fn process_csv( + input: &Path, + output: &Path, + key: &[u8; 32], + decrypt: bool, +) -> Result> { + let reader = BufReader::new(File::open(input)?); + let mut writer = BufWriter::new(File::create(output)?); + let mut lines = reader.lines(); + + // Ligne 1 : en-têtes — copie directe (gère le BOM UTF-8 Windows) + let header = lines.next().ok_or("Fichier vide")??; + let header = header.strip_prefix('\u{feff}').unwrap_or(&header); + writeln!(writer, "{header}")?; + + // Ligne 2 : types — détecte les colonnes Symbolic à traiter + let types_line = lines.next().ok_or("Ligne de types manquante")??; + writeln!(writer, "{types_line}")?; + + let symbolic: Vec = types_line + .split(';') + .map(|t| t.trim().eq_ignore_ascii_case("symbolic")) + .collect(); + + // Lignes de données + let mut count = 0usize; + for line in lines { + let line = line?; + if line.trim().is_empty() { + continue; + } + + let fields: Vec<&str> = line.split(';').collect(); + let mut out_fields = Vec::with_capacity(fields.len()); + + for (i, field) in fields.iter().enumerate() { + if i < symbolic.len() && symbolic[i] { + let val = if decrypt { + crypto::decrypt(field, key)? + } else { + crypto::encrypt(field, key)? + }; + out_fields.push(val); + } else { + out_fields.push(field.to_string()); + } + } + + writeln!(writer, "{}", out_fields.join(";"))?; + count += 1; + } + + writer.flush()?; + Ok(count) +}