diff --git a/Cargo.lock b/Cargo.lock index 6d5298e..ff6880d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,18 +50,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "atty" @@ -100,9 +100,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -116,6 +116,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cfg-if" version = "1.0.0" @@ -143,16 +149,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap", @@ -160,9 +166,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -173,27 +179,27 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -210,9 +216,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer", "crypto-common", @@ -230,9 +236,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -242,10 +248,25 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.5" +name = "fuser" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "104ed58f182bc2975062cd3fab229e82b5762de420e26cf5645f661402694599" +dependencies = [ + "libc", + "log", + "memchr", + "page_size", + "smallvec", + "users", + "zerocopy", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -253,9 +274,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", @@ -274,9 +295,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -319,9 +340,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -339,21 +360,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -370,6 +385,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -378,9 +399,19 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "page_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "password-hash" @@ -440,36 +471,36 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -478,9 +509,19 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rpassword" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "rustcryptfs" @@ -490,11 +531,24 @@ dependencies = [ "clap", "env_logger", "log", + "rpassword", "rustcryptfs-lib", + "rustcryptfs-mount", "serde", "serde_json", ] +[[package]] +name = "rustcryptfs-fuse" +version = "0.1.0" +dependencies = [ + "fuser", + "libc", + "log", + "rustcryptfs-lib", + "thiserror", +] + [[package]] name = "rustcryptfs-lib" version = "0.1.0" @@ -512,11 +566,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rustcryptfs-mount" +version = "0.1.0" +dependencies = [ + "rustcryptfs-fuse", +] + [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "salsa20" @@ -542,18 +603,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -562,9 +623,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -573,15 +634,21 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "strsim" version = "0.10.0" @@ -596,15 +663,27 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -616,24 +695,24 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -648,9 +727,15 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" @@ -662,6 +747,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "version_check" version = "0.9.4" @@ -670,9 +765,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" @@ -704,3 +799,24 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zerocopy" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index b17d160..4edd13b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,7 @@ [workspace] members = [ "rustcryptfs", - "rustcryptfs-lib" + "rustcryptfs-lib", + "rustcryptfs-fuse", + "rustcryptfs-mount" ] diff --git a/README.md b/README.md index 7271f11..61ef5ff 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ # RustCryptFS +An implementation of [gocryptfs](https://github.com/rfjakob/gocryptfs) in Rust. + +## Supported plaforms and features +- [x] Linux (via FUSE) + - [x] read + - [ ] write +- [ ] Windows + - [ ] read + - [ ] write \ No newline at end of file diff --git a/rustcryptfs-fuse/Cargo.toml b/rustcryptfs-fuse/Cargo.toml new file mode 100644 index 0000000..08e7cd0 --- /dev/null +++ b/rustcryptfs-fuse/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rustcryptfs-fuse" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fuser = { version = "0.11", default-features = false } +log = "0.4" +rustcryptfs-lib = { path = "../rustcryptfs-lib" } +thiserror = "1.0" +libc = "0.2" \ No newline at end of file diff --git a/rustcryptfs-fuse/src/encrypted_filesystem.rs b/rustcryptfs-fuse/src/encrypted_filesystem.rs new file mode 100644 index 0000000..7f5f5e1 --- /dev/null +++ b/rustcryptfs-fuse/src/encrypted_filesystem.rs @@ -0,0 +1,382 @@ +use std::{ + collections::BTreeMap, + ffi::OsStr, + fs::{File, FileType as StdFileType}, + io::{Error as IoError, Read, Result as IoResult, Seek, SeekFrom}, + ops::Add, + os::unix::prelude::{FileTypeExt, MetadataExt, OsStrExt, PermissionsExt}, + path::{Path, PathBuf}, + time::{Duration, UNIX_EPOCH}, +}; + +use fuser::{FileAttr, FileType, Filesystem, FUSE_ROOT_ID}; +use rustcryptfs_lib::{content::ContentEnc, GocryptFs}; + +use crate::{ + error::{ErrorExt, Result}, + inode_cache::{InodeCache, InodeCacheExt}, +}; + +trait OptionExt { + fn enoent(self) -> IoResult; +} + +impl OptionExt for Option { + fn enoent(self) -> IoResult { + match self { + Some(r) => Ok(r), + None => Err(IoError::from_raw_os_error(libc::ENOENT)), + } + } +} + +const BLOCK_SIZE: u64 = 4096; + +pub struct EncryptedFs { + fs: GocryptFs, + inode_cache: InodeCache, +} + +impl EncryptedFs { + pub fn new

(path: P, password: &str) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + + log::info!("Opening dir ..."); + let fs = GocryptFs::open(path, password)?; + + println!("Filesystem mounted and ready."); + + let mut inode_cache = BTreeMap::new(); + inode_cache.insert(FUSE_ROOT_ID, path.to_path_buf()); + + Ok(Self { fs, inode_cache }) + } + + pub fn mount

(self, mountpoint: P) -> std::io::Result<()> + where + P: AsRef, + { + fuser::mount2(self, mountpoint, &[]) + } + + fn get_file_type(file_type: StdFileType) -> FileType { + if file_type.is_file() { + FileType::RegularFile + } else if file_type.is_dir() { + FileType::Directory + } else if file_type.is_symlink() { + FileType::Symlink + } else if file_type.is_socket() { + FileType::Socket + } else if file_type.is_char_device() { + FileType::CharDevice + } else if file_type.is_block_device() { + FileType::BlockDevice + } else if file_type.is_fifo() { + FileType::NamedPipe + } else { + unimplemented!() + } + } + + fn get_path(&self, ino: u64) -> Option<&PathBuf> { + self.inode_cache.get_path(ino) + } + + fn get_attr

(path: P, ino: u64) -> std::io::Result + where + P: AsRef, + { + let meta = std::fs::metadata(&path)?; + + let file_type = Self::get_file_type(meta.file_type()); + + let file_size = if meta.is_file() { + ContentEnc::get_real_size(meta.size()) + } else { + meta.size() + }; + + Ok(FileAttr { + ino, + size: file_size, + blocks: (file_size + BLOCK_SIZE - 1) / BLOCK_SIZE, + atime: meta.accessed()?, + mtime: meta.modified()?, + ctime: UNIX_EPOCH.add(Duration::new(meta.ctime() as u64, 0)), + crtime: UNIX_EPOCH.add(Duration::new(meta.ctime() as u64, 0)), + kind: file_type, + perm: meta.permissions().mode() as u16, + nlink: meta.nlink() as u32, + uid: meta.uid(), + gid: meta.gid(), + rdev: 0, + blksize: BLOCK_SIZE as u32, + flags: 0, + }) + } + + fn lookup_impl( + &mut self, + parent: u64, + name: &std::ffi::OsStr, + ) -> rustcryptfs_lib::error::Result<(Duration, FileAttr, u64)> { + let parent = self.get_path(parent).enoent()?; + let iv = std::fs::read(parent.join("gocryptfs.diriv"))?; + let dir_decoder = self.fs.filename_decoder().get_cipher_for_dir(&iv); + + let encrypted_name = dir_decoder.encrypt_filename(&name.to_string_lossy())?; + + let encrypted_name = match &encrypted_name { + rustcryptfs_lib::filename::EncodedFilename::ShortFilename(s) => s, + rustcryptfs_lib::filename::EncodedFilename::LongFilename(l) => l.filename(), + }; + + let file_path = parent.join(encrypted_name); + + if file_path.exists() { + let (ino, file_path) = self.inode_cache.get_or_insert_inode(file_path); + + Ok((Duration::new(0, 0), Self::get_attr(file_path, ino)?, 0)) + } else { + Err(IoError::from_raw_os_error(libc::ENOENT).into()) + } + } + + fn read_dir_impl( + &mut self, + ino: u64, + offset: i64, + reply: &mut fuser::ReplyDirectory, + ) -> rustcryptfs_lib::error::Result<()> { + let folder_path = &self.inode_cache.get_path(ino).enoent()?.clone(); + let iv = std::fs::read(folder_path.join("gocryptfs.diriv"))?; + + let dir_decoder = self.fs.filename_decoder().get_cipher_for_dir(&iv); + + if offset == 0 { + let ino_parent = if ino == FUSE_ROOT_ID { + FUSE_ROOT_ID + } else { + let parent = folder_path.parent().enoent()?; + self.inode_cache + .iter() + .find_map(|(ino, p)| if p == parent { Some(*ino) } else { None }) + .enoent()? + }; + + if !reply.add(ino, 1, FileType::Directory, ".") { + if reply.add(ino_parent, 2, FileType::Directory, "..") { + return Ok(()); + } + } else { + return Ok(()); + } + } + + for (index, (meta, encrypted_name, name)) in std::fs::read_dir(folder_path)? + .flat_map(|e| e.ok()) + .flat_map(|dir| match extract_name(&dir, folder_path, &dir_decoder) { + Ok(v) => v, + Err(e) => { + log::error!( + "Failed to extract name of entry {:?} : {}", + dir.file_name(), + e + ); + None + } + }) + .skip(offset as usize) + .enumerate() + { + let (inode, _) = self + .inode_cache + .get_or_insert_inode(folder_path.join(&encrypted_name)); + + let file_type = Self::get_file_type(meta.file_type()); + + let buffer_full: bool = reply.add( + inode, + offset + index as i64 + 1 + 2, + file_type, + OsStr::from_bytes(name.as_bytes()), + ); + + if buffer_full { + break; + } + } + Ok(()) + } + + fn read_impl( + &mut self, + ino: u64, + offset: i64, + size: usize, + ) -> rustcryptfs_lib::error::Result> { + let file_path = self.get_path(ino).enoent()?; + let mut file = File::open(file_path)?; + let decoder = self.fs.content_decoder(); + + let mut buf = [0u8; 18]; + let n = file.read(&mut buf)?; + let id = if n < 18 { None } else { Some(&buf[2..]) }; + + let mut block_index = offset as u64 / 4096; + + let mut buffer = Vec::with_capacity(size); + + let mut rem = size; + + let mut buf = [0u8; 4096 + 32]; + + file.seek(SeekFrom::Start(18 + block_index * (4096 + 32)))?; + + { + let n = file.read(&mut buf)?; + + let res = decoder.decrypt_block(&buf[..n], block_index, id)?; + + let seek = (offset as u64 - block_index * 4096) as usize; + buffer.extend_from_slice(&res[seek..]); + + block_index += 1; + + rem -= res.len() - seek; + } + + while rem > 0 { + let n = file.read(&mut buf)?; + + if n == 0 { + break; + } + + let res = decoder.decrypt_block(&buf[..n], block_index, id)?; + + let size = res.len().min(rem); + + buffer.extend_from_slice(&res[..size]); + + block_index += 1; + + rem -= size; + } + Ok(buffer) + } +} + +impl Filesystem for EncryptedFs { + fn access( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _mask: i32, + reply: fuser::ReplyEmpty, + ) { + if let Some(_path) = self.get_path(ino) { + reply.ok() + } else { + reply.error(libc::ENOENT) + } + } + + fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + if let Some(path) = self.get_path(ino) { + match Self::get_attr(path, ino) { + Ok(attr) => reply.attr(&Duration::new(0, 0), &attr), + Err(e) => reply.error(e.raw_os_error().unwrap()), + } + } else { + reply.error(libc::ENOENT) + } + } + + fn lookup( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + match self.lookup_impl(parent, name) { + Ok((ttl, attr, generation)) => reply.entry(&ttl, &attr, generation), + Err(e) => { + log::debug!("error on lookup : {}", e); + reply.error(e.to_raw_code()) + } + } + } + + fn readdir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: u64, + offset: i64, + mut reply: fuser::ReplyDirectory, + ) { + match self.read_dir_impl(ino, offset, &mut reply) { + Ok(()) => reply.ok(), + Err(e) => { + log::debug!("error on readdir : {}", e); + reply.error(e.to_raw_code()) + } + } + } + + fn read( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: fuser::ReplyData, + ) { + match self.read_impl(ino, offset, size as usize) { + Ok(data) => reply.data(&data), + Err(e) => { + log::error!("read : {}", e); + reply.error(e.to_raw_code()) + } + } + } +} + +fn extract_name( + dir: &std::fs::DirEntry, + folder_path: &Path, + dir_decoder: &rustcryptfs_lib::filename::DirFilenameCipher, +) -> rustcryptfs_lib::error::Result> { + let filename = dir.file_name(); + let filename = filename.to_string_lossy(); + if filename != "gocryptfs.conf" && filename != "gocryptfs.diriv" { + if filename.starts_with("gocryptfs.longname.") { + if !filename.ends_with(".name") { + let filename = + std::fs::read_to_string(folder_path.join(format!("{}.name", filename)))?; + let decrypted_filename = dir_decoder.decode_filename(filename.as_str())?; + Ok(Some((dir.metadata()?, filename, decrypted_filename))) + } else { + Ok(None) + } + } else { + let decrypted_filename = dir_decoder.decode_filename(&*filename)?; + Ok(Some(( + dir.metadata()?, + filename.to_string(), + decrypted_filename, + ))) + } + } else { + Ok(None) + } +} diff --git a/rustcryptfs-fuse/src/error.rs b/rustcryptfs-fuse/src/error.rs new file mode 100644 index 0000000..13b06cf --- /dev/null +++ b/rustcryptfs-fuse/src/error.rs @@ -0,0 +1,33 @@ +use rustcryptfs_lib::filename::FilenameCipherError; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + RustCryptFsError(#[from] rustcryptfs_lib::error::Error), + + #[error(transparent)] + RustCryptFsFilenameError(#[from] FilenameCipherError), +} + + +pub(crate) trait ErrorExt { + fn to_raw_code(&self) -> i32; +} + +impl ErrorExt for rustcryptfs_lib::error::Error { + fn to_raw_code(&self) -> i32 { + match self { + rustcryptfs_lib::error::Error::FilenameCipherError(_) => libc::EIO, + rustcryptfs_lib::error::Error::ContentCipherError(_) => libc::EIO, + rustcryptfs_lib::error::Error::ConfigError(_) => todo!(), + rustcryptfs_lib::error::Error::JsonError(_) => todo!(), + rustcryptfs_lib::error::Error::IoError(e) => e.raw_os_error().unwrap(), + } + } +} \ No newline at end of file diff --git a/rustcryptfs-fuse/src/inode_cache.rs b/rustcryptfs-fuse/src/inode_cache.rs new file mode 100644 index 0000000..be91d95 --- /dev/null +++ b/rustcryptfs-fuse/src/inode_cache.rs @@ -0,0 +1,31 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +pub(crate) type InodeCache = BTreeMap; + +pub(crate) trait InodeCacheExt { + fn get_or_insert_inode(&mut self, file_path: PathBuf) -> (u64, PathBuf); + + fn get_path(&self, ino: u64) -> Option<&PathBuf>; +} + +impl InodeCacheExt for InodeCache { + // TODO Try to avoid clone + fn get_or_insert_inode(&mut self, file_path: PathBuf) -> (u64, PathBuf) { + if let Some((ino, path)) = + self.iter() + .find_map(|(i, p)| if p.eq(&file_path) { Some((i, p)) } else { None }) + { + (*ino, path.clone()) + } else { + let ino = self.len() as u64 + 1; + self.insert(ino, file_path); + + (ino, self.get(&ino).unwrap().clone()) + } + } + + fn get_path(&self, ino: u64) -> Option<&PathBuf> { + // TODO CHECK PERM + self.get(&ino) + } +} diff --git a/rustcryptfs-fuse/src/lib.rs b/rustcryptfs-fuse/src/lib.rs new file mode 100644 index 0000000..8e1278d --- /dev/null +++ b/rustcryptfs-fuse/src/lib.rs @@ -0,0 +1,6 @@ +mod encrypted_filesystem; +mod inode_cache; + +pub mod error; + +pub use encrypted_filesystem::EncryptedFs; \ No newline at end of file diff --git a/rustcryptfs-lib/src/config/error.rs b/rustcryptfs-lib/src/config/error.rs new file mode 100644 index 0000000..17fdadc --- /dev/null +++ b/rustcryptfs-lib/src/config/error.rs @@ -0,0 +1,29 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error(transparent)] + ScryptError(#[from] ScryptError), + #[error(transparent)] + HdkfError(#[from] hkdf::InvalidLength), + #[error("Failed to decode base64")] + Base64Error(#[from] base64::DecodeError), + #[error("Failed to decrypt master key")] + MasterKeyDecryptError(), + #[error("Invalid master key length")] + InvalidMasterKeyLengthError(), +} + +impl From for ConfigError { + fn from(_: aes_gcm::Error) -> Self { + Self::MasterKeyDecryptError() + } +} + +#[derive(Debug, Error)] +pub enum ScryptError { + #[error(transparent)] + InvalidParams(#[from] scrypt::errors::InvalidParams), + #[error(transparent)] + InvalidOutputLen(#[from] scrypt::errors::InvalidOutputLen), +} diff --git a/rustcryptfs-lib/src/config.rs b/rustcryptfs-lib/src/config/mod.rs similarity index 88% rename from rustcryptfs-lib/src/config.rs rename to rustcryptfs-lib/src/config/mod.rs index 97b2f2e..5be2d69 100644 --- a/rustcryptfs-lib/src/config.rs +++ b/rustcryptfs-lib/src/config/mod.rs @@ -9,7 +9,9 @@ use aes_gcm::{ }; use hkdf::Hkdf; -use crate::error::{FilenameDecryptError, Result, ScryptError}; +mod error; + +pub use error::*; /// An enum that contain all the feature flag a gocryptfs config can have. #[derive(serde::Deserialize, Debug, PartialEq, Eq, Hash)] @@ -69,7 +71,7 @@ impl CryptConf { /// See gocryptfs documentation about [master key](https://nuetzlich.net/gocryptfs/forward_mode_crypto/#master-key-storage). /// /// ![TODO NAME THIS IMAGE](https://nuetzlich.net/gocryptfs/img/master-key.svg) - pub fn get_master_key(&self, password: &[u8]) -> Result<[u8; 32]> { + pub fn get_master_key(&self, password: &[u8]) -> Result<[u8; 32], ConfigError> { let block = base64::decode(&self.encrypted_key)?; let key = self.scrypt_object.get_hkdf_key(password)?; @@ -77,12 +79,14 @@ impl CryptConf { let tag = &block[block.len() - 16..]; let ciphertext = &block[16..block.len() - 16]; - let mut buf: [u8; 32] = ciphertext.try_into().expect("TODO"); + let mut buf: [u8; 32] = ciphertext + .try_into() + .map_err(|_| ConfigError::InvalidMasterKeyLengthError())?; let aes = AesGcm::::new(Key::from_slice(&key)); aes.decrypt_in_place_detached( - GenericArray::from_slice(&nonce), + GenericArray::from_slice(nonce), &[0u8, 0, 0, 0, 0, 0, 0, 0], &mut buf, GenericArray::from_slice(tag), @@ -133,19 +137,14 @@ pub struct ScryptObject { } impl ScryptObject { - fn get_hkdf_key(&self, password: &[u8]) -> std::result::Result, FilenameDecryptError> { + fn get_hkdf_key(&self, password: &[u8]) -> Result, ConfigError> { let mut key = [0u8; 32]; let params = scrypt::Params::new((self.n as f64).log2() as u8, self.r, self.p) - .map_err(|e| ScryptError::from(e))?; + .map_err(ScryptError::from)?; - scrypt::scrypt( - password, - &base64::decode(&self.salt).unwrap(), - ¶ms, - &mut key, - ) - .map_err(|e| ScryptError::from(e))?; + scrypt::scrypt(password, &base64::decode(&self.salt)?, ¶ms, &mut key) + .map_err(ScryptError::from)?; let hdkf = Hkdf::::new(None, &key); hdkf.expand(b"AES-GCM file content encryption", &mut key)?; diff --git a/rustcryptfs-lib/src/content/error.rs b/rustcryptfs-lib/src/content/error.rs new file mode 100644 index 0000000..74bdb54 --- /dev/null +++ b/rustcryptfs-lib/src/content/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ContentCipherError { + #[error("Block is too short")] + BlockTooShort(), + #[error("all-zero nonce")] + AllZeroNonce(), + #[error("Failed to decrypt content")] + ContentDecryptError(), + #[error(transparent)] + HdkfError(#[from] hkdf::InvalidLength), +} + +impl From for ContentCipherError { + fn from(_: aes_gcm::Error) -> Self { + Self::ContentDecryptError() + } +} diff --git a/rustcryptfs-lib/src/content/mod.rs b/rustcryptfs-lib/src/content/mod.rs new file mode 100644 index 0000000..f5d1811 --- /dev/null +++ b/rustcryptfs-lib/src/content/mod.rs @@ -0,0 +1,108 @@ +//! Utilities for file encryption. + +use aes_gcm::{aead::generic_array::GenericArray, aes::Aes256, AeadInPlace, AesGcm, NewAead}; +use cipher::consts::{U16, U32}; +use hkdf::Hkdf; + +mod error; + +pub use error::*; + +type Aes256Gcm = AesGcm; + +/// ContentEnc implement all methods related to file encryption. +pub struct ContentEnc { + key: GenericArray, + iv_len: usize, +} + +impl ContentEnc { + /// Init a new ContentEnc from the master key and the iv len. + pub fn new(master_key: &[u8], iv_len: u8) -> Result { + let mut key = [0u8; 32]; + let hdkf = Hkdf::::new(None, master_key); + hdkf.expand(b"AES-GCM file content encryption", &mut key)?; + + Ok(Self { + key: GenericArray::from(key), + iv_len: iv_len as usize, + }) + } + + /// Decrypt a encrypted block of len (iv_len + decrypted_block_size + iv_len), with the block number and the file id. + pub fn decrypt_block( + &self, + block: &[u8], + block_number: u64, + file_id: Option<&[u8]>, + ) -> Result, ContentCipherError> { + // TODO NOT BOX + if block.is_empty() { + return Ok(block.into()); + } + + if block.iter().all(|f| *f == 0) { + todo!("black hole") + } + + if block.len() < self.iv_len { + return Err(ContentCipherError::BlockTooShort()); + } + + let nonce = &block[..self.iv_len]; + let tag = &block[block.len() - self.iv_len..]; + let ciphertext = &block[self.iv_len..block.len() - self.iv_len]; + + if nonce.iter().all(|f| *f == 0) { + return Err(ContentCipherError::AllZeroNonce()); + } + + let mut buf = Vec::from(ciphertext); + + let mut aad = Vec::from(block_number.to_be_bytes()); + if let Some(file_id) = file_id { + aad.extend(file_id); + } + + let aes = Aes256Gcm::new(&self.key); + + aes.decrypt_in_place_detached( + GenericArray::from_slice(nonce), + &aad, + &mut buf, + GenericArray::from_slice(tag), + )?; + + Ok(buf.to_vec()) + } + + /// Return the decrypted size of a file, based on the encrypted size. + pub fn get_real_size(encrypted_size: u64) -> u64 { + if encrypted_size == 0 { + 0 + } else { + let x = (encrypted_size - 50) / 4128; + + let y = (encrypted_size - 50) - x * 4128; + x * 4096 + y + } + } +} + +#[cfg(test)] +mod test { + use super::ContentEnc; + + #[test] + fn test_get_real_size() { + assert_eq!(0, ContentEnc::get_real_size(0)); + + for real_size in 1..4096 * 4 + 1 { + let nbr_full_blocks = real_size / 4096; + let encrypted_size = + 18 + nbr_full_blocks * (4096 + 32) + real_size - nbr_full_blocks * 4096 + 32; + + assert_eq!(real_size, ContentEnc::get_real_size(encrypted_size)); + } + } +} diff --git a/rustcryptfs-lib/src/content_enc.rs b/rustcryptfs-lib/src/content_enc.rs deleted file mode 100644 index ed94ff3..0000000 --- a/rustcryptfs-lib/src/content_enc.rs +++ /dev/null @@ -1,72 +0,0 @@ -use aes_gcm::{aead::generic_array::GenericArray, aes::Aes256, AeadInPlace, AesGcm, NewAead}; -use cipher::consts::{U16, U32}; -use hkdf::Hkdf; - -use crate::error::{Result, DecryptError}; - -type Aes256Gcm = AesGcm; - -pub struct ContentEnc { - key: GenericArray, - iv_len: usize, -} - -impl ContentEnc { - pub fn new(master_key: &[u8], iv_len: u8) -> Self { - let mut key = [0u8; 32]; - let hdkf = Hkdf::::new(None, &master_key); - hdkf.expand(b"AES-GCM file content encryption", &mut key) - .unwrap(); - - Self { - key: GenericArray::from(key), - iv_len: iv_len as usize, - } - } - - pub fn decrypt_block( - &self, - block: &[u8], - block_number: u64, - file_id: Option<&[u8]>, - ) -> Result> { - // TODO NOT BOX - if block.len() == 0 { - return Ok(block.into()); - } - - if block.iter().all(|f| *f == 0) { - todo!("black hole") - } - - if block.len() < self.iv_len { - return Err(DecryptError::BlockTooShort().into()); - } - - let nonce = &block[..self.iv_len]; - let tag = &block[block.len() - self.iv_len..]; - let ciphertext = &block[self.iv_len..block.len() - self.iv_len]; - - if nonce.iter().all(|f| *f == 0) { - return Err(DecryptError::AllZeroNonce().into()); - } - - let mut buf = Vec::from(ciphertext); - - let mut aad = Vec::from(block_number.to_be_bytes()); - if let Some(file_id) = file_id { - aad.extend(file_id); - } - - let aes = Aes256Gcm::new(&self.key); - - aes.decrypt_in_place_detached( - GenericArray::from_slice(nonce), - &aad, - &mut buf, - GenericArray::from_slice(tag), - )?; - - return Ok(buf.to_vec()); - } -} diff --git a/rustcryptfs-lib/src/error.rs b/rustcryptfs-lib/src/error.rs index 582dcec..fa68606 100644 --- a/rustcryptfs-lib/src/error.rs +++ b/rustcryptfs-lib/src/error.rs @@ -1,49 +1,20 @@ use thiserror::Error; +use crate::{config::ConfigError, content::ContentCipherError, filename::FilenameCipherError}; + pub type Result = std::result::Result; +/// An error that wrap all the errors in this lib. #[derive(Debug, Error)] pub enum Error { - #[error("Failed to decrypt content")] - ContentDecryptError(), - #[error("Failed to decrypt filename")] - FilenameDecryptError(#[from] FilenameDecryptError), - #[error("Failed to decode base64")] - Base64Error(#[from] base64::DecodeError), #[error(transparent)] - DecodeError(#[from] DecryptError), -} - -impl From for Error { - fn from(_: aes_gcm::Error) -> Self { - Self::ContentDecryptError() - } -} - -#[derive(Debug, Error)] -pub enum DecryptError { - #[error("Block is too short")] - BlockTooShort(), - #[error("all-zero nonce")] - AllZeroNonce(), -} - -#[derive(Debug, Error)] -pub enum FilenameDecryptError { + FilenameCipherError(#[from] FilenameCipherError), #[error(transparent)] - ScryptError(#[from] ScryptError), - #[error("Failed to decode base64")] - Base64Error(#[from] base64::DecodeError), + ContentCipherError(#[from] ContentCipherError), #[error(transparent)] - HdkfError(#[from] hkdf::InvalidLength), - #[error("Failed to decrypt filename")] - DecryptError(), -} - -#[derive(Debug, Error)] -pub enum ScryptError { - #[error(transparent)] - InvalidParams(#[from] scrypt::errors::InvalidParams), - #[error(transparent)] - InvalidOutputLen(#[from] scrypt::errors::InvalidOutputLen), + ConfigError(#[from] ConfigError), + #[error(transparent)] + JsonError(#[from] serde_json::Error), + #[error(transparent)] + IoError(#[from] std::io::Error), } diff --git a/rustcryptfs-lib/src/filename/dir_filename_cipher.rs b/rustcryptfs-lib/src/filename/dir_filename_cipher.rs new file mode 100644 index 0000000..26db9ab --- /dev/null +++ b/rustcryptfs-lib/src/filename/dir_filename_cipher.rs @@ -0,0 +1,51 @@ +use cipher::{block_padding::Pkcs7, Iv, Key, KeyIvInit}; + +use super::{EmeCipher, EncodedFilename, FilenameCipherError, IntoDecodable}; + +/// DirFilenameCipher allow you to cipher and decipher filenames in a directory. +/// +/// TODO : document structure of a gocryptfs dir or put a link. +pub struct DirFilenameCipher<'a, 'b> { + filename_key: &'a Key, + iv: &'b Iv, +} + +impl<'a, 'b> DirFilenameCipher<'a, 'b> { + pub fn new(filename_key: &'a Key, iv: &'b Iv) -> Self { + Self { filename_key, iv } + } + + /// Decipher a filename. + /// + /// Name muste be the name of the file if it is a short filename, or the content of the long .name file otherwise. + pub fn decode_filename(&self, name: S) -> Result + where + S: IntoDecodable, + { + let cipher = EmeCipher::new(self.filename_key, self.iv); + + let mut filename = base64::decode_config(name.to_decodable(), base64::URL_SAFE_NO_PAD)?; + let filename_decoded = cipher + .decrypt_padded_mut::(&mut filename) + .map_err(|_| FilenameCipherError::DecryptError())?; + + Ok(String::from_utf8_lossy(filename_decoded).to_string()) + } + + /// Cipher a filename. + pub fn encrypt_filename( + &self, + plain_text_name: &str, + ) -> Result { + let mut cipher = EmeCipher::new(self.filename_key, self.iv); + let mut res = [0u8; 2048]; + + let filename_encrypted = cipher + .encrypt_padded_b2b_mut::(plain_text_name.as_bytes(), &mut res) + .map_err(|_| FilenameCipherError::EncryptError())?; + + let filename = base64::encode_config(filename_encrypted, base64::URL_SAFE_NO_PAD); + + Ok(filename.into()) + } +} diff --git a/rustcryptfs-lib/src/filename/error.rs b/rustcryptfs-lib/src/filename/error.rs new file mode 100644 index 0000000..00052e2 --- /dev/null +++ b/rustcryptfs-lib/src/filename/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum FilenameCipherError { + #[error("Failed to decode base64")] + Base64Error(#[from] base64::DecodeError), + #[error(transparent)] + HdkfError(#[from] hkdf::InvalidLength), + #[error("Failed to decrypt filename")] + DecryptError(), + #[error("Failed to encrypt filename")] + EncryptError() +} diff --git a/rustcryptfs-lib/src/filename/filename_encoded.rs b/rustcryptfs-lib/src/filename/filename_encoded.rs index 2edc65e..a708122 100644 --- a/rustcryptfs-lib/src/filename/filename_encoded.rs +++ b/rustcryptfs-lib/src/filename/filename_encoded.rs @@ -1,49 +1,44 @@ -use std::path::Path; +use sha2::{Digest, Sha256}; -/// EncodedFilename -#[derive(Debug, PartialEq)] +/// Represent an encrypted filename. +/// +/// An encrypted filename can have two forms : long or short. +/// TODO: Document +#[derive(Debug, PartialEq, Eq)] pub enum EncodedFilename { ShortFilename(String), LongFilename(LongFilename), } -impl EncodedFilename { - fn new

(file: P) -> crate::error::Result - where - P: AsRef, - { - let path = file.as_ref(); - - let filename = path.file_name().unwrap().to_str().expect("Failed to get filename"); - - if filename.starts_with("gocryptfs.longname.") { - if !filename.ends_with(".name") { - let long = std::fs::read_to_string( - path.parent().unwrap().join(format!("{}.name", filename)), - ).unwrap(); - Ok(EncodedFilename::LongFilename(LongFilename { - filename: filename.to_string(), - filename_content: long, - })) - } else { - panic!() - } - } else { - Ok(EncodedFilename::ShortFilename(filename.to_string())) - } - } +#[derive(Debug, PartialEq, Eq)] +pub struct LongFilename { + filename: String, + filename_content: String, } -#[derive(Debug, PartialEq)] -pub struct LongFilename { - pub filename: String, - pub filename_content: String, +impl LongFilename { + pub fn filename(&self) -> &str { + self.filename.as_ref() + } + + pub fn filename_content(&self) -> &str { + self.filename_content.as_ref() + } } impl From for EncodedFilename { fn from(filename: String) -> Self { if filename.len() > 255 { - unimplemented!() + let mut hasher = Sha256::new(); + hasher.update(filename.as_bytes()); + + Self::LongFilename(LongFilename { + filename: format!( + "gocryptfs.longname.{}", + base64::encode_config(hasher.finalize(), base64::URL_SAFE_NO_PAD) + ), + filename_content: filename, + }) } else { Self::ShortFilename(filename) } @@ -51,11 +46,11 @@ impl From for EncodedFilename { } pub trait IntoDecodable { - fn to_decodable<'s>(&'s self) -> &'s str; + fn to_decodable(&self) -> &str; } impl IntoDecodable for EncodedFilename { - fn to_decodable<'s>(&'s self) -> &'s str { + fn to_decodable(&self) -> &str { match self { Self::ShortFilename(s) => s.as_str(), Self::LongFilename(l) => l.filename_content.as_str(), @@ -64,13 +59,13 @@ impl IntoDecodable for EncodedFilename { } impl IntoDecodable for String { - fn to_decodable<'s>(&'s self) -> &'s str { + fn to_decodable(&self) -> &str { self } } impl<'a> IntoDecodable for &'a str { - fn to_decodable<'s>(&'s self) -> &'s str { + fn to_decodable(&self) -> &str { self } } diff --git a/rustcryptfs-lib/src/filename/mod.rs b/rustcryptfs-lib/src/filename/mod.rs index 3492a13..3adcab8 100644 --- a/rustcryptfs-lib/src/filename/mod.rs +++ b/rustcryptfs-lib/src/filename/mod.rs @@ -1,25 +1,30 @@ +//! Utilities for filename encryption. +//! use aes::Aes256; -use cipher::{block_padding::Pkcs7, inout::InOutBufReserved, Iv, Key, KeyIvInit}; +use cipher::{Iv, Key}; use eme_mode::DynamicEme; use hkdf::Hkdf; -use crate::error::FilenameDecryptError; - pub(crate) type EmeCipher = DynamicEme; +mod dir_filename_cipher; +mod error; mod filename_encoded; +pub use dir_filename_cipher::*; +pub use error::*; pub use filename_encoded::*; -// TODO RENAME -pub struct FilenameDecoder { +/// FilenameCipher allow you to retrieve a DirFilenameCipher, used to cipher and decipher filenames. +pub struct FilenameCipher { filename_key: Key, } -impl FilenameDecoder { - pub fn new(master_key: &[u8]) -> Result { +impl FilenameCipher { + /// Create a new FilenameCipher, from the master key. + pub fn new(master_key: &[u8]) -> Result { let mut key = [0u8; 32]; - let hdkf = Hkdf::::new(None, &master_key); + let hdkf = Hkdf::::new(None, master_key); hdkf.expand(b"EME filename encryption", &mut key)?; Ok(Self { @@ -27,54 +32,10 @@ impl FilenameDecoder { }) } - pub fn get_decoder_for_dir<'a, 'b>(&'a self, iv: &'b [u8]) -> DirFilenameDecoder<'a, 'b> { + /// Get the cipher for a directory, allowing you to decipher files in this dir. + pub fn get_cipher_for_dir<'a, 'b>(&'a self, iv: &'b [u8]) -> DirFilenameCipher<'a, 'b> { let iv = Iv::::from_slice(iv); - DirFilenameDecoder { - filename_key: &self.filename_key, - iv, - } - } -} - -// TODO RENAME -pub struct DirFilenameDecoder<'a, 'b> { - filename_key: &'a Key, - iv: &'b Iv, -} - -impl<'a, 'b> DirFilenameDecoder<'a, 'b> { - pub fn decode_filename(&self, name: S) -> Result - where - S: IntoDecodable, - { - let cipher = EmeCipher::new(self.filename_key, self.iv); - - let mut filename = base64::decode_config(name.to_decodable(), base64::URL_SAFE_NO_PAD)?; - let filename_decoded = cipher - .decrypt_padded_mut::(&mut filename) - .map_err(|_| FilenameDecryptError::DecryptError())?; - - Ok(String::from_utf8_lossy(filename_decoded).to_string()) - } - - pub fn encrypt_filename( - &self, - plain_text_name: &str, - ) -> Result { - let mut cipher = EmeCipher::new(self.filename_key, self.iv); - let mut res = [0u8; 2048]; - - let filename_encrypted = cipher - .encrypt_padded_inout_mut::( - InOutBufReserved::from_slices(plain_text_name.as_bytes(), &mut res).unwrap(), - ) - .map_err(|_| FilenameDecryptError::DecryptError())?; // TODO RENAME ERROR - - // TODO LONG FILENAME - - let filename = base64::encode_config(filename_encrypted, base64::URL_SAFE_NO_PAD); - - Ok(filename.into()) + DirFilenameCipher::new(&self.filename_key, iv) } } @@ -82,15 +43,15 @@ impl<'a, 'b> DirFilenameDecoder<'a, 'b> { mod test { use crate::filename::EncodedFilename; - use super::FilenameDecoder; + use super::FilenameCipher; #[test] fn test_encrypt() { let master_key = base64::decode("9gtUW9XiiefEgEXEkbONI6rnUsd2yh5UZZLG0V8Bxgk=").unwrap(); let dir_iv = base64::decode("6ysCeWOp2euF1x39gth8KQ==").unwrap(); - let decoder = FilenameDecoder::new(&master_key).expect("Failed to get file decoder"); - let dir_decoder = decoder.get_decoder_for_dir(&dir_iv); + let decoder = FilenameCipher::new(&master_key).expect("Failed to get file decoder"); + let dir_decoder = decoder.get_cipher_for_dir(&dir_iv); let encoded = dir_decoder .encrypt_filename("7.mp4") @@ -107,8 +68,8 @@ mod test { let master_key = base64::decode("9gtUW9XiiefEgEXEkbONI6rnUsd2yh5UZZLG0V8Bxgk=").unwrap(); let dir_iv = base64::decode("6ysCeWOp2euF1x39gth8KQ==").unwrap(); - let decoder = FilenameDecoder::new(&master_key).expect("Failed to get file decoder"); - let dir_decoder = decoder.get_decoder_for_dir(&dir_iv); + let decoder = FilenameCipher::new(&master_key).expect("Failed to get file decoder"); + let dir_decoder = decoder.get_cipher_for_dir(&dir_iv); let decrypted = dir_decoder .decode_filename("vTBajRt-yCpxB7Sly0E7lQ") diff --git a/rustcryptfs-lib/src/io/mod.rs b/rustcryptfs-lib/src/io/mod.rs deleted file mode 100644 index 5852394..0000000 --- a/rustcryptfs-lib/src/io/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::{ - error::Error, - fs::File, - io::Read, - path::{Path, PathBuf}, -}; - -use crate::filename::{EncodedFilename, FilenameDecoder}; - -#[derive(Debug)] -pub struct DirCache { - filename: String, - dir_iv: [u8; 16], - dir_entries: Vec, -} - -impl DirCache { - pub fn load_from_path

(path: P) -> Self - where - P: AsRef, - { - let path = path.as_ref(); - - let mut dir_iv = [0u8; 16]; - { - let dir_iv_path = path.join("gocryptfs.diriv"); - - let mut file = File::open(dir_iv_path).unwrap(); - - file.read_exact(&mut dir_iv).unwrap(); - } - - let dir_entries = path - .read_dir() - .unwrap() - .filter_map(|f| { - if let Ok(entry) = f { - if entry.file_name() != "gocryptfs.conf" - && entry.file_name() != "gocryptfs.diriv" - { - Some(entry) - } else { - None - } - } else { - None - } - }) - .map(|f| DirEntry::try_from(f.path().as_path())) - .filter_map(|f| f.ok()) - .collect(); - - Self { - filename: path.to_string_lossy().to_string(), - dir_iv, - dir_entries, - } - } - - pub fn lookup

( - &self, - filename_decoder: &FilenameDecoder, - decrypted_path: P, - ) -> Option - where - P: AsRef, - { - let decrypted_path = decrypted_path.as_ref(); - - let mut components = decrypted_path.components(); - - let component = components.next().expect("lol"); - - let decoder = filename_decoder.get_decoder_for_dir(&self.dir_iv); - - let segment = decoder - .encrypt_filename(component.as_os_str().to_str().unwrap()) - .expect("lol"); - - let segment_path = match segment { - EncodedFilename::ShortFilename(filename) => PathBuf::from(filename), - EncodedFilename::LongFilename(long_filename) => PathBuf::from(long_filename.filename), - }; - - if segment_path.is_dir() { - let (size, _) = components.size_hint(); - - if size > 0 { - unimplemented!() - } else { - unimplemented!() - //None - } - } else { - // component.as_path() - unimplemented!() - }; - - } - - fn lookup_internal

( - &self, - filename_decoder: &FilenameDecoder, - decrypted_path: P, - dir: &DirCache, - ) -> Option - where - P: AsRef, - { - unimplemented!() - } -} - -#[derive(Debug)] -pub enum DirEntry { - Dir(DirCache), - File(String), -} - -impl DirEntry { - /// Returns `true` if the dir entry is [`Dir`]. - /// - /// [`Dir`]: DirEntry::Dir - #[must_use] - pub fn is_dir(&self) -> bool { - matches!(self, Self::Dir(..)) - } - - /// Returns `true` if the dir entry is [`File`]. - /// - /// [`File`]: DirEntry::File - #[must_use] - pub fn is_file(&self) -> bool { - matches!(self, Self::File(..)) - } -} - -impl TryFrom<&Path> for DirEntry { - type Error = Box; // TODO - - fn try_from(path: &Path) -> Result { - Ok(if path.is_dir() { - DirEntry::Dir(DirCache::load_from_path(path)) - } else { - DirEntry::File( - path.components() - .last() - .unwrap() - .as_os_str() - .to_string_lossy() - .to_string(), - ) - }) - } -} diff --git a/rustcryptfs-lib/src/lib.rs b/rustcryptfs-lib/src/lib.rs index 759ebc5..60122eb 100644 --- a/rustcryptfs-lib/src/lib.rs +++ b/rustcryptfs-lib/src/lib.rs @@ -1,18 +1,18 @@ -//! A library to write gocryptfs compatible programs +//! A library to write gocryptfs compatible programs. -use std::{fs::File, path::Path}; +use std::{fs::File, io::Read, path::Path}; -use content_enc::ContentEnc; -use filename::FilenameDecoder; +use content::ContentEnc; +use filename::FilenameCipher; pub mod config; -pub mod content_enc; +pub mod content; pub mod error; pub mod filename; /// A GocryptFs encrypted directory pub struct GocryptFs { - filename_decoder: FilenameDecoder, + filename_decoder: FilenameCipher, content_decoder: ContentEnc, } @@ -26,18 +26,25 @@ impl GocryptFs { { let base_path = encrypted_dir_path.as_ref(); - let config = { - let mut config_file = - File::open(base_path.join("gocryptfs.conf")).expect("failed to get config"); + let mut config_file = + File::open(base_path.join("gocryptfs.conf"))?; - serde_json::from_reader::<_, config::CryptConf>(&mut config_file) - .expect("failed to parse config") - }; + Self::load_from_reader(&mut config_file, password.as_bytes()) + } - let master_key = config.get_master_key(password.as_bytes())?; + /// Load a gocryptfs from the config. + /// + /// reader_config must be a reader of a valid `gocryptfs.conf`. + pub fn load_from_reader(reader_config: &mut R, password: &[u8]) -> error::Result + where + R: Read, + { + let config = serde_json::from_reader::<_, config::CryptConf>(reader_config)?; - let filename_decoder = FilenameDecoder::new(&master_key)?; - let content_decoder = ContentEnc::new(&master_key, 16); // TODO IV LEN + let master_key = config.get_master_key(password)?; + + let filename_decoder = FilenameCipher::new(&master_key)?; + let content_decoder = ContentEnc::new(&master_key, 16)?; // TODO IV LEN Ok(Self { filename_decoder, @@ -45,10 +52,12 @@ impl GocryptFs { }) } - pub fn filename_decoder(&self) -> &FilenameDecoder { + /// Get the [`filename decoder`](struct@FilenameCipher) attached to this GocryptFs. + pub fn filename_decoder(&self) -> &FilenameCipher { &self.filename_decoder } + /// Get the [`content decoder`](struct@ContentEnc) attached to this GocryptFs. pub fn content_decoder(&self) -> &ContentEnc { &self.content_decoder } diff --git a/rustcryptfs-mount/Cargo.toml b/rustcryptfs-mount/Cargo.toml new file mode 100644 index 0000000..fc3e0af --- /dev/null +++ b/rustcryptfs-mount/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rustcryptfs-mount" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[target.'cfg(target_os = "linux")'.dependencies] +rustcryptfs-fuse = { path = "../rustcryptfs-fuse" } diff --git a/rustcryptfs-mount/src/lib.rs b/rustcryptfs-mount/src/lib.rs new file mode 100644 index 0000000..7a51b40 --- /dev/null +++ b/rustcryptfs-mount/src/lib.rs @@ -0,0 +1,14 @@ +use std::path::Path; + +use rustcryptfs_fuse::EncryptedFs; + +#[cfg(target_os = "linux")] +pub fn mount

(path: P, mount_point: P, password: &str) -> rustcryptfs_fuse::error::Result<()> +where + P: AsRef, +{ + let fs = EncryptedFs::new(path, password)?; + + fs.mount(mount_point)?; + Ok(()) +} diff --git a/rustcryptfs/Cargo.toml b/rustcryptfs/Cargo.toml index 0d74ac6..4db0ef5 100644 --- a/rustcryptfs/Cargo.toml +++ b/rustcryptfs/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["mount"] +mount = ["rustcryptfs-mount"] + [dependencies] anyhow = "1.0.53" serde = { version = "1.0.136", features = ["derive"] } @@ -12,4 +16,6 @@ serde_json = "1.0.78" clap = { version = "3.1.18", features = ["derive"] } log = "0.4.17" rustcryptfs-lib = { path = "../rustcryptfs-lib" } -env_logger = "0.9.0" \ No newline at end of file +env_logger = "0.9.0" +rpassword = "7.0.0" +rustcryptfs-mount = { path = "../rustcryptfs-mount", optional = true } \ No newline at end of file diff --git a/rustcryptfs/src/args.rs b/rustcryptfs/src/args.rs index b57bba8..7cc85e8 100644 --- a/rustcryptfs/src/args.rs +++ b/rustcryptfs/src/args.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "mount")] +use std::path::PathBuf; + use clap::{Parser, Subcommand}; #[derive(Debug, Parser)] @@ -12,8 +15,12 @@ pub(crate) enum Commands { /// Decrypt a file Decrypt(DecryptCommand), - // List file contained in a directory + /// List file contained in a directory Ls(LsCommand), + + #[cfg(feature = "mount")] + /// Mount an encrypted folder + Mount(MountCommand), } #[derive(Debug, Parser)] @@ -42,4 +49,18 @@ pub(crate) struct LsCommand { /// The password #[clap(short, long)] pub(crate) password : Option -} \ No newline at end of file +} + +#[cfg(feature = "mount")] +#[derive(Debug, Parser)] +pub(crate) struct MountCommand { + /// The directory + pub(crate) path: PathBuf, + + /// The mount point + pub(crate) mountpoint: PathBuf, + + /// The password + #[clap(short, long)] + pub(crate) password: Option, +} diff --git a/rustcryptfs/src/main.rs b/rustcryptfs/src/main.rs index a8e9b1a..2e70937 100644 --- a/rustcryptfs/src/main.rs +++ b/rustcryptfs/src/main.rs @@ -9,39 +9,49 @@ use clap::Parser; use args::{DecryptCommand, LsCommand}; use rustcryptfs_lib::GocryptFs; +#[cfg(feature = "mount")] +use args::MountCommand; + mod args; fn main() -> anyhow::Result<()> { env_logger::init(); let args = args::Args::parse(); - log::debug!("{:?}", args); match &args.command { args::Commands::Decrypt(c) => decrypt_file(c), args::Commands::Ls(c) => ls(c), + #[cfg(feature = "mount")] + args::Commands::Mount(c) => mount(c), } } fn ls(c: &LsCommand) -> anyhow::Result<()> { let folder_path = Path::new(&c.folder_path); + let password = if let Some(password) = &c.password { + password.clone() + } else { + rpassword::prompt_password("Your password: ")? + }; + let fs = GocryptFs::open( c.gocryptfs_path .as_ref() - .map(|p| Path::new(p)) + .map(Path::new) .unwrap_or(folder_path), - c.password.as_ref().expect("Please input a password"), + &password, )?; let filename_decoder = fs.filename_decoder(); let iv = std::fs::read(folder_path.join("gocryptfs.diriv"))?; - let dir_decoder = filename_decoder.get_decoder_for_dir(&iv); + let dir_decoder = filename_decoder.get_cipher_for_dir(&iv); for dir in std::fs::read_dir(folder_path)?.flat_map(|e| e.ok()) { let filename = dir.file_name(); - let filename = filename.to_str().unwrap(); + let filename = filename.to_string_lossy(); if filename != "gocryptfs.conf" && filename != "gocryptfs.diriv" { if filename.starts_with("gocryptfs.longname.") { @@ -52,28 +62,33 @@ fn ls(c: &LsCommand) -> anyhow::Result<()> { println!("{}", res); } } - } else { - if let Ok(res) = dir_decoder.decode_filename(filename) { - println!("{}", res); - } - }; + } else if let Ok(res) = dir_decoder.decode_filename(&*filename) { + println!("{}", res); + } } } - return Ok(()); + Ok(()) } fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> { let file_path = Path::new(&c.file_path); + + let password = if let Some(password) = &c.password { + password.clone() + } else { + rpassword::prompt_password("Your password: ")? + }; + let fs = GocryptFs::open( c.gocryptfs_path .as_ref() - .map(|p| Path::new(p)) + .map(Path::new) .unwrap_or_else(|| file_path.parent().unwrap()), - c.password.as_ref().expect("Please input a password"), + &password, )?; - let mut file = File::open(file_path).unwrap(); + let mut file = File::open(file_path)?; let enc = fs.content_decoder(); @@ -92,7 +107,7 @@ fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> { stdout.write_all(&res)?; - if res.len() == 0 { + if res.is_empty() { break; } @@ -102,3 +117,26 @@ fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> { Ok(()) } + +#[cfg(target_os = "linux")] +#[cfg(feature = "mount")] +fn mount(mount: &MountCommand) -> anyhow::Result<()> { + use anyhow::Context; + + let password = if let Some(password) = &mount.password { + password.clone() + } else { + rpassword::prompt_password("Your password: ")? + }; + + rustcryptfs_mount::mount(&mount.path, &mount.mountpoint, &password) + .context("Failed to run fuse fs")?; + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +#[cfg(feature = "mount")] +fn mount(mount: &MountCommand) -> anyhow::Result<()> { + unimplemented!() +}