├── .babelrc ├── .gitignore ├── .kbignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── SIGNED.md ├── dictionary ├── files └── pg1661.txt ├── help ├── decrypt.help ├── default.help ├── encrypt.help ├── help.help └── id.help ├── mlck ├── mlck-debug ├── module.js ├── package.json ├── src ├── cli │ ├── commands │ │ ├── decrypt.js │ │ ├── encrypt.js │ │ ├── help.js │ │ ├── id.js │ │ ├── license.js │ │ └── version.js │ ├── helpers │ │ ├── help.js │ │ ├── id.js │ │ ├── passphrase.js │ │ ├── profile.js │ │ └── unknown.js │ ├── main.js │ └── objects │ │ ├── dictionary.js │ │ └── profile.js ├── common │ ├── debug.js │ ├── util.js │ └── version.js └── module │ └── index.js └── tests └── minilock.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .*.tmp 3 | 4 | node_modules 5 | 6 | build 7 | 8 | *.log 9 | 10 | *.txt 11 | *.minilock 12 | 13 | -------------------------------------------------------------------------------- /.kbignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Makefile 3 | .npmignore 4 | .gitignore 5 | .babelrc 6 | .*.tmp 7 | .*.swp 8 | *.txt 9 | *.minilock 10 | *.log 11 | 12 | .git 13 | build 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.tmp 3 | .*.swp 4 | *.txt 5 | *.minilock 6 | *.log 7 | 8 | .gitignore 9 | .npmignore 10 | .babelrc 11 | Makefile 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Manish Jethani 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | build watch: babel_flags += -s 4 | watch: babel_flags += -w 5 | 6 | build watch: 7 | babel $(babel_flags) -d build src 8 | 9 | test: 10 | babel-tape-runner tests/**/*.js 11 | 12 | $(VERSION): 13 | npm version $(VERSION) --no-git-tag-version 14 | 15 | version: $(VERSION) 16 | 17 | .npmignore: .gitignore 18 | sort -ru .gitignore | grep -v '^build$$' > .npmignore 19 | echo '.gitignore .npmignore .babelrc Makefile' | tr ' ' '\n' >> .npmignore 20 | 21 | .kbignore: .npmignore 22 | sort -ru .npmignore > .kbignore 23 | echo .git >> .kbignore 24 | echo build >> .kbignore 25 | 26 | sign: .kbignore 27 | keybase dir sign -p kb 28 | 29 | verify: 30 | keybase dir verify 31 | 32 | ifdef VERSION 33 | tag: version sign 34 | git commit -am 'Signed PGP:E6B74303' 35 | git tag v$(VERSION) 36 | 37 | publish: 38 | git checkout master 39 | git merge develop 40 | touch .gitignore 41 | make tag VERSION=$(VERSION) 42 | make 43 | npm publish 44 | endif 45 | 46 | clean: 47 | rm -rf build 48 | git checkout SIGNED.md 49 | 50 | .PHONY: clean version build watch sign verify tag publish 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub version](https://badge.fury.io/gh/mjethani%2FminiLock-cli.svg)](http://badge.fury.io/gh/mjethani%2FminiLock-cli) 2 | 3 | miniLock-cli is a Node.js command line version of the miniLock encryption software. 4 | 5 | You can read about miniLock here: 6 | 7 | https://minilock.io/ 8 | 9 | The CLI version is written from scratch using the same crypto libraries as the original Google Chrome app. 10 | 11 | ## Installation 12 | 13 | Install [Node.js](https://nodejs.org/). 14 | 15 | Then run the following command: 16 | 17 | ```console 18 | $ npm install -g minilock-cli@0.2.13 19 | /usr/local/bin/mlck -> /usr/local/lib/node_modules/minilock-cli/mlck 20 | minilock-cli@0.2.13 /usr/local/lib/node_modules/minilock-cli 21 | ├── bs58@2.0.1 22 | ├── nacl-stream@0.3.3 23 | ├── scrypt-async@1.0.1 24 | ├── blake2s-js@1.0.3 25 | ├── tweetnacl@0.13.1 26 | └── zxcvbn@1.0.0 27 | $ 28 | ``` 29 | 30 | Verify the installation: 31 | 32 | ```console 33 | $ mlck --version 34 | miniLock-cli v0.2.13 35 | $ 36 | ``` 37 | 38 | ## Tutorial 39 | 40 | Let's get started! 41 | 42 | ### Generate an ID 43 | 44 | First, you need a miniLock ID. 45 | 46 | ```console 47 | $ mlck id alice@example.com --save 48 | period dry million besides usually wild everybody 49 | 50 | Passphrase (leave blank to quit): 51 | ``` 52 | 53 | Enter a good passphrase, such as the one shown before the prompt. You need [~100 bits of entropy](https://xkcd.com/936/). Any 7-8 _randomly selected_ words out of the English lexicon should be fine. 54 | 55 | If you insist on using a simple passphrase like "hello" (not recommended at all!), you must use the `--passphrase` option. 56 | 57 | ```console 58 | $ mlck id alice@example.com --save --passphrase='hello' 59 | 60 | Your miniLock ID: LRFbCrhCeN2uVCdDXd2bagoCM1fVcGvUzwhfVdqfyVuhi. 61 | 62 | $ 63 | ``` 64 | 65 | You can look up your miniLock ID any time. 66 | 67 | ```console 68 | $ mlck id 69 | 70 | Your miniLock ID: LRFbCrhCeN2uVCdDXd2bagoCM1fVcGvUzwhfVdqfyVuhi. 71 | 72 | $ 73 | ``` 74 | 75 | Once you're sure about it (i.e. you've picked a good passphrase that is also easy to remember), you can publish it [on Twitter](https://twitter.com/100101010000/status/589422009534164992), [on your website](https://blog.manishjethani.com/minilock.txt.asc), and on various other channels. If people know your miniLock ID they can encrypt information to you even anonymously. 76 | 77 | ### Encrypt a file 78 | 79 | Let's say you have a text file called `message.txt` containing the following message: 80 | 81 | ``` 82 | The PIN code is 1337. 83 | 84 | Withdraw 10,100 euros and meet me at Frederick Street at 5pm. 85 | 86 | Don't forget my chocolate! 87 | ``` 88 | 89 | Now you can encrypt it to the miniLock ID gT1csvpmQDNRQSMkqc1Sz7ZWYzGZkmedPKEpgqjdNTy7Y using the following command: 90 | 91 | ```console 92 | $ mlck encrypt -f message.txt gT1csvpmQDNRQSMkqc1Sz7ZWYzGZkmedPKEpgqjdNTy7Y 93 | Passphrase (leave blank to quit): 94 | ``` 95 | 96 | Once again, it asks you for your passphrase. This time it's to identify you as the sender. If you wish to send anonymously (using a randomly generated sender ID), use the `--anonymous` option. 97 | 98 | Note that you _can_ send anonymously even if the message itself contains identifying information. 99 | 100 | Here's the full interaction: 101 | 102 | ```console 103 | $ mlck encrypt -f message.txt gT1csvpmQDNRQSMkqc1Sz7ZWYzGZkmedPKEpgqjdNTy7Y 104 | Passphrase (leave blank to quit): 105 | 106 | Encrypted from LRFbCrhCeN2uVCdDXd2bagoCM1fVcGvUzwhfVdqfyVuhi. 107 | 108 | Wrote 1075 bytes to message.txt.minilock 109 | 110 | $ 111 | ``` 112 | 113 | Now you can send the file `message.txt.minilock` to its intended recipient. 114 | 115 | ### Decrypt a file 116 | 117 | Your friend Bob receives a file called `message.txt.minilock` in the mail. Luckily he has miniLock-cli installed. He proceeds to decrypt the file using his email address and his passphrase. 118 | 119 | ```console 120 | $ mlck decrypt -f message.txt.minilock -e bob@example.com --passphrase='puff magic dragon sea frolic autumn mist lee' 121 | --- BEGIN MESSAGE --- 122 | The PIN code is 1337. 123 | 124 | Withdraw 10,100 euros and meet me at Frederick Street at 5pm. 125 | 126 | Don't forget my chocolate! 127 | --- END MESSAGE --- 128 | 129 | Message from LRFbCrhCeN2uVCdDXd2bagoCM1fVcGvUzwhfVdqfyVuhi. 130 | 131 | Original filename: message.txt 132 | 133 | $ 134 | ``` 135 | 136 | Bob knows exactly what you mean by "Don't forget my chocolate!" 137 | 138 | ## Links 139 | 140 | Here are some useful links: 141 | 142 | * [miniLock README.md](https://github.com/kaepora/miniLock/blob/master/README.md) 143 | * [Usable Crypto: Introducing miniLock (HOPE X)](https://vimeo.com/101237413) 144 | * [The Ultra-Simple App That Lets Anyone Encrypt Anything](http://www.wired.com/2014/07/minilock-simple-encryption/) 145 | * [A Few Thoughts on Cryptographic Engineering: What's the matter with PGP?](http://blog.cryptographyengineering.com/2014/08/whats-matter-with-pgp.html) 146 | 147 | -------------------------------------------------------------------------------- /SIGNED.md: -------------------------------------------------------------------------------- 1 | ##### Signed by https://keybase.io/mj 2 | ``` 3 | -----BEGIN PGP SIGNATURE----- 4 | Comment: GPGTools - https://gpgtools.org 5 | 6 | iQIcBAABCgAGBQJWVtyYAAoJEPvclVzmt0MDR98P/2gpbtAynXwnMf2BODGI80VB 7 | 4BOJDslNCbZVfGDqyapr6vu76zvqF/i2GLDem9ki5OXSctgt/Y5dbe4SxgKB8lm9 8 | Tdblu3OBt6KaZ5kw3QkiR8rfPnuikA/y0V9uyi7PgSJT8Jk7/nZw9joO63p2LdZW 9 | 4Bg4hD7DmDc6pqCyAA1LMDNMnyz2LFmco7TZQj+FoieDgF52u9DZi9AYvtcpSbRG 10 | j2/Vj+JNikk5sHl2fMTRBpc/EQQfxz+Oy2NMRF7TxN40O8krDt8snUy6Nrw4NPOZ 11 | GRq4fZ8dfczEluH9jTjNE+T1CqITzQjE+n1m0JX8fxmHG5zKyITIt+yDzG1H4DNQ 12 | RBfnr15DivwBuHmSFAZkuAPxFmydmkslBgPElF4XJ143LkTye1yxC/Nl1MkrZh0E 13 | /O1f2+zEVUhLPDB1kCxCymt/adN5gU9UN0jP1W2Xo7mU3TgOgapd3GbkUdFhxt9r 14 | NO0vm5+IzX20Vk9P7xItfxlT4wdNrD4htbL332e/DkJAX3/PgMHoxlXlApFj/gm9 15 | 2IphrLezk5Y7+c/h1p5p7If22OKs6OiU7Pbu5HhX4QXHLk6aW3bwIHvAmBauTfFK 16 | s/5pUX1I5+sx67ZR5OrLd45lNvEZD/oKI3YHAjvjSzN6nM4zhxVGuGu6ZarbLVZP 17 | OUl3JJdxq+8NZXgZyg3u 18 | =YnIW 19 | -----END PGP SIGNATURE----- 20 | 21 | ``` 22 | 23 | 24 | 25 | ### Begin signed statement 26 | 27 | #### Expect 28 | 29 | ``` 30 | size exec file contents 31 | ./ 32 | 98 .kbignore 61d84567e9afd22c579d2dd25d5e825e367e2725d3dda8a0f84b513869f88c84 33 | 6016 dictionary a23bd82e1e917dec4a63a92746267d3b3aa92fb0cae7cff7f07aaf30a17a707f 34 | files/ 35 | help/ 36 | 251 decrypt.help 79b2a8c4759c7f5c05545e770f25f624bd788cbdcef70b1d3aea3e4b82a15667 37 | 699 default.help daf2189b2b270559d2c91bffd5c1726a056dba175b00e727f7385cd97b967d84 38 | 610 encrypt.help d9a07bce15db3a89159b1f432d0444eef53e1bbe80df0d64222ba58f0fd822d6 39 | 51 help.help a02fd4027119f8d399badf9b24d495d235fc969f9e09efe2e934b1dfee64d1cd 40 | 260 id.help e8623f98fbcfaf6dab0a17d8605fdbd802ee36848a16836a2d93aa7073a54aeb 41 | 732 LICENSE e7fa0c5707aa3eae23e841a73ea57cda21f3bd87b90ba3ea254ca5bdec29d386 42 | 53 x mlck aaca78e6cfe21e39c58aa9e55e72182df25639c6a1a46c74e613e8f81e7f7054 43 | 95 x mlck-debug fe0b84163b7d41b893cf553c4fe3bf74de7d43b218763365aaea6f9a658c5911 44 | 45 module.js 677c8ff80275ef8a4c27ee869b706ec76d15a01ae8fb378ec77254510a90153e 45 | 1324 package.json 688d4448edd899cafd2868008b695a66075565c64802c7fc591bfaec889fe707 46 | 4342 README.md b28ceecb2835cb2f2ab474e1c68dd6f4a9de81d574643261d6a972a5977310da 47 | src/ 48 | 192 debug.js 87e52763058f92273017f0205118d7406d4f162219b7afbb3ef40526185e63ce 49 | 978 dictionary.js cbf19262bb76558adbcc30a42128431e506c9b3fbc0463374ae4f44999f0c5c3 50 | 17435 main.js 5e8284bc5314695090c3cf366725e2c5fcad82bc908e4bb62155688ecae3b2a0 51 | 17705 minilock.js 510010ad81e9d553be3809be3d41d8802b7121133bf2124594bb6134cfdcafe6 52 | 967 profile.js e7eaef857ceb6d4b606f81d5a489bc61a497da7e65807902b39fc5baf55c9e47 53 | 5824 util.js bc17fce0a153ceaf9cfcd41b783de0a8e84e2c91c69633fbeb914758f3379bfe 54 | 72 version.js 15f53c3664bfca8dc503ff0475cacbcd7eefc769fab868493e99facf5a7a4457 55 | tests/ 56 | 6178 minilock.js 3a61b77966bd1af0ba24934825d50541a46a3f970fb0f7eb870caa95e5b21bab 57 | ``` 58 | 59 | #### Ignore 60 | 61 | ``` 62 | /SIGNED.md 63 | ``` 64 | 65 | #### Presets 66 | 67 | ``` 68 | kb # ignore anything as described by .kbignore files 69 | ``` 70 | 71 | 72 | 73 | ### End signed statement 74 | 75 |
76 | 77 | #### Notes 78 | 79 | With keybase you can sign any directory's contents, whether it's a git repo, 80 | source code distribution, or a personal documents folder. It aims to replace the drudgery of: 81 | 82 | 1. comparing a zipped file to a detached statement 83 | 2. downloading a public key 84 | 3. confirming it is in fact the author's by reviewing public statements they've made, using it 85 | 86 | All in one simple command: 87 | 88 | ```bash 89 | keybase dir verify 90 | ``` 91 | 92 | There are lots of options, including assertions for automating your checks. 93 | 94 | For more info, check out https://keybase.io/docs/command_line/code_signing -------------------------------------------------------------------------------- /dictionary: -------------------------------------------------------------------------------- 1 | # http://simple.wikipedia.org/wiki/Wikipedia:List_of_1000_basic_words 2 | 3 | I 4 | 5 | a 6 | about 7 | above 8 | across 9 | act 10 | active 11 | activity 12 | add 13 | afraid 14 | after 15 | again 16 | age 17 | ago 18 | agree 19 | air 20 | all 21 | alone 22 | along 23 | already 24 | always 25 | am 26 | amount 27 | an 28 | and 29 | angry 30 | another 31 | answer 32 | any 33 | anyone 34 | anything 35 | anytime 36 | appear 37 | apple 38 | are 39 | area 40 | arm 41 | army 42 | around 43 | arrive 44 | art 45 | as 46 | ask 47 | at 48 | attack 49 | aunt 50 | autumn 51 | away 52 | baby 53 | back 54 | bad 55 | bag 56 | ball 57 | bank 58 | base 59 | basket 60 | bath 61 | be 62 | bean 63 | bear 64 | beautiful 65 | bed 66 | bedroom 67 | beer 68 | before 69 | begin 70 | behave 71 | behind 72 | bell 73 | below 74 | besides 75 | best 76 | better 77 | between 78 | big 79 | bird 80 | birth 81 | birthday 82 | bit 83 | bite 84 | black 85 | bleed 86 | block 87 | blood 88 | blow 89 | blue 90 | board 91 | boat 92 | body 93 | boil 94 | bone 95 | book 96 | border 97 | born 98 | borrow 99 | both 100 | bottle 101 | bottom 102 | bowl 103 | box 104 | boy 105 | branch 106 | brave 107 | bread 108 | break 109 | breakfast 110 | breathe 111 | bridge 112 | bright 113 | bring 114 | brother 115 | brown 116 | brush 117 | build 118 | burn 119 | bus 120 | business 121 | busy 122 | but 123 | buy 124 | by 125 | cake 126 | call 127 | can 128 | candle 129 | cap 130 | car 131 | card 132 | care 133 | careful 134 | careless 135 | carry 136 | case 137 | cat 138 | catch 139 | central 140 | century 141 | certain 142 | chair 143 | chance 144 | change 145 | chase 146 | cheap 147 | cheese 148 | chicken 149 | child 150 | children 151 | chocolate 152 | choice 153 | choose 154 | circle 155 | city 156 | class 157 | clean 158 | clear 159 | clever 160 | climb 161 | clock 162 | close 163 | cloth 164 | clothes 165 | cloud 166 | cloudy 167 | coat 168 | coffee 169 | coin 170 | cold 171 | collect 172 | colour 173 | comb 174 | come 175 | comfortable 176 | common 177 | compare 178 | complete 179 | computer 180 | condition 181 | contain 182 | continue 183 | control 184 | cook 185 | cool 186 | copper 187 | corn 188 | corner 189 | correct 190 | cost 191 | count 192 | country 193 | course 194 | cover 195 | crash 196 | cross 197 | cry 198 | cup 199 | cupboard 200 | cut 201 | dance 202 | dangerous 203 | dark 204 | daughter 205 | day 206 | dead 207 | decide 208 | decrease 209 | deep 210 | deer 211 | depend 212 | desk 213 | destroy 214 | develop 215 | die 216 | different 217 | difficult 218 | dinner 219 | direction 220 | dirty 221 | discover 222 | dish 223 | do 224 | dog 225 | door 226 | double 227 | down 228 | draw 229 | dream 230 | dress 231 | drink 232 | drive 233 | drop 234 | dry 235 | duck 236 | dust 237 | duty 238 | each 239 | ear 240 | early 241 | earn 242 | earth 243 | east 244 | easy 245 | eat 246 | education 247 | effect 248 | egg 249 | eight 250 | either 251 | electric 252 | elephant 253 | else 254 | empty 255 | end 256 | enemy 257 | enjoy 258 | enough 259 | enter 260 | entrance 261 | equal 262 | escape 263 | even 264 | evening 265 | event 266 | ever 267 | every 268 | everybody 269 | everyone 270 | exact 271 | examination 272 | example 273 | except 274 | excited 275 | exercise 276 | expect 277 | expensive 278 | explain 279 | extremely 280 | eye 281 | face 282 | fact 283 | fail 284 | fall 285 | false 286 | family 287 | famous 288 | far 289 | farm 290 | fast 291 | fat 292 | father 293 | fault 294 | fear 295 | feed 296 | feel 297 | female 298 | fever 299 | few 300 | fight 301 | fill 302 | film 303 | find 304 | fine 305 | finger 306 | finish 307 | fire 308 | first 309 | fish 310 | fit 311 | five 312 | fix 313 | flag 314 | flat 315 | float 316 | floor 317 | flour 318 | flower 319 | fly 320 | fold 321 | food 322 | fool 323 | foot 324 | football 325 | for 326 | force 327 | foreign 328 | forest 329 | forget 330 | forgive 331 | fork 332 | form 333 | four 334 | fox 335 | free 336 | freedom 337 | freeze 338 | fresh 339 | friend 340 | friendly 341 | from 342 | front 343 | fruit 344 | full 345 | fun 346 | funny 347 | furniture 348 | further 349 | future 350 | game 351 | garden 352 | gate 353 | general 354 | gentleman 355 | get 356 | gift 357 | give 358 | glad 359 | glass 360 | go 361 | goat 362 | god 363 | gold 364 | good 365 | goodbye 366 | grandfather 367 | grandmother 368 | grass 369 | grave 370 | great 371 | green 372 | grey 373 | ground 374 | group 375 | grow 376 | gun 377 | hair 378 | half 379 | hall 380 | hammer 381 | hand 382 | happen 383 | happy 384 | hard 385 | hat 386 | hate 387 | have 388 | he 389 | head 390 | healthy 391 | hear 392 | heart 393 | heaven 394 | heavy 395 | height 396 | hello 397 | help 398 | hen 399 | her 400 | here 401 | hers 402 | hide 403 | high 404 | hill 405 | him 406 | his 407 | hit 408 | hobby 409 | hold 410 | hole 411 | holiday 412 | home 413 | hope 414 | horse 415 | hospital 416 | hot 417 | hotel 418 | hour 419 | house 420 | how 421 | hundred 422 | hungry 423 | hurry 424 | hurt 425 | husband 426 | ice 427 | idea 428 | if 429 | important 430 | in 431 | increase 432 | inside 433 | into 434 | introduce 435 | invent 436 | invite 437 | iron 438 | is 439 | island 440 | it 441 | its 442 | jelly 443 | job 444 | join 445 | juice 446 | jump 447 | just 448 | keep 449 | key 450 | kill 451 | kind 452 | king 453 | kitchen 454 | knee 455 | knife 456 | knock 457 | know 458 | ladder 459 | lady 460 | lamp 461 | land 462 | large 463 | last 464 | late 465 | lately 466 | laugh 467 | lazy 468 | lead 469 | leaf 470 | learn 471 | leave 472 | left 473 | leg 474 | lend 475 | length 476 | less 477 | lesson 478 | let 479 | letter 480 | library 481 | lie 482 | life 483 | light 484 | like 485 | lion 486 | lip 487 | list 488 | listen 489 | little 490 | live 491 | lock 492 | lonely 493 | long 494 | look 495 | lose 496 | lot 497 | love 498 | low 499 | lower 500 | luck 501 | machine 502 | main 503 | make 504 | male 505 | man 506 | many 507 | map 508 | mark 509 | market 510 | marry 511 | matter 512 | may 513 | me 514 | meal 515 | mean 516 | measure 517 | meat 518 | medicine 519 | meet 520 | member 521 | mention 522 | method 523 | middle 524 | milk 525 | million 526 | mind 527 | minute 528 | miss 529 | mistake 530 | mix 531 | model 532 | modern 533 | moment 534 | money 535 | monkey 536 | month 537 | moon 538 | more 539 | morning 540 | most 541 | mother 542 | mountain 543 | mouth 544 | move 545 | much 546 | music 547 | must 548 | my 549 | name 550 | narrow 551 | nation 552 | nature 553 | near 554 | nearly 555 | neck 556 | need 557 | needle 558 | neighbour 559 | neither 560 | net 561 | never 562 | new 563 | news 564 | newspaper 565 | next 566 | nice 567 | night 568 | nine 569 | no 570 | noble 571 | noise 572 | none 573 | nor 574 | north 575 | nose 576 | not 577 | nothing 578 | notice 579 | now 580 | number 581 | obey 582 | object 583 | ocean 584 | of 585 | off 586 | offer 587 | office 588 | often 589 | oil 590 | old 591 | on 592 | one 593 | only 594 | open 595 | opposite 596 | or 597 | orange 598 | order 599 | other 600 | our 601 | out 602 | outside 603 | over 604 | own 605 | page 606 | pain 607 | paint 608 | pair 609 | pan 610 | paper 611 | parent 612 | park 613 | part 614 | partner 615 | party 616 | pass 617 | past 618 | path 619 | pay 620 | peace 621 | pen 622 | pencil 623 | people 624 | pepper 625 | per 626 | perfect 627 | period 628 | person 629 | petrol 630 | photograph 631 | piano 632 | pick 633 | picture 634 | piece 635 | pig 636 | pin 637 | pink 638 | place 639 | plane 640 | plant 641 | plastic 642 | plate 643 | play 644 | please 645 | pleased 646 | plenty 647 | pocket 648 | point 649 | poison 650 | police 651 | polite 652 | pool 653 | poor 654 | popular 655 | position 656 | possible 657 | potato 658 | pour 659 | power 660 | present 661 | press 662 | pretty 663 | prevent 664 | price 665 | prince 666 | prison 667 | private 668 | prize 669 | probably 670 | problem 671 | produce 672 | promise 673 | proper 674 | protect 675 | provide 676 | public 677 | pull 678 | punish 679 | pupil 680 | push 681 | put 682 | queen 683 | question 684 | quick 685 | quiet 686 | quite 687 | radio 688 | rain 689 | rainy 690 | raise 691 | reach 692 | read 693 | ready 694 | real 695 | really 696 | receive 697 | record 698 | red 699 | remember 700 | remind 701 | remove 702 | rent 703 | repair 704 | repeat 705 | reply 706 | report 707 | rest 708 | restaurant 709 | result 710 | return 711 | rice 712 | rich 713 | ride 714 | right 715 | ring 716 | rise 717 | road 718 | rob 719 | rock 720 | room 721 | round 722 | rubber 723 | rude 724 | rule 725 | ruler 726 | run 727 | rush 728 | sad 729 | safe 730 | sail 731 | salt 732 | same 733 | sand 734 | save 735 | say 736 | school 737 | science 738 | scissors 739 | search 740 | seat 741 | second 742 | see 743 | seem 744 | sell 745 | send 746 | sentence 747 | serve 748 | seven 749 | several 750 | sex 751 | shade 752 | shadow 753 | shake 754 | shape 755 | share 756 | sharp 757 | she 758 | sheep 759 | sheet 760 | shelf 761 | shine 762 | ship 763 | shirt 764 | shoe 765 | shoot 766 | shop 767 | short 768 | should 769 | shoulder 770 | shout 771 | show 772 | sick 773 | side 774 | signal 775 | silence 776 | silly 777 | silver 778 | similar 779 | simple 780 | since 781 | sing 782 | single 783 | sink 784 | sister 785 | sit 786 | six 787 | size 788 | skill 789 | skin 790 | skirt 791 | sky 792 | sleep 793 | slip 794 | slow 795 | small 796 | smell 797 | smile 798 | smoke 799 | snow 800 | so 801 | soap 802 | sock 803 | soft 804 | some 805 | someone 806 | something 807 | sometimes 808 | son 809 | soon 810 | sorry 811 | sound 812 | soup 813 | south 814 | space 815 | speak 816 | special 817 | speed 818 | spell 819 | spend 820 | spoon 821 | sport 822 | spread 823 | spring 824 | square 825 | stamp 826 | stand 827 | star 828 | start 829 | station 830 | stay 831 | steal 832 | steam 833 | step 834 | still 835 | stomach 836 | stone 837 | stop 838 | store 839 | storm 840 | story 841 | strange 842 | street 843 | strong 844 | structure 845 | student 846 | study 847 | stupid 848 | subject 849 | substance 850 | successful 851 | such 852 | sudden 853 | sugar 854 | suitable 855 | summer 856 | sun 857 | sunny 858 | support 859 | sure 860 | surprise 861 | sweet 862 | swim 863 | sword 864 | table 865 | take 866 | talk 867 | tall 868 | taste 869 | taxi 870 | tea 871 | teach 872 | team 873 | tear 874 | telephone 875 | television 876 | tell 877 | ten 878 | tennis 879 | terrible 880 | test 881 | than 882 | that 883 | the 884 | their 885 | then 886 | there 887 | therefore 888 | these 889 | thick 890 | thin 891 | thing 892 | think 893 | third 894 | this 895 | though 896 | threat 897 | three 898 | tidy 899 | tie 900 | title 901 | to 902 | today 903 | toe 904 | together 905 | tomorrow 906 | tonight 907 | too 908 | tool 909 | tooth 910 | top 911 | total 912 | touch 913 | town 914 | train 915 | tram 916 | travel 917 | tree 918 | trouble 919 | true 920 | trust 921 | try 922 | turn 923 | twice 924 | type 925 | ugly 926 | uncle 927 | under 928 | understand 929 | unit 930 | until 931 | up 932 | use 933 | useful 934 | usual 935 | usually 936 | vegetable 937 | very 938 | village 939 | visit 940 | voice 941 | wait 942 | wake 943 | walk 944 | want 945 | warm 946 | was 947 | wash 948 | waste 949 | watch 950 | water 951 | way 952 | we 953 | weak 954 | wear 955 | weather 956 | wedding 957 | week 958 | weight 959 | welcome 960 | well 961 | were 962 | west 963 | wet 964 | what 965 | wheel 966 | when 967 | where 968 | which 969 | while 970 | white 971 | who 972 | why 973 | wide 974 | wife 975 | wild 976 | will 977 | win 978 | wind 979 | window 980 | wine 981 | winter 982 | wire 983 | wise 984 | wish 985 | with 986 | without 987 | woman 988 | wonder 989 | word 990 | work 991 | world 992 | worry 993 | yard 994 | yell 995 | yesterday 996 | yet 997 | you 998 | young 999 | your 1000 | zero 1001 | zoo 1002 | -------------------------------------------------------------------------------- /help/decrypt.help: -------------------------------------------------------------------------------- 1 | usage: mlck decrypt [--email=] 2 | [--file=] [--output-file=] [--armor] 3 | [--passphrase=] 4 | 5 | Decrypt a file 6 | 7 | e.g. 8 | 9 | (1) mlck decrypt --file=cat.gif 10 | 11 | Decrypt the file cat.gif. 12 | 13 | -------------------------------------------------------------------------------- /help/default.help: -------------------------------------------------------------------------------- 1 | usage: mlck id [] [--passphrase=] [--save] 2 | mlck encrypt [ ...] [--self] [--email=] 3 | [--file=] [--output-file=] [--armor] 4 | [--passphrase=] 5 | [--anonymous] 6 | mlck decrypt [--email=] 7 | [--file=] [--output-file=] [--armor] 8 | [--passphrase=] 9 | mlck help [] 10 | mlck --version 11 | mlck --license 12 | 13 | The following commands are available: 14 | 15 | id Generate a miniLock ID 16 | encrypt Encrypt a file 17 | decrypt Decrypt a file 18 | help Print help on a topic 19 | 20 | -------------------------------------------------------------------------------- /help/encrypt.help: -------------------------------------------------------------------------------- 1 | usage: mlck encrypt [ ...] [--self] [--email=] 2 | [--file=] [--output-file=] [--armor] 3 | [--passphrase=] 4 | [--anonymous] 5 | 6 | Encrypt a file 7 | 8 | e.g. 9 | 10 | (1) mlck encrypt psciyAZ9aqFPqS5c27k4VYkNSnbt5ACfMpUB5tnQme9Px > cat.gif 11 | 12 | Encrypt a message to the miniLock ID 13 | psciyAZ9aqFPqS5c27k4VYkNSnbt5ACfMpUB5tnQme9Px and save the 14 | output in a file called cat.gif. 15 | 16 | (2) mlck encrypt --email=bob@example.com --self --file=passport.jpeg 17 | 18 | Encrypt the file passport.jpeg from bob@example.com to 19 | bob@example.com. 20 | 21 | -------------------------------------------------------------------------------- /help/help.help: -------------------------------------------------------------------------------- 1 | usage: mlck help [] 2 | 3 | Print help on a topic 4 | 5 | -------------------------------------------------------------------------------- /help/id.help: -------------------------------------------------------------------------------- 1 | usage: mlck id [] [--passphrase=] [--save] 2 | 3 | Generate a miniLock ID 4 | 5 | e.g. 6 | 7 | (1) mlck id alice@example.com --save 8 | 9 | Generate a miniLock ID for alice@example.com and save it to 10 | your profile. 11 | 12 | (2) mlck id 13 | 14 | Print your miniLock ID. 15 | 16 | -------------------------------------------------------------------------------- /mlck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./build/cli/main').run() 4 | 5 | // vim: et ts=2 sw=2 6 | -------------------------------------------------------------------------------- /mlck-debug: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('source-map-support').install() 4 | 5 | require('./build/cli/main').run() 6 | 7 | // vim: et ts=2 sw=2 8 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/module') 2 | 3 | // vim: et ts=2 sw=2 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minilock-cli", 3 | "version": "0.2.14", 4 | "description": "A command line version of miniLock", 5 | "author": { 6 | "name": "Manish Jethani", 7 | "email": "manish.jethani@gmail.com", 8 | "url": "http://manishjethani.com/" 9 | }, 10 | "homepage": "http://mjethani.github.io/miniLock-cli/", 11 | "bin": { 12 | "mlck": "./mlck" 13 | }, 14 | "scripts": { 15 | "make": "make", 16 | "build": "make build", 17 | "test": "make test", 18 | "watch": "make watch" 19 | }, 20 | "keywords": [ 21 | "encryption", 22 | "minilock" 23 | ], 24 | "preferGlobal": true, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mjethani/miniLock-cli.git" 28 | }, 29 | "dependencies": { 30 | "blake2s-js": "dchest/blake2s-js.git#64e449f020c47b88f3edb417efe0fbe65ebdf86e", 31 | "bs58": "cryptocoinjs/bs58.git#d3ee704d5f6e9d450333395ba17ce5c654f19d72", 32 | "nacl-stream": "dchest/nacl-stream-js.git#cb00b7f06a953ebb7e3895c3a22d6d05208d25a5", 33 | "scrypt-async": "dchest/scrypt-async-js.git#ac2e88a22fbb8856ab2f95e85430900f54fec1c9", 34 | "tweetnacl": "dchest/tweetnacl-js.git#abfbce7c68c8ad0b3b8a90b769e1f67885237aac", 35 | "zxcvbn": "dropbox/zxcvbn.git#063315ee6400116dacbd244f1dc98a54e0c53aec" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "6.3.17", 39 | "babel-preset-es2015": "6.3.13", 40 | "babel-tape-runner": "2.0.0", 41 | "node-bufferstream": "^0.1.1", 42 | "source-map-support": "^0.3.3", 43 | "tape": "^4.2.2" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/mjethani/miniLock-cli/issues", 47 | "email": "manish.jethani@gmail.com" 48 | }, 49 | "license": "ISC" 50 | } 51 | -------------------------------------------------------------------------------- /src/cli/commands/decrypt.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import * as miniLock from '../../module' 4 | 5 | import { die, hex, logError, parseArgs } from '../../common/util' 6 | 7 | import debug from '../../common/debug' 8 | 9 | import { generateId } from '../helpers/id' 10 | import { readPassphrase } from '../helpers/passphrase' 11 | import { getProfile } from '../helpers/profile' 12 | import { handleUnknownOption } from '../helpers/unknown' 13 | 14 | function decryptFile(keyPair, file, outputFile, { armor } = {}) { 15 | return new Promise((resolve, reject) => { 16 | if (typeof file !== 'string' && process.stdin.isTTY) { 17 | console.error('Reading from stdin ...') 18 | } 19 | 20 | const inputStream = typeof file === 'string' ? fs.createReadStream(file) 21 | : process.stdin 22 | 23 | const outputFilename = typeof outputFile === 'string' ? outputFile 24 | : null 25 | 26 | if (typeof outputFilename === 'string') { 27 | debug(`Writing to file ${outputFilename}`) 28 | } else if (!process.stdout.isTTY) { 29 | debug('Writing to stdout') 30 | } 31 | 32 | const outputStream = typeof outputFilename === 'string' 33 | ? fs.createWriteStream(outputFilename) : process.stdout 34 | 35 | miniLock.decryptStream(keyPair, inputStream, outputStream, { 36 | armor, 37 | envelope: { 38 | before: '\n--- BEGIN MESSAGE ---\n', 39 | after: '\n--- END MESSAGE ---\n' 40 | } 41 | }, (error, outputByteCount, { senderId, originalFilename } = {}) => { 42 | if (error) { 43 | reject(error) 44 | } else { 45 | resolve([ outputByteCount, outputFilename, 46 | { senderId, originalFilename } ]) 47 | } 48 | }) 49 | }) 50 | } 51 | 52 | export function execute(args) { 53 | const defaultOptions = { 54 | 'email': null, 55 | 'passphrase': null, 56 | 'secret': null, 57 | 'file': null, 58 | 'output-file': null, 59 | 'armor': false, 60 | } 61 | 62 | const shortcuts = { 63 | '-e': '--email=', 64 | '-P': '--passphrase=', 65 | '-f': '--file=', 66 | '-o': '--output-file=', 67 | '-a': '--armor', 68 | } 69 | 70 | const options = parseArgs(args, defaultOptions, shortcuts) 71 | 72 | if (options['!?'].length > 0) { 73 | handleUnknownOption(options['!?'][0], Object.keys(defaultOptions)) 74 | } 75 | 76 | let { 77 | 'email': email, 78 | 'passphrase': passphrase, 79 | 'secret': secret, 80 | 'file': file, 81 | 'output-file': outputFile, 82 | 'armor': armor, 83 | } = options 84 | 85 | let profile = null 86 | 87 | if (typeof secret !== 'string') { 88 | profile = getProfile() 89 | 90 | secret = profile && profile.secret || null 91 | } 92 | 93 | let keyPair = typeof email !== 'string' && secret && 94 | miniLock.keyPairFromSecret(secret) 95 | 96 | if (!keyPair) { 97 | if (typeof email !== 'string' && profile) { 98 | email = profile.email 99 | } 100 | 101 | if (typeof email !== 'string') { 102 | die('Email required.') 103 | } 104 | 105 | if (typeof passphrase !== 'string' && !process.stdin.isTTY) { 106 | die('No passphrase given; no terminal available.') 107 | } 108 | } 109 | 110 | const checkId = !keyPair && profile && email === profile.email && profile.id 111 | 112 | const promise = keyPair ? Promise.resolve() 113 | : typeof passphrase === 'string' ? Promise.resolve(passphrase) 114 | : readPassphrase(0) 115 | 116 | promise.then(passphrase => { 117 | if (!keyPair) { 118 | debug(`Using passphrase ${passphrase}`) 119 | } 120 | 121 | if (!keyPair) { 122 | debug(`Generating key pair with email ${email}` + 123 | ` and passphrase ${passphrase}`) 124 | 125 | return generateId(email, passphrase) 126 | 127 | } else { 128 | return Promise.resolve([ 129 | miniLock.miniLockId(keyPair.publicKey), 130 | keyPair 131 | ]) 132 | } 133 | 134 | }).then(([ id, keyPair_ ]) => { 135 | keyPair = keyPair_ 136 | 137 | debug(`Our public key is ${hex(keyPair.publicKey)}`) 138 | debug(`Our secret key is ${hex(keyPair.secretKey)}`) 139 | 140 | if (checkId && id !== checkId) { 141 | console.error(`Incorrect passphrase for ${email}`) 142 | 143 | die() 144 | } 145 | 146 | debug('Begin file decryption') 147 | 148 | return decryptFile(keyPair, file, outputFile, { armor }) 149 | 150 | }).then(([ outputByteCount, outputFilename, 151 | { senderId, originalFilename } = {} ]) => { 152 | debug('File decryption complete') 153 | 154 | if (process.stdout.isTTY) { 155 | console.log() 156 | console.log(`Message from ${senderId}.`) 157 | console.log() 158 | 159 | if (originalFilename) { 160 | console.log(`Original filename: ${originalFilename}`) 161 | console.log() 162 | } 163 | 164 | if (typeof outputFilename === 'string') { 165 | console.log(`Wrote ${outputByteCount} bytes to ${outputFilename}`) 166 | console.log() 167 | } 168 | } 169 | 170 | }).catch(error => { 171 | if (error === miniLock.ERR_PARSE_ERROR) { 172 | console.error('The file appears corrupt.') 173 | } else if (error === miniLock.ERR_UNSUPPORTED_VERSION) { 174 | console.error('This miniLock version is not supported.') 175 | } else if (error === miniLock.ERR_NOT_A_RECIPIENT) { 176 | console.error(`The message is not intended for` + 177 | ` ${miniLock.miniLockId(keyPair.publicKey)}.`) 178 | } else if (error === miniLock.ERR_MESSAGE_INTEGRITY_CHECK_FAILED) { 179 | console.error('The message is corrupt.') 180 | } else { 181 | logError(error) 182 | } 183 | 184 | die() 185 | }) 186 | } 187 | 188 | // vim: et ts=2 sw=2 189 | -------------------------------------------------------------------------------- /src/cli/commands/encrypt.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | import * as miniLock from '../../module' 6 | 7 | import { die, hex, logError, parseArgs } from '../../common/util' 8 | 9 | import debug from '../../common/debug' 10 | 11 | import { generateId } from '../helpers/id' 12 | import { readPassphrase } from '../helpers/passphrase' 13 | import { getProfile } from '../helpers/profile' 14 | import { handleUnknownOption } from '../helpers/unknown' 15 | 16 | function encryptFile(keyPair, file, outputFile, ids, 17 | { armor, includeSelf } = {}) { 18 | return new Promise((resolve, reject) => { 19 | if (typeof file !== 'string' && process.stdin.isTTY) { 20 | console.error('Reading from stdin ...') 21 | } 22 | 23 | const inputStream = typeof file === 'string' ? fs.createReadStream(file) 24 | : process.stdin 25 | 26 | const outputFilename = typeof outputFile === 'string' ? outputFile 27 | : typeof file === 'string' ? `${file}.minilock` 28 | : null 29 | 30 | if (typeof outputFilename === 'string') { 31 | debug(`Writing to file ${outputFilename}`) 32 | } else if (!process.stdout.isTTY) { 33 | debug('Writing to stdout') 34 | } 35 | 36 | if (!armor && typeof outputFilename !== 'string' && process.stdout.isTTY) { 37 | console.error('WARNING: Not writing output to terminal.') 38 | } 39 | 40 | const outputStream = typeof outputFilename === 'string' 41 | ? fs.createWriteStream(outputFilename) : armor || !process.stdout.isTTY 42 | ? process.stdout : null 43 | 44 | miniLock.encryptStream(keyPair, inputStream, outputStream, ids, { 45 | filename: typeof file === 'string' ? path.basename(file) : null, 46 | armor, 47 | includeSelf 48 | }, (error, outputByteCount) => { 49 | if (error) { 50 | reject(error) 51 | } else { 52 | resolve([ outputByteCount, outputFilename ]) 53 | } 54 | }) 55 | }) 56 | } 57 | 58 | export function execute(args) { 59 | const defaultOptions = { 60 | 'email': null, 61 | 'passphrase': null, 62 | 'secret': null, 63 | 'file': null, 64 | 'output-file': null, 65 | 'armor': false, 66 | 'self': false, 67 | 'anonymous': false, 68 | } 69 | 70 | const shortcuts = { 71 | '-e': '--email=', 72 | '-P': '--passphrase=', 73 | '-f': '--file=', 74 | '-o': '--output-file=', 75 | '-a': '--armor', 76 | } 77 | 78 | const options = parseArgs(args, defaultOptions, shortcuts) 79 | 80 | if (options['!?'].length > 0) { 81 | handleUnknownOption(options['!?'][0], Object.keys(defaultOptions)) 82 | } 83 | 84 | let ids = options['...'].slice() 85 | 86 | let { 87 | 'email': email, 88 | 'passphrase': passphrase, 89 | 'secret': secret, 90 | 'file': file, 91 | 'output-file': outputFile, 92 | 'armor': armor, 93 | 'self': includeSelf, 94 | 'anonymous': anonymous, 95 | } = options 96 | 97 | for (let id of ids) { 98 | if (!miniLock.validateId(id)) { 99 | die(`${id} doesn't look like a valid miniLock ID.`) 100 | } 101 | } 102 | 103 | let profile = null 104 | 105 | if (typeof secret !== 'string') { 106 | profile = getProfile() 107 | 108 | secret = profile && profile.secret || null 109 | } 110 | 111 | let keyPair = !anonymous && typeof email !== 'string' && secret && 112 | miniLock.keyPairFromSecret(secret) 113 | 114 | if (!keyPair) { 115 | if (typeof email !== 'string' && profile) { 116 | email = profile.email 117 | } 118 | 119 | if (!anonymous && typeof email !== 'string') { 120 | die('Email required.') 121 | } 122 | 123 | if (!anonymous && typeof passphrase !== 'string' && !process.stdin.isTTY) { 124 | die('No passphrase given; no terminal available.') 125 | } 126 | } 127 | 128 | const checkId = !anonymous && !keyPair && profile && 129 | email === profile.email && profile.id 130 | 131 | const promise = anonymous || keyPair ? Promise.resolve() 132 | : typeof passphrase === 'string' ? Promise.resolve(passphrase) 133 | : readPassphrase(0) 134 | 135 | promise.then(passphrase => { 136 | if (!anonymous && !keyPair) { 137 | debug(`Using passphrase ${passphrase}`) 138 | } 139 | 140 | if (anonymous || !keyPair) { 141 | let email_ = email 142 | let passphrase_ = passphrase 143 | 144 | if (anonymous) { 145 | // Generate a random passphrase. 146 | email_ = 'Anonymous' 147 | passphrase_ = crypto.randomBytes(32).toString('base64') 148 | } 149 | 150 | debug(`Generating key pair with email ${email_}` + 151 | ` and passphrase ${passphrase_}`) 152 | 153 | return generateId(email_, passphrase_) 154 | 155 | } else { 156 | return Promise.resolve([ 157 | miniLock.miniLockId(keyPair.publicKey), 158 | keyPair 159 | ]) 160 | } 161 | 162 | }).then(([ id, keyPair_ ]) => { 163 | keyPair = keyPair_ 164 | 165 | debug(`Our public key is ${hex(keyPair.publicKey)}`) 166 | debug(`Our secret key is ${hex(keyPair.secretKey)}`) 167 | 168 | if (!anonymous && checkId && id !== checkId) { 169 | console.error(`Incorrect passphrase for ${email}`) 170 | 171 | die() 172 | } 173 | 174 | debug('Begin file encryption') 175 | 176 | return encryptFile(keyPair, file, outputFile, ids, { armor, includeSelf }) 177 | 178 | }).then(([ outputByteCount, outputFilename ]) => { 179 | debug('File encryption complete') 180 | 181 | if (process.stdout.isTTY) { 182 | console.log() 183 | console.log(`Encrypted from` + 184 | ` ${miniLock.miniLockId(keyPair.publicKey)}.`) 185 | console.log() 186 | 187 | if (typeof outputFilename === 'string') { 188 | console.log(`Wrote ${outputByteCount} bytes to ${outputFilename}`) 189 | console.log() 190 | } 191 | } 192 | 193 | }).catch(error => { 194 | logError(error) 195 | 196 | die() 197 | }) 198 | } 199 | 200 | // vim: et ts=2 sw=2 201 | -------------------------------------------------------------------------------- /src/cli/commands/help.js: -------------------------------------------------------------------------------- 1 | import { printHelp } from '../helpers/help' 2 | 3 | export function execute([ topic ]) { 4 | printHelp(topic) 5 | } 6 | 7 | // vim: et ts=2 sw=2 8 | -------------------------------------------------------------------------------- /src/cli/commands/id.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import path from 'path' 3 | 4 | import * as miniLock from '../../module' 5 | 6 | import { die, home, logError, parseArgs } from '../../common/util' 7 | 8 | import debug from '../../common/debug' 9 | 10 | import Profile from '../objects/profile' 11 | 12 | import { generateId } from '../helpers/id' 13 | import { readPassphrase } from '../helpers/passphrase' 14 | import { getProfile } from '../helpers/profile' 15 | import { handleUnknownOption } from '../helpers/unknown' 16 | 17 | function printId(id) { 18 | if (process.stdout.isTTY) { 19 | console.log() 20 | console.log(`Your miniLock ID: ${id}.`) 21 | console.log() 22 | } else { 23 | console.log(id) 24 | } 25 | } 26 | 27 | function saveId(email, id, keyPair) { 28 | const data = {} 29 | 30 | if (keyPair) { 31 | // Store only the secret key. If it's compromised, you have to get a new 32 | // one. No other information is leaked. 33 | data.secret = miniLock.miniLockId(keyPair.secretKey) 34 | } else { 35 | data.email = email 36 | data.id = id 37 | } 38 | 39 | Profile.saveToFile(new Profile(data), path.resolve(home(), '.mlck', 40 | 'profile.json')) 41 | } 42 | 43 | export function execute(args) { 44 | const defaultOptions = { 45 | 'email': null, 46 | 'passphrase': null, 47 | 'secret': null, 48 | 'anonymous': false, 49 | 'save': false, 50 | 'save-key': false, 51 | } 52 | 53 | const shortcuts = { 54 | '-e': '--email=', 55 | '-P': '--passphrase=', 56 | } 57 | 58 | const options = parseArgs(args, defaultOptions, shortcuts) 59 | 60 | if (options['!?'].length > 0) { 61 | handleUnknownOption(options['!?'][0], Object.keys(defaultOptions)) 62 | } 63 | 64 | let { 65 | 'email': email, 66 | 'passphrase': passphrase, 67 | 'secret': secret, 68 | 'anonymous': anonymous, 69 | 'save': save, 70 | 'save-key': saveKey, 71 | } = options 72 | 73 | if (options['...'][0]) { 74 | email = options['...'][0] 75 | } 76 | 77 | if (anonymous) { 78 | // Generate a random passphrase. 79 | email = 'Anonymous' 80 | passphrase = crypto.randomBytes(32).toString('base64') 81 | } 82 | 83 | if (typeof email === 'string') { 84 | const promise = typeof passphrase === 'string' 85 | ? Promise.resolve(passphrase) 86 | : readPassphrase() 87 | 88 | promise.then(passphrase => { 89 | if (!anonymous) { 90 | debug(`Using passphrase ${passphrase}`) 91 | } 92 | 93 | debug(`Generating key pair with email ${email}` + 94 | ` and passphrase ${passphrase}`) 95 | 96 | return generateId(email, passphrase) 97 | 98 | }).then(([ id, keyPair ]) => { 99 | if (saveKey) { 100 | saveId(email, id, keyPair) 101 | } else if (save) { 102 | saveId(email, id) 103 | } 104 | 105 | printId(id) 106 | 107 | }).catch(error => { 108 | logError(error) 109 | 110 | die() 111 | }) 112 | 113 | } else if (typeof secret === 'string') { 114 | const keyPair = miniLock.keyPairFromSecret(secret) 115 | 116 | if (saveKey) { 117 | saveId(null, null, keyPair) 118 | } 119 | 120 | printId(miniLock.miniLockId(keyPair.publicKey)) 121 | 122 | } else { 123 | const profile = getProfile() 124 | 125 | if (profile && profile.id) { 126 | printId(profile.id) 127 | } else if (profile && profile.secret) { 128 | const keyPair = miniLock.keyPairFromSecret(profile.secret) 129 | 130 | printId(miniLock.miniLockId(keyPair.publicKey)) 131 | } else { 132 | console.error('No profile data available.') 133 | } 134 | } 135 | } 136 | 137 | // vim: et ts=2 sw=2 138 | -------------------------------------------------------------------------------- /src/cli/commands/license.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export function execute() { 5 | process.stdout.write(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 6 | 'LICENSE'))) 7 | } 8 | 9 | // vim: et ts=2 sw=2 10 | -------------------------------------------------------------------------------- /src/cli/commands/version.js: -------------------------------------------------------------------------------- 1 | import version from '../../common/version' 2 | 3 | export function execute() { 4 | console.log(`miniLock-cli v${version}`) 5 | } 6 | 7 | // vim: et ts=2 sw=2 8 | -------------------------------------------------------------------------------- /src/cli/helpers/help.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export function printUsage() { 5 | try { 6 | const help = fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 7 | 'help', 'default.help'), 'utf8') 8 | process.stderr.write(help.split('\n\n')[0] + '\n\n') 9 | } catch (error) { 10 | } 11 | } 12 | 13 | export function printHelp(topic) { 14 | try { 15 | const help = fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 16 | 'help', `${topic || 'default'}.help`), 'utf8') 17 | process.stdout.write(help) 18 | } catch (error) { 19 | printUsage() 20 | } 21 | } 22 | 23 | // vim: et ts=2 sw=2 24 | -------------------------------------------------------------------------------- /src/cli/helpers/id.js: -------------------------------------------------------------------------------- 1 | import * as miniLock from '../../module' 2 | 3 | export function generateId(email, passphrase) { 4 | return new Promise(resolve => { 5 | miniLock.getKeyPair(passphrase, email, keyPair => { 6 | resolve([ miniLock.miniLockId(keyPair.publicKey), keyPair ]) 7 | }) 8 | }) 9 | } 10 | 11 | // vim: et ts=2 sw=2 12 | -------------------------------------------------------------------------------- /src/cli/helpers/passphrase.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import zxcvbn from 'zxcvbn' 4 | 5 | import { prompt } from '../../common/util' 6 | 7 | import Dictionary from '../objects/dictionary' 8 | 9 | let dictionary = null 10 | 11 | function loadDictionary() { 12 | try { 13 | dictionary = Dictionary.loadFromFile(path.resolve(__dirname, '..', '..', 14 | '..', 'dictionary')) 15 | } catch (error) { 16 | dictionary = new Dictionary() 17 | } 18 | } 19 | 20 | function randomPassphrase(entropy) { 21 | if (!dictionary) { 22 | loadDictionary() 23 | } 24 | 25 | if (dictionary.wordCount === 0) { 26 | return null 27 | } 28 | 29 | let passphrase = '' 30 | 31 | while (zxcvbn(passphrase).entropy < entropy) { 32 | // Pick a random word from the dictionary and add it to the passphrase. 33 | passphrase += (passphrase && ' ' || '') + dictionary.randomWord() 34 | } 35 | 36 | return passphrase 37 | } 38 | 39 | export function readPassphrase(minEntropy = 100) { 40 | return new Promise((resolve, reject) => { 41 | if (minEntropy) { 42 | // Display a dictionary-based random passphrase as a hint/suggestion. 43 | const example = randomPassphrase(minEntropy) 44 | 45 | if (example) { 46 | console.log(example) 47 | console.log() 48 | } 49 | } 50 | 51 | prompt('Passphrase (leave blank to quit): ', true) 52 | .then(passphrase => { 53 | if (passphrase === '') { 54 | die() 55 | } 56 | 57 | const entropy = zxcvbn(passphrase).entropy 58 | 59 | if (entropy < minEntropy) { 60 | console.log() 61 | console.log(`Entropy: ${entropy}/${minEntropy}`) 62 | console.log() 63 | console.log('Let\'s try once more ...') 64 | console.log() 65 | 66 | resolve(readPassphrase(minEntropy)) 67 | } else { 68 | resolve(passphrase) 69 | } 70 | }).catch(error => { 71 | reject(error) 72 | }) 73 | }) 74 | } 75 | 76 | // vim: et ts=2 sw=2 77 | -------------------------------------------------------------------------------- /src/cli/helpers/profile.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { home } from '../../common/util' 4 | 5 | import Profile from '../objects/profile' 6 | 7 | let profile = null 8 | 9 | function loadProfile() { 10 | try { 11 | profile = Profile.loadFromFile(path.resolve(home(), '.mlck', 12 | 'profile.json')) 13 | } catch (error) { 14 | if (error instanceof SyntaxError) { 15 | console.error('WARNING: Profile data is corrupt.') 16 | } 17 | } 18 | } 19 | 20 | export function getProfile() { 21 | if (profile === null) { 22 | loadProfile() 23 | 24 | if (!profile) { 25 | profile = undefined 26 | } 27 | } 28 | 29 | return profile || null 30 | } 31 | 32 | // vim: et ts=2 sw=2 33 | -------------------------------------------------------------------------------- /src/cli/helpers/unknown.js: -------------------------------------------------------------------------------- 1 | import { die, findCloseMatches } from '../../common/util' 2 | 3 | import { printUsage } from './help' 4 | 5 | function printClosestMatches(string, candidateList) { 6 | const closeMatches = findCloseMatches(string, candidateList, { 7 | distanceThreshold: 2 8 | }) 9 | 10 | if (closeMatches.length > 1) { 11 | console.error('Did you mean one of these?') 12 | } else if (closeMatches.length === 1) { 13 | console.error('Did you mean this?') 14 | } 15 | 16 | for (let match of closeMatches) { 17 | console.error('\t' + match) 18 | } 19 | } 20 | 21 | export function handleUnknownCommand(command, knownCommands) { 22 | if (command) { 23 | console.error(`Unknown command '${command}'.\n\nSee 'mlck --help'.\n`) 24 | 25 | // Find and display close matches using Levenshtein distance. 26 | printClosestMatches(command, knownCommands) 27 | } else { 28 | printUsage() 29 | } 30 | 31 | die() 32 | } 33 | 34 | export function handleUnknownOption(option, knownOptions) { 35 | console.error(`Unknown option '${option}'.\n\nSee 'mlck --help'.\n`) 36 | 37 | if (option.slice(0, 2) === '--') { 38 | // Find and display close matches using Levenshtein distance. 39 | printClosestMatches(option.slice(2), knownOptions) 40 | } 41 | 42 | die() 43 | } 44 | 45 | // vim: et ts=2 sw=2 46 | -------------------------------------------------------------------------------- /src/cli/main.js: -------------------------------------------------------------------------------- 1 | import { parseArgs } from '../common/util' 2 | 3 | import { setDebugFunc } from '../common/debug' 4 | 5 | import { handleUnknownCommand, handleUnknownOption } from './helpers/unknown' 6 | 7 | function handleCommand(command) { 8 | const args = process.argv[2].slice(0, 2) === '--' ? [] : process.argv.slice(3) 9 | 10 | require(`./commands/${command}`).execute(args) 11 | } 12 | 13 | export function run() { 14 | if (process.argv[2] === '--debug') { 15 | process.argv.splice(2, 1) 16 | 17 | setDebugFunc((...rest) => { 18 | console.error(...rest) 19 | }) 20 | } 21 | 22 | const defaultOptions = { 23 | 'help': false, 24 | 'version': false, 25 | 'license': false, 26 | } 27 | 28 | const shortcuts = { 29 | '-h': '--help', 30 | '-?': '--help', 31 | '-V': '--version', 32 | } 33 | 34 | const options = parseArgs([ process.argv[2] || '' ], defaultOptions, 35 | shortcuts) 36 | 37 | if (options['!?'].length > 0) { 38 | handleUnknownOption(options['!?'][0], Object.keys(defaultOptions)) 39 | } 40 | 41 | let { 42 | 'help': help, 43 | 'version': version, 44 | 'license': license, 45 | } = options 46 | 47 | if (help) { 48 | handleCommand('help') 49 | 50 | return 51 | 52 | } else if (version) { 53 | handleCommand('version') 54 | 55 | return 56 | 57 | } else if (license) { 58 | handleCommand('license') 59 | 60 | return 61 | } 62 | 63 | const command = process.argv[2] 64 | 65 | switch (command) { 66 | case 'id': 67 | case 'encrypt': 68 | case 'decrypt': 69 | case 'help': 70 | case 'version': 71 | case 'license': 72 | handleCommand(command) 73 | 74 | break 75 | 76 | default: 77 | handleUnknownCommand(command, [ 78 | 'id', 79 | 'encrypt', 80 | 'decrypt', 81 | 'help', 82 | 'version', 83 | 'license', 84 | ]) 85 | } 86 | } 87 | 88 | function main() { 89 | run() 90 | } 91 | 92 | if (require.main === module) { 93 | main() 94 | } 95 | 96 | // vim: et ts=2 sw=2 97 | -------------------------------------------------------------------------------- /src/cli/objects/dictionary.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import fs from 'fs' 3 | 4 | const words_ = Symbol() 5 | 6 | export default class Dictionary { 7 | static loadFromFile(filename) { 8 | const data = fs.readFileSync(filename, { encoding: 'utf8' }) 9 | 10 | const words = data.split('\n').map(line => 11 | // Trim spaces and strip out comments. 12 | line.replace(/^\s*|\s*$/g, '').replace(/^#.*/, '') 13 | ).filter(line => 14 | // Skip blank lines. 15 | line !== '' 16 | ) 17 | 18 | return new Dictionary(words) 19 | } 20 | 21 | constructor(words) { 22 | this[words_] = Array.isArray(words) ? words.slice() : [] 23 | } 24 | 25 | get wordCount() { 26 | return this[words_].length 27 | } 28 | 29 | wordAt(index) { 30 | return index < this[words_].length ? this[words_][index] : null 31 | } 32 | 33 | randomWord() { 34 | if (this[words_].length === 0) { 35 | return null 36 | } 37 | 38 | const randomNumber = crypto.randomBytes(2).readUInt16BE() 39 | const index = Math.floor((randomNumber / 0x10000) * this[words_].length) 40 | 41 | return this[words_][index] 42 | } 43 | } 44 | 45 | // vim: et ts=2 sw=2 46 | -------------------------------------------------------------------------------- /src/cli/objects/profile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const VERSION = '0.1' 5 | 6 | const data_ = Symbol() 7 | 8 | export default class Profile { 9 | static loadFromFile(filename) { 10 | return new Profile(fs.readFileSync(filename, { encoding: 'utf8' })) 11 | } 12 | 13 | static saveToFile(profile, filename) { 14 | // Create profile directory. 15 | try { 16 | fs.mkdirSync(path.dirname(filename)) 17 | } catch (error) { 18 | if (error.code !== 'EEXIST') { 19 | throw error 20 | } 21 | } 22 | 23 | fs.writeFileSync(filename, JSON.stringify(profile[data_])) 24 | } 25 | 26 | constructor(data) { 27 | if (typeof data !== 'string') { 28 | data = JSON.stringify(data) 29 | } 30 | 31 | this[data_] = JSON.parse(data) 32 | 33 | if (this[data_].version === undefined) { 34 | this[data_].version = VERSION 35 | } 36 | } 37 | 38 | get version() { 39 | return this[data_].version 40 | } 41 | 42 | get email() { 43 | return this[data_].email 44 | } 45 | 46 | get id() { 47 | return this[data_].id 48 | } 49 | 50 | get secret() { 51 | return this[data_].secret 52 | } 53 | } 54 | 55 | // vim: et ts=2 sw=2 56 | -------------------------------------------------------------------------------- /src/common/debug.js: -------------------------------------------------------------------------------- 1 | let debugFunc = null 2 | 3 | export default function debug(...rest) { 4 | if (debugFunc) { 5 | debugFunc(...rest) 6 | } 7 | } 8 | 9 | export function setDebugFunc(func) { 10 | debugFunc = func 11 | } 12 | 13 | // vim: et ts=2 sw=2 14 | -------------------------------------------------------------------------------- /src/common/util.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import os from 'os' 3 | import readline from 'readline' 4 | 5 | export function sortBy(array, prop) { 6 | return array.sort((a, b) => -(a[prop] < b[prop]) || +(a[prop] > b[prop])) 7 | } 8 | 9 | export function stringDistance(s, t) { 10 | const a = new Array(t.length + 1) 11 | for (let x = 0; x < a.length; x++) { 12 | a[x] = x 13 | } 14 | 15 | for (let j = 1; j <= s.length; j++) { 16 | let p = a[0]++ 17 | for (let k = 1; k <= t.length; k++) { 18 | const o = a[k] 19 | if (s[j - 1] === t[k - 1]) { 20 | a[k] = p 21 | } else { 22 | a[k] = Math.min(a[k - 1] + 1, a[k] + 1, p + 1) 23 | } 24 | p = o 25 | } 26 | } 27 | 28 | return a[t.length] 29 | } 30 | 31 | export function findCloseMatches(string, candidateList, 32 | { distanceThreshold = 1 } = {}) { 33 | const matches = candidateList.map(candidate => { 34 | // Split candidate into individual components. e.g. 'output-file' becomes a 35 | // list containing 'output', 'file', and 'output-file'. 36 | const candidateWords = candidate.split('-') 37 | if (candidateWords.length > 1) { 38 | candidateWords.push(candidate) 39 | } 40 | 41 | const distance = candidateWords.reduce((distance, word) => 42 | // Take the lowest distance. 43 | Math.min(distance, stringDistance(string, word)) 44 | , 45 | Infinity) 46 | 47 | return { candidate, distance } 48 | 49 | }).filter(match => match.distance <= distanceThreshold) 50 | 51 | sortBy(matches, 'distance') 52 | 53 | return matches.map(match => match.candidate) 54 | } 55 | 56 | export function arrayCompare(a, b) { 57 | if (a === b || (a == null && b == null)) { 58 | return true 59 | } 60 | 61 | if (a == null || b == null || isNaN(a.length) || isNaN(b.length) || 62 | a.length !== b.length) { 63 | return false 64 | } 65 | 66 | const n = a.length 67 | 68 | for (let i = 0; i < n; i++) { 69 | if (a[i] !== b[i]) { 70 | return false 71 | } 72 | } 73 | 74 | return true 75 | } 76 | 77 | export function isBrowser() { 78 | return typeof window !== 'undefined' 79 | } 80 | 81 | export function hex(data) { 82 | return new Buffer(data).toString('hex') 83 | } 84 | 85 | export function async(func, ...args) { 86 | process.nextTick(() => { 87 | func(...args) 88 | }) 89 | } 90 | 91 | export function die(...rest) { 92 | if (rest.length > 0) { 93 | console.error(...rest) 94 | } 95 | 96 | process.exit(1) 97 | } 98 | 99 | export function logError(error) { 100 | if (error) { 101 | console.error(error.toString()) 102 | } 103 | } 104 | 105 | export function parseArgs(args, ...rest) { 106 | // This function parses command line arguments of two kinds: 107 | // '--long-name[=]' and '-n []' 108 | // 109 | // If the value is omitted, it's assumed to be a boolean true. 110 | // 111 | // You can pass in default values and a mapping of short names to long names 112 | // as the first and second arguments respectively. 113 | 114 | const defaultOptions = typeof rest[0] === 'object' && rest.shift() || 115 | Object.create(null) 116 | const shortcuts = typeof rest[0] === 'object' && rest.shift() || 117 | Object.create(null) 118 | 119 | let expect = null 120 | let stop = false 121 | 122 | let obj = Object.create(defaultOptions) 123 | 124 | obj = Object.defineProperty(obj, '...', { value: [] }) 125 | obj = Object.defineProperty(obj, '!?', { value: [] }) 126 | 127 | // Preprocessing. 128 | args = args.reduce((newArgs, arg) => { 129 | if (!stop) { 130 | if (arg === '--') { 131 | stop = true 132 | 133 | // Split '-xyz' into '-x', '-y', '-z'. 134 | } else if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { 135 | arg = arg.slice(1).split('').map(v => '-' + v) 136 | } 137 | } 138 | 139 | return newArgs.concat(arg) 140 | }, 141 | []) 142 | 143 | stop = false 144 | 145 | return args.reduce((obj, arg, index) => { 146 | const single = !stop && arg[0] === '-' && arg[1] !== '-' 147 | 148 | if (!(single && !(arg = shortcuts[arg]))) { 149 | if (!stop && arg.slice(0, 2) === '--') { 150 | if (arg.length > 2) { 151 | let eq = arg.indexOf('=') 152 | 153 | if (eq === -1) { 154 | eq = arg.length 155 | } 156 | 157 | const name = arg.slice(2, eq) 158 | 159 | if (!single && !Object.prototype.hasOwnProperty.call(defaultOptions, 160 | name)) { 161 | obj['!?'].push(arg.slice(0, eq)) 162 | 163 | return obj 164 | } 165 | 166 | if (single && eq === arg.length - 1) { 167 | obj[expect = name] = '' 168 | 169 | return obj 170 | } 171 | 172 | obj[name] = typeof defaultOptions[name] === 'boolean' && 173 | eq === arg.length || arg.slice(eq + 1) 174 | 175 | } else { 176 | stop = true 177 | } 178 | } else if (expect) { 179 | obj[expect] = arg 180 | 181 | } else if (rest.length > 0) { 182 | obj[rest.shift()] = arg 183 | 184 | } else { 185 | obj['...'].push(arg) 186 | } 187 | 188 | } else if (single) { 189 | obj['!?'].push(args[index]) 190 | } 191 | 192 | expect = null 193 | 194 | return obj 195 | }, 196 | obj) 197 | } 198 | 199 | export function prompt(label, quiet) { 200 | return new Promise((resolve, reject) => { 201 | if (!process.stdin.isTTY || !process.stdout.isTTY) { 202 | throw new Error('No TTY') 203 | } 204 | 205 | if (typeof quiet !== 'boolean') { 206 | quiet = false 207 | } 208 | 209 | if (typeof label === 'string') { 210 | process.stdout.write(label) 211 | } 212 | 213 | const rl = readline.createInterface({ 214 | input: process.stdin, 215 | // The quiet argument is for things like passwords. It turns off standard 216 | // output so nothing is displayed. 217 | output: !quiet && process.stdout || null, 218 | terminal: true 219 | }) 220 | 221 | rl.on('line', line => { 222 | try { 223 | rl.close() 224 | 225 | if (quiet) { 226 | process.stdout.write(os.EOL) 227 | } 228 | 229 | resolve(line) 230 | 231 | } catch (error) { 232 | reject(error) 233 | } 234 | }) 235 | }) 236 | } 237 | 238 | export function home() { 239 | return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'] 240 | } 241 | 242 | export function streamHash(stream, algorithm, { encoding } = {}) { 243 | return new Promise((resolve, reject) => { 244 | const hash = crypto.createHash(algorithm) 245 | 246 | if (typeof encoding === 'string') { 247 | hash.setEncoding(encoding) 248 | } 249 | 250 | stream.on('error', error => { 251 | hash.end() 252 | 253 | reject(error) 254 | }) 255 | 256 | stream.on('end', () => { 257 | hash.end() 258 | 259 | resolve(hash.read()) 260 | }) 261 | 262 | stream.pipe(hash) 263 | }) 264 | } 265 | 266 | // vim: et ts=2 sw=2 267 | -------------------------------------------------------------------------------- /src/common/version.js: -------------------------------------------------------------------------------- 1 | export default require('../../package.json').version 2 | 3 | // vim: et ts=2 sw=2 4 | -------------------------------------------------------------------------------- /src/module/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import path from 'path' 4 | 5 | import BLAKE2s from 'blake2s-js' 6 | import Base58 from 'bs58' 7 | import nacl from 'tweetnacl' 8 | import nacl_ from 'nacl-stream' 9 | import scrypt from 'scrypt-async' 10 | 11 | import { async, hex, isBrowser } from '../common/util' 12 | 13 | import debug from '../common/debug' 14 | 15 | import version from '../common/version' 16 | 17 | export const ERR_PARSE_ERROR = 'Parse error' 18 | export const ERR_UNSUPPORTED_VERSION = 'Unsupported version' 19 | export const ERR_NOT_A_RECIPIENT = 'Not a recipient' 20 | export const ERR_MESSAGE_INTEGRITY_CHECK_FAILED = 21 | 'Message integrity check failed' 22 | 23 | const ENCRYPTION_CHUNK_SIZE = 256 24 | const ARMOR_WIDTH = 64 25 | 26 | function getScryptKey(key, salt, callback) { 27 | scrypt(key, salt, 17, 8, 32, 1000, 28 | keyBytes => callback(nacl.util.decodeBase64(keyBytes)), 29 | 'base64') 30 | } 31 | 32 | export function getKeyPair(key, salt, callback) { 33 | const keyHash = new BLAKE2s(32) 34 | keyHash.update(nacl.util.decodeUTF8(key)) 35 | 36 | getScryptKey(keyHash.digest(), nacl.util.decodeUTF8(salt), 37 | keyBytes => callback(nacl.box.keyPair.fromSecretKey(keyBytes))) 38 | } 39 | 40 | export function miniLockId(publicKey) { 41 | const id = new Uint8Array(33) 42 | 43 | id.set(publicKey) 44 | 45 | const hash = new BLAKE2s(1) 46 | hash.update(publicKey) 47 | 48 | // The last byte is the checksum. 49 | id[32] = hash.digest()[0] 50 | 51 | return Base58.encode(id) 52 | } 53 | 54 | export function keyFromId(id) { 55 | return new Uint8Array(Base58.decode(id).slice(0, 32)) 56 | } 57 | 58 | export function keyPairFromSecret(secret) { 59 | return nacl.box.keyPair.fromSecretKey(keyFromId(secret)) 60 | } 61 | 62 | export function validateKey(key) { 63 | if (!key || !(key.length >= 40 && key.length <= 50) || 64 | !/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ 65 | .test(key)) { 66 | return false 67 | } 68 | 69 | return nacl.util.decodeBase64(key).length === 32 70 | } 71 | 72 | export function validateId(id) { 73 | if (!/^[1-9ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{40,55}$/ 74 | .test(id)) { 75 | return false 76 | } 77 | 78 | const bytes = Base58.decode(id) 79 | if (bytes.length !== 33) { 80 | return false 81 | } 82 | 83 | const hash = new BLAKE2s(1) 84 | hash.update(bytes.slice(0, 32)) 85 | 86 | return hash.digest()[0] === bytes[32] 87 | } 88 | 89 | function temporaryFilename() { 90 | return path.resolve(os.tmpdir(), 91 | '.mlck-' + hex(nacl.randomBytes(32)) + '.tmp') 92 | } 93 | 94 | function readableArray(array) { 95 | const fakeReadable = {} 96 | 97 | fakeReadable.on = (event, listener) => { 98 | if (event === 'readable') { 99 | async(() => { 100 | array.slice().forEach(() => { 101 | listener() 102 | }) 103 | }) 104 | } else if (event === 'end') { 105 | async(listener) 106 | } 107 | } 108 | 109 | fakeReadable.read = () => array.shift() 110 | 111 | return fakeReadable 112 | } 113 | 114 | function asciiArmor(data, indent) { 115 | let ascii = new Buffer(data).toString('base64') 116 | 117 | const lines = [] 118 | 119 | if ((indent = Math.max(0, indent | 0)) > 0) { 120 | // Indent first line. 121 | lines.push(ascii.slice(0, ARMOR_WIDTH - indent)) 122 | 123 | ascii = ascii.slice(ARMOR_WIDTH - indent) 124 | } 125 | 126 | while (ascii.length > 0) { 127 | lines.push(ascii.slice(0, ARMOR_WIDTH)) 128 | 129 | ascii = ascii.slice(ARMOR_WIDTH) 130 | } 131 | 132 | return lines.join('\n') 133 | } 134 | 135 | function makeHeader(ids, senderInfo, fileInfo) { 136 | const ephemeral = nacl.box.keyPair() 137 | const header = { 138 | version: 1, 139 | ephemeral: nacl.util.encodeBase64(ephemeral.publicKey), 140 | decryptInfo: {} 141 | } 142 | 143 | debug(`Ephemeral public key is ${hex(ephemeral.publicKey)}`) 144 | debug(`Ephemeral secret key is ${hex(ephemeral.secretKey)}`) 145 | 146 | for (let id of ids) { 147 | debug(`Adding recipient ${id}`) 148 | 149 | const nonce = nacl.randomBytes(24) 150 | const publicKey = keyFromId(id) 151 | 152 | debug(`Using nonce ${hex(nonce)}`) 153 | 154 | let decryptInfo = { 155 | senderID: senderInfo.id, 156 | recipientID: id, 157 | fileInfo: fileInfo 158 | } 159 | 160 | decryptInfo.fileInfo = nacl.util.encodeBase64(nacl.box( 161 | nacl.util.decodeUTF8(JSON.stringify(decryptInfo.fileInfo)), 162 | nonce, 163 | publicKey, 164 | senderInfo.secretKey 165 | )) 166 | 167 | decryptInfo = nacl.util.encodeBase64(nacl.box( 168 | nacl.util.decodeUTF8(JSON.stringify(decryptInfo)), 169 | nonce, 170 | publicKey, 171 | ephemeral.secretKey 172 | )) 173 | 174 | header.decryptInfo[nacl.util.encodeBase64(nonce)] = decryptInfo 175 | } 176 | 177 | return JSON.stringify(header) 178 | } 179 | 180 | function extractDecryptInfo(header, secretKey) { 181 | let decryptInfo = null 182 | 183 | const ephemeral = nacl.util.decodeBase64(header.ephemeral) 184 | 185 | for (let i in header.decryptInfo) { 186 | const nonce = nacl.util.decodeBase64(i) 187 | 188 | debug(`Trying nonce ${hex(nonce)}`) 189 | 190 | decryptInfo = nacl.util.decodeBase64(header.decryptInfo[i]) 191 | decryptInfo = nacl.box.open(decryptInfo, nonce, ephemeral, secretKey) 192 | 193 | if (decryptInfo) { 194 | decryptInfo = JSON.parse(nacl.util.encodeUTF8(decryptInfo)) 195 | 196 | debug(`Recipient ID is ${decryptInfo.recipientID}`) 197 | debug(`Sender ID is ${decryptInfo.senderID}`) 198 | 199 | decryptInfo.fileInfo = nacl.util.decodeBase64(decryptInfo.fileInfo) 200 | decryptInfo.fileInfo = nacl.box.open(decryptInfo.fileInfo, nonce, 201 | keyFromId(decryptInfo.senderID), secretKey) 202 | 203 | decryptInfo.fileInfo = JSON.parse( 204 | nacl.util.encodeUTF8(decryptInfo.fileInfo) 205 | ) 206 | 207 | debug(`File key is` + 208 | ` ${hex(nacl.util.decodeBase64(decryptInfo.fileInfo.fileKey))}`) 209 | debug(`File nonce is` + 210 | ` ${hex(nacl.util.decodeBase64(decryptInfo.fileInfo.fileNonce))}`) 211 | debug(`File hash is` + 212 | ` ${hex(nacl.util.decodeBase64(decryptInfo.fileInfo.fileHash))}`) 213 | break 214 | } 215 | } 216 | 217 | return decryptInfo 218 | } 219 | 220 | function encryptChunk(chunk, encryptor, output, hash) { 221 | if (chunk && chunk.length > ENCRYPTION_CHUNK_SIZE) { 222 | for (let i = 0; i < chunk.length; i += ENCRYPTION_CHUNK_SIZE) { 223 | encryptChunk(chunk.slice(i, i + ENCRYPTION_CHUNK_SIZE), 224 | encryptor, output, hash) 225 | } 226 | } else { 227 | chunk = encryptor.encryptChunk(new Uint8Array(chunk || []), !chunk) 228 | 229 | debug(`Encrypted chunk ${hex(chunk)}`) 230 | 231 | if (Array.isArray(output)) { 232 | output.push(new Buffer(chunk)) 233 | } else { 234 | output.write(new Buffer(chunk)) 235 | } 236 | 237 | if (hash) { 238 | hash.update(chunk) 239 | } 240 | } 241 | } 242 | 243 | function decryptChunk(chunk, decryptor, output, hash) { 244 | while (true) { 245 | const length = chunk.length >= 4 ? chunk.readUIntLE(0, 4, true) : 0 246 | 247 | if (chunk.length < 4 + 16 + length) { 248 | break 249 | } 250 | 251 | const encrypted = new Uint8Array(chunk.slice(0, 4 + 16 + length)) 252 | const decrypted = decryptor.decryptChunk(encrypted, false) 253 | 254 | chunk = chunk.slice(4 + 16 + length) 255 | 256 | if (decrypted) { 257 | debug(`Decrypted chunk ${hex(decrypted)}`) 258 | 259 | if (Array.isArray(output)) { 260 | output.push(new Buffer(decrypted)) 261 | } else { 262 | output.write(new Buffer(decrypted)) 263 | } 264 | } 265 | 266 | if (hash) { 267 | hash.update(encrypted) 268 | } 269 | } 270 | 271 | return chunk 272 | } 273 | 274 | export function encryptStream(keyPair, inputStream, outputStream, ids, 275 | { filename, armor, includeSelf } = {}, callback) { 276 | const browser = isBrowser() 277 | 278 | const fromId = miniLockId(keyPair.publicKey) 279 | 280 | debug(`Our miniLock ID is ${fromId}`) 281 | 282 | const senderInfo = { 283 | id: fromId, 284 | secretKey: keyPair.secretKey 285 | } 286 | 287 | const fileKey = nacl.randomBytes(32) 288 | const fileNonce = nacl.randomBytes(16) 289 | 290 | debug(`Using file key ${hex(fileKey)}`) 291 | debug(`Using file nonce ${hex(fileNonce)}`) 292 | 293 | const encryptor = nacl_.stream.createEncryptor(fileKey, fileNonce, 294 | ENCRYPTION_CHUNK_SIZE) 295 | const hash = new BLAKE2s(32) 296 | 297 | // This is where the encrypted chunks go. 298 | let encrypted = [] 299 | 300 | const filenameBuffer = new Buffer(256).fill(0) 301 | 302 | if (typeof filename === 'string') { 303 | filenameBuffer.write(filename) 304 | } 305 | 306 | let encryptedDataFile = null 307 | 308 | // The first chunk is the 256-byte null-padded filename. If input is stdin, 309 | // filename is blank. If the UTF-8-encoded filename is greater than 256 310 | // bytes, it is truncated. 311 | encryptChunk(filenameBuffer, encryptor, encrypted, hash) 312 | 313 | let inputByteCount = 0 314 | 315 | inputStream.on('error', error => { 316 | if (encryptedDataFile) { 317 | fs.unlink(encryptedDataFile, () => {}) 318 | } 319 | 320 | callback(error) 321 | }) 322 | 323 | inputStream.on('readable', () => { 324 | const chunk = inputStream.read() 325 | if (chunk !== null) { 326 | inputByteCount += chunk.length 327 | 328 | // If input exceeds the 4K threshold (picked arbitrarily), switch from 329 | // writing to an array to writing to a file. This way we can do 330 | // extremely large files. 331 | if (!browser && inputByteCount > 4 * 1024 && Array.isArray(encrypted)) { 332 | // Generate a random filename for writing encrypted chunks to instead of 333 | // keeping everything in memory. 334 | encryptedDataFile = temporaryFilename() 335 | 336 | const stream = fs.createWriteStream(encryptedDataFile) 337 | 338 | for (let chunk of encrypted) { 339 | stream.write(chunk) 340 | } 341 | 342 | encrypted = stream 343 | } 344 | 345 | // Encrypt this chunk. 346 | encryptChunk(chunk, encryptor, encrypted, hash) 347 | } 348 | }) 349 | 350 | inputStream.on('end', () => { 351 | // Finish up with the encryption. 352 | encryptChunk(null, encryptor, encrypted, hash) 353 | 354 | encryptor.clean() 355 | 356 | // This is the 32-byte BLAKE2 hash of all the ciphertext. 357 | const fileHash = hash.digest() 358 | 359 | debug(`File hash is ${hex(fileHash)}`) 360 | 361 | const fileInfo = { 362 | fileKey: nacl.util.encodeBase64(fileKey), 363 | fileNonce: nacl.util.encodeBase64(fileNonce), 364 | fileHash: nacl.util.encodeBase64(fileHash) 365 | } 366 | 367 | // Pack the sender and file information into a header. 368 | const header = makeHeader(includeSelf ? ids.concat(fromId) : ids, 369 | senderInfo, fileInfo) 370 | 371 | const headerLength = new Buffer(4) 372 | headerLength.writeUInt32LE(header.length) 373 | 374 | debug(`Header length is ${hex(headerLength)}`) 375 | 376 | let outputByteCount = 0 377 | 378 | let buffer = new Buffer(0) 379 | 380 | let asciiIndent = 0 381 | 382 | let outputHeader = Buffer.concat([ 383 | // The file always begins with the magic bytes 0x6d696e694c6f636b. 384 | new Buffer('miniLock'), headerLength, new Buffer(header) 385 | ]) 386 | 387 | if (armor) { 388 | // https://tools.ietf.org/html/rfc4880#section-6 389 | 390 | buffer = outputHeader.slice(outputHeader.length - 391 | outputHeader.length % 3) 392 | outputHeader = asciiArmor(outputHeader.slice(0, outputHeader.length - 393 | outputHeader.length % 3)) 394 | 395 | asciiIndent = outputHeader.length % (ARMOR_WIDTH + 1) 396 | 397 | outputHeader = '-----BEGIN MINILOCK FILE-----\n' + 398 | 'Version: miniLock-cli v' + version + '\n' + 399 | '\n' + 400 | outputHeader 401 | } 402 | 403 | if (outputStream) { 404 | outputStream.write(outputHeader) 405 | } 406 | 407 | outputByteCount += outputHeader.length 408 | 409 | if (Array.isArray(encrypted)) { 410 | encrypted.end = async 411 | } 412 | 413 | encrypted.end(() => { 414 | if (Array.isArray(encrypted)) { 415 | // Wrap array into a stream-like interface. 416 | encrypted = readableArray(encrypted) 417 | } else { 418 | encrypted = fs.createReadStream(encryptedDataFile) 419 | } 420 | 421 | encrypted.on('error', error => { 422 | async(() => { 423 | if (encryptedDataFile) { 424 | fs.unlink(encryptedDataFile, () => {}) 425 | } 426 | 427 | callback(error) 428 | }) 429 | }) 430 | 431 | encrypted.on('readable', () => { 432 | let chunk = encrypted.read() 433 | if (chunk !== null) { 434 | if (armor) { 435 | chunk = Buffer.concat([ buffer, chunk ]) 436 | 437 | const index = chunk.length - chunk.length % 3 438 | 439 | buffer = chunk.slice(index) 440 | chunk = asciiArmor(chunk.slice(0, index), asciiIndent) 441 | 442 | asciiIndent = (chunk.length + asciiIndent) % (ARMOR_WIDTH + 1) 443 | } 444 | 445 | if (outputStream) { 446 | outputStream.write(chunk) 447 | } 448 | 449 | outputByteCount += chunk.length 450 | } 451 | }) 452 | 453 | encrypted.on('end', () => { 454 | if (armor) { 455 | const chunk = asciiArmor(buffer, asciiIndent) + 456 | '\n-----END MINILOCK FILE-----\n' 457 | 458 | if (outputStream) { 459 | outputStream.write(chunk) 460 | } 461 | 462 | outputByteCount += chunk.length 463 | } 464 | 465 | async(() => { 466 | if (encryptedDataFile) { 467 | // Attempt to delete the temporary file, but ignore any error. 468 | fs.unlink(encryptedDataFile, () => {}) 469 | } 470 | 471 | callback(null, outputByteCount) 472 | }) 473 | }) 474 | }) 475 | }) 476 | } 477 | 478 | export function decryptStream(keyPair, inputStream, outputStream, 479 | { armor, envelope } = {}, callback) { 480 | const browser = isBrowser() 481 | 482 | const toId = miniLockId(keyPair.publicKey) 483 | 484 | debug(`Our miniLock ID is ${toId}`) 485 | 486 | let asciiBuffer = '' 487 | 488 | let armorHeaders = null 489 | 490 | let headerLength = NaN 491 | let header = null 492 | 493 | let decryptInfo = null 494 | 495 | let decryptor = null 496 | 497 | const hash = new BLAKE2s(32) 498 | 499 | let buffer = new Buffer(0) 500 | 501 | let error_ = null 502 | 503 | let originalFilename = null 504 | 505 | let outputByteCount = 0 506 | 507 | inputStream.on('error', error => { 508 | callback(error) 509 | }) 510 | 511 | inputStream.on('readable', () => { 512 | let chunk = inputStream.read() 513 | 514 | if (error_ !== null) { 515 | return 516 | } 517 | 518 | if (chunk !== null) { 519 | if (armor) { 520 | asciiBuffer += chunk.toString() 521 | 522 | chunk = new Buffer(0) 523 | 524 | let index = -1 525 | 526 | if (!armorHeaders && asciiBuffer.slice(0, 30) === 527 | '-----BEGIN MINILOCK FILE-----\n' && 528 | (index = asciiBuffer.indexOf('\n\n')) !== -1) { 529 | armorHeaders = asciiBuffer.slice(30, index).toString().split('\n') 530 | 531 | asciiBuffer = asciiBuffer.slice(index + 2) 532 | } 533 | 534 | if (armorHeaders) { 535 | // Strip out newlines and other whitespace. 536 | asciiBuffer = asciiBuffer.replace(/\s+/g, '') 537 | 538 | // Decode as many 32-bit groups as possible now and leave the 539 | // balance for later. 540 | index = asciiBuffer.length - asciiBuffer.length % 4 541 | 542 | chunk = new Buffer(asciiBuffer.slice(0, index), 'base64') 543 | asciiBuffer = asciiBuffer.slice(index) 544 | } 545 | } 546 | 547 | try { 548 | // Read chunk into buffer. 549 | buffer = Buffer.concat([ buffer, chunk ]) 550 | } catch (error) { 551 | // If the buffer length exceeds 0x3fffffff, it'll throw a RangeError. 552 | callback((error_ = error) instanceof RangeError 553 | ? ERR_PARSE_ERROR : error) 554 | return 555 | } 556 | 557 | if (!header) { 558 | try { 559 | if (isNaN(headerLength) && buffer.length >= 12) { 560 | if (buffer[0] !== 0x6d || 561 | buffer[1] !== 0x69 || 562 | buffer[2] !== 0x6e || 563 | buffer[3] !== 0x69 || 564 | buffer[4] !== 0x4c || 565 | buffer[5] !== 0x6f || 566 | buffer[6] !== 0x63 || 567 | buffer[7] !== 0x6b 568 | ) { 569 | throw ERR_PARSE_ERROR 570 | } 571 | 572 | // Read the 4-byte header length, which is after the initial 8 573 | // magic bytes of 'miniLock'. 574 | headerLength = buffer.readUIntLE(8, 4, true) 575 | 576 | if (headerLength > 0x3fffffff) { 577 | throw ERR_PARSE_ERROR 578 | } 579 | 580 | buffer = new Buffer(buffer.slice(12)) 581 | } 582 | 583 | if (!isNaN(headerLength)) { 584 | // Look for the JSON opening brace. 585 | if (buffer.length > 0 && buffer[0] !== 0x7b) { 586 | throw ERR_PARSE_ERROR 587 | } 588 | 589 | if (buffer.length >= headerLength) { 590 | // Read the header and parse the JSON object. 591 | header = JSON.parse(buffer.slice(0, headerLength).toString()) 592 | 593 | if (header.version !== 1) { 594 | throw ERR_UNSUPPORTED_VERSION 595 | } 596 | 597 | if (!validateKey(header.ephemeral)) { 598 | throw ERR_PARSE_ERROR 599 | } 600 | 601 | if (!(decryptInfo = extractDecryptInfo(header, 602 | keyPair.secretKey)) || 603 | decryptInfo.recipientID !== toId) { 604 | throw ERR_NOT_A_RECIPIENT 605 | } 606 | 607 | // Shift the buffer pointer. 608 | buffer = buffer.slice(headerLength) 609 | } 610 | } 611 | } catch (error) { 612 | callback((error_ = error) instanceof SyntaxError 613 | ? ERR_PARSE_ERROR : error) 614 | return 615 | } 616 | } 617 | 618 | if (decryptInfo) { 619 | if (!decryptor) { 620 | // Time to deal with the ciphertext. 621 | decryptor = nacl_.stream.createDecryptor( 622 | nacl.util.decodeBase64(decryptInfo.fileInfo.fileKey), 623 | nacl.util.decodeBase64(decryptInfo.fileInfo.fileNonce), 624 | 0x100000) 625 | 626 | if (!browser && envelope && envelope.before && 627 | outputStream === process.stdout && process.stdout.isTTY) { 628 | outputStream.write(envelope.before) 629 | } 630 | } 631 | 632 | const array = [] 633 | 634 | // Decrypt as many chunks as possible. 635 | buffer = decryptChunk(buffer, decryptor, array, hash) 636 | 637 | if (!originalFilename && array.length > 0) { 638 | // The very first chunk is the original filename. 639 | originalFilename = array.shift().toString() 640 | } 641 | 642 | // Write each decrypted chunk to the output stream. 643 | for (let chunk of array) { 644 | outputStream.write(chunk) 645 | 646 | outputByteCount += chunk.length 647 | } 648 | } 649 | } 650 | }) 651 | 652 | inputStream.on('end', () => { 653 | if (error_ !== null) { 654 | return 655 | } 656 | 657 | if (!browser && envelope && envelope.after && 658 | outputStream === process.stdout && process.stdout.isTTY) { 659 | outputStream.write(envelope.after) 660 | } 661 | 662 | if (nacl.util.encodeBase64(hash.digest()) !== 663 | decryptInfo.fileInfo.fileHash) { 664 | // The 32-byte BLAKE2 hash of the ciphertext must match the value in 665 | // the header. 666 | callback(ERR_MESSAGE_INTEGRITY_CHECK_FAILED) 667 | } else { 668 | callback(null, outputByteCount, { 669 | senderId: decryptInfo.senderID, 670 | // Strip out any trailing null characters. 671 | originalFilename: (originalFilename + '\0').slice(0, 672 | originalFilename.indexOf('\0')) 673 | }) 674 | } 675 | }) 676 | } 677 | 678 | // vim: et ts=2 sw=2 679 | -------------------------------------------------------------------------------- /tests/minilock.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import test from 'tape' 5 | 6 | import BufferStream from 'node-bufferstream' 7 | 8 | import * as miniLock from '../module' 9 | 10 | import { arrayCompare, streamHash } from '../build/common/util' 11 | 12 | const aliceEmail = 'alice@example.com' 13 | const alicePassphrase = 'hello' 14 | 15 | const aliceKeyPair = { 16 | secretKey: new Uint8Array([ 17 | 105, 153, 192, 70, 95, 105, 180, 199, 18 | 169, 151, 67, 224, 178, 224, 209, 69, 19 | 234, 147, 238, 66, 127, 152, 14, 32, 20 | 55, 171, 14, 191, 7, 133, 47, 255 21 | ]), 22 | publicKey: new Uint8Array([ 23 | 65, 94, 4, 73, 20, 126, 121, 243, 24 | 226, 39, 222, 88, 2, 148, 174, 15, 25 | 86, 5, 90, 172, 113, 187, 237, 122, 26 | 2, 20, 169, 43, 215, 95, 95, 0 27 | ]) 28 | } 29 | 30 | const aliceId = 'LRFbCrhCeN2uVCdDXd2bagoCM1fVcGvUzwhfVdqfyVuhi' 31 | 32 | const aliceSecret = 'YNTSLs54CD6bDBku65anRRbZDRUbQmhVifKjd9roWidCW' 33 | 34 | const bobEmail = 'bob@example.com' 35 | const bobPassphrase = 'puff magic dragon sea frolic autumn mist lee' 36 | 37 | const bobKeyPair = { 38 | secretKey: new Uint8Array([ 39 | 227, 80, 170, 57, 30, 98, 123, 180, 40 | 6, 29, 163, 64, 78, 131, 236, 241, 41 | 251, 99, 238, 12, 101, 147, 250, 187, 42 | 64, 114, 65, 232, 207, 81, 40, 116 43 | ]), 44 | publicKey: new Uint8Array([ 45 | 132, 203, 156, 65, 68, 203, 196, 170, 46 | 132, 106, 255, 242, 87, 133, 19, 253, 47 | 86, 60, 32, 106, 98, 164, 96, 229, 48 | 192, 193, 93, 203, 173, 41, 155, 117 49 | ]) 50 | } 51 | 52 | const bobId = 'gT1csvpmQDNRQSMkqc1Sz7ZWYzGZkmedPKEpgqjdNTy7Y' 53 | 54 | const bobSecret = '2AXYnJ54waq3c1wxpGJoVqrWDN1j1HHbDfbp7HSkDyfj2A' 55 | 56 | test('Generate a key pair from an email address and a passphrase', t => { 57 | miniLock.getKeyPair(alicePassphrase, aliceEmail, keyPair => { 58 | t.ok(arrayCompare(keyPair.secretKey, aliceKeyPair.secretKey), 59 | 'Secret key should be correct') 60 | t.ok(arrayCompare(keyPair.publicKey, aliceKeyPair.publicKey), 61 | 'Public key should be correct') 62 | 63 | t.end() 64 | }) 65 | }) 66 | 67 | test('Convert a public key into a miniLock ID', t => { 68 | const id = miniLock.miniLockId(aliceKeyPair.publicKey) 69 | 70 | t.ok(id === aliceId, 'ID should be correct') 71 | 72 | t.end() 73 | }) 74 | 75 | test('Convert a miniLock ID into a key', t => { 76 | const key = miniLock.keyFromId(aliceId) 77 | 78 | t.ok(arrayCompare(key, aliceKeyPair.publicKey), 'Key should be correct') 79 | 80 | t.end() 81 | }) 82 | 83 | test('Convert a secret into a key pair', t => { 84 | const keyPair = miniLock.keyPairFromSecret(aliceSecret) 85 | 86 | t.ok(arrayCompare(keyPair.secretKey, aliceKeyPair.secretKey), 87 | 'Secret key should be correct') 88 | t.ok(arrayCompare(keyPair.publicKey, aliceKeyPair.publicKey), 89 | 'Public key should be correct') 90 | 91 | t.end() 92 | }) 93 | 94 | test('Encrypt a message to self and decrypt it', t => { 95 | const message = 'This is a secret.' 96 | 97 | const encrypted = new BufferStream() 98 | 99 | miniLock.encryptStream(aliceKeyPair, new BufferStream(message), encrypted, 100 | [], { includeSelf: true }, 101 | (error, outputByteCount) => { 102 | if (error) { 103 | t.comment(`ERROR: ${error.toString()}`) 104 | 105 | t.fail('There should be no error') 106 | 107 | t.end() 108 | return 109 | } 110 | 111 | t.ok(outputByteCount === 979, 'Output byte count should be correct') 112 | 113 | const decrypted = new BufferStream() 114 | 115 | decrypted.setEncoding('utf8') 116 | 117 | miniLock.decryptStream(aliceKeyPair, encrypted, decrypted, {}, 118 | (error, outputByteCount, { senderId } = {}) => { 119 | if (error) { 120 | t.comment(`ERROR: ${error.toString()}`) 121 | 122 | t.fail('There should be no error') 123 | 124 | t.end() 125 | return 126 | } 127 | 128 | t.ok(senderId === aliceId, 'Sender ID should be correct') 129 | t.ok(decrypted.read() === message, 'Decrypted should match message') 130 | 131 | t.end() 132 | }) 133 | }) 134 | }) 135 | 136 | test('Encrypt a message with the armor option and decrypt it', t => { 137 | const message = 'This is a secret.' 138 | 139 | const encrypted = new BufferStream() 140 | 141 | miniLock.encryptStream(aliceKeyPair, new BufferStream(message), encrypted, 142 | [ bobId ], { armor: true }, 143 | (error, outputByteCount) => { 144 | if (error) { 145 | t.comment(`ERROR: ${error.toString()}`) 146 | 147 | t.fail('There should be no error') 148 | 149 | t.end() 150 | return 151 | } 152 | 153 | t.ok(outputByteCount === 1418, 'Output byte count should be correct') 154 | 155 | const decrypted = new BufferStream() 156 | 157 | decrypted.setEncoding('utf8') 158 | 159 | miniLock.decryptStream(bobKeyPair, encrypted, decrypted, { armor: true }, 160 | (error, outputByteCount, { senderId } = {}) => { 161 | if (error) { 162 | t.comment(`ERROR: ${error.toString()}`) 163 | 164 | t.fail('There should be no error') 165 | 166 | t.end() 167 | return 168 | } 169 | 170 | t.ok(senderId === aliceId, 'Sender ID should be correct') 171 | t.ok(decrypted.read() === message, 'Decrypted should match message') 172 | 173 | t.end() 174 | }) 175 | }) 176 | }) 177 | 178 | test('Encrypt a file and decrypt it', t => { 179 | const filename = 'pg1661.txt' 180 | 181 | const fileDigest = '242ec73a70f0a03dcbe007e32038e7deeaee004aaec9a09a07fa322743440fa8' 182 | 183 | const encrypted = new BufferStream(null, null, { highWaterMark: 0x100000 }) 184 | 185 | miniLock.encryptStream(aliceKeyPair, 186 | fs.createReadStream(path.resolve('files', filename)), encrypted, 187 | [ bobId ], { filename }, 188 | (error, outputByteCount) => { 189 | if (error) { 190 | t.comment(`ERROR: ${error.toString()}`) 191 | 192 | t.fail('There should be no error') 193 | 194 | t.end() 195 | return 196 | } 197 | 198 | t.ok(outputByteCount === 642355, 'Output byte count should be correct') 199 | 200 | const decrypted = new BufferStream(null, null, { highWaterMark: 0x100000 }) 201 | 202 | miniLock.decryptStream(bobKeyPair, encrypted, decrypted, {}, 203 | (error, outputByteCount, { senderId, originalFilename } = {}) => { 204 | if (error) { 205 | t.comment(`ERROR: ${error.toString()}`) 206 | 207 | t.fail('There should be no error') 208 | 209 | t.end() 210 | return 211 | } 212 | 213 | t.ok(senderId === aliceId, 'Sender ID should be correct') 214 | t.ok(originalFilename === filename, 215 | 'Original filename should match filename') 216 | 217 | streamHash(decrypted, 'sha256', { encoding: 'hex' }).then(digest => { 218 | t.ok(digest === fileDigest, 'Digest should match file digest') 219 | 220 | t.end() 221 | }).catch(error => { 222 | t.comment(`ERROR: ${error.toString()}`) 223 | 224 | t.fail('There should be no error') 225 | 226 | t.end() 227 | }) 228 | }) 229 | }) 230 | }) 231 | 232 | // vim: et ts=2 sw=2 233 | --------------------------------------------------------------------------------