├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs └── src ├── cli.rs ├── dmg.rs ├── downloads.rs ├── main.rs └── zip.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | encrypted-*.dmg 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.40" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.0.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 16 | 17 | [[package]] 18 | name = "base64" 19 | version = "0.21.7" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "1.2.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "2.4.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 34 | 35 | [[package]] 36 | name = "cfg-if" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 40 | 41 | [[package]] 42 | name = "chrono" 43 | version = "0.4.19" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 46 | dependencies = [ 47 | "num-integer", 48 | "num-traits", 49 | ] 50 | 51 | [[package]] 52 | name = "clap" 53 | version = "2.33.3" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 56 | dependencies = [ 57 | "bitflags 1.2.1", 58 | "textwrap", 59 | "unicode-width", 60 | ] 61 | 62 | [[package]] 63 | name = "console" 64 | version = "0.15.8" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 67 | dependencies = [ 68 | "encode_unicode", 69 | "lazy_static", 70 | "libc", 71 | "unicode-width", 72 | "windows-sys 0.52.0", 73 | ] 74 | 75 | [[package]] 76 | name = "deranged" 77 | version = "0.3.11" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 80 | dependencies = [ 81 | "powerfmt", 82 | ] 83 | 84 | [[package]] 85 | name = "dialoguer" 86 | version = "0.11.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 89 | dependencies = [ 90 | "console", 91 | "shell-words", 92 | "thiserror", 93 | "zeroize", 94 | ] 95 | 96 | [[package]] 97 | name = "directories" 98 | version = "5.0.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 101 | dependencies = [ 102 | "dirs-sys", 103 | ] 104 | 105 | [[package]] 106 | name = "dirs-sys" 107 | version = "0.4.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 110 | dependencies = [ 111 | "libc", 112 | "option-ext", 113 | "redox_users", 114 | "windows-sys 0.48.0", 115 | ] 116 | 117 | [[package]] 118 | name = "edmgutil" 119 | version = "0.1.0" 120 | dependencies = [ 121 | "anyhow", 122 | "chrono", 123 | "clap", 124 | "dialoguer", 125 | "directories", 126 | "plist", 127 | "serde", 128 | "structopt", 129 | "url", 130 | "uuid", 131 | "which", 132 | "xattr", 133 | ] 134 | 135 | [[package]] 136 | name = "either" 137 | version = "1.10.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 140 | 141 | [[package]] 142 | name = "encode_unicode" 143 | version = "0.3.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 146 | 147 | [[package]] 148 | name = "equivalent" 149 | version = "1.0.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 152 | 153 | [[package]] 154 | name = "errno" 155 | version = "0.3.8" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 158 | dependencies = [ 159 | "libc", 160 | "windows-sys 0.52.0", 161 | ] 162 | 163 | [[package]] 164 | name = "form_urlencoded" 165 | version = "1.0.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 168 | dependencies = [ 169 | "matches", 170 | "percent-encoding", 171 | ] 172 | 173 | [[package]] 174 | name = "getrandom" 175 | version = "0.2.3" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 178 | dependencies = [ 179 | "cfg-if", 180 | "libc", 181 | "wasi", 182 | ] 183 | 184 | [[package]] 185 | name = "hashbrown" 186 | version = "0.14.3" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 189 | 190 | [[package]] 191 | name = "heck" 192 | version = "0.3.2" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 195 | dependencies = [ 196 | "unicode-segmentation", 197 | ] 198 | 199 | [[package]] 200 | name = "home" 201 | version = "0.5.9" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 204 | dependencies = [ 205 | "windows-sys 0.52.0", 206 | ] 207 | 208 | [[package]] 209 | name = "idna" 210 | version = "0.2.3" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 213 | dependencies = [ 214 | "matches", 215 | "unicode-bidi", 216 | "unicode-normalization", 217 | ] 218 | 219 | [[package]] 220 | name = "indexmap" 221 | version = "2.2.3" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" 224 | dependencies = [ 225 | "equivalent", 226 | "hashbrown", 227 | ] 228 | 229 | [[package]] 230 | name = "itoa" 231 | version = "1.0.10" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 234 | 235 | [[package]] 236 | name = "lazy_static" 237 | version = "1.4.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 240 | 241 | [[package]] 242 | name = "libc" 243 | version = "0.2.153" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 246 | 247 | [[package]] 248 | name = "line-wrap" 249 | version = "0.1.1" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" 252 | dependencies = [ 253 | "safemem", 254 | ] 255 | 256 | [[package]] 257 | name = "linux-raw-sys" 258 | version = "0.4.13" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 261 | 262 | [[package]] 263 | name = "matches" 264 | version = "0.1.8" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 267 | 268 | [[package]] 269 | name = "memchr" 270 | version = "2.7.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 273 | 274 | [[package]] 275 | name = "num-conv" 276 | version = "0.1.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 279 | 280 | [[package]] 281 | name = "num-integer" 282 | version = "0.1.44" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 285 | dependencies = [ 286 | "autocfg", 287 | "num-traits", 288 | ] 289 | 290 | [[package]] 291 | name = "num-traits" 292 | version = "0.2.14" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 295 | dependencies = [ 296 | "autocfg", 297 | ] 298 | 299 | [[package]] 300 | name = "once_cell" 301 | version = "1.19.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 304 | 305 | [[package]] 306 | name = "option-ext" 307 | version = "0.2.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 310 | 311 | [[package]] 312 | name = "percent-encoding" 313 | version = "2.1.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 316 | 317 | [[package]] 318 | name = "plist" 319 | version = "1.6.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" 322 | dependencies = [ 323 | "base64", 324 | "indexmap", 325 | "line-wrap", 326 | "quick-xml", 327 | "serde", 328 | "time", 329 | ] 330 | 331 | [[package]] 332 | name = "powerfmt" 333 | version = "0.2.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 336 | 337 | [[package]] 338 | name = "proc-macro-error" 339 | version = "1.0.4" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 342 | dependencies = [ 343 | "proc-macro-error-attr", 344 | "proc-macro2", 345 | "quote", 346 | "syn 1.0.72", 347 | "version_check", 348 | ] 349 | 350 | [[package]] 351 | name = "proc-macro-error-attr" 352 | version = "1.0.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 355 | dependencies = [ 356 | "proc-macro2", 357 | "quote", 358 | "version_check", 359 | ] 360 | 361 | [[package]] 362 | name = "proc-macro2" 363 | version = "1.0.78" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 366 | dependencies = [ 367 | "unicode-ident", 368 | ] 369 | 370 | [[package]] 371 | name = "quick-xml" 372 | version = "0.31.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 375 | dependencies = [ 376 | "memchr", 377 | ] 378 | 379 | [[package]] 380 | name = "quote" 381 | version = "1.0.35" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 384 | dependencies = [ 385 | "proc-macro2", 386 | ] 387 | 388 | [[package]] 389 | name = "redox_syscall" 390 | version = "0.2.8" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" 393 | dependencies = [ 394 | "bitflags 1.2.1", 395 | ] 396 | 397 | [[package]] 398 | name = "redox_users" 399 | version = "0.4.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 402 | dependencies = [ 403 | "getrandom", 404 | "redox_syscall", 405 | ] 406 | 407 | [[package]] 408 | name = "rustix" 409 | version = "0.38.31" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 412 | dependencies = [ 413 | "bitflags 2.4.2", 414 | "errno", 415 | "libc", 416 | "linux-raw-sys", 417 | "windows-sys 0.52.0", 418 | ] 419 | 420 | [[package]] 421 | name = "safemem" 422 | version = "0.3.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 425 | 426 | [[package]] 427 | name = "serde" 428 | version = "1.0.196" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 431 | dependencies = [ 432 | "serde_derive", 433 | ] 434 | 435 | [[package]] 436 | name = "serde_derive" 437 | version = "1.0.196" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 440 | dependencies = [ 441 | "proc-macro2", 442 | "quote", 443 | "syn 2.0.48", 444 | ] 445 | 446 | [[package]] 447 | name = "shell-words" 448 | version = "1.1.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 451 | 452 | [[package]] 453 | name = "structopt" 454 | version = "0.3.21" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" 457 | dependencies = [ 458 | "clap", 459 | "lazy_static", 460 | "structopt-derive", 461 | ] 462 | 463 | [[package]] 464 | name = "structopt-derive" 465 | version = "0.4.14" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" 468 | dependencies = [ 469 | "heck", 470 | "proc-macro-error", 471 | "proc-macro2", 472 | "quote", 473 | "syn 1.0.72", 474 | ] 475 | 476 | [[package]] 477 | name = "syn" 478 | version = "1.0.72" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "unicode-xid", 485 | ] 486 | 487 | [[package]] 488 | name = "syn" 489 | version = "2.0.48" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "unicode-ident", 496 | ] 497 | 498 | [[package]] 499 | name = "textwrap" 500 | version = "0.11.0" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 503 | dependencies = [ 504 | "unicode-width", 505 | ] 506 | 507 | [[package]] 508 | name = "thiserror" 509 | version = "1.0.57" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 512 | dependencies = [ 513 | "thiserror-impl", 514 | ] 515 | 516 | [[package]] 517 | name = "thiserror-impl" 518 | version = "1.0.57" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 521 | dependencies = [ 522 | "proc-macro2", 523 | "quote", 524 | "syn 2.0.48", 525 | ] 526 | 527 | [[package]] 528 | name = "time" 529 | version = "0.3.34" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" 532 | dependencies = [ 533 | "deranged", 534 | "itoa", 535 | "num-conv", 536 | "powerfmt", 537 | "serde", 538 | "time-core", 539 | "time-macros", 540 | ] 541 | 542 | [[package]] 543 | name = "time-core" 544 | version = "0.1.2" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 547 | 548 | [[package]] 549 | name = "time-macros" 550 | version = "0.2.17" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" 553 | dependencies = [ 554 | "num-conv", 555 | "time-core", 556 | ] 557 | 558 | [[package]] 559 | name = "tinyvec" 560 | version = "1.3.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "4ac2e1d4bd0f75279cfd5a076e0d578bbf02c22b7c39e766c437dd49b3ec43e0" 563 | dependencies = [ 564 | "tinyvec_macros", 565 | ] 566 | 567 | [[package]] 568 | name = "tinyvec_macros" 569 | version = "0.1.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 572 | 573 | [[package]] 574 | name = "unicode-bidi" 575 | version = "0.3.5" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 578 | dependencies = [ 579 | "matches", 580 | ] 581 | 582 | [[package]] 583 | name = "unicode-ident" 584 | version = "1.0.12" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 587 | 588 | [[package]] 589 | name = "unicode-normalization" 590 | version = "0.1.19" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 593 | dependencies = [ 594 | "tinyvec", 595 | ] 596 | 597 | [[package]] 598 | name = "unicode-segmentation" 599 | version = "1.7.1" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 602 | 603 | [[package]] 604 | name = "unicode-width" 605 | version = "0.1.8" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 608 | 609 | [[package]] 610 | name = "unicode-xid" 611 | version = "0.2.2" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 614 | 615 | [[package]] 616 | name = "url" 617 | version = "2.2.2" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 620 | dependencies = [ 621 | "form_urlencoded", 622 | "idna", 623 | "matches", 624 | "percent-encoding", 625 | "serde", 626 | ] 627 | 628 | [[package]] 629 | name = "uuid" 630 | version = "1.7.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" 633 | dependencies = [ 634 | "getrandom", 635 | ] 636 | 637 | [[package]] 638 | name = "version_check" 639 | version = "0.9.3" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 642 | 643 | [[package]] 644 | name = "wasi" 645 | version = "0.10.2+wasi-snapshot-preview1" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 648 | 649 | [[package]] 650 | name = "which" 651 | version = "6.0.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" 654 | dependencies = [ 655 | "either", 656 | "home", 657 | "once_cell", 658 | "rustix", 659 | "windows-sys 0.52.0", 660 | ] 661 | 662 | [[package]] 663 | name = "windows-sys" 664 | version = "0.48.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 667 | dependencies = [ 668 | "windows-targets 0.48.5", 669 | ] 670 | 671 | [[package]] 672 | name = "windows-sys" 673 | version = "0.52.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 676 | dependencies = [ 677 | "windows-targets 0.52.0", 678 | ] 679 | 680 | [[package]] 681 | name = "windows-targets" 682 | version = "0.48.5" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 685 | dependencies = [ 686 | "windows_aarch64_gnullvm 0.48.5", 687 | "windows_aarch64_msvc 0.48.5", 688 | "windows_i686_gnu 0.48.5", 689 | "windows_i686_msvc 0.48.5", 690 | "windows_x86_64_gnu 0.48.5", 691 | "windows_x86_64_gnullvm 0.48.5", 692 | "windows_x86_64_msvc 0.48.5", 693 | ] 694 | 695 | [[package]] 696 | name = "windows-targets" 697 | version = "0.52.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 700 | dependencies = [ 701 | "windows_aarch64_gnullvm 0.52.0", 702 | "windows_aarch64_msvc 0.52.0", 703 | "windows_i686_gnu 0.52.0", 704 | "windows_i686_msvc 0.52.0", 705 | "windows_x86_64_gnu 0.52.0", 706 | "windows_x86_64_gnullvm 0.52.0", 707 | "windows_x86_64_msvc 0.52.0", 708 | ] 709 | 710 | [[package]] 711 | name = "windows_aarch64_gnullvm" 712 | version = "0.48.5" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 715 | 716 | [[package]] 717 | name = "windows_aarch64_gnullvm" 718 | version = "0.52.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 721 | 722 | [[package]] 723 | name = "windows_aarch64_msvc" 724 | version = "0.48.5" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 727 | 728 | [[package]] 729 | name = "windows_aarch64_msvc" 730 | version = "0.52.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 733 | 734 | [[package]] 735 | name = "windows_i686_gnu" 736 | version = "0.48.5" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 739 | 740 | [[package]] 741 | name = "windows_i686_gnu" 742 | version = "0.52.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 745 | 746 | [[package]] 747 | name = "windows_i686_msvc" 748 | version = "0.48.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 751 | 752 | [[package]] 753 | name = "windows_i686_msvc" 754 | version = "0.52.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 757 | 758 | [[package]] 759 | name = "windows_x86_64_gnu" 760 | version = "0.48.5" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 763 | 764 | [[package]] 765 | name = "windows_x86_64_gnu" 766 | version = "0.52.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 769 | 770 | [[package]] 771 | name = "windows_x86_64_gnullvm" 772 | version = "0.48.5" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 775 | 776 | [[package]] 777 | name = "windows_x86_64_gnullvm" 778 | version = "0.52.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 781 | 782 | [[package]] 783 | name = "windows_x86_64_msvc" 784 | version = "0.48.5" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 787 | 788 | [[package]] 789 | name = "windows_x86_64_msvc" 790 | version = "0.52.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 793 | 794 | [[package]] 795 | name = "xattr" 796 | version = "1.3.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 799 | dependencies = [ 800 | "libc", 801 | "linux-raw-sys", 802 | "rustix", 803 | ] 804 | 805 | [[package]] 806 | name = "zeroize" 807 | version = "1.7.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 810 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edmgutil" 3 | version = "0.1.0" 4 | authors = ["Armin Ronacher "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1.0.40" 11 | dialoguer = { version = "0.11.0", default-features = false, features = ["password"] } 12 | which = "6.0.0" 13 | uuid = { version = "1.7.0", features = ["v4"] } 14 | plist = "1.6.0" 15 | serde = { version = "1.0.126", features = ["derive"] } 16 | structopt = { version = "0.3.21", default-features = false } 17 | clap = { version = "2.33.3", default-features = false } 18 | chrono = { version = "0.4.19", default-features = false, features = ["std"] } 19 | xattr = "1.3.1" 20 | directories = "5.0.1" 21 | url = { version = "2.2.2", features = ["serde"] } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # edmgutil 2 | 3 | `edmgutil` is a simple wrapper utility to hdiutil to help you work with disposable, encrypted 4 | DMGs. It can decompress an encrypted ZIP into a newly mounted encrypted DMG, create empty 5 | throwaway DMGs and automatically eject expired ones. This makes transferring and working with 6 | data that should only live for a short period of time for debugging purposes to developer 7 | machines a more convenient endeavour. The volume is individually encrypted and gets destroyed 8 | when ejected. 9 | 10 | It also instructs the backup tool to disable backing up the volume in case someone accidentally 11 | adds it. 12 | 13 | ## Installation 14 | 15 | ``` 16 | cargo install --git https://github.com/getsentry/edmgutil --branch main edmgutil 17 | ``` 18 | 19 | Note that this requires `7z` to be installed. If you don't have it: 20 | 21 | ``` 22 | brew install p7zip 23 | ``` 24 | 25 | ## Importing Encrypted Zip Archives 26 | 27 | ``` 28 | edmgutil import /path/to/encrypted.zip 29 | ``` 30 | 31 | It will prompt for the password, then create an encrypted volumne with the same password and then 32 | extract the zip file into it and then delete the created dmg (unless `-k` is passed). 33 | 34 | Once the DMG is ejected everything is gone again. 35 | 36 | When the DMG is created a timestamp is frozen into it (defined by `--days`, defaults to 7). It's 37 | recommended to run `edmgutil eject --expired` regularly to automatically unmount expired 38 | images for instance by putting it into your crontab (see `edmgutil cron`). 39 | 40 | To create an encrypted zip use 7zip: 41 | 42 | ``` 43 | 7za a -tzip -p'the password' -mem=AES256 encrypted.zip folder 44 | ``` 45 | 46 | Just make sure to use a long password, maybe something like this: 47 | 48 | ``` 49 | openssl rand -hex 32 50 | ``` 51 | 52 | ## Creating Empty DMGs 53 | 54 | To create an empty, encrypted DMG use the `new` command and provide the size of the DMG in 55 | megabytes. Alternatively you can provide a descriptive name which will become the volume name: 56 | 57 | ``` 58 | edmgutil new --size 100 --name "My Stuff" 59 | ``` 60 | 61 | ## Listing / Ejecting 62 | 63 | To list and eject encrypted DMGs you can use the following commands: 64 | 65 | ``` 66 | edmgutil list 67 | edmgutil eject --expired 68 | edmgutil eject --all 69 | edmgutil eject /Volumes/EncryptedVolume 70 | ``` 71 | 72 | ## Crontab 73 | 74 | To ensure that expired images are ejected automatically when possible can can install a crontab 75 | which runs ejecting hourly: 76 | 77 | ``` 78 | edmgutil cron --install 79 | ``` 80 | 81 | ## Download Folder Monitoring 82 | 83 | Because browsers love to download files unprompted into the default download location it's not 84 | uncommon for you to accidentally places files there you really don't want to retain there. 85 | The `find-downloads` command can be useful for manual spot checking. 86 | 87 | This will list all files that were downloaded from `your-domain.tld`: 88 | 89 | ``` 90 | edmgutil find-downloads -d your-domain.tld 91 | ``` 92 | 93 | If subdomains should be included as well, use a `*`: 94 | 95 | ``` 96 | edmgutil find-downloads -d '*.your-domain.tld' 97 | ``` 98 | 99 | For additional information you can turn on verbose mode which shows the exact 100 | source of the file by URL: 101 | 102 | ``` 103 | edmgutil find-downloads -d your-domain.tld -v 104 | ``` 105 | 106 | Additionally files from those domains older than N days can be auto deleted: 107 | 108 | ``` 109 | edmgutil find-downloads -d your-domain.tld --delete --days=7 110 | ``` 111 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | match std::env::var("CARGO_CFG_TARGET_OS").as_deref() { 3 | Ok("macos") => {} 4 | _ => { 5 | panic!("unsupported operating system. edmgutil only works on macos"); 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::AppSettings; 4 | use structopt::StructOpt; 5 | 6 | /// A utility to work with disposable encryptd DMGs. 7 | #[derive(Debug, StructOpt)] 8 | #[structopt( 9 | global_setting(AppSettings::UnifiedHelpMessage), 10 | global_setting(AppSettings::VersionlessSubcommands) 11 | )] 12 | pub enum Commands { 13 | New(NewCommand), 14 | Import(ImportCommand), 15 | List(ListCommand), 16 | Eject(EjectCommand), 17 | Cron(CronCommand), 18 | FindDownloads(FindDownloadsCommand), 19 | } 20 | 21 | #[derive(Debug, StructOpt)] 22 | pub struct ImageOptions { 23 | /// the amount of days the image is good to keep 24 | #[structopt(long = "days", default_value = "7")] 25 | pub days: u32, 26 | /// the volume name of the dmg 27 | #[structopt(short = "n", long = "name")] 28 | pub volume_name: Option, 29 | /// keep the source DMG instead of deleting it 30 | #[structopt(short = "k", long = "keep")] 31 | pub keep_dmg: bool, 32 | /// provide the passphrase for the image 33 | #[structopt(short = "p", long = "password")] 34 | pub password: Option, 35 | } 36 | 37 | /// creates a new encrypted DMG and mounts it 38 | /// 39 | /// This command can create an encrypted DMG, mounts it and normally 40 | /// disposes of the source DMG so that everything gets deleted when 41 | /// the image is unmounted. 42 | #[derive(Debug, StructOpt)] 43 | pub struct NewCommand { 44 | #[structopt(flatten)] 45 | pub image_opts: ImageOptions, 46 | /// the size for the encrypted DMG in megabytes 47 | #[structopt(short = "s", long = "size", default_value = "100")] 48 | pub size: usize, 49 | } 50 | 51 | /// imports an encrypted zip as encrypted DMG and mounts it 52 | #[derive(Debug, StructOpt)] 53 | pub struct ImportCommand { 54 | #[structopt(flatten)] 55 | pub image_opts: ImageOptions, 56 | /// the extra size for the encrypted DMG in megabytes 57 | #[structopt(long = "extra-size", default_value = "100")] 58 | pub extra_size: usize, 59 | /// the path of the input zip archive 60 | #[structopt(name = "path")] 61 | pub path: PathBuf, 62 | } 63 | 64 | /// ejects encrypted dmgs 65 | #[derive(Debug, StructOpt)] 66 | #[structopt(setting(AppSettings::ArgRequiredElseHelp))] 67 | pub struct EjectCommand { 68 | /// ejects all mounted encrypted volumes 69 | #[structopt(long = "all", short = "a", conflicts_with("path"))] 70 | pub all: bool, 71 | /// ejects expired encrypted volumes 72 | #[structopt(long = "expired", short = "e", conflicts_with("path"))] 73 | pub expired: bool, 74 | /// the path of the volume to eject 75 | #[structopt(name = "path")] 76 | pub path: Option, 77 | } 78 | 79 | /// list all mounted encrypted DMGs 80 | #[derive(Debug, StructOpt)] 81 | pub struct ListCommand { 82 | /// provides extra information 83 | #[structopt(long = "verbose", short = "v")] 84 | pub verbose: bool, 85 | } 86 | 87 | /// installs or uninstalls the cron 88 | #[derive(Debug, StructOpt)] 89 | #[structopt(setting(AppSettings::ArgRequiredElseHelp))] 90 | pub struct CronCommand { 91 | /// installs the cron 92 | #[structopt(long = "install")] 93 | pub install: bool, 94 | /// uninstalls the cron 95 | #[structopt(long = "uninstall")] 96 | pub uninstall: bool, 97 | } 98 | 99 | /// helps monitoring the download folder for problematic sources 100 | /// 101 | /// This command lets you quickly show all the files in your downloads folder 102 | /// which come from a specific source. This way you can easily periodically 103 | /// check that it does not contain files you don't expect it to be there. 104 | #[derive(Debug, StructOpt)] 105 | pub struct FindDownloadsCommand { 106 | /// the domains to look out for. 107 | /// 108 | /// When *.domain.tld is used it looks for any subdomain of the domain 109 | /// including the apex of the domain. 110 | #[structopt(long = "domain", short = "d")] 111 | pub domains: Vec, 112 | /// provide additional information when listing files 113 | #[structopt(long = "verbose", short = "v")] 114 | pub verbose: bool, 115 | /// an alternative folder than the default download folder to search 116 | pub path: Option, 117 | /// Automatically delete all found files. 118 | #[structopt(long = "delete")] 119 | pub delete: bool, 120 | /// Only list or delete files older than this value. 121 | #[structopt(long = "days")] 122 | pub days: Option, 123 | } 124 | -------------------------------------------------------------------------------- /src/dmg.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | io::Write, 4 | path::{Path, PathBuf}, 5 | process::{Command, Stdio}, 6 | time::{Duration, SystemTime}, 7 | }; 8 | 9 | use anyhow::{bail, Error}; 10 | use serde::Deserialize; 11 | 12 | #[derive(Deserialize, Debug)] 13 | #[serde(rename_all = "kebab-case")] 14 | struct HdiUtilSystemEntity { 15 | mount_point: Option, 16 | } 17 | 18 | #[derive(Deserialize, Debug)] 19 | #[serde(rename_all = "kebab-case")] 20 | struct HdiUtilImage { 21 | system_entities: Vec, 22 | } 23 | 24 | #[derive(Deserialize, Debug)] 25 | struct HdiUtilInfo { 26 | images: Vec, 27 | } 28 | 29 | pub fn make_dmg(path: &Path, volume_name: &str, size: usize, password: &str) -> Result<(), Error> { 30 | let mut child = Command::new("hdiutil") 31 | .arg("create") 32 | .arg("-megabytes") 33 | .arg(size.to_string()) 34 | .arg("-ov") 35 | .arg("-volname") 36 | .arg(volume_name) 37 | .arg("-fs") 38 | .arg("HFS+") 39 | .arg("-encryption") 40 | .arg("AES-256") 41 | .arg("-stdinpass") 42 | .arg(path) 43 | .stdin(Stdio::piped()) 44 | .spawn()?; 45 | let mut stdin = child.stdin.take().unwrap(); 46 | stdin.write_all(password.as_bytes())?; 47 | drop(stdin); 48 | child.wait()?; 49 | Ok(()) 50 | } 51 | 52 | pub fn mount_dmg(path: &Path, password: &str) -> Result { 53 | let mut child = Command::new("hdiutil") 54 | .arg("attach") 55 | .arg("-stdinpass") 56 | .arg(path) 57 | .stdin(Stdio::piped()) 58 | .stdout(Stdio::piped()) 59 | .spawn()?; 60 | let mut stdin = child.stdin.take().unwrap(); 61 | stdin.write_all(password.as_bytes())?; 62 | drop(stdin); 63 | let output = child.wait_with_output()?; 64 | let to_parse = std::str::from_utf8(&output.stdout)?; 65 | for line in to_parse.lines() { 66 | if !line.contains("\tApple_HFS") { 67 | continue; 68 | } 69 | return Ok(PathBuf::from( 70 | line.trim_end_matches(&['\n'][..]) 71 | .splitn(3, '\t') 72 | .nth(2) 73 | .unwrap(), 74 | )); 75 | } 76 | 77 | bail!("failed to mount dmg"); 78 | } 79 | 80 | pub fn list_volumes() -> Result, Error> { 81 | let output = Command::new("hdiutil") 82 | .arg("info") 83 | .arg("-plist") 84 | .stdout(Stdio::piped()) 85 | .spawn()? 86 | .wait_with_output()?; 87 | let info: HdiUtilInfo = plist::from_bytes(&output.stdout)?; 88 | let mut encrypted_volumes = vec![]; 89 | 90 | for image in info.images { 91 | for entity in image.system_entities { 92 | if let Some(mount_point) = entity.mount_point { 93 | if let Some(ts) = 94 | fs::read_to_string(mount_point.join(".encrypted-volume-good-until")) 95 | .ok() 96 | .and_then(|x| x.parse().ok()) 97 | .map(|x| SystemTime::UNIX_EPOCH + Duration::from_secs(x)) 98 | { 99 | encrypted_volumes.push((mount_point, ts)); 100 | } 101 | } 102 | } 103 | } 104 | 105 | Ok(encrypted_volumes) 106 | } 107 | 108 | pub fn eject(path: &Path) -> Result<(), Error> { 109 | Command::new("hdiutil") 110 | .arg("eject") 111 | .arg(path) 112 | .stdout(Stdio::null()) 113 | .spawn()? 114 | .wait()?; 115 | Ok(()) 116 | } 117 | 118 | pub fn secure_volume(path: &Path, days: u32) -> Result<(), Error> { 119 | let good_until = (SystemTime::now() + Duration::from_secs((days as u64) * 60 * 60 * 24)) 120 | .duration_since(SystemTime::UNIX_EPOCH)?; 121 | fs::write(path.join(".metadata_never_index"), "")?; 122 | fs::write( 123 | path.join(".encrypted-volume-good-until"), 124 | good_until.as_secs().to_string(), 125 | )?; 126 | 127 | // place one of the custom icons shipped with macos as volume icon so it can be easily 128 | // told apart form other DMGs. 129 | if fs::copy( 130 | "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/iDiskUserIcon.icns", 131 | path.join(".VolumeIcon.icns"), 132 | ) 133 | .is_ok() 134 | { 135 | Command::new("SetFile") 136 | .arg("-a") 137 | .arg("C") 138 | .arg(path) 139 | .spawn()? 140 | .wait()?; 141 | } 142 | 143 | Command::new("mdutil") 144 | .arg("-E") 145 | .arg("-i") 146 | .arg("off") 147 | .arg(path) 148 | .stdout(Stdio::null()) 149 | .stderr(Stdio::null()) 150 | .spawn()? 151 | .wait()?; 152 | Ok(()) 153 | } 154 | -------------------------------------------------------------------------------- /src/downloads.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use anyhow::Error; 7 | use url::Url; 8 | 9 | pub fn find_downloads_in_folder( 10 | download_dir: &Path, 11 | is_match: impl Fn(&Url, &Path) -> bool, 12 | ) -> Result, Error> { 13 | let mut matches = vec![]; 14 | 15 | for entry in fs::read_dir(download_dir)? { 16 | let entry = entry?; 17 | let attr = xattr::get(entry.path(), "com.apple.metadata:kMDItemWhereFroms"); 18 | if let Ok(Some(encoded_plist)) = attr { 19 | let might_be_urls: Vec = plist::from_bytes(&encoded_plist)?; 20 | let parsed_urls = might_be_urls 21 | .into_iter() 22 | .filter_map(|x| Url::parse(&x).ok()) 23 | .collect::>(); 24 | if let Some(source) = parsed_urls 25 | .into_iter() 26 | .find(|url| is_match(url, &entry.path())) 27 | { 28 | matches.push((entry.path().to_owned(), source)); 29 | } 30 | } 31 | } 32 | 33 | matches.sort_by_cached_key(|x| x.0.file_name().map(|x| x.to_owned())); 34 | 35 | Ok(matches) 36 | } 37 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Write, 3 | fs, 4 | path::{Path, PathBuf}, 5 | process::Command, 6 | time::{Duration, SystemTime}, 7 | }; 8 | 9 | use anyhow::{anyhow, bail, Error}; 10 | use chrono::{DateTime, Utc}; 11 | use dialoguer::Password; 12 | use structopt::StructOpt; 13 | use uuid::Uuid; 14 | use which::which; 15 | 16 | use crate::{ 17 | cli::{ 18 | Commands, CronCommand, EjectCommand, FindDownloadsCommand, ImageOptions, ImportCommand, 19 | ListCommand, NewCommand, 20 | }, 21 | downloads::find_downloads_in_folder, 22 | }; 23 | 24 | mod cli; 25 | mod dmg; 26 | mod downloads; 27 | mod zip; 28 | 29 | #[derive(Debug)] 30 | struct PrepareResult { 31 | password: String, 32 | dmg_path: PathBuf, 33 | mounted_at: PathBuf, 34 | } 35 | 36 | fn prepare_dmg( 37 | opts: &ImageOptions, 38 | size: usize, 39 | source_path: Option<&Path>, 40 | ) -> Result { 41 | let password = match opts.password { 42 | Some(ref password) => password.clone(), 43 | None => { 44 | if !opts.keep_dmg && source_path.is_none() { 45 | Uuid::new_v4().simple().to_string() 46 | } else { 47 | Password::new().with_prompt("password").interact()? 48 | } 49 | } 50 | }; 51 | 52 | let volume_name = match opts.volume_name { 53 | Some(ref name) => name.as_str(), 54 | None => source_path 55 | .and_then(|x| x.file_stem().and_then(|x| x.to_str())) 56 | .unwrap_or("EncryptedScratchpad"), 57 | }; 58 | let dmg_path = 59 | std::env::temp_dir().join(format!("encrypted-{}-{}.dmg", Uuid::new_v4(), volume_name)); 60 | 61 | println!("[1] Creating encrypted DMG"); 62 | dmg::make_dmg(&dmg_path, volume_name, size, &password)?; 63 | println!("[2] Mounting DMG"); 64 | let mounted_at = dmg::mount_dmg(&dmg_path, &password)?; 65 | println!("[3] Securing mounted volume"); 66 | dmg::secure_volume(&mounted_at, opts.days)?; 67 | 68 | Ok(PrepareResult { 69 | password, 70 | dmg_path, 71 | mounted_at, 72 | }) 73 | } 74 | 75 | fn finalize_dmg(opts: &ImageOptions, result: &PrepareResult) -> Result<(), Error> { 76 | if opts.keep_dmg { 77 | println!("Placed encrypted DMG at: {}", result.dmg_path.display()); 78 | } else { 79 | fs::remove_file(&result.dmg_path)?; 80 | } 81 | println!("Mounted encrypted DMG at: {}", result.mounted_at.display()); 82 | println!("Ummount with: umount \"{}\"", result.mounted_at.display()); 83 | Ok(()) 84 | } 85 | 86 | fn new_command(args: NewCommand) -> Result<(), Error> { 87 | let result = prepare_dmg(&args.image_opts, args.size, None)?; 88 | finalize_dmg(&args.image_opts, &result)?; 89 | Ok(()) 90 | } 91 | 92 | fn import_command(args: ImportCommand) -> Result<(), Error> { 93 | let input_path = fs::canonicalize(&args.path)?; 94 | 95 | if !fs::metadata(&input_path).map_or(false, |x| x.is_file()) { 96 | bail!("source archive is not a file"); 97 | } 98 | let size = zip::get_uncompressed_zip_size(&input_path)? + args.extra_size; 99 | let result = prepare_dmg(&args.image_opts, size, Some(&input_path))?; 100 | if !zip::check_password(&input_path, &result.password)? { 101 | bail!("invalid password"); 102 | } 103 | println!("[4] Extracting encrypted zip"); 104 | zip::extract(&input_path, &result.mounted_at, &result.password)?; 105 | finalize_dmg(&args.image_opts, &result)?; 106 | Ok(()) 107 | } 108 | 109 | fn list_command(args: ListCommand) -> Result<(), Error> { 110 | let encrypted_volumes = dmg::list_volumes()?; 111 | for (mount_point, expires) in encrypted_volumes { 112 | println!("{}", mount_point.display()); 113 | if args.verbose { 114 | println!( 115 | " expires: {}", 116 | chrono::DateTime::::from(expires) 117 | ); 118 | } 119 | } 120 | Ok(()) 121 | } 122 | 123 | fn eject_command(args: EjectCommand) -> Result<(), Error> { 124 | let encrypted_volumes = dmg::list_volumes()?; 125 | let reference_path = args.path.as_ref().and_then(|x| fs::canonicalize(x).ok()); 126 | let mut image_found = false; 127 | 128 | let now = SystemTime::now(); 129 | for (mount_point, expires) in encrypted_volumes { 130 | let expired = expires < now; 131 | let is_match = 132 | reference_path.is_some() && fs::canonicalize(&mount_point).ok() == reference_path; 133 | if (args.expired && expired) || args.all || is_match { 134 | println!( 135 | "Ejecting {}volume {}", 136 | if expired { "expired " } else { "" }, 137 | mount_point.display() 138 | ); 139 | dmg::eject(&mount_point)?; 140 | } 141 | if is_match { 142 | image_found = true; 143 | } 144 | } 145 | 146 | if !image_found && args.path.is_some() { 147 | bail!("volume was not mounted"); 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | fn cron_command(args: CronCommand) -> Result<(), Error> { 154 | Command::new("crontab") 155 | .arg("-e") 156 | .env( 157 | "CRONTAB_MODE", 158 | if args.install { "install" } else { "uninstall" }, 159 | ) 160 | .env("EDITOR", std::env::current_exe()?) 161 | .spawn()? 162 | .wait()?; 163 | Ok(()) 164 | } 165 | 166 | fn matches_domain(pattern: &str, target: &str) -> bool { 167 | if let Some(rest) = pattern.strip_prefix("*.") { 168 | target == rest 169 | || target.ends_with(rest) 170 | && target.as_bytes().get(target.len() - rest.len() - 1) == Some(&b'.') 171 | } else { 172 | target == pattern 173 | } 174 | } 175 | 176 | fn find_downloads_command(args: FindDownloadsCommand) -> Result<(), Error> { 177 | let dirs = directories::UserDirs::new(); 178 | let download_dir = match args.path { 179 | Some(ref path) => path.as_path(), 180 | None => dirs 181 | .as_ref() 182 | .and_then(|x| x.download_dir()) 183 | .ok_or_else(|| anyhow!("could not find download dir"))?, 184 | }; 185 | 186 | let domains = &args.domains; 187 | let date_threshold = args 188 | .days 189 | .map(|days| SystemTime::now() - Duration::from_secs(60 * 60 * 24 * days as u64)); 190 | let matches = find_downloads_in_folder(download_dir, move |url, path| { 191 | if let Some(threshold) = date_threshold { 192 | if let Some(date) = fs::metadata(path).ok().and_then(|x| x.created().ok()) { 193 | if date >= threshold { 194 | return false; 195 | } 196 | } 197 | } 198 | domains.is_empty() 199 | || domains.iter().any(|x| { 200 | url.domain() 201 | .map_or(false, |domain| matches_domain(x.as_str(), domain)) 202 | }) 203 | })?; 204 | 205 | for (path, source) in &matches { 206 | println!("{}", path.display()); 207 | if args.verbose { 208 | println!(" source: {}", source); 209 | let created = fs::metadata(path) 210 | .and_then(|x| x.created()) 211 | .map(DateTime::::from); 212 | if let Ok(created) = created { 213 | println!(" created: {}", created); 214 | } 215 | } 216 | } 217 | 218 | if args.delete { 219 | for (path, _) in &matches { 220 | fs::remove_file(path).ok(); 221 | } 222 | println!( 223 | "Deleted {} file{}", 224 | matches.len(), 225 | if matches.len() == 1 { "" } else { "s" } 226 | ); 227 | } 228 | 229 | Ok(()) 230 | } 231 | 232 | fn do_cronedit() -> Result { 233 | let mut cron = String::new(); 234 | let add = match std::env::var("CRONTAB_MODE").as_deref() { 235 | Ok("install") => true, 236 | Ok("uninstall") => false, 237 | _ => return Ok(false), 238 | }; 239 | 240 | let path = std::env::args_os().nth(1).unwrap(); 241 | let executable = std::env::current_exe()?; 242 | let cron_cmd = format!("{} eject --expired", executable.display()); 243 | let mut found = false; 244 | 245 | for line in fs::read_to_string(&path)?.lines() { 246 | if line.trim().ends_with(&cron_cmd) { 247 | found = true; 248 | if !add { 249 | continue; 250 | } 251 | } 252 | writeln!(cron, "{}", line)?; 253 | } 254 | 255 | if add && !found { 256 | writeln!(cron, "0 * * * * {}", cron_cmd)?; 257 | } 258 | 259 | fs::write(&path, cron)?; 260 | 261 | Ok(true) 262 | } 263 | 264 | fn main() -> Result<(), Error> { 265 | if do_cronedit()? { 266 | return Ok(()); 267 | } 268 | 269 | let commands = Commands::from_args(); 270 | 271 | if which("7z").is_err() { 272 | bail!("7z is not available"); 273 | } 274 | 275 | match commands { 276 | Commands::New(args) => new_command(args), 277 | Commands::Import(args) => import_command(args), 278 | Commands::List(args) => list_command(args), 279 | Commands::Eject(args) => eject_command(args), 280 | Commands::Cron(args) => cron_command(args), 281 | Commands::FindDownloads(args) => find_downloads_command(args), 282 | } 283 | } 284 | 285 | #[test] 286 | fn test_matches_domain() { 287 | assert!(matches_domain("*.sentry.io", "sentry.io")); 288 | assert!(matches_domain("*.sentry.io", "whatever.sentry.io")); 289 | assert!(matches_domain("*.sentry.io", "whatever.else.sentry.io")); 290 | assert!(!matches_domain("*.sentry.io", "whatever.else.sentry.com")); 291 | } 292 | -------------------------------------------------------------------------------- /src/zip.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::Path, 3 | process::{Command, Stdio}, 4 | }; 5 | 6 | use anyhow::Error; 7 | 8 | pub fn get_uncompressed_zip_size(path: &Path) -> Result { 9 | let child = Command::new("7z") 10 | .arg("l") 11 | .arg(path) 12 | .stdout(Stdio::piped()) 13 | .spawn()? 14 | .wait_with_output()?; 15 | let output = std::str::from_utf8(&child.stdout)?; 16 | let last_line = output.trim().lines().last().unwrap(); 17 | let bytes: u64 = last_line.split_ascii_whitespace().nth(2).unwrap().parse()?; 18 | Ok((bytes / 1024) as usize + 1) 19 | } 20 | 21 | pub fn check_password(path: &Path, password: &str) -> Result { 22 | let child = Command::new("7z") 23 | .arg("t") 24 | .arg(&format!("-p{}", password)) 25 | .arg(path) 26 | .stdout(Stdio::piped()) 27 | .stderr(Stdio::piped()) 28 | .spawn()? 29 | .wait_with_output()?; 30 | let output = std::str::from_utf8(&child.stdout)?; 31 | let err = std::str::from_utf8(&child.stderr)?; 32 | Ok(!err.contains("ERROR: Wrong password") && output.contains("Everything is Ok")) 33 | } 34 | 35 | pub fn extract(src: &Path, dst: &Path, password: &str) -> Result<(), Error> { 36 | Command::new("7z") 37 | .arg("x") 38 | .arg("-bsp2") 39 | .arg(&format!("-p{}", password)) 40 | .arg("-y") 41 | .arg(src) 42 | .current_dir(dst) 43 | .stdout(Stdio::null()) 44 | .spawn()? 45 | .wait()?; 46 | Ok(()) 47 | } 48 | --------------------------------------------------------------------------------