├── .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 | [](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 |
--------------------------------------------------------------------------------