├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── algorithm.rs ├── clear_on_drop.rs ├── config.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aho-corasick" 3 | version = "0.6.8" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | dependencies = [ 6 | "memchr 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "ansi_term" 11 | version = "0.11.0" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "arrayvec" 19 | version = "0.4.7" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | dependencies = [ 22 | "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 23 | ] 24 | 25 | [[package]] 26 | name = "atty" 27 | version = "0.2.11" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | dependencies = [ 30 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 31 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 33 | ] 34 | 35 | [[package]] 36 | name = "bit-set" 37 | version = "0.4.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | dependencies = [ 40 | "bit-vec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 41 | ] 42 | 43 | [[package]] 44 | name = "bit-vec" 45 | version = "0.4.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | 48 | [[package]] 49 | name = "bitflags" 50 | version = "1.0.4" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | 53 | [[package]] 54 | name = "byteorder" 55 | version = "1.2.6" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "cfg-if" 60 | version = "0.1.5" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | 63 | [[package]] 64 | name = "clap" 65 | version = "2.32.0" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | dependencies = [ 68 | "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 75 | ] 76 | 77 | [[package]] 78 | name = "conv" 79 | version = "0.3.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | dependencies = [ 82 | "custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 83 | ] 84 | 85 | [[package]] 86 | name = "crossbeam-deque" 87 | version = "0.2.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | dependencies = [ 90 | "crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 91 | "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 92 | ] 93 | 94 | [[package]] 95 | name = "crossbeam-epoch" 96 | version = "0.3.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | dependencies = [ 99 | "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 102 | "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 104 | "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 105 | "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 106 | ] 107 | 108 | [[package]] 109 | name = "crossbeam-utils" 110 | version = "0.2.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | dependencies = [ 113 | "cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 114 | ] 115 | 116 | [[package]] 117 | name = "custom_derive" 118 | version = "0.1.7" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | 121 | [[package]] 122 | name = "data-encoding" 123 | version = "1.2.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | 126 | [[package]] 127 | name = "data-encoding" 128 | version = "2.1.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | 131 | [[package]] 132 | name = "derive_builder" 133 | version = "0.5.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | dependencies = [ 136 | "derive_builder_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 138 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 139 | ] 140 | 141 | [[package]] 142 | name = "derive_builder_core" 143 | version = "0.2.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | dependencies = [ 146 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "either" 152 | version = "1.5.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | 155 | [[package]] 156 | name = "errno" 157 | version = "0.2.4" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | dependencies = [ 160 | "errno-dragonfly 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 161 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 162 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 163 | ] 164 | 165 | [[package]] 166 | name = "errno-dragonfly" 167 | version = "0.1.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | dependencies = [ 170 | "gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 172 | ] 173 | 174 | [[package]] 175 | name = "fancy-regex" 176 | version = "0.1.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | dependencies = [ 179 | "bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 180 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 183 | ] 184 | 185 | [[package]] 186 | name = "gcc" 187 | version = "0.3.54" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | 190 | [[package]] 191 | name = "itertools" 192 | version = "0.7.8" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | dependencies = [ 195 | "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 196 | ] 197 | 198 | [[package]] 199 | name = "kernel32-sys" 200 | version = "0.2.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | dependencies = [ 203 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 204 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 205 | ] 206 | 207 | [[package]] 208 | name = "lazy_static" 209 | version = "0.2.11" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | 212 | [[package]] 213 | name = "lazy_static" 214 | version = "1.1.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | dependencies = [ 217 | "version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 218 | ] 219 | 220 | [[package]] 221 | name = "libc" 222 | version = "0.2.43" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | 225 | [[package]] 226 | name = "memchr" 227 | version = "1.0.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | dependencies = [ 230 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 231 | ] 232 | 233 | [[package]] 234 | name = "memchr" 235 | version = "2.0.2" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | dependencies = [ 238 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 239 | ] 240 | 241 | [[package]] 242 | name = "memoffset" 243 | version = "0.2.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | 246 | [[package]] 247 | name = "mpw" 248 | version = "0.0.1" 249 | dependencies = [ 250 | "byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", 252 | "conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 253 | "data-encoding 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 254 | "errno 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 255 | "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 256 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 258 | "ring-pwhash 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", 259 | "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 260 | "serde 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", 261 | "serde_derive 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", 262 | "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 263 | "zxcvbn 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 264 | ] 265 | 266 | [[package]] 267 | name = "nodrop" 268 | version = "0.1.12" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | 271 | [[package]] 272 | name = "num_cpus" 273 | version = "1.8.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | dependencies = [ 276 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 277 | ] 278 | 279 | [[package]] 280 | name = "proc-macro2" 281 | version = "0.4.19" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | dependencies = [ 284 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 285 | ] 286 | 287 | [[package]] 288 | name = "quick-error" 289 | version = "1.2.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | 292 | [[package]] 293 | name = "quote" 294 | version = "0.3.15" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | 297 | [[package]] 298 | name = "quote" 299 | version = "0.6.8" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | dependencies = [ 302 | "proc-macro2 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", 303 | ] 304 | 305 | [[package]] 306 | name = "rayon" 307 | version = "0.8.2" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | dependencies = [ 310 | "rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", 311 | ] 312 | 313 | [[package]] 314 | name = "rayon-core" 315 | version = "1.4.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | dependencies = [ 318 | "crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 319 | "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 320 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 321 | "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 322 | ] 323 | 324 | [[package]] 325 | name = "redox_syscall" 326 | version = "0.1.40" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | 329 | [[package]] 330 | name = "redox_termios" 331 | version = "0.1.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | dependencies = [ 334 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 335 | ] 336 | 337 | [[package]] 338 | name = "regex" 339 | version = "0.2.11" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | dependencies = [ 342 | "aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", 343 | "memchr 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 344 | "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", 345 | "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 346 | "utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 347 | ] 348 | 349 | [[package]] 350 | name = "regex" 351 | version = "1.0.5" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | dependencies = [ 354 | "aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", 355 | "memchr 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 356 | "regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", 357 | "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 358 | "utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 359 | ] 360 | 361 | [[package]] 362 | name = "regex-syntax" 363 | version = "0.5.6" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | dependencies = [ 366 | "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 367 | ] 368 | 369 | [[package]] 370 | name = "regex-syntax" 371 | version = "0.6.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | dependencies = [ 374 | "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 375 | ] 376 | 377 | [[package]] 378 | name = "ring" 379 | version = "0.12.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | dependencies = [ 382 | "gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)", 383 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 384 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 385 | "rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 386 | "untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 387 | ] 388 | 389 | [[package]] 390 | name = "ring-pwhash" 391 | version = "0.12.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | dependencies = [ 394 | "data-encoding 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 395 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 396 | "ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 397 | ] 398 | 399 | [[package]] 400 | name = "rpassword" 401 | version = "2.0.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | dependencies = [ 404 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 405 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 406 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 407 | ] 408 | 409 | [[package]] 410 | name = "scopeguard" 411 | version = "0.3.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | 414 | [[package]] 415 | name = "serde" 416 | version = "1.0.78" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | 419 | [[package]] 420 | name = "serde_derive" 421 | version = "1.0.78" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | dependencies = [ 424 | "proc-macro2 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", 425 | "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", 426 | "syn 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", 427 | ] 428 | 429 | [[package]] 430 | name = "strsim" 431 | version = "0.7.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | 434 | [[package]] 435 | name = "syn" 436 | version = "0.11.11" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | dependencies = [ 439 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 440 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 441 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 442 | ] 443 | 444 | [[package]] 445 | name = "syn" 446 | version = "0.15.3" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | dependencies = [ 449 | "proc-macro2 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", 450 | "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", 451 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 452 | ] 453 | 454 | [[package]] 455 | name = "synom" 456 | version = "0.11.3" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | dependencies = [ 459 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 460 | ] 461 | 462 | [[package]] 463 | name = "termion" 464 | version = "1.5.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | dependencies = [ 467 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 468 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 469 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 470 | ] 471 | 472 | [[package]] 473 | name = "textwrap" 474 | version = "0.10.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | dependencies = [ 477 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 478 | ] 479 | 480 | [[package]] 481 | name = "thread_local" 482 | version = "0.3.6" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | dependencies = [ 485 | "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 486 | ] 487 | 488 | [[package]] 489 | name = "time" 490 | version = "0.1.40" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | dependencies = [ 493 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 494 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 495 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 496 | ] 497 | 498 | [[package]] 499 | name = "toml" 500 | version = "0.4.6" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | dependencies = [ 503 | "serde 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", 504 | ] 505 | 506 | [[package]] 507 | name = "ucd-util" 508 | version = "0.1.1" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | 511 | [[package]] 512 | name = "unicode-width" 513 | version = "0.1.5" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | 516 | [[package]] 517 | name = "unicode-xid" 518 | version = "0.0.4" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | 521 | [[package]] 522 | name = "unicode-xid" 523 | version = "0.1.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | 526 | [[package]] 527 | name = "untrusted" 528 | version = "0.5.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | 531 | [[package]] 532 | name = "utf8-ranges" 533 | version = "1.0.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | 536 | [[package]] 537 | name = "vec_map" 538 | version = "0.8.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | 541 | [[package]] 542 | name = "version_check" 543 | version = "0.1.4" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | 546 | [[package]] 547 | name = "winapi" 548 | version = "0.2.8" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | 551 | [[package]] 552 | name = "winapi" 553 | version = "0.3.5" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | dependencies = [ 556 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 557 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 558 | ] 559 | 560 | [[package]] 561 | name = "winapi-build" 562 | version = "0.1.1" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | 565 | [[package]] 566 | name = "winapi-i686-pc-windows-gnu" 567 | version = "0.4.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | 570 | [[package]] 571 | name = "winapi-x86_64-pc-windows-gnu" 572 | version = "0.4.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | 575 | [[package]] 576 | name = "zxcvbn" 577 | version = "1.0.1" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | dependencies = [ 580 | "derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 581 | "fancy-regex 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 582 | "itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", 583 | "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 584 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 585 | "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 586 | "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 587 | ] 588 | 589 | [metadata] 590 | "checksum aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "68f56c7353e5a9547cbd76ed90f7bb5ffc3ba09d4ea9bd1d8c06c8b1142eeb5a" 591 | "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 592 | "checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" 593 | "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 594 | "checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" 595 | "checksum bit-vec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b4ff8b16e6076c3e14220b39fbc1fabb6737522281a388998046859400895f" 596 | "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 597 | "checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781" 598 | "checksum cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3" 599 | "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" 600 | "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" 601 | "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" 602 | "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150" 603 | "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" 604 | "checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" 605 | "checksum data-encoding 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d867ddbf09de0b73e09ec798972fb7f870495a0893f6f736c1855448c5a56789" 606 | "checksum data-encoding 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "67df0571a74bf0d97fb8b2ed22abdd9a48475c96bd327db968b7d9cace99655e" 607 | "checksum derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c998e6ab02a828dd9735c18f154e14100e674ed08cb4e1938f0e4177543f439" 608 | "checksum derive_builder_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "735e24ee9e5fa8e16b86da5007856e97d592e11867e45d76e0c0d0a164a0b757" 609 | "checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" 610 | "checksum errno 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2a071601ed01b988f896ab14b95e67335d1eeb50190932a1320f7fe3cadc84e" 611 | "checksum errno-dragonfly 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" 612 | "checksum fancy-regex 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0decc1ae13c103787f0a543307ee8dac9deceecdb463aceaff35bbb7068d86c2" 613 | "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" 614 | "checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450" 615 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 616 | "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 617 | "checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7" 618 | "checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" 619 | "checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" 620 | "checksum memchr 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a3b4142ab8738a78c51896f704f83c11df047ff1bda9a92a661aa6361552d93d" 621 | "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" 622 | "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" 623 | "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" 624 | "checksum proc-macro2 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "ffe022fb8c8bd254524b0b3305906c1921fa37a84a644e29079a9e62200c3901" 625 | "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" 626 | "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 627 | "checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5" 628 | "checksum rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b614fe08b6665cb9a231d07ac1364b0ef3cb3698f1239ee0c4c3a88a524f54c8" 629 | "checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356" 630 | "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" 631 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 632 | "checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" 633 | "checksum regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2069749032ea3ec200ca51e4a31df41759190a88edca0d2d86ee8bedf7073341" 634 | "checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" 635 | "checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" 636 | "checksum ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f7d28b30a72c01b458428e0ae988d4149c20d902346902be881e3edc4bb325c" 637 | "checksum ring-pwhash 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "394374d86779b376a2a0ff4fa4d080f3993420d3a2c2342492df9852ff78c053" 638 | "checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451" 639 | "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" 640 | "checksum serde 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)" = "92ec94e2754699adddbbc4f555791bd3acc2a2f5574cba16c93a4a9cf4a04415" 641 | "checksum serde_derive 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)" = "0fb622d85245add5327d4f08b2d24fd51fa5d35fe1bba19ee79a1f211e9ac0ff" 642 | "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" 643 | "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 644 | "checksum syn 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e5c1514eb7bb4216fc722b3cd08783d326d7de0d62f6d5e48a774f610bc97cb6" 645 | "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 646 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 647 | "checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" 648 | "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" 649 | "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" 650 | "checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" 651 | "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" 652 | "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 653 | "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 654 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 655 | "checksum untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f392d7819dbe58833e26872f5f6f0d68b7bbbe90fc3667e98731c4a15ad9a7ae" 656 | "checksum utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd70f467df6810094968e2fce0ee1bd0e87157aceb026a8c083bcf5e25b9efe4" 657 | "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 658 | "checksum version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7716c242968ee87e5542f8021178248f267f295a5c4803beae8b8b7fd9bc6051" 659 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 660 | "checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" 661 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 662 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 663 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 664 | "checksum zxcvbn 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "487b0578c3941a953b2fcad6a1fbee09b80746776783d6a8b556ff9c628f07f8" 665 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Vinzent Steinberg "] 3 | description = "A stateless password management solution." 4 | license = "MIT" 5 | name = "mpw" 6 | readme = "README.md" 7 | repository = "https://github.com/vks/mpw-rs" 8 | version = "0.0.1" 9 | 10 | [dependencies] 11 | byteorder = "1" 12 | conv = "0.3.3" 13 | data-encoding = "1" 14 | errno = "0.2" 15 | lazy_static = "1" 16 | libc = "0.2" 17 | ring = "0.12" 18 | ring-pwhash = "0.12" 19 | rpassword = "2" 20 | serde = "1" 21 | serde_derive = "1" 22 | toml = "0.4" 23 | zxcvbn = "1" 24 | 25 | [dependencies.clap] 26 | features = ["unstable", "color"] 27 | version = "2.25" 28 | 29 | [profile] 30 | 31 | [profile.dev] 32 | opt-level = 2 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | THE SOFTWARE IS PROVIDED "AS IS" AND VINZENT STEINBERG AND THE AUTHORS DISCLAIM 2 | ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 3 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VINZENT STEINBERG OR THE AUTHORS 4 | BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 5 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 6 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 7 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | 9 | 10 | # mpw [![Build Status](https://travis-ci.org/vks/mpw-rs.svg?branch=master)](https://travis-ci.org/vks/mpw-rs) 11 | 12 | A stateless password management solution. 13 | 14 | This implements the [Master Password](https://ssl.masterpasswordapp.com/algorithm.html) 15 | algorithm. It derives passwords for websites from your full name, the website's 16 | name and your master password. Because of that, it is not necessary to store 17 | any password. 18 | 19 | ## Status 20 | 21 | This is mostly a toy project. It implements all features of the 22 | [official C implementation](https://github.com/Lyndir/MasterPassword). 23 | Additionally it can store the parameters required to derive the passwords in a 24 | TOML file. It can also store encrypted passwords. (Of course this loses the 25 | advantages over traditional password managers.) 26 | 27 | ## Examples 28 | 29 | You need to specify all parameters for password generation via the command 30 | line. The only exception is the master password, which is read interactively for 31 | security reasons. For instance, let us suppose our full name is "John Doe" and 32 | we want to generate a login password for github.com using the popular master 33 | password "password": 34 | 35 | $ mpw --name "John Doe" github.com 36 | Please enter the master password: 37 | Identicon: ╔░╝⌚ 38 | Password for github.com: VubeNazoRihe4( 39 | 40 | The identicon is generated from your full name and your master password. It 41 | serves as a visual indicator whether you made a typo while entering them. The 42 | generated password is not random, it is deterministically derived from you full 43 | name, your master password and the name of the site. Default parameters were 44 | used when generating the password, you can change them via passing additional 45 | command line parameters. See `mpw --help`. 46 | 47 | For convenience, you can use a file to store the parameters of your passwords. 48 | This is save, because the parameters are assumed to be public information. You 49 | can also store encrypted, user-defined passwords in this file. Doing this 50 | corresponds to using a traditional stateful password manager. An example: 51 | 52 | $ mpw --config passwords.toml --name "John Doe" --add github.com 53 | $ mpw --config passwords.toml --store wikipedia.org 54 | Please enter the master password: 55 | Identicon: ╔░╝⌚ 56 | Please enter the site password to be stored: 57 | $ mpw --config passwords.toml 58 | Please enter the master password: 59 | Identicon: ╔░╝⌚ 60 | Password for github.com: VubeNazoRihe4( 61 | Password for wikipedia.org: secret 62 | $ cat passwords.toml 63 | full_name = "John Doe" 64 | 65 | [[sites]] 66 | name = "github.com" 67 | 68 | [[sites]] 69 | encrypted = "yyCo1ILGvCYn6o8jvcOslbwMaU2Gf02zxoYR2apYc9Fn0s0+HH7Czgk+6slb6Xsz" 70 | name = "wikipedia.org" 71 | type = "stored" 72 | 73 | The keywords in the config are the same as the for long command line parameters. 74 | 75 | ## Comparison to traditional stateful password managers 76 | 77 | ### Advantages 78 | 79 | * Passwords are never stored 80 | * No brute force attacks against stored passwords possible 81 | * No synchronization of devices required 82 | 83 | ### Disadvantages 84 | 85 | * If one password is compromised, your master password can be brute forced 86 | (this is mitigated by a strong key-derivation function) 87 | * Changing the master password requires changing all passwords 88 | * Changing the algorithm requires changing all passwords 89 | (unless you introduce some non-secret state you have to store or remember) 90 | * Changing a site password introduces a counter 91 | (some non-secret state you have to store or remember) 92 | * Losing your master password compromises all passwords 93 | (for traditional managers you would have to lose your master password *and* 94 | the stored passwords) 95 | * You can't use custom passwords 96 | * You can't use custom password generation templates 97 | 98 | -------------------------------------------------------------------------------- /src/algorithm.rs: -------------------------------------------------------------------------------- 1 | //! This implements the Maser Password algorithm. 2 | //! See http://masterpasswordapp.com/algorithm.html. 3 | 4 | extern crate conv; 5 | extern crate ring; 6 | extern crate ring_pwhash; 7 | extern crate data_encoding; 8 | extern crate byteorder; 9 | 10 | use std::convert::{TryInto, TryFrom}; 11 | use std::cmp::max; 12 | use std::io; 13 | use std::io::Write; 14 | use std::error::Error as StdError; 15 | use std::fmt; 16 | 17 | use self::ring::{aead, digest, hmac, rand}; 18 | use self::ring::rand::{SecureRandom, SystemRandom}; 19 | use self::ring_pwhash::scrypt::{scrypt, ScryptParams}; 20 | use self::data_encoding::hex; 21 | use self::byteorder::{BigEndian, WriteBytesExt}; 22 | use self::conv::ValueInto; 23 | 24 | use clear_on_drop::ClearOnDrop; 25 | 26 | lazy_static! { 27 | /// Scrypt parameters used by the Master Password algorithm. 28 | static ref SCRYPT_PARAMS: ScryptParams = ScryptParams::new(15, 8, 2); 29 | } 30 | 31 | /// Represent which variant of password to generate. 32 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 33 | pub enum SiteVariant { 34 | /// Generate the password to login with. 35 | Password, 36 | /// Generate the login name to log in as. 37 | Login, 38 | /// Generate the answer to a security question. 39 | Answer, 40 | } 41 | 42 | impl SiteVariant { 43 | /// Try to construct a SiteVariant from a string. 44 | /// 45 | /// Returns None if the string does not correspond to a variant. 46 | pub fn from_str(s: &str) -> Option { 47 | match s { 48 | "p" | "password" 49 | => Some(SiteVariant::Password), 50 | "l" | "login" 51 | => Some(SiteVariant::Login), 52 | "a" | "answer" 53 | => Some(SiteVariant::Answer), 54 | _ => None, 55 | } 56 | } 57 | } 58 | 59 | impl ::serde::Serialize for SiteVariant { 60 | fn serialize(&self, serializer: S) -> Result 61 | where S: ::serde::Serializer 62 | { 63 | serializer.serialize_str(match *self { 64 | SiteVariant::Password => "password", 65 | SiteVariant::Login => "login", 66 | SiteVariant::Answer => "answer", 67 | }) 68 | } 69 | } 70 | 71 | impl<'de> ::serde::Deserialize<'de> for SiteVariant { 72 | fn deserialize(deserializer: D) -> Result 73 | where D: ::serde::Deserializer<'de> 74 | { 75 | struct Visitor; 76 | 77 | impl<'de> ::serde::de::Visitor<'de> for Visitor { 78 | type Value = SiteVariant; 79 | 80 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 81 | write!(formatter, r#"one of the following strings: "p", "password", "l", "login", "a", "answer""#) 82 | } 83 | 84 | fn visit_str(self, value: &str) -> Result 85 | where E: ::serde::de::Error 86 | { 87 | SiteVariant::from_str(value) 88 | .ok_or_else(|| E::invalid_value(::serde::de::Unexpected::Str(value), &self)) 89 | } 90 | } 91 | 92 | deserializer.deserialize_str(Visitor) 93 | } 94 | } 95 | 96 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 97 | /// Type of the site password. 98 | pub enum SiteType { 99 | GeneratedMaximum, 100 | GeneratedLong, 101 | GeneratedMedium, 102 | GeneratedBasic, 103 | GeneratedShort, 104 | GeneratedPIN, 105 | GeneratedName, 106 | GeneratedPhrase, 107 | Stored, 108 | } 109 | 110 | impl SiteType { 111 | /// Try to construct a SiteType from a string. 112 | /// 113 | /// Returns None if the string does not correspond to a variant. 114 | pub fn from_str(s: &str) -> Option { 115 | match s { 116 | "x" | "max" | "maximum" 117 | => Some(SiteType::GeneratedMaximum), 118 | "l" | "long" 119 | => Some(SiteType::GeneratedLong), 120 | "m" | "med" | "medium" 121 | => Some(SiteType::GeneratedMedium), 122 | "b" | "basic" 123 | => Some(SiteType::GeneratedBasic), 124 | "s" | "short" 125 | => Some(SiteType::GeneratedShort), 126 | "i" | "pin" 127 | => Some(SiteType::GeneratedPIN), 128 | "n" | "name" 129 | => Some(SiteType::GeneratedName), 130 | "p" | "phrase" 131 | => Some(SiteType::GeneratedPhrase), 132 | "stored" 133 | => Some(SiteType::Stored), 134 | _ => None, 135 | } 136 | } 137 | } 138 | 139 | impl ::serde::Serialize for SiteType { 140 | fn serialize(&self, serializer: S) -> Result 141 | where S: ::serde::Serializer 142 | { 143 | serializer.serialize_str(match *self { 144 | SiteType::GeneratedMaximum => "maximum", 145 | SiteType::GeneratedLong => "long", 146 | SiteType::GeneratedMedium => "medium", 147 | SiteType::GeneratedBasic => "basic", 148 | SiteType::GeneratedShort => "short", 149 | SiteType::GeneratedPIN => "pin", 150 | SiteType::GeneratedName => "name", 151 | SiteType::GeneratedPhrase => "phrase", 152 | SiteType::Stored => "stored", 153 | }) 154 | } 155 | } 156 | 157 | impl<'de> ::serde::Deserialize<'de> for SiteType { 158 | fn deserialize(deserializer: D) -> Result 159 | where D: ::serde::Deserializer<'de> 160 | { 161 | struct Visitor; 162 | 163 | impl<'de> ::serde::de::Visitor<'de> for Visitor { 164 | type Value = SiteType; 165 | 166 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 167 | write!(formatter, r#"one of the following strings: "x", "max", "maximum", "l", "long", "m", "med", "medium", "b", "basic", "s", "short", "i", "pin", "n", "name", "p", "phrase", "stored""#) 168 | } 169 | 170 | fn visit_str(self, value: &str) -> Result 171 | where E: ::serde::de::Error 172 | { 173 | SiteType::from_str(value) 174 | .ok_or_else(|| E::invalid_value(::serde::de::Unexpected::Str(value), &self)) 175 | } 176 | } 177 | 178 | deserializer.deserialize_str(Visitor) 179 | } 180 | } 181 | 182 | /// Represent a password variant as a string. 183 | fn scope_for_variant(variant: SiteVariant) -> &'static str { 184 | match variant { 185 | SiteVariant::Password => "com.lyndir.masterpassword", 186 | SiteVariant::Login => "com.lyndir.masterpassword.login", 187 | SiteVariant::Answer => "com.lyndir.masterpassword.answer", 188 | } 189 | } 190 | 191 | /// Master Password algorithm kind of error. 192 | #[derive(Debug, Clone, Copy)] 193 | pub enum ErrorKind { 194 | /// An `std::io::Error` occured. 195 | Io, 196 | /// The full name was longer than 2^32 bytes. 197 | FullNameTooLong, 198 | /// The site name was longer than 2^32 bytes. 199 | SiteNameTooLong, 200 | /// The site context was longer than 2^32 bytes. 201 | SiteContextTooLong, 202 | } 203 | 204 | /// Master Password algorithm error. 205 | #[derive(Debug)] 206 | pub struct Error { 207 | // TODO: maybe rather use Cow? 208 | pub message: String, 209 | pub kind: ErrorKind, 210 | } 211 | 212 | impl From for Error { 213 | fn from(kind: ErrorKind) -> Error { 214 | let message = match kind { 215 | ErrorKind::Io => "IO error", 216 | ErrorKind::FullNameTooLong => "full name too long", 217 | ErrorKind::SiteNameTooLong => "site name too long", 218 | ErrorKind::SiteContextTooLong => "site context too long", 219 | }; 220 | Error { message: message.into(), kind: kind } 221 | } 222 | } 223 | 224 | impl From for Error { 225 | fn from(e: io::Error) -> Error { 226 | Error { 227 | message: e.description().into(), 228 | kind: ErrorKind::Io, 229 | } 230 | } 231 | } 232 | 233 | /// Derive a master key from a full name and a master password. 234 | pub fn master_key_for_user_v3(full_name: &[u8], master_password: &[u8]) 235 | -> Result, Error> 236 | { 237 | let mut master_key_salt = Vec::new(); 238 | master_key_salt.write_all(scope_for_variant(SiteVariant::Password).as_bytes())?; 239 | let master_key_salt_len = full_name.len().try_into().map_err(|_| 240 | Error::from(ErrorKind::FullNameTooLong))?; 241 | master_key_salt.write_u32::(master_key_salt_len)?; 242 | master_key_salt.write_all(full_name)?; 243 | assert!(!master_key_salt.is_empty()); 244 | 245 | let mut master_key = ClearOnDrop::new([0; 64]); 246 | scrypt(master_password, &master_key_salt, &SCRYPT_PARAMS, &mut *master_key); 247 | 248 | Ok(master_key) 249 | } 250 | 251 | /// Deterministially generate a password for a site. 252 | pub fn password_for_site_v3(master_key: &[u8; 64], site_name: &[u8], site_type: SiteType, 253 | site_counter: u32, site_variant: SiteVariant, site_context: &[u8]) 254 | -> Result, Error> 255 | { 256 | let mut site_password_salt = Vec::new(); 257 | let site_scope = scope_for_variant(site_variant).as_bytes(); 258 | site_password_salt.write_all(site_scope)?; 259 | let site_name_len = site_name.len().try_into().map_err(|_| 260 | Error::from(ErrorKind::SiteNameTooLong))?; 261 | site_password_salt.write_u32::(site_name_len)?; 262 | site_password_salt.write_all(site_name)?; 263 | site_password_salt.write_u32::(site_counter)?; 264 | if !site_context.is_empty() { 265 | let site_context_len = site_context.len().try_into().map_err(|_| 266 | Error::from(ErrorKind::SiteContextTooLong))?; 267 | site_password_salt.write_u32::(site_context_len)?; 268 | site_password_salt.write_all(site_context)?; 269 | } 270 | debug_assert!(!site_password_salt.is_empty()); 271 | 272 | let signing_key = hmac::SigningKey::new(&digest::SHA256, master_key); 273 | let digest = hmac::sign(&signing_key, &site_password_salt); 274 | let site_password_seed = digest.as_ref(); 275 | debug_assert!(!site_password_seed.is_empty()); 276 | 277 | // Encode the password from the seed using the template. 278 | let site_password = generate_password(site_type, &site_password_seed); 279 | 280 | Ok(site_password) 281 | } 282 | 283 | /// Generate a password for the given site type from a given seed. 284 | fn generate_password(site_type: SiteType, seed: &[u8]) -> ClearOnDrop { 285 | let template = template_for_type(site_type, seed[0]); 286 | if template.len() >= seed.len() { 287 | panic!(format!("template too long for given password seed: {} >= {}", 288 | template.len(), seed.len())); 289 | } 290 | let mut password = ClearOnDrop::new(String::with_capacity(template.len())); 291 | for (i, c) in template.chars().enumerate() { 292 | password.push( 293 | character_from_class(c, seed[i + 1]) 294 | ); 295 | } 296 | 297 | password 298 | } 299 | 300 | /// Generate a random password for the given site type. 301 | pub fn random_password_for_site(rng: &SystemRandom, site_type: SiteType) -> Result, ()> { 302 | let mut seed = ClearOnDrop::new(vec![0; 21]); 303 | rng.fill(seed.as_mut()).map_err(|_| ())?; 304 | Ok(generate_password(site_type, &seed)) 305 | } 306 | 307 | /// Return an array of internal strings that express the template to use for the given type. 308 | fn templates_for_type(ty: SiteType) -> Vec<&'static str> { 309 | match ty { 310 | SiteType::GeneratedMaximum => vec![ 311 | "anoxxxxxxxxxxxxxxxxx", "axxxxxxxxxxxxxxxxxno" 312 | ], 313 | SiteType::GeneratedLong => vec![ 314 | "CvcvnoCvcvCvcv", "CvcvCvcvnoCvcv", "CvcvCvcvCvcvno", "CvccnoCvcvCvcv", 315 | "CvccCvcvnoCvcv", "CvccCvcvCvcvno", "CvcvnoCvccCvcv", "CvcvCvccnoCvcv", 316 | "CvcvCvccCvcvno", "CvcvnoCvcvCvcc", "CvcvCvcvnoCvcc", "CvcvCvcvCvccno", 317 | "CvccnoCvccCvcv", "CvccCvccnoCvcv", "CvccCvccCvcvno", "CvcvnoCvccCvcc", 318 | "CvcvCvccnoCvcc", "CvcvCvccCvccno", "CvccnoCvcvCvcc", "CvccCvcvnoCvcc", 319 | "CvccCvcvCvccno" 320 | ], 321 | SiteType::GeneratedMedium => vec![ 322 | "CvcnoCvc", "CvcCvcno" 323 | ], 324 | SiteType::GeneratedBasic => vec![ 325 | "aaanaaan", "aannaaan", "aaannaaa" 326 | ], 327 | SiteType::GeneratedShort => vec![ 328 | "Cvcn", 329 | ], 330 | SiteType::GeneratedPIN => vec![ 331 | "nnnn", 332 | ], 333 | SiteType::GeneratedName => vec![ 334 | "cvccvcvcv", 335 | ], 336 | SiteType::GeneratedPhrase => vec![ 337 | "cvcc cvc cvccvcv cvc", "cvc cvccvcvcv cvcv", "cv cvccv cvc cvcvccv", 338 | ], 339 | SiteType::Stored 340 | => panic!("Expected generated type"), 341 | } 342 | } 343 | 344 | /// Return an internal string that contains the password encoding template of the given type. 345 | fn template_for_type(ty: SiteType, seed_byte: u8) -> &'static str { 346 | let templates = templates_for_type(ty); 347 | let count = u8::try_from(templates.len()).unwrap(); 348 | //^ This unwrap is safe, because the templates are hardcoded and much shorter than 256 349 | // characters. 350 | templates[usize::from(seed_byte % count)] 351 | } 352 | 353 | /// Return an internal string that contains all the characters occuring in the given class. 354 | /// 355 | /// - 'V': uppercase vowel 356 | /// - 'C': uppercase consonant 357 | /// - 'v': lowercase vowel 358 | /// - 'c': lowercase consonant 359 | /// - 'A': upper case letter 360 | /// - 'a': letter (any case) 361 | /// - 'n': digit 362 | /// - 'o': special symbol 363 | /// - 'x': letter (any case) or digit or special symbol 364 | fn characters_in_class(class: char) -> &'static str { 365 | match class { 366 | 'V' => "AEIOU", 367 | 'C' => "BCDFGHJKLMNPQRSTVWXYZ", 368 | 'v' => "aeiou", 369 | 'c' => "bcdfghjklmnpqrstvwxyz", 370 | 'A' => "AEIOUBCDFGHJKLMNPQRSTVWXYZ", 371 | 'a' => "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", 372 | 'n' => "0123456789", 373 | 'o' => "@&%?,=[]_:-+*$#!'^~;()/.", 374 | 'x' => "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()", 375 | ' ' => " ", 376 | _ => panic!("Unknown character class"), 377 | } 378 | } 379 | 380 | /// Calculate the bits of entropy of a given template. 381 | fn entropy_of_template(template: &str) -> f64 { 382 | let mut bits = 0.; 383 | for class in template.chars() { 384 | let possibilities: f64 = characters_in_class(class).len().value_into() 385 | .expect("failed to convert `usize` to `f64`"); 386 | bits += possibilities.log2(); 387 | } 388 | bits 389 | } 390 | 391 | /// Return a character from given character class that encodes the given byte. 392 | fn character_from_class(class: char, seed_byte: u8) -> char { 393 | let class_chars = characters_in_class(class); 394 | let index = usize::from(seed_byte % u8::try_from(class_chars.len()).unwrap()); 395 | class_chars.chars().nth(index).unwrap() 396 | //^ These unwraps are save, because the character classes are hardcoded and shorter than 256 397 | // characters. 398 | } 399 | 400 | /// Encode a fingerprint for a buffer. 401 | pub fn id_for_buf(buf: &[u8]) -> String { 402 | let digest = digest::digest(&digest::SHA256, buf); 403 | hex::encode(digest.as_ref()) 404 | } 405 | 406 | /// Encode a visual fingerprint for a user. 407 | pub fn identicon(full_name: &[u8], master_password: &[u8]) -> String { 408 | let left_arm = [ "╔", "╚", "╰", "═" ]; 409 | let right_arm = [ "╗", "╝", "╯", "═" ]; 410 | let body = [ "█", "░", "▒", "▓", "☺", "☻" ]; 411 | let accessory = [ 412 | "◈", "◎", "◐", "◑", "◒", "◓", "☀", "☁", "☂", "☃", "☄", "★", "☆", "☎", 413 | "☏", "⎈", "⌂", "☘", "☢", "☣", "☕", "⌚", "⌛", "⏰", "⚡", "⛄", "⛅", "☔", 414 | "♔", "♕", "♖", "♗", "♘", "♙", "♚", "♛", "♜", "♝", "♞", "♟", "♨", "♩", 415 | "♪", "♫", "⚐", "⚑", "⚔", "⚖", "⚙", "⚠", "⌘", "⏎", "✄", "✆", "✈", "✉", "✌" 416 | ]; 417 | 418 | let signing_key = hmac::SigningKey::new(&digest::SHA256, master_password); 419 | let digest = hmac::sign(&signing_key, full_name); 420 | let identicon_seed = digest.as_ref(); 421 | 422 | // TODO color 423 | 424 | let get_part = |set: &[&'static str], seed: u8| { 425 | set[usize::from(seed % u8::try_from(set.len()).unwrap())] 426 | //^ This unwrap is safe, because the sets are short and hardcoded above. 427 | }; 428 | let mut identicon = String::with_capacity(256); 429 | identicon.push_str(get_part(&left_arm[..], identicon_seed[0])); 430 | identicon.push_str(get_part(&body[..], identicon_seed[1])); 431 | identicon.push_str(get_part(&right_arm[..], identicon_seed[2])); 432 | identicon.push_str(get_part(&accessory[..], identicon_seed[3])); 433 | identicon 434 | } 435 | 436 | /// Length of the nonce of the used encryption algorithm (chacha20). 437 | const NONCE_LEN: usize = 12; 438 | /// Length to which short passwords are padded before encryption. 439 | /// 440 | /// This is chosen to be the same length as the longest generated password. 441 | /// Note that this has to be smaller than 256 due to how he padding is done. 442 | const PAD_LEN: usize = 20; 443 | 444 | /// Calculate the length of the clear text after padding. 445 | fn padded_len(clear_text_len: usize) -> usize { 446 | max(clear_text_len + 1, PAD_LEN) 447 | } 448 | 449 | /// Calculate the minimal length of the encryption buffer. 450 | pub fn min_buffer_len(clear_text_len: usize) -> usize { 451 | padded_len(clear_text_len) + NONCE_LEN + aead::MAX_TAG_LEN 452 | } 453 | 454 | /// Pad the password of length `len` to a minimal length `PAD_LEN`. 455 | /// 456 | /// Panics if the buffer is too short. 457 | /// 458 | /// This is to avoid making it possible to gain information on the length of 459 | /// short passwords. 460 | fn pad(buf: &mut [u8], len: usize) { 461 | let make_message = |need, got| 462 | format!("padding buffer too short: need {}, got {}", need, got); 463 | assert!(buf.len() >= PAD_LEN, make_message(PAD_LEN, buf.len())); 464 | assert!(buf.len() >= len + 1, make_message(len + 1, buf.len())); 465 | let padding_byte = if len >= PAD_LEN { 0 } else { (PAD_LEN - len).try_into().unwrap() }; 466 | //^ This unwrap is safe, because `PAD_LEN` is small. 467 | for b in &mut buf[len..] { 468 | *b = padding_byte; 469 | } 470 | } 471 | 472 | /// Remove the padding from a password. 473 | /// 474 | /// This is the inverse of `pad`. 475 | fn unpad(buf: &[u8]) -> &[u8] { 476 | let padding_byte = buf[buf.len() - 1]; 477 | let padding_size = usize::from(padding_byte); 478 | for byte in &buf[buf.len() - padding_size..] { 479 | debug_assert_eq!(*byte, padding_byte); 480 | } 481 | if padding_byte != 0 { 482 | &buf[0..buf.len() - padding_size] 483 | } else { 484 | &buf[0..buf.len() - 1] 485 | } 486 | } 487 | 488 | /// Encrypt data using the master key. 489 | /// 490 | /// This is not specified by the Master Password algorithm. 491 | pub fn encrypt(clear_text: &[u8], master_key: &[u8; 64], buffer: &mut [u8]) { 492 | assert!(buffer.len() >= min_buffer_len(clear_text.len())); 493 | 494 | { 495 | let (mut nonce, mut rest) = buffer.split_at_mut(NONCE_LEN); 496 | 497 | let rng = rand::SystemRandom::new(); 498 | rng.fill(nonce).expect("failed to generate random nonce"); 499 | 500 | { 501 | let (mut input, _) = rest.split_at_mut(clear_text.len()); 502 | input.clone_from_slice(clear_text); 503 | } 504 | 505 | // Pad short passwords so their length cannot be guessed by looking 506 | // at the cipher text. 507 | let (mut input, _) = rest.split_at_mut(padded_len(clear_text.len())); 508 | pad(&mut input, clear_text.len()); 509 | } 510 | 511 | let key = aead::SealingKey::new(&aead::CHACHA20_POLY1305, &master_key[0..32]) 512 | .expect("invalid CHACHA20_POLY1305 key"); 513 | let (nonce, mut in_out) = buffer.split_at_mut(NONCE_LEN); 514 | aead::seal_in_place(&key, nonce, &[], in_out, aead::MAX_TAG_LEN) 515 | .expect("failed to encrypt password"); 516 | } 517 | 518 | /// Decrypt data using the master key. 519 | /// Decryption is in-place, a slice to the decrypted clear text is returned. 520 | /// 521 | /// This is not specified by the Master Password algorithm. 522 | pub fn decrypt<'a>(master_key: &[u8; 64], buffer: &'a mut [u8]) -> &'a [u8] { 523 | let key = aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &master_key[0..32]) 524 | .expect("invalid CHACHA20_POLY1305 key"); 525 | assert!(buffer.len() > NONCE_LEN, "invalid cipher text"); 526 | let (nonce, mut in_out) = buffer.split_at_mut(NONCE_LEN); 527 | let padded = aead::open_in_place(&key, nonce, &[], 0, in_out) 528 | .expect("failed to decrypt password"); 529 | unpad(padded) 530 | } 531 | 532 | #[test] 533 | fn test_key_for_user_v3() { 534 | let full_name = "John Doe"; 535 | let master_password = "password"; 536 | let master_key = master_key_for_user_v3( 537 | full_name.as_bytes(), 538 | master_password.as_bytes() 539 | ).unwrap(); 540 | let expected_master_key: [u8; 64] = [ 541 | 27, 177, 181, 88, 106, 115, 177, 174, 150, 213, 214, 9, 53, 44, 141, 542 | 132, 20, 254, 89, 228, 224, 58, 95, 52, 226, 174, 130, 64, 244, 84, 216, 543 | 6, 136, 210, 95, 208, 201, 115, 81, 48, 112, 177, 183, 129, 50, 44, 115, 544 | 10, 86, 114, 44, 225, 160, 170, 250, 210, 194, 87, 12, 220, 20, 36, 120, 545 | 232 546 | ]; 547 | assert_eq!(&master_key[..], &expected_master_key[..]); 548 | } 549 | 550 | #[test] 551 | fn test_template_entropy() { 552 | use SiteType::*; 553 | 554 | /// Calculate minimal bits of entropy. 555 | // TODO: Figure out how to calculate actual entropy 556 | fn bits(ty: SiteType) -> f64 { 557 | let mut min = ::std::f64::INFINITY; 558 | for t in &templates_for_type(ty) { 559 | min = entropy_of_template(*t).min(min); 560 | } 561 | min 562 | } 563 | 564 | assert!(bits(GeneratedMaximum) > 118.4); 565 | assert!(bits(GeneratedLong) > 48.1); 566 | assert!(bits(GeneratedMedium) > 30.1); 567 | assert!(bits(GeneratedBasic) > 38.4); 568 | assert!(bits(GeneratedShort) > 14.4); 569 | assert!(bits(GeneratedPIN) > 13.2); 570 | assert!(bits(GeneratedName) > 31.2); 571 | assert!(bits(GeneratedPhrase) > 55.7); 572 | } 573 | 574 | #[test] 575 | fn test_password_for_site_v3() { 576 | let full_name = "John Doe"; 577 | let master_password = "password"; 578 | let master_key = master_key_for_user_v3( 579 | full_name.as_bytes(), 580 | master_password.as_bytes() 581 | ).unwrap(); 582 | let site_name = "google.com"; 583 | let password = password_for_site_v3( 584 | &master_key, site_name.as_bytes(), SiteType::GeneratedLong, 1, 585 | SiteVariant::Password, &[] 586 | ).unwrap(); 587 | assert_eq!(*password, "QubnJuvaMoke2~"); 588 | } 589 | 590 | #[test] 591 | fn test_identicon() { 592 | let full_name = "John Doe"; 593 | let master_password = "password"; 594 | let identicon = identicon(full_name.as_bytes(), master_password.as_bytes()); 595 | assert_eq!(identicon, "╔░╝⌚"); 596 | } 597 | 598 | #[test] 599 | fn test_unicode_user_name() { 600 | let full_name = "Max Müller"; 601 | let master_password = "passwort"; 602 | let identicon = identicon(full_name.as_bytes(), master_password.as_bytes()); 603 | assert_eq!(identicon, "═▒╝♚"); 604 | let master_key = master_key_for_user_v3( 605 | full_name.as_bytes(), 606 | master_password.as_bytes() 607 | ).unwrap(); 608 | let site_name = "de.wikipedia.org"; 609 | let password = password_for_site_v3( 610 | &master_key, site_name.as_bytes(), SiteType::GeneratedLong, 1, 611 | SiteVariant::Password, &[] 612 | ).unwrap(); 613 | assert_eq!(*password, "DaknJezb6,Zula"); 614 | } 615 | 616 | #[test] 617 | fn test_unicode_site_name() { 618 | let full_name = "Zhang Wei"; 619 | let master_password = "password"; 620 | let identicon = identicon(full_name.as_bytes(), master_password.as_bytes()); 621 | assert_eq!(identicon, "╔░╗◒"); 622 | let master_key = master_key_for_user_v3( 623 | full_name.as_bytes(), 624 | master_password.as_bytes() 625 | ).unwrap(); 626 | let site_name = "山东大学.cn"; 627 | let password = password_for_site_v3( 628 | &master_key, site_name.as_bytes(), SiteType::GeneratedLong, 1, 629 | SiteVariant::Password, &[] 630 | ).unwrap(); 631 | assert_eq!(*password, "ZajmGabl0~Zoza"); 632 | } 633 | 634 | #[test] 635 | fn test_padding_short() { 636 | let mut vec = vec![1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 637 | pad(&mut vec, 5); 638 | assert_eq!(&vec, 639 | &[1, 2, 3, 4, 5, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15]); 640 | assert_eq!(unpad(&vec), &[1, 2, 3, 4, 5]); 641 | } 642 | 643 | #[test] 644 | fn test_padding_long() { 645 | for &len in &[PAD_LEN, PAD_LEN + 1] { 646 | let mut vec = vec![100; len + 1]; 647 | pad(&mut vec, len); 648 | let mut expected = vec![100; len + 1]; 649 | expected[len] = 0; 650 | assert_eq!(&vec, &expected); 651 | assert_eq!(unpad(&vec), &vec![100; len][..]); 652 | } 653 | } 654 | 655 | #[test] 656 | fn test_encryption() { 657 | let clear_text = b"This is a secret."; 658 | let key = [1; 64]; 659 | let mut buffer = vec![0; min_buffer_len(clear_text.len())]; 660 | encrypt(clear_text, &key, &mut buffer); 661 | let decrypted = decrypt(&key, &mut buffer); 662 | assert_eq!(clear_text, decrypted); 663 | } 664 | -------------------------------------------------------------------------------- /src/clear_on_drop.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | extern crate errno; 3 | 4 | use std::convert::AsMut; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::intrinsics; 7 | 8 | use self::libc::c_void; 9 | use self::errno::{errno, Errno}; 10 | 11 | #[derive(Debug, Clone, Copy)] 12 | enum Error { 13 | /// Some of the specified address range does not correspond to mapped pages 14 | /// in the address space of the process. 15 | NoMemory, 16 | /// The caller is not privileged, but needs privilege to perform the 17 | /// requested operation. 18 | Permission, 19 | /// Some or all of the specified address range could not be locked. 20 | Again, 21 | /// Address was not a multiple of the page size (not on Linux). 22 | Invalid, 23 | /// Other, unexpected error. 24 | Other(Errno), 25 | } 26 | 27 | impl From for Error { 28 | fn from(errno: Errno) -> Error { 29 | let Errno(error_code) = errno; 30 | match error_code { 31 | libc::ENOMEM => Error::NoMemory, 32 | libc::EPERM => Error::Permission, 33 | libc::EAGAIN => Error::Again, 34 | libc::EINVAL => Error::Invalid, 35 | _ => Error::Other(errno), 36 | } 37 | } 38 | } 39 | 40 | /// Lock part of the calling process's virtual memory into RAM. 41 | /// 42 | /// This prevents that memory from being paged to the swap area. 43 | fn mlock(slice: &[u8]) -> Result<(), Error> { 44 | let return_code = unsafe { 45 | libc::mlock(slice.as_ptr() as *const c_void, slice.len()) 46 | }; 47 | if return_code == 0 { 48 | return Ok(()); 49 | } 50 | Err(errno().into()) 51 | } 52 | 53 | /// Unlock pages in the given address range. 54 | /// 55 | /// After this call, all pages that contain a part of the specified memory 56 | /// range can be moved to external swap space again by the kernel. 57 | fn munlock(slice: &[u8]) -> Result<(), Error> { 58 | let return_code = unsafe { 59 | libc::munlock(slice.as_ptr() as *const c_void, slice.len()) 60 | }; 61 | if return_code == 0 { 62 | return Ok(()); 63 | } 64 | Err(errno().into()) 65 | } 66 | 67 | /// A cheap, mutable reference-to-mutable reference conversion. 68 | /// 69 | /// Because it is implemented for String as well, it is unsafe to call. 70 | /// (It allows violating memory safety by setting a non-UTF-8 string.) 71 | pub trait UnsafeAsMut { 72 | unsafe fn as_mut(&mut self) -> &mut [u8]; 73 | } 74 | 75 | /* Ideally this should be used, but it conflicts with the implementation for String. 76 | * Furthermore, AsMut is not implemented for [u8; 64]. 77 | 78 | impl> UnsafeAsMut for T { 79 | unsafe fn as_mut(&mut self) -> &mut [u8] { 80 | self.as_mut() 81 | } 82 | } 83 | */ 84 | 85 | impl UnsafeAsMut for Vec { 86 | unsafe fn as_mut(&mut self) -> &mut [u8] { 87 | AsMut::as_mut(self) 88 | } 89 | } 90 | 91 | impl UnsafeAsMut for [u8; 64] { 92 | unsafe fn as_mut(&mut self) -> &mut [u8] { 93 | self 94 | } 95 | } 96 | 97 | impl UnsafeAsMut for String { 98 | unsafe fn as_mut(&mut self) -> &mut [u8] { 99 | AsMut::as_mut(self.as_mut_vec()) 100 | } 101 | } 102 | 103 | /// A container representing a byte slice that is set to zero on drop. 104 | /// 105 | /// Useful to make sure that secret data is cleared from memory after use. 106 | // TODO: Investigate mprotect. 107 | #[derive(Debug)] 108 | pub struct ClearOnDrop { 109 | container: Box 110 | } 111 | 112 | impl ClearOnDrop { 113 | pub fn new(container: T) -> ClearOnDrop { 114 | // Make sure the string is not swapped by using mlock. 115 | let mut result = ClearOnDrop { container: Box::new(container) }; 116 | unsafe { 117 | let slice = result.container.deref_mut().as_mut(); 118 | let _ = mlock(slice); // This sometimes fails for some reason. 119 | } 120 | result 121 | } 122 | } 123 | 124 | impl Deref for ClearOnDrop { 125 | type Target = T; 126 | 127 | fn deref(&self) -> &T { 128 | &self.container 129 | } 130 | } 131 | 132 | impl DerefMut for ClearOnDrop { 133 | fn deref_mut(&mut self) -> &mut T { 134 | &mut self.container 135 | } 136 | } 137 | 138 | impl Drop for ClearOnDrop { 139 | #[inline(never)] 140 | fn drop(&mut self) { 141 | // We use a volatile memset that makes sure it is not optimized away. It 142 | // is safe to overwrite strings with zeros, because it is valid UTF-8. 143 | unsafe { 144 | let slice = self.container.deref_mut().as_mut(); 145 | intrinsics::volatile_set_memory(slice.as_ptr() as *mut c_void, 0, slice.len()); 146 | let _ = munlock(slice); // This sometimes fails for some reason. 147 | } 148 | } 149 | } 150 | 151 | #[test] 152 | fn test_clear_on_drop_string() { 153 | let s: String = "hello".to_string(); 154 | let _ = ClearOnDrop::new(s); 155 | } 156 | 157 | #[test] 158 | fn test_clear_on_drop_vec() { 159 | let v: Vec = b"hello".to_vec(); 160 | let _ = ClearOnDrop::new(v); 161 | } 162 | 163 | #[test] 164 | fn test_clear_on_drop_array() { 165 | let a = [1; 64]; 166 | let _ = ClearOnDrop::new(a); 167 | } 168 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | extern crate toml; 2 | 3 | use std::borrow::Cow; 4 | 5 | use algorithm::{SiteType, SiteVariant}; 6 | 7 | 8 | /// Merge two options, prefering Some and the new one. 9 | pub fn merge_options(old: Option, new: Option) -> Option { 10 | match (old.is_some(), new.is_some()) { 11 | (true, true) => new, 12 | (true, false) => old, 13 | (false, true) => new, 14 | (false, false) => None, 15 | } 16 | } 17 | 18 | /// Configuration kind of error. 19 | #[derive(Debug, Clone, Copy)] 20 | pub enum ErrorKind { 21 | /// Tried to merge configs for different full names. 22 | ConflictingFullName, 23 | /// Tried to merge configs with conflicting stored passwords. 24 | ConflictingStoredPasswords, 25 | /// Got a stored password when supposed to generate one. 26 | ConflictingStoredGenerated, 27 | } 28 | 29 | /// Master Password algorithm error. 30 | #[derive(Debug)] 31 | pub struct Error { 32 | // TODO: maybe rather use Cow? 33 | pub message: String, 34 | pub kind: ErrorKind, 35 | } 36 | 37 | impl From for Error { 38 | fn from(kind: ErrorKind) -> Error { 39 | let message = match kind { 40 | ErrorKind::ConflictingFullName 41 | => "can only merge configs for the same site", 42 | ErrorKind::ConflictingStoredPasswords 43 | => "cannot merge two encrypted passwords for the same site", 44 | ErrorKind::ConflictingStoredGenerated 45 | => "got a stored password for a supposedly generated password", 46 | }; 47 | Error { message: message.into(), kind: kind } 48 | } 49 | } 50 | 51 | /// Represent the configuration state that can be stored on disk. 52 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 53 | pub struct Config<'a> { 54 | #[serde(borrow)] 55 | pub full_name: Option>, 56 | pub sites: Option>>, 57 | } 58 | 59 | impl<'a> Config<'a> { 60 | /// Create a new empty configuration. 61 | pub fn new() -> Config<'a> { 62 | Config { full_name: None, sites: None } 63 | } 64 | 65 | /// Try to create a configuration given a TOML string. 66 | pub fn from_str(s: &'a str) -> Result, toml::de::Error> { 67 | toml::from_str(s) 68 | } 69 | 70 | /// Encode the config as a TOML string. 71 | pub fn encode(&self) -> Result { 72 | toml::to_string(self) 73 | } 74 | 75 | /// Merge another configuration into this one. 76 | /// 77 | /// Values from the other configuration are prefered unless None. 78 | pub fn merge(&mut self, other: Config<'a>) { 79 | if other.full_name.is_some() { 80 | self.full_name = other.full_name; 81 | } 82 | if let Some(other_sites) = other.sites { 83 | if let Some(ref mut sites) = self.sites { 84 | sites.extend(other_sites); 85 | } else { 86 | self.sites = Some(other_sites); 87 | } 88 | } 89 | } 90 | } 91 | 92 | /// The configuration that can be stored about a site. 93 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 94 | pub struct SiteConfig<'a> { 95 | #[serde(borrow)] 96 | pub name: Cow<'a, str>, 97 | #[serde(rename = "type")] 98 | pub type_: Option, 99 | pub counter: Option, 100 | pub variant: Option, 101 | #[serde(borrow)] 102 | pub context: Option>, 103 | #[serde(borrow)] 104 | pub encrypted: Option>, 105 | } 106 | 107 | impl<'a> SiteConfig<'a> { 108 | /// Create a new site configuration with the given domain name. 109 | pub fn new(name: &'a str) -> SiteConfig<'a> { 110 | SiteConfig { 111 | name: name.into(), 112 | type_: None, 113 | counter: None, 114 | variant: None, 115 | context: None, 116 | encrypted: None, 117 | } 118 | } 119 | 120 | /// Merge another configuration into this one. 121 | /// 122 | /// Values from the other configuration are prefered unless None. 123 | /// Panics if the configurations are not for the same website. 124 | pub fn merge(&mut self, other: SiteConfig<'a>) -> Result<(), Error> { 125 | if self.name != other.name { 126 | return Err(Error::from(ErrorKind::ConflictingFullName)); 127 | } 128 | self.type_ = merge_options(self.type_, other.type_); 129 | self.counter = merge_options(self.counter, other.counter); 130 | self.variant = merge_options(self.variant, other.variant); 131 | if !(self.encrypted.is_none() && other.encrypted.is_none()) { 132 | return Err(Error::from(ErrorKind::ConflictingStoredPasswords)); 133 | } 134 | if other.context.is_some() { 135 | self.context = other.context; 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | /// The configuration state of a site with all default values plugged in. 142 | #[derive(Debug, Clone, PartialEq, Eq)] 143 | pub struct Site<'a> { 144 | pub name: Cow<'a, str>, 145 | pub type_: SiteType, 146 | pub counter: u32, 147 | pub variant: SiteVariant, 148 | pub context: Cow<'a, str>, 149 | pub encrypted: Option>, 150 | } 151 | 152 | impl<'a> Site<'a> { 153 | /// Create a site from a given config. Missing values are filled with defaults. 154 | pub fn from_config(config: &'a SiteConfig<'a>) -> Result, Error> { 155 | let variant = config.variant.unwrap_or(SiteVariant::Password); 156 | let encrypted = match config.encrypted { 157 | Some(ref s) => Some(s.as_ref().into()), 158 | None => None, 159 | }; 160 | let type_ = config.type_.unwrap_or( 161 | if encrypted.is_none() { 162 | match variant { 163 | SiteVariant::Password => SiteType::GeneratedLong, 164 | SiteVariant::Login => SiteType::GeneratedName, 165 | SiteVariant::Answer => SiteType::GeneratedPhrase, 166 | } 167 | } else { 168 | SiteType::Stored 169 | } 170 | ); 171 | if encrypted.is_some() && type_ != SiteType::Stored { 172 | return Err(Error::from(ErrorKind::ConflictingStoredGenerated)); 173 | } 174 | let context = match config.context { 175 | Some(ref s) => s.as_ref().into(), 176 | None => "".into(), 177 | }; 178 | 179 | Ok(Site { 180 | name: config.name.as_ref().into(), 181 | type_: type_, 182 | counter: config.counter.unwrap_or(1), 183 | variant: variant, 184 | context: context, 185 | encrypted: encrypted, 186 | }) 187 | } 188 | } 189 | 190 | #[test] 191 | fn test_config_merge() { 192 | let mut c1 = Config::new(); 193 | let mut c2 = Config::new(); 194 | let mut c3 = Config::new(); 195 | 196 | let wikipedia = SiteConfig::new("wikipedia.org"); 197 | let github = SiteConfig::new("github.com"); 198 | c2.sites = Some(vec![wikipedia.clone()]); 199 | c3.sites = Some(vec![github.clone()]); 200 | c1.merge(c2); 201 | assert_eq!(c1.sites, Some(vec![wikipedia.clone()])); 202 | c1.merge(c3); 203 | assert_eq!(c1.sites, Some(vec![wikipedia, github])); 204 | } 205 | 206 | #[test] 207 | fn test_config_encode() { 208 | let mut c = Config::new(); 209 | assert_eq!(c.encode().unwrap(), ""); 210 | c.full_name = Some("John Doe".into()); 211 | assert_eq!(c.encode().unwrap(), "full_name = \"John Doe\"\n"); 212 | 213 | let wikipedia = SiteConfig::new("wikipedia.org"); 214 | c.sites = Some(vec![wikipedia]); 215 | assert_eq!(c.encode().unwrap(), 216 | r#"full_name = "John Doe" 217 | 218 | [[sites]] 219 | name = "wikipedia.org" 220 | "#); 221 | 222 | let mut github = SiteConfig::new("github.com"); 223 | github.type_ = Some(SiteType::GeneratedMaximum); 224 | github.counter = Some(1); 225 | github.variant = Some(SiteVariant::Password); 226 | github.context = Some("".into()); 227 | let bitbucket = SiteConfig::new("bitbucket.org"); 228 | c.sites = Some(vec![github, bitbucket]); 229 | assert_eq!(c.encode().unwrap(), 230 | r#"full_name = "John Doe" 231 | 232 | [[sites]] 233 | name = "github.com" 234 | type = "maximum" 235 | counter = 1 236 | variant = "password" 237 | context = "" 238 | 239 | [[sites]] 240 | name = "bitbucket.org" 241 | "#); 242 | } 243 | 244 | #[test] 245 | fn test_variant_encode() { 246 | #[derive(Debug, Serialize)] 247 | struct V { variant: SiteVariant } 248 | assert_eq!(toml::to_string(&V { variant: SiteVariant::Password }).unwrap(), 249 | "variant = \"password\"\n"); 250 | } 251 | 252 | #[test] 253 | fn test_type_encode() { 254 | #[derive(Debug, Serialize)] 255 | struct T { type_: SiteType } 256 | assert_eq!(toml::to_string(&T { type_: SiteType::GeneratedLong }).unwrap(), 257 | "type_ = \"long\"\n"); 258 | } 259 | 260 | #[test] 261 | fn test_config_decode() { 262 | let config_str = r#"full_name = "John Doe" 263 | 264 | [[sites]] 265 | name = "github.com" 266 | type = "maximum" 267 | "#; 268 | let config = Config::from_str(config_str).unwrap(); 269 | 270 | let mut expected_config = Config::new(); 271 | expected_config.full_name = Some("John Doe".into()); 272 | let mut github = SiteConfig::new("github.com"); 273 | github.type_ = Some(SiteType::GeneratedMaximum); 274 | expected_config.sites = Some(vec![github]); 275 | 276 | assert_eq!(config, expected_config); 277 | } 278 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(try_from, core_intrinsics)] 2 | 3 | #[macro_use] 4 | extern crate lazy_static; 5 | #[macro_use] 6 | extern crate clap; 7 | extern crate ring; 8 | extern crate rpassword; 9 | extern crate serde; 10 | #[macro_use] 11 | extern crate serde_derive; 12 | extern crate data_encoding; 13 | extern crate zxcvbn; 14 | 15 | use std::io::{Read, Write}; 16 | use std::fs::File; 17 | 18 | use clap::{Arg, App, AppSettings}; 19 | use ring::rand::SystemRandom; 20 | use rpassword::read_password; 21 | use data_encoding::base64; 22 | use zxcvbn::zxcvbn; 23 | 24 | mod algorithm; 25 | mod clear_on_drop; 26 | mod config; 27 | 28 | use algorithm::{SiteVariant, SiteType, random_password_for_site, 29 | master_key_for_user_v3, password_for_site_v3, identicon, min_buffer_len, 30 | encrypt, decrypt}; 31 | use clear_on_drop::ClearOnDrop; 32 | use config::{merge_options, Config, SiteConfig, Site}; 33 | 34 | static TYPE_HELP: &'static str = 35 | "The password's template\n\ 36 | (defaults to 'long' for password, 'name' for login, 'phrase' for answer)\n\ 37 | \n\ 38 | x, max, maximum 20 characters, contains symbols.\n\ 39 | l, long Copy-friendly, 14 characters, contains symbols.\n\ 40 | m, med, medium Copy-friendly, 8 characters, contains symbols.\n\ 41 | b, basic 8 characters, no symbols.\n\ 42 | s, short Copy-friendly, 4 characters, no symbols.\n\ 43 | i, pin 4 numbers.\n\ 44 | n, name 9 letter name.\n\ 45 | p, phrase 20 character sentence.\n"; 46 | 47 | /// Flush to make sure the prompt is visible. 48 | fn flush() { 49 | std::io::stdout().flush().unwrap_or_exit("could not flush stdout"); 50 | } 51 | 52 | /// Read the master password from stdin and generate the master key. 53 | fn generate_master_key(full_name: &str) -> ClearOnDrop<[u8; 64]> { 54 | print!("Please enter the master password: "); 55 | flush(); 56 | let master_password = read_password().unwrap_or_exit("could not read master password"); 57 | 58 | let identicon = identicon(full_name.as_bytes(), master_password.as_bytes()); 59 | println!("Identicon: {}", identicon); 60 | if let Ok(evaluation) = zxcvbn(&master_password, &[full_name]) { 61 | let time = &evaluation.crack_times_display.offline_slow_hashing_1e4_per_second; 62 | match evaluation.score { 63 | 0 => println!("Your password is trivial, it can be cracked in {}.", time), 64 | 1 => println!("Your password is very weak, it can be cracked in {}.", time), 65 | 2 => println!("Your password is weak, it can be cracked in {}.", time), 66 | 3 => println!("Your password is so-so."), 67 | _ => println!("Your password is great!"), 68 | } 69 | if let Some(feedback) = evaluation.feedback { 70 | if let Some(warning) = feedback.warning { 71 | println!("{}", warning); 72 | } 73 | for s in &feedback.suggestions { 74 | println!("{}", s); 75 | } 76 | } 77 | } else if master_password.is_empty() { 78 | println!("Your password is empty, it can be cracked in less than a second."); 79 | } else { 80 | println!("Could not evaluate password, most likely because of non-ASCII symbols."); 81 | } 82 | let master_key = master_key_for_user_v3( 83 | full_name.as_bytes(), 84 | master_password.as_bytes() 85 | ).unwrap_or_exit("could not generate master key"); 86 | master_key 87 | } 88 | 89 | /// Read a site password to be stored from stdin. 90 | fn get_site_password() -> ClearOnDrop { 91 | print!("Please enter the site password to be stored: "); 92 | flush(); 93 | let password = read_password().unwrap_or_exit("could not read site password"); 94 | ClearOnDrop::new(password) 95 | } 96 | 97 | /// Exit the program with an error message. 98 | fn exit(message: &str) -> ! { 99 | let err = clap::Error::with_description(message, clap::ErrorKind::InvalidValue); 100 | // Ther ErrorKind does not really matter, because we are only interested in exiting and 101 | // creating a nice error message in case of failure. 102 | err.exit() 103 | } 104 | 105 | trait UnwrapOrExit 106 | where Self: Sized 107 | { 108 | /// Unwrap the value or execute a closure. 109 | fn unwrap_or_else(self, f: F) -> T 110 | where F: FnOnce() -> T; 111 | 112 | /// Unwrap the value or exit the program with an error message. 113 | fn unwrap_or_exit(self, message: &str) -> T { 114 | self.unwrap_or_else(|| exit(message)) 115 | } 116 | } 117 | 118 | impl UnwrapOrExit for Option { 119 | fn unwrap_or_else(self, f: F) -> T 120 | where F: FnOnce() -> T 121 | { 122 | self.unwrap_or_else(f) 123 | } 124 | } 125 | 126 | impl UnwrapOrExit for Result { 127 | fn unwrap_or_else(self, f: F) -> T 128 | where F: FnOnce() -> T 129 | { 130 | self.unwrap_or_else(|_| f()) 131 | } 132 | } 133 | 134 | fn main() { 135 | let matches = App::new("Master Password") 136 | .about("A stateless password management solution.") 137 | .version(crate_version!()) 138 | .setting(AppSettings::HidePossibleValuesInHelp) 139 | .arg(Arg::with_name("site") 140 | .help("The domain name of the site.") 141 | .number_of_values(1) 142 | .index(1) 143 | .required_unless("config")) 144 | .arg(Arg::with_name("full name") 145 | .long("name") 146 | .short("u") 147 | .help("The full name of the user.\nOptional if given in config.") 148 | .required_unless_one(&["config", "generate name"]) 149 | .number_of_values(1) 150 | .takes_value(true)) 151 | .arg(Arg::with_name("type") 152 | .long("type") 153 | .short("t") 154 | .help(TYPE_HELP) 155 | .next_line_help(true) 156 | .takes_value(true) 157 | .number_of_values(1) 158 | .possible_values(&[ 159 | "x", "max", "maximum", 160 | "l", "long", 161 | "m", "med", "medium", 162 | "b", "basic", 163 | "s", "short", 164 | "i", "pin", 165 | "n", "name", 166 | "p", "phrase", 167 | ])) 168 | .arg(Arg::with_name("counter") 169 | .long("counter") 170 | .short("c") 171 | .help("The value of the site counter.") 172 | .takes_value(true) 173 | .number_of_values(1)) 174 | .arg(Arg::with_name("variant") 175 | .long("variant") 176 | .short("v") 177 | .help("The kind of content to generate (defaults to 'password')\n\ 178 | \n\ 179 | p, password Generate a password\n\ 180 | l, login Generate a login name\n\ 181 | a, answer Generate an answer to a question") 182 | .next_line_help(true) 183 | .takes_value(true) 184 | .number_of_values(1) 185 | .possible_values(&[ 186 | "p", "password", 187 | "l", "login", 188 | "a", "answer" 189 | ])) 190 | .arg(Arg::with_name("context") 191 | .long("context") 192 | .short("C") 193 | .help("Empty for a universal site or the most significant word(s) of the question.") 194 | .takes_value(true) 195 | .number_of_values(1)) 196 | .arg(Arg::with_name("dump") 197 | .long("dump") 198 | .short("d") 199 | .help("Dump the configuration as a TOML.")) 200 | .arg(Arg::with_name("config") 201 | .long("config") 202 | .short("i") 203 | .help("Read/write configuration from/to a TOML file.") 204 | .takes_value(true) 205 | .number_of_values(1)) 206 | .arg(Arg::with_name("add") 207 | .long("add") 208 | .short("a") 209 | .help("Add parameters of site password to configuration file.") 210 | .requires_all(&["site", "config"]) 211 | .conflicts_with_all(&["replace", "delete", "store"])) 212 | .arg(Arg::with_name("replace") 213 | .long("replace") 214 | .short("r") 215 | .help("Replace parameters of all site passwords in configuration file.\n\ 216 | Does not delete stored passwords.") 217 | .requires_all(&["site", "config"]) 218 | .conflicts_with_all(&["add", "delete", "store"])) 219 | .arg(Arg::with_name("delete") 220 | .long("delete") 221 | .short("D") 222 | .help("Delete parameters of all site passwords in configuration file.\n\ 223 | Does not delete stored passwords.") 224 | .requires_all(&["site", "config"]) 225 | .conflicts_with_all(&["add", "replace", "store"])) 226 | .arg(Arg::with_name("store") 227 | .long("store") 228 | .short("s") 229 | .help("Encrypt and store a password") 230 | .requires_all(&["site", "config"]) 231 | .conflicts_with_all(&["add", "delete", "replace"])) 232 | .arg(Arg::with_name("generate name") 233 | .long("generate-name") 234 | .short("g") 235 | .help("Generate a random full name.\n\ 236 | If the name is kept secret, this is more secure.") 237 | .requires("config") 238 | .conflicts_with("full name")) 239 | .get_matches(); 240 | 241 | // If given, read config from path. 242 | let config_path = matches.value_of("config"); 243 | let mut config_string = String::new(); 244 | let mut config = if let Some(path) = config_path { 245 | let file = File::open(path); 246 | match file { 247 | Ok(mut f) => { 248 | f.read_to_string(&mut config_string) 249 | .unwrap_or_exit("could not read given config file"); 250 | Config::from_str(&config_string) 251 | .unwrap_or_exit("could not parse given config file") 252 | }, 253 | Err(_) => { 254 | // If the config file is not present, assume an empty config. 255 | // TODO: make sure we get an error if the config is the only argument 256 | Config::new() 257 | } 258 | } 259 | } else { 260 | Config::new() 261 | }; 262 | 263 | // Read config from CLI parameters. 264 | let mut param_config = Config::new(); 265 | let rng = SystemRandom::new(); 266 | param_config.full_name = if matches.is_present("generate name") { 267 | let name: String = random_password_for_site(&rng, SiteType::GeneratedMaximum) 268 | .unwrap_or_exit("failed to generate random full name").clone(); 269 | println!("generated random full name: \"{}\"", name); 270 | Some(name.into()) 271 | } else { 272 | matches.value_of("full name").map(Into::into) 273 | }; 274 | let param_site_name = matches.value_of("site"); 275 | if let Some(name) = param_site_name { 276 | let param_site_config = SiteConfig { 277 | name: name.into(), 278 | type_: matches.value_of("type").map(|s| SiteType::from_str(s).unwrap()), 279 | //^ This unwrap is safe, because clap already did the check. 280 | counter: matches.value_of("counter") 281 | .map(|c| c.parse().unwrap_or_exit("counter must be an unsigned 32-bit integer")), 282 | variant: matches.value_of("variant").map(|s| SiteVariant::from_str(s).unwrap()), 283 | //^ This unwrap is safe, because clap already did the check. 284 | context: matches.value_of("context").map(Into::into), 285 | encrypted: None, 286 | }; 287 | param_config.sites = Some(vec![param_site_config]); 288 | } 289 | 290 | if matches.is_present("replace") || matches.is_present("delete") { 291 | // Remove all sites that have the given name, unless they stored a 292 | // password. 293 | let param_site_name = param_site_name.unwrap(); 294 | //^ This unwrap is safe, because clap already did the check. 295 | if let Some(ref mut sites) = config.sites { 296 | sites.retain(|s| 297 | s.name != param_site_name || s.encrypted.is_some()); 298 | } 299 | } 300 | 301 | let mut master_key = None; 302 | 303 | // Merge parameters into config. 304 | if let (Some(config_name), Some(param_name)) = 305 | (config.full_name.as_ref(), param_config.full_name.as_ref()) 306 | { 307 | if config_name != param_name { 308 | exit("full name given as parameter conflicts with config"); 309 | } 310 | } 311 | if matches.is_present("store") { 312 | let full_name = merge_options( 313 | config.full_name.as_ref(), 314 | param_config.full_name.as_ref(), 315 | ).unwrap_or_exit("need full name to generate master key"); 316 | let key = generate_master_key(full_name); 317 | 318 | let password = get_site_password(); 319 | let mut buffer = vec![0; min_buffer_len(password.len())]; 320 | encrypt(password.as_ref(), &key, &mut buffer); 321 | let site = &mut param_config.sites.as_mut().unwrap()[0]; 322 | //^ This unwrap is safe, because we now it was set to Some before. 323 | site.encrypted = Some( 324 | base64::encode(&buffer).into() 325 | ); 326 | site.type_ = Some(SiteType::Stored); 327 | master_key = Some(key); 328 | } 329 | config.merge(param_config); 330 | 331 | if matches.is_present("add") || 332 | matches.is_present("replace") || 333 | matches.is_present("delete") || 334 | matches.is_present("store") { 335 | // Overwrite config file. 336 | let s = config.encode() 337 | .unwrap_or_exit("could not encode config"); 338 | debug_assert!(s != ""); 339 | let path = config_path.as_ref().unwrap(); 340 | //^ This unwrap is safe, because clap already did the check. 341 | let mut f = File::create(path) 342 | .unwrap_or_exit("could not overwrite given config file"); 343 | f.write_all(s.as_bytes()) 344 | .unwrap_or_exit("could not write to given config file"); 345 | return; 346 | } 347 | 348 | if matches.is_present("dump") { 349 | // Output config. 350 | let s = config.encode() 351 | .unwrap_or_exit("could not encode config"); 352 | debug_assert!(s != ""); 353 | println!("{}", s); 354 | return; 355 | } 356 | 357 | let full_name = config.full_name.as_ref() 358 | .unwrap_or_exit("need full name to generate master key"); 359 | 360 | let site_configs = config.sites.as_ref() 361 | .unwrap_or_exit("need a site via command line parameters or via config"); 362 | 363 | let master_key = if let Some(key) = master_key { 364 | key 365 | } else { 366 | generate_master_key(full_name) 367 | }; 368 | 369 | // Generate or decrypt passwords. 370 | println!(); 371 | for site_config in site_configs { 372 | let site = Site::from_config(site_config).unwrap_or_else(|e| exit(&e.message)); 373 | // If a site was given, skip all other sites. 374 | // FIXME: site from parameter should not be printed if already present? 375 | if let Some(name) = param_site_name { 376 | if name != site.name { 377 | continue; 378 | } 379 | } 380 | // We have to define the containers of the passwords here, so that the 381 | // slices into them survive until we print the password. 382 | let mut buffer = ClearOnDrop::new(vec![]); 383 | let password_string; 384 | let password = match site.type_ { 385 | SiteType::Stored => { 386 | let encrypted = site.encrypted.as_ref() 387 | .unwrap_or_exit("found stored password without 'encrypted' field") 388 | .as_bytes(); 389 | let decoded = &base64::decode(encrypted) 390 | .unwrap_or_exit("could not decode 'encrypted' field"); 391 | buffer.resize(decoded.len(), 0); 392 | buffer.clone_from_slice(decoded); 393 | let decrypted = decrypt(&master_key, &mut buffer); 394 | std::str::from_utf8(decrypted).unwrap_or_exit("could not decrypt stored password") 395 | }, 396 | _ => { 397 | password_string = password_for_site_v3( 398 | &master_key, 399 | site.name.as_bytes(), 400 | site.type_, 401 | site.counter, 402 | site.variant, 403 | site.context.as_bytes() 404 | ).unwrap_or_exit("could not generate site password"); 405 | &password_string 406 | }, 407 | }; 408 | // TODO: print non-default parameters 409 | println!("Password for {}: {}", site.name, password); 410 | } 411 | } 412 | --------------------------------------------------------------------------------