├── .eslintrc ├── .gitignore ├── .jshintrc ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── ROADMAP.md ├── assets ├── IDR_CLOSE_DIALOG.png ├── IDR_CLOSE_DIALOG_H.png ├── icon-composer-encrypt@1x.png ├── icon-composer-encrypt@2x.png ├── icon-plugins-encryption@2x.png └── icon.png ├── latex └── source-code.tex ├── lib ├── composer │ ├── composer-loader.es6 │ └── mime-writer.es6 ├── decryption │ ├── in-process-decrypter.es6 │ └── index.es6 ├── flux │ ├── actions │ │ ├── message-cache-actions.es6 │ │ └── pgp-actions.es6 │ ├── download-watcher.es6 │ ├── stores │ │ ├── message-cache-store.es6 │ │ └── pgp-store.es6 │ └── tasks │ │ └── decryption-request.es6 ├── keybase-sidebar.es6 ├── keybase │ ├── keybase-integration.es6 │ └── store │ │ ├── keybase-actions.es6 │ │ └── keybase-store.es6 ├── main.es6 ├── message-loader │ ├── message-loader-extension.es6 │ └── message-loader-header.es6 ├── settings │ ├── config-schema-item.es6 │ ├── keybase-login-section.es6 │ ├── preferences-component.es6 │ ├── settings-field.es6 │ └── sig-chain-section.es6 ├── utils │ ├── Logger.es6 │ ├── flow-error.es6 │ ├── gpg-utils.es6 │ └── html-parser.es6 ├── worker-frontend.es6 └── worker │ ├── event-processor.es6 │ ├── kbpgp │ ├── hkp-cacher.es6 │ ├── hkp.es6 │ ├── kbpgp-decrypt.es6 │ └── key-store.es6 │ ├── logger.es6 │ ├── nylas-env-wrapper.js │ ├── worker-entry.js │ └── worker-protocol.js ├── package.json ├── spec ├── main-spec.js └── message-loader-header-spec.js ├── stylesheets ├── main.less └── smalltalk.css └── test-kbpgp ├── README.md └── test-kbpgp.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "globals": { 5 | "NylasEnv": false 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "jasmine": true 11 | }, 12 | "rules": { 13 | "no-console": [1], 14 | "react/sort-comp": [1, { 15 | "order": [ 16 | 'displayName', 17 | 'containerStyles', 18 | 'defaultState', 19 | 'static-methods', 20 | 'state', 21 | 'lifecycle', 22 | 'everything-else', 23 | 'render' 24 | ] 25 | }] 26 | }, 27 | "settings": { 28 | "import/resolver": { 29 | "node": { 30 | "extensions": [ 31 | ".es6", 32 | ".jsx", 33 | ".js" 34 | ] 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /kbpgp 3 | 4 | /latex 5 | !/latex/source-code.tex 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | 1. Create `~/.nylas/dev/packages` if it does not exist 6 | ``` 7 | mkdir -p ~/.nylas/dev/packages 8 | ``` 9 | 2. Clone the Cypher repository to `~/.nylas/dev/packages/cypher` by: 10 | ``` 11 | git clone https://github.com/mbilker/cypher ~/.nylas/dev/packages/cypher 12 | ``` 13 | 3. Start N1 with debug flags or in developer mode (In the top menu bar "Developer" -> "Run with Debug Flags") 14 | 15 | > Note: Whenever you make a change, run `eslint` to ensure all variables are defined. If you have an undefined variable, correct the error and re-run `eslint` and check the issue is fixed. Once the you have ensured that, restart N1 using Ctrl + Q to fully exit N1. Closing the window does not suffice as N1 continues to run in the background. 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **NEW**: Nylas is developing their [own implementation](https://github.com/nylas/N1/tree/wip/keybase) (unstable) 3 | 4 | # Cypher 5 | - Matt Bilker 6 | 7 | Small package for decrypting PGP-encrypted email. 8 | 9 | - Works with Facebook PGP, OS X GPGTools, and command line GnuPG 10 | - Encryption using PGP public keys 11 | - Keybase integration with tracked users to allow for easy selection for users 12 | 13 | **Incomplete** spec tests are available for this package at the moment. I have not fully 14 | designed them yet. 15 | 16 | **Do not** trust the security of this package. It is not audited, fully tested, 17 | or safe at all. 18 | 19 | ## TODO 20 | 21 | - Key Management 22 | - [ ] Store keys 23 | - Encryption 24 | - [x] Form to enter Keybase username 25 | - [ ] Allow for method of encryption to be set in settings (e.g. smart card through GPG) 26 | - Decryption 27 | - [ ] TTL for decryption key passphrase 28 | - Keybase.io 29 | - [x] Login 30 | - [x] Encryption 31 | - [ ] Decryption 32 | - [x] Download "tracked" users list 33 | - Preferences 34 | - [ ] Option to encrypt whole email with quoted text or without it 35 | - [ ] Clearsign Signature and Encrypt 36 | - [ ] Better detection of PGP encrypted emails 37 | - [x] Text input for passphrase 38 | - [ ] Spec tests for all features 39 | 40 | # License 41 | 42 | This software is licensed under the GPLv3. For more information see https://www.gnu.org/licenses/gpl-3.0.txt and http://choosealicense.com/licenses/gpl-3.0/. 43 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | This roadmap is not in priority order 4 | 5 | - Selection of decryption engine 6 | - GPG verification and check signing status of message 7 | - Indication to user that message is signed by the sender 8 | - Figure out GPGTools message signing (sign-then-encrypt, but what did they sign?) 9 | - Windows support 10 | - Verification of child worker and test it before trusting it 11 | - Audit 12 | - Clearsigning 13 | 14 | There is more, but I cannot think of it off the top of my head. Please submit an 15 | issue if there is a feature or bug that needs to be implemented or fixed, repectively. 16 | -------------------------------------------------------------------------------- /assets/IDR_CLOSE_DIALOG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/IDR_CLOSE_DIALOG.png -------------------------------------------------------------------------------- /assets/IDR_CLOSE_DIALOG_H.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/IDR_CLOSE_DIALOG_H.png -------------------------------------------------------------------------------- /assets/icon-composer-encrypt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/icon-composer-encrypt@1x.png -------------------------------------------------------------------------------- /assets/icon-composer-encrypt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/icon-composer-encrypt@2x.png -------------------------------------------------------------------------------- /assets/icon-plugins-encryption@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/icon-plugins-encryption@2x.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbilker/cypher/8a99490199ed8c84c8f6f8536eca9252adb10a8a/assets/icon.png -------------------------------------------------------------------------------- /latex/source-code.tex: -------------------------------------------------------------------------------- 1 | %!TEX TS-program = xelatex 2 | %!TEX encoding = UTF-8 Unicode 3 | 4 | \documentclass[10pt, letterpaper]{article} 5 | \usepackage{fontspec} 6 | \usepackage{xunicode} 7 | \usepackage{xltxtra} 8 | \usepackage[protrusion=true,final]{microtype} 9 | 10 | \usepackage[margin=0.5in]{geometry} 11 | \usepackage{amsmath} 12 | \usepackage{minted} 13 | 14 | \defaultfontfeatures[\sffamily]{Mapping=tex-text} 15 | \setmainfont{Times New Roman} 16 | \setmonofont{Inconsolata} 17 | 18 | % no red boxes on parser error: 19 | \makeatletter 20 | \expandafter\def\csname PYGdefault@tok@err\endcsname{\def\PYGdefault@bc##1{{\strut ##1}}} 21 | \makeatother 22 | 23 | \linespread{0.8} 24 | 25 | \setlength{\footskip}{13pt} 26 | 27 | \usepackage{fancyhdr} 28 | \fancypagestyle{norule}{ % 29 | \renewcommand{\headrulewidth}{0pt} 30 | \renewcommand{\footrulewidth}{0pt} 31 | } 32 | \fancyhf{} 33 | \pagestyle{fancy} 34 | \pagestyle{norule} 35 | 36 | \begin{document} 37 | \fancyfoot[LO]{TSA Team 2146--901} 38 | \fancyfoot[RO]{pg. \thepage} 39 | 40 | \setcounter{page}{13} 41 | 42 | \section{lib/main.js} 43 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/main.js} 44 | 45 | \section{lib/composer/mime-writer.js} 46 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/composer/mime-writer.js} 47 | 48 | \section{lib/composer/composer-loader.es6} 49 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/composer/composer-loader.es6} 50 | 51 | \section{lib/message-loader/message-loader-extension.js} 52 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/message-loader/message-loader-extension.js} 53 | 54 | \section{lib/message-loader/message-loader-header.js} 55 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/message-loader/message-loader-header.js} 56 | 57 | \section{lib/settings/config-schema-item.js} 58 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/settings/config-schema-item.js} 59 | 60 | \section{lib/settings/preferences-component.js} 61 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/settings/preferences-component.js} 62 | 63 | \section{lib/settings/keybase-login-section.js} 64 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/settings/keybase-login-section.js} 65 | 66 | \section{lib/settings/sig-chain-section.js} 67 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/settings/sig-chain-section.js} 68 | 69 | \section{lib/worker-frontend.js} 70 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker-frontend.js} 71 | 72 | \section{lib/flux/actions/pgp-actions.js} 73 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/actions/pgp-actions.js} 74 | 75 | \section{lib/flux/actions/message-cache-actions.js} 76 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/actions/message-cache-actions.js} 77 | 78 | \section{lib/flux/stores/message-cache-store.js} 79 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/stores/message-cache-store.js} 80 | 81 | \section{lib/flux/stores/pgp-store.js} 82 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/stores/pgp-store.js} 83 | 84 | \section{lib/flux/download-watcher.es6} 85 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/download-watcher.es6} 86 | 87 | \section{lib/flux/tasks/decryption-request.es6} 88 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/flux/tasks/decryption-request.es6} 89 | 90 | \section{lib/utils/gpg-utils.js} 91 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/utils/gpg-utils.js} 92 | 93 | \section{lib/utils/html-parser.js} 94 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/utils/html-parser.js} 95 | 96 | \section{lib/utils/flow-error.js} 97 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/utils/flow-error.js} 98 | 99 | \section{lib/utils/Logger.js} 100 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/utils/Logger.js} 101 | 102 | \section{lib/keybase/index.es6} 103 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/keybase/index.es6} 104 | 105 | \section{lib/keybase/keybase-integration.es6} 106 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/keybase/keybase-integration.es6} 107 | 108 | \section{lib/keybase/store/keybase-actions.es6} 109 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/keybase/store/keybase-actions.es6} 110 | 111 | \section{lib/keybase/store/keybase-store.es6} 112 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/keybase/store/keybase-store.es6} 113 | 114 | \section{lib/keybase-sidebar.js} 115 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/keybase-sidebar.js} 116 | 117 | \section{lib/decryption/in-process-decrypter.es6} 118 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/decryption/in-process-decrypter.es6} 119 | 120 | \section{lib/decryption/index.es6} 121 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/decryption/index.es6} 122 | 123 | \section{lib/worker/nylas-env-wrapper.js} 124 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/nylas-env-wrapper.js} 125 | 126 | \section{lib/worker/kbpgp/kbpgp-decrypt.es6} 127 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/kbpgp/kbpgp-decrypt.es6} 128 | 129 | \section{lib/worker/kbpgp/hkp-cacher.es6} 130 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/kbpgp/hkp-cacher.es6} 131 | 132 | \section{lib/worker/kbpgp/key-store.es6} 133 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/kbpgp/key-store.es6} 134 | 135 | \section{lib/worker/kbpgp/hkp.es6} 136 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/kbpgp/hkp.es6} 137 | 138 | \section{lib/worker/worker-entry.js} 139 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/worker-entry.js} 140 | 141 | \section{lib/worker/event-processor.es6} 142 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/event-processor.es6} 143 | 144 | \section{lib/worker/logger.es6} 145 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/logger.es6} 146 | 147 | \section{lib/worker/worker-protocol.js} 148 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/lib/worker/worker-protocol.js} 149 | 150 | \section{spec/main-spec.js} 151 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/spec/main-spec.js} 152 | 153 | \section{spec/message-loader-header-spec.js} 154 | \inputminted{javascript}{/home/mbilker/.nylas/dev/packages/cypher/spec/message-loader-header-spec.js} 155 | 156 | %\inputminted{less}{stylesheets/main.less} 157 | 158 | \end{document} 159 | -------------------------------------------------------------------------------- /lib/composer/composer-loader.es6: -------------------------------------------------------------------------------- 1 | /* eslint react/sort-comp: 0 */ 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import { Actions, DraftStore, QuotedHTMLTransformer, React, ReactDOM } from 'nylas-exports'; 7 | import { RetinaImg } from 'nylas-component-kit'; 8 | 9 | import kbpgp from 'kbpgp'; 10 | import rimraf from 'rimraf'; 11 | 12 | import KeybaseStore from '../keybase/store/keybase-store'; 13 | import KeybaseRemote from '../keybase/keybase-integration'; 14 | import Logger from '../utils/Logger'; 15 | import MIMEWriter from './mime-writer'; 16 | 17 | const NO_OP = function noop() {}; 18 | const SPAN_STYLES = 'font-family:monospace,monospace;white-space:pre;'; 19 | const rimrafPromise = Promise.promisify(rimraf); 20 | 21 | /** 22 | * Adds a button to encrypt the message body with a PGP user key from Keybase. 23 | * User needs to specify which user to encrypt with. Script will download the 24 | * key and present the user's Keybase profile to ensure verification. 25 | */ 26 | class ComposerLoader extends React.Component { 27 | static displayName = 'ComposerLoader'; 28 | 29 | static propTypes = { 30 | draftClientId: React.PropTypes.string.isRequired, 31 | }; 32 | 33 | temporaryAttachmentLocation = path.join(KeybaseStore._configurationDirPath, 'attachments'); 34 | 35 | state = { 36 | username: '', 37 | }; 38 | 39 | constructor(props) { 40 | super(props); 41 | 42 | this.onClick = this.onClick.bind(this); 43 | this.onChange = this.onChange.bind(this); 44 | this.onSubmit = this.onSubmit.bind(this); 45 | this._ensureConfigurationDirectoryExists = this._ensureConfigurationDirectoryExists.bind(this); 46 | this.render = this.render.bind(this); 47 | 48 | this.log = Logger.create(`ComposerLoader(${props.draftClientId})`); 49 | 50 | this._ensureConfigurationDirectoryExists(); 51 | 52 | global.$pgpComposer = this; 53 | } 54 | 55 | onClick() { 56 | const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect(); 57 | const popover = ( 58 |
59 |
60 |
61 | PGP Encrypt: 62 |
63 | 64 |
65 |
66 | 67 | 73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | ); 85 | 86 | Actions.openPopover(popover, { originRect: buttonRect, direction: 'up' }); 87 | } 88 | 89 | onChange(e) { 90 | this.log.info('change', e); 91 | this.setState({ 92 | username: e.target.value, 93 | }); 94 | } 95 | 96 | onSubmit() { 97 | Actions.closePopover(); 98 | 99 | const { draftClientId } = this.props; 100 | const { username } = this.state; 101 | 102 | this.log.info('submit', username); 103 | return KeybaseRemote.publicKeyForUsername(username).then(armoredKey => { 104 | if (!armoredKey) { 105 | return Promise.reject(new Error(`No public key for username ${username}`)); 106 | } 107 | 108 | return this._importPublicKey(armoredKey).then(publicKey => [ 109 | DraftStore.sessionForClientId(draftClientId), 110 | publicKey, 111 | ]).spread((session, publicKey) => { 112 | const draftHtml = session.draft().body; 113 | const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml); 114 | 115 | const fingerprint = kbpgp.util.format_fingerprint(publicKey.get_pgp_fingerprint()); 116 | const bodyHeader = this._formatBodyHeader(username, fingerprint); 117 | 118 | return this._encryptMessage(text, publicKey).then((pgpMessage) => { 119 | const temporaryDir = path.join(this.temporaryAttachmentLocation, draftClientId); 120 | const attachmentPath = path.join(temporaryDir, 'encrypted.asc'); 121 | 122 | return fs.accessAsync(temporaryDir, fs.F_OK).then(() => 123 | rimrafPromise(temporaryDir) 124 | , NO_OP).then(() => 125 | fs.mkdirAsync(temporaryDir) 126 | ).then(() => 127 | fs.writeFileAsync(attachmentPath, pgpMessage) 128 | ).then(() => 129 | Actions.addAttachment({ 130 | messageClientId: draftClientId, 131 | filePath: attachmentPath, 132 | }) 133 | ); 134 | }).then(() => { 135 | const body = QuotedHTMLTransformer.appendQuotedHTML(bodyHeader, draftHtml); 136 | 137 | session.changes.add({ body }); 138 | session.changes.commit(); 139 | }); 140 | }).catch((err) => { 141 | this.log.error(err); 142 | }); 143 | }); 144 | } 145 | 146 | _formatBodyHeader(username, fingerprint) { 147 | const spanUser = `${username}`; 148 | const spanFingerprint = `${fingerprint}`; 149 | return `This message is encrypted for ${spanUser} with key fingerprint ${spanFingerprint}.`; 150 | } 151 | 152 | _importPublicKey(publicKey) { 153 | const importFromArmoredPgp = Promise.promisify(kbpgp.KeyManager.import_from_armored_pgp); 154 | 155 | return importFromArmoredPgp({ 156 | armored: publicKey, 157 | }).then(([keyManager]) => keyManager); 158 | } 159 | 160 | _encryptMessage(text, encryptFor) { 161 | const box = Promise.promisify(kbpgp.box); 162 | 163 | const writer = new MIMEWriter(); 164 | writer.writePart(text, { type: 'text/html; charset="UTF-8"' }); 165 | const msg = writer.end(); 166 | 167 | return box({ 168 | msg, 169 | encrypt_for: encryptFor, 170 | }).then(([pgpMessage]) => pgpMessage); 171 | } 172 | 173 | _ensureConfigurationDirectoryExists() { 174 | fs.access(this.temporaryAttachmentLocation, fs.F_OK, (err1) => { 175 | if (err1) { 176 | this.log.info('Temporary attachment directory missing, creating'); 177 | fs.mkdir(this.temporaryAttachmentLocation, (err2) => { 178 | if (err2) { 179 | this.log.error('Temporary attachment directory creation unsuccessful', err2); 180 | } else { 181 | this.log.info('Temporary attachment directory creation successful'); 182 | } 183 | }); 184 | } 185 | }); 186 | } 187 | 188 | render() { 189 | return ( 190 | 194 | ); 195 | } 196 | } 197 | 198 | export default ComposerLoader; 199 | -------------------------------------------------------------------------------- /lib/composer/mime-writer.es6: -------------------------------------------------------------------------------- 1 | import mimelib from 'mimelib'; 2 | import uuid from 'uuid'; 3 | 4 | const CR = '\r'; 5 | const LF = '\n'; 6 | const CRLF = CR + LF; 7 | 8 | // MIME Writer to create the MIME encoded emails before encryption. Normally 9 | // the N1 Sync Engine does this itself, but for the case of secrecy of emails 10 | // from the Sync Engine the emails are encoded in the N1 clients 11 | // 12 | // Based on https://github.com/isaacs/multipart-js 13 | export default class MIMEWriter { 14 | constructor(boundary = `PGP-N1=_${uuid().toUpperCase()}`) { 15 | this._boundary = boundary; 16 | this._output = ''; 17 | 18 | this.writePart = this.writePart.bind(this); 19 | this.end = this.end.bind(this); 20 | this._writeHeader = this._writeHeader.bind(this); 21 | 22 | this._writeHeader(); 23 | } 24 | 25 | writePart(message, { 26 | encoding = '7bit', 27 | type = `text/plain; charset="UTF-8"`, 28 | name, 29 | filename, 30 | } = {}) { 31 | let opener = `--${this._boundary}${CRLF}`; 32 | opener += `Content-Type: ${type}`; 33 | 34 | if (name) opener += `; name="${name}"`; 35 | if (filename) opener += `; filename="${filename}"`; 36 | 37 | opener = mimelib.foldLine(opener); 38 | opener += CRLF; 39 | 40 | this._output += opener; 41 | this._output += mimelib.foldLine(`Content-Transfer-Encoding: ${encoding}`); 42 | this._output += CRLF; 43 | this._output += CRLF; 44 | this._output += message + CRLF; 45 | 46 | return this; 47 | } 48 | 49 | end() { 50 | this._output = `${this._output}${CRLF}--${this._boundary}--${CRLF}`; 51 | 52 | return this._output; 53 | } 54 | 55 | _writeHeader() { 56 | let header = `Content-Type: multipart/signed; ${CRLF}`; 57 | header += `\tboundary="${this._boundary}";${CRLF}`; 58 | header += `\tprotocol="application/pgp-signature"${CRLF}`; 59 | header += CRLF + CRLF; 60 | 61 | this._output = header; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/decryption/in-process-decrypter.es6: -------------------------------------------------------------------------------- 1 | import openpgp from 'openpgp'; 2 | import smalltalk from 'smalltalk'; 3 | 4 | import Logger from '../utils/Logger'; 5 | 6 | class InProcessDecrypter { 7 | constructor() { 8 | super(); 9 | 10 | this.log = Logger.create('InProcessDecrypter'); 11 | } 12 | 13 | decrypt(text, pgpkey) { 14 | return new Promise((resolve, reject) => { 15 | this.log.info('Reading secret key'); 16 | const key = openpgp.key.readArmored(pgpkey); 17 | 18 | if (key.err && key.err.length) { 19 | key.err.forEach((a, i) => { 20 | this.log.error(`Secret key read error [${i}]:`, a); 21 | }); 22 | 23 | reject(key.err[0]); 24 | return; 25 | } 26 | 27 | this.log.info('Read secret key'); 28 | resolve(key.keys[0]); 29 | }).then((km) => { 30 | const startTime = process.hrtime(); 31 | 32 | // TODO: get key fingerprint from openpgpjs 33 | const msg = `PGP Key with fingerprint TODO needs to be decrypted`; 34 | return smalltalk.passphrase('PGP Passphrase', msg).then((passphrase) => { 35 | km.decrypt(passphrase); 36 | 37 | const elapsed = process.hrtime(startTime); 38 | this.log.info(`Decrypted secret key in ${elapsed[0] * 1e3 + elapsed[1] / 1e6}ms`); 39 | 40 | return km; 41 | }); 42 | }).then((km) => 43 | openpgp.decryptMessage(km, openpgp.message.readArmored(text)) 44 | ); 45 | } 46 | } 47 | 48 | export default InProcessDecrypter; 49 | -------------------------------------------------------------------------------- /lib/decryption/index.es6: -------------------------------------------------------------------------------- 1 | // import InProcessDecrypter from './in-process-decrypter'; 2 | import WorkerFrontend from '../worker-frontend'; 3 | 4 | /** 5 | * Decrypter selection 6 | * 7 | * Disabled the InProcessDecrypter because OpenPGP.js includes a Promise polyfill 8 | * that overrides bluebird globally 9 | */ 10 | export function selectDecrypter() { 11 | const chosen = 'WORKER_PROCESS'; 12 | let decrypter = null; 13 | 14 | if (chosen === 'WORKER_PROCESS') { 15 | decrypter = WorkerFrontend; // WORKER_PROCESS 16 | } 17 | // } else if (chosen === "IN_PROCESS") { 18 | // decrypter = new InProcessDecrypter(); // IN_PROCESS 19 | // } 20 | 21 | return decrypter.decrypt; 22 | } 23 | -------------------------------------------------------------------------------- /lib/flux/actions/message-cache-actions.es6: -------------------------------------------------------------------------------- 1 | /* eslint guard-for-in: 0 */ 2 | 3 | import { Reflux } from 'nylas-exports'; 4 | 5 | const CacheActions = Reflux.createActions([ 6 | 'store', 7 | ]); 8 | 9 | for (const key in CacheActions) { 10 | CacheActions[key].sync = true; 11 | } 12 | 13 | export default CacheActions; 14 | -------------------------------------------------------------------------------- /lib/flux/actions/pgp-actions.es6: -------------------------------------------------------------------------------- 1 | /* eslint guard-for-in: 0 */ 2 | 3 | import { Reflux } from 'nylas-exports'; 4 | 5 | const MessageActions = Reflux.createActions([ 6 | 'decrypt', 7 | 'retry', 8 | ]); 9 | 10 | for (const key in MessageActions) { 11 | MessageActions[key].sync = true; 12 | } 13 | 14 | export default MessageActions; 15 | -------------------------------------------------------------------------------- /lib/flux/download-watcher.es6: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { FileDownloadStore } from 'nylas-exports'; 3 | 4 | import FlowError from '../utils/flow-error'; 5 | import Logger from '../utils/Logger'; 6 | 7 | class DownloadWatcher { 8 | constructor() { 9 | // Object of promises of attachments needed for decryption 10 | this.deferreds = new Map(); 11 | 12 | this.promiseForPendingFile = this.promiseForPendingFile.bind(this); 13 | this.getFilePromise = this.getFilePromise.bind(this); 14 | this.onDownloadStoreChange = this.onDownloadStoreChange.bind(this); 15 | 16 | this.log = Logger.create('DownloadWatcher'); 17 | 18 | this._storeUnlisten = FileDownloadStore.listen(this.onDownloadStoreChange); 19 | 20 | global.$pgpDownloadWatcher = this; 21 | } 22 | 23 | // PUBLIC 24 | 25 | promiseForPendingFile(fileId) { 26 | if (this.deferreds.has(fileId)) { 27 | return this.deferreds.get(fileId).promise; 28 | } 29 | 30 | const deferred = Promise.defer(); 31 | this.deferreds.set(fileId, deferred); 32 | 33 | return deferred.promise; 34 | // .then((text) => { 35 | // this._deferreds.delete(fileId); 36 | // return text; 37 | // }); 38 | } 39 | 40 | getFilePromise(fileId) { 41 | return this.deferreds[fileId]; 42 | } 43 | 44 | unlisten() { 45 | if (this._storeUnlisten) { 46 | this._storeUnlisten(); 47 | } 48 | } 49 | 50 | fetchFile(filePath) { 51 | // async fs.exists was throwing because the first argument was true, 52 | // found fs.access as a suitable replacement 53 | return fs.accessAsync(filePath, fs.F_OK | fs.R_OK).then(() => 54 | fs.readFileAsync(filePath, 'utf8') 55 | ).then((text) => { 56 | this.log.info('Read attachment from disk'); 57 | if (!text) { 58 | return Promise.reject(new FlowError('No text in attachment', true)); 59 | } 60 | return text; 61 | }); 62 | } 63 | 64 | /** 65 | * @private 66 | * Handles the downloaded files and checks if PGP attachments have been fully 67 | * downloaded. 68 | * 69 | * If the file is in the 'finished' state, then it will read the 70 | * file from disk and tell the {PGPStore} that the file has downloaded via a 71 | * promise. 72 | * 73 | * If there was an error reading the file from disk, then it will tell 74 | * {PGPStore} the error to display to the user. 75 | * 76 | * TODO: Figure out why this does not handle more than one file. 77 | */ 78 | onDownloadStoreChange() { 79 | const changes = FileDownloadStore.downloadDataForFiles([...this.deferreds.keys()]); 80 | 81 | this.log.info('Download Store Changes:', changes); 82 | Object.keys(changes).forEach((fileId) => { 83 | const file = changes[fileId]; 84 | 85 | if (file && file.state === 'finished' && this.deferreds.has(file.fileId)) { 86 | this.log.info(`Checking ${file.fileId}`); 87 | 88 | this.fetchFile(file.targetPath).then((text) => { 89 | const deferred = this.deferreds.get(file.fileId); 90 | if (deferred && deferred.resolve) { 91 | this.log.info(`Found downloaded attachment ${fileId}`); 92 | deferred.resolve(text); 93 | this.deferreds.delete(file.fileId); 94 | } else { 95 | this.log.error('watching promise undefined'); 96 | } 97 | }).catch((err) => { 98 | const deferred = this.deferreds.get(file.fileId); 99 | if (deferred && deferred.reject) { 100 | this.log.error('Download attachment inaccessable', err); 101 | deferred.reject(new FlowError('Downloaded attachment inaccessable', true)); 102 | this.deferreds.delete(file.fileId); 103 | } 104 | }); 105 | } 106 | }); 107 | } 108 | } 109 | 110 | export default new DownloadWatcher(); 111 | -------------------------------------------------------------------------------- /lib/flux/stores/message-cache-store.es6: -------------------------------------------------------------------------------- 1 | import NylasStore from 'nylas-store'; 2 | 3 | import CacheActions from '../actions/message-cache-actions'; 4 | 5 | class MessageCacheStore extends NylasStore { 6 | constructor() { 7 | super(); 8 | 9 | // State-based variables for storing messages when resetting 10 | // MessageBodyProcessor cache 11 | this.cachedMessages = new Map(); 12 | 13 | this.listenTo(CacheActions.store, this._store); 14 | 15 | global.$pgpMessageCacheStore = this; 16 | } 17 | 18 | haveCachedBody(messageId) { 19 | return this.cachedMessages.has(messageId); 20 | } 21 | 22 | getCachedBody(messageId) { 23 | return this.cachedMessages.get(messageId); 24 | } 25 | 26 | _store(messageId, result) { 27 | this.cachedMessages.set(messageId, result); 28 | } 29 | } 30 | 31 | export default new MessageCacheStore(); 32 | -------------------------------------------------------------------------------- /lib/flux/stores/pgp-store.es6: -------------------------------------------------------------------------------- 1 | import NylasStore from 'nylas-store'; 2 | import { FileDownloadStore, MessageBodyProcessor } from 'nylas-exports'; 3 | 4 | import DownloadWatcher from '../download-watcher'; 5 | import MessageActions from '../actions/pgp-actions'; 6 | 7 | import DecryptionRequest from '../tasks/decryption-request'; 8 | import FlowError from '../../utils/flow-error'; 9 | import GPGUtils from '../../utils/gpg-utils'; 10 | import Logger from '../../utils/Logger'; 11 | 12 | import smalltalk from 'smalltalk'; 13 | 14 | /** 15 | * The main management class for the PGP plugin for the decryption function. 16 | * Handles almost all the decrpytion processing. 17 | * 18 | * THANK YOU GPGTOOLS! The `MimePart+GPGMail.m` is such a good guide to PGP 19 | * mail decryption. 20 | * 21 | * @class PGPStore 22 | */ 23 | class PGPStore extends NylasStore { 24 | constructor() { 25 | super(); 26 | 27 | // Store status of message decryption for MessageLoaderHeader 28 | this._state = new Map(); 29 | 30 | // Binding `this` to each method that uses `this` 31 | this.shouldDecryptMessage = this.shouldDecryptMessage.bind(this); 32 | this._decryptMessage = this._decryptMessage.bind(this); 33 | this._retryMessage = this._retryMessage.bind(this); 34 | this.setState = this.setState.bind(this); 35 | this.mainDecrypt = this.mainDecrypt.bind(this); 36 | this.getAttachmentAndKey = this.getAttachmentAndKey.bind(this); 37 | this.retrievePGPAttachment = this.retrievePGPAttachment.bind(this); 38 | this._decryptAndResetCache = this._decryptAndResetCache.bind(this); 39 | 40 | this.log = Logger.create('PGPStore'); 41 | 42 | this.listenTo(MessageActions.decrypt, this._decryptMessage); 43 | this.listenTo(MessageActions.retry, this._retryMessage); 44 | 45 | global.$pgpPGPStore = this; 46 | } 47 | 48 | /** 49 | * The quick check method if the message has a valid attachment. 50 | * 51 | * Returns true only if there is at least one attachment and one of the 52 | * attachments has the 'pgp', 'gpg', or 'asc' extensions. 53 | * 54 | * GPGTools (and other clients) send two attachments, where the first 55 | * "metadata" attachment contains the string "Version: 1" and the second 56 | * attachment is the encrypted message. 57 | * 58 | * Though, the "metadata" attachment's `contentType` is 59 | * 'application/pgp-encrypted' and the encrypted message attachment is 60 | * 'application/octet-stream', which is annoying to deal with. 61 | * 62 | * @param {object} message - the message to check for appropriate attachment 63 | */ 64 | shouldDecryptMessage(message) { 65 | if (!message) { 66 | this.log.error('No message passed as argument'); 67 | return false; 68 | } 69 | 70 | if (!message.files) { 71 | this.log.error(`${message.id}: No files array as part of message`); 72 | return false; 73 | } 74 | 75 | if (message.files.length < 1) { 76 | this.log.info(`${message.id}: Failed attachment test`); 77 | return false; 78 | } 79 | 80 | const extensionTest = (file) => { 81 | const ext = file.displayExtension(); 82 | 83 | // [@"pgp", @"gpg", @"asc"] 84 | // https://github.com/GPGTools/GPGMail/blob/master/Source/MimePart%2BGPGMail.m#L643 85 | if (ext === 'pgp' || 86 | ext === 'gpg' || 87 | ext === 'asc') { 88 | return true; 89 | } 90 | 91 | return false; 92 | }; 93 | 94 | if (!message.files.some(extensionTest)) { 95 | this.log.info(`${message.id}: Failed extension test`); 96 | return false; 97 | } 98 | 99 | return true; 100 | } 101 | 102 | /** 103 | * Returns the state of the decrypter to @class{MessageLoaderHeader} to 104 | * display its status back to the user. 105 | * 106 | * @param {number} messageId - the message to retrieve state 107 | */ 108 | getState(messageId) { 109 | return this._state.get(messageId); 110 | } 111 | 112 | /** 113 | * @private 114 | * Action Event: Decrypt message and reset the MessageBodyProcessor cached 115 | * message body for ONLY the message that corresponds to thedecrypted message 116 | */ 117 | _decryptMessage(message) { 118 | this.log.info('Told to decrypt', message); 119 | this._decryptAndResetCache(message); 120 | } 121 | 122 | /** 123 | * @private 124 | * Action Event: Retry decryption after an error occurred 125 | * 126 | * This is because kbpgp would fail with Electron's buffer implementation 127 | */ 128 | _retryMessage(message) { 129 | if (this._state.has(message.id) && this._state.get(message.id).decrypting) { 130 | this.log.error('Told to retry decrypt, but in the middle of decryption'); 131 | return; 132 | } 133 | 134 | this.log.info('Told to retry decrypt', message); 135 | this._state.delete(message.id); 136 | this._decryptAndResetCache(message); 137 | } 138 | 139 | /** 140 | * @private 141 | * Set state for a particular instance of message decryption and tell 142 | * MessageLoaderHeader about the change in state, which the views will update 143 | * and re-render 144 | * 145 | * @callback MessageLoaderHeader._onPGPStoreChange 146 | */ 147 | setState(messageId, state) { 148 | const prevState = this._state.get(messageId) || {}; 149 | const nextState = Object.assign(prevState, state); 150 | this._state.set(messageId, nextState); 151 | this.trigger(messageId, nextState); 152 | } 153 | 154 | /** 155 | * @private 156 | * The main brains of this project. This retrieves the attachment and secret 157 | * key (someone help me find a (secure) way to store the secret key) in 158 | * parallel. 159 | * 160 | * Triggers a re-render through setState 161 | * @return {promise} request resolves after successful decryption, 162 | * rejects if the decryption fails at any stage 163 | */ 164 | mainDecrypt(message) { 165 | if (this._state.has(message.id) && this._state.get(message.id).decrypting) { 166 | return Promise.reject(`Already decrypting ${message.id}`); 167 | } 168 | 169 | const request = new DecryptionRequest(this, message); 170 | 171 | // console.group(`[PGP] Message: ${message.id}`); 172 | return request.run(); 173 | // .finally(() => { 174 | // console.groupEnd(); 175 | // }); 176 | } 177 | 178 | retrievePGPAttachment(message, notify) { 179 | this.log.info(`Attachments: ${message.files.length}`); 180 | 181 | // Check for GPGTools-like message, even though we aren't MIME parsed yet, 182 | // this still applies because the `octet-stream` attachments take 183 | // precedence 184 | // https://github.com/GPGTools/GPGMail/blob/master/Source/MimePart%2BGPGMail.m#L665 185 | let dataPart = null; 186 | let dataIndex = null; 187 | let lastContentType = ''; 188 | if (message.files.length >= 1) { 189 | const { files } = message; 190 | 191 | files.forEach((file, i) => { 192 | if ((file.contentType === 'application/pgp-signature') || // EmailPGP-style encryption 193 | ((file.contentType === 'application/octet-stream' && !dataPart) || 194 | (lastContentType === 'application/pgp-encrypted')) || // GPGTools-style encryption 195 | (file.contentType === 'application/pgp-encrypted' && !dataPart)) { // Fallback 196 | dataPart = file; 197 | dataIndex = i; 198 | lastContentType = file.contentType; 199 | } 200 | }); 201 | } 202 | 203 | if (dataPart) { 204 | const filePath = FileDownloadStore.pathForFile(dataPart); 205 | this.log.info(`Using file[${dataIndex}] =`, dataPart); 206 | 207 | return DownloadWatcher.fetchFile(filePath).catch(() => { 208 | notify('Waiting for encrypted message attachment to download...'); 209 | this.log.info('Attachment file inaccessable, creating pending promise'); 210 | 211 | return DownloadWatcher.promiseForPendingFile(dataPart.id); 212 | }); 213 | } 214 | 215 | return Promise.reject(new FlowError('No valid attachment')); 216 | } 217 | 218 | // Retrieves the attachment and encrypted secret key for code divergence later 219 | getAttachmentAndKey(message, notify) { 220 | const dropdownQuestion = 'Which PGP key should be used for decryption of this message?'; 221 | 222 | notify('Waiting for decryption key selection...'); 223 | 224 | return GPGUtils.getKeys().then((gpgKeys) => { 225 | const keys = {}; 226 | for (const key of gpgKeys) { 227 | if (!key) continue; 228 | keys[key.key] = `[${key.type}] ${key.fpr}`; 229 | } 230 | 231 | const pgpkeyPromise = smalltalk.dropdown('PGP Key', dropdownQuestion, keys).catch(() => 232 | Promise.reject(new FlowError('Cancelled key selection', true)) 233 | ); 234 | // .then(() => { 235 | // const storedKey = require('../stored-key').getKey(); 236 | // return storedKey; 237 | // }); 238 | 239 | const promises = [ 240 | this.retrievePGPAttachment(message, notify), 241 | pgpkeyPromise, 242 | ]; 243 | 244 | return Promise.all(promises); 245 | }).spread((text, pgpkey) => { 246 | if (!text) { 247 | return Promise.reject(new FlowError('No text in attachment', true)); 248 | } 249 | if (!pgpkey) { 250 | return Promise.reject(new FlowError('No key in pgpkey variable', true)); 251 | } 252 | return [text, pgpkey]; 253 | }); 254 | } 255 | 256 | _decryptAndResetCache(message) { 257 | return this.mainDecrypt(message).then(() => { 258 | if (this._state.has(message.id) && !this._state.get(message.id).lastError) { 259 | MessageBodyProcessor.updateCacheForMessage(message); 260 | } 261 | }).catch((err) => { 262 | this.log.error(err); 263 | }); 264 | } 265 | } 266 | 267 | export default new PGPStore(); 268 | -------------------------------------------------------------------------------- /lib/flux/tasks/decryption-request.es6: -------------------------------------------------------------------------------- 1 | import { extractHTML } from '../../utils/html-parser'; 2 | import { selectDecrypter } from '../../decryption'; 3 | import CacheActions from '../actions/message-cache-actions'; 4 | import FlowError from '../../utils/flow-error'; 5 | import Logger from '../../utils/Logger'; 6 | 7 | const log = Logger.create(`DecryptionRequest`); 8 | 9 | export default class DecryptionRequest { 10 | constructor(parent, message) { 11 | this.store = parent; 12 | this.message = message; 13 | this.messageId = message.id; 14 | 15 | this.setState = this.setState.bind(this); 16 | this.notify = this.notify.bind(this); 17 | this.afterDecrypt = this.afterDecrypt.bind(this); 18 | this.onMatch = this.onMatch.bind(this); 19 | this.onError = this.onError.bind(this); 20 | this.run = this.run.bind(this); 21 | 22 | this.startDecrypt = null; 23 | } 24 | 25 | setState(state) { 26 | this.store.setState(this.messageId, state); 27 | } 28 | 29 | notify(msg) { 30 | this.setState({ statusMessage: msg }); 31 | } 32 | 33 | afterDecrypt(result) { 34 | const endDecrypt = process.hrtime(this.startDecrypt); 35 | log.info(`Decryption engine took ${endDecrypt[0] * 1e3 + endDecrypt[1] / 1e6}ms`); 36 | 37 | this.setState({ rawMessage: result.text, signedBy: result.signedBy }); 38 | return result; 39 | } 40 | 41 | onMatch(match) { 42 | CacheActions.store(this.messageId, match); 43 | 44 | this.setState({ 45 | decrypting: false, 46 | decryptedMessage: match, 47 | statusMessage: null, 48 | }); 49 | 50 | return match; 51 | } 52 | 53 | onError(err) { 54 | if (err instanceof FlowError) { 55 | log.error(err.title); 56 | } else { 57 | log.error(err.stack); 58 | } 59 | 60 | this.setState({ 61 | decrypting: false, 62 | done: true, 63 | lastError: err, 64 | }); 65 | } 66 | 67 | run() { 68 | this.setState({ decrypting: true }); 69 | 70 | const decrypter = selectDecrypter().bind(null, this.notify); 71 | this.startDecrypt = process.hrtime(); 72 | 73 | return this.store.getAttachmentAndKey(this.message, this.notify) 74 | .spread(decrypter) 75 | .then(this.afterDecrypt) 76 | .then(extractHTML) 77 | .then(this.onMatch) 78 | .catch(this.onError); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/keybase-sidebar.es6: -------------------------------------------------------------------------------- 1 | import { React, MessageStore } from 'nylas-exports'; 2 | 3 | import _ from 'lodash'; 4 | // import kbpgp from 'kbpgp'; 5 | // import PKESK from 'kbpgp/lib/openpgp/packet/sess'; 6 | 7 | import PGPStore from './flux/stores/pgp-store'; 8 | import KeybaseRemote from './keybase/keybase-integration'; 9 | // import proto from './worker/worker-protocol'; 10 | // import WorkerFrontend from './worker-frontend'; 11 | 12 | import Logger from './utils/Logger'; 13 | 14 | class KeybaseSidebar extends React.Component { 15 | static displayName = 'KeybaseSidebar'; 16 | 17 | /** 18 | * Providing container styles tells the app how to constrain 19 | * the column your component is being rendered in. The min and 20 | * max size of the column are chosen automatically based on 21 | * these values. 22 | */ 23 | static containerStyles = { 24 | order: 1, 25 | flexShrink: 0, 26 | }; 27 | 28 | /** 29 | * This sidebar component listens to the PGPStore for any changes to 30 | * decryption of the current message. If there is a decryption operation on 31 | * the current message, then this component will display relevant Keybase.io 32 | * information if the contact has a Keybase.io account. 33 | */ 34 | constructor(props) { 35 | super(props); 36 | 37 | this.proofs = this.proofs.bind(this); 38 | this.cryptocoins = this.cryptocoins.bind(this); 39 | this.onPGPStoreChange = this.onPGPStoreChange.bind(this); 40 | this.renderContent = this.renderContent.bind(this); 41 | this.render = this.render.bind(this); 42 | 43 | this.log = Logger.create('KeybaseSidebar'); 44 | 45 | this.state = { data: null }; 46 | this.getInitialPGPState(); 47 | 48 | global.$pgpKeybaseSidebar = this; 49 | } 50 | 51 | componentDidMount() { 52 | this._mounted = true; 53 | 54 | this.unsubscribes = []; 55 | this.unsubscribes.push(PGPStore.listen(this.onPGPStoreChange)); 56 | } 57 | 58 | componentWillUnmount() { 59 | this._mounted = false; 60 | 61 | for (const unsubscribe of this.unsubscribes) { 62 | if (unsubscribe) { 63 | unsubscribe(); 64 | } 65 | } 66 | } 67 | 68 | onPGPStoreChange(id, /* state */) { 69 | // Bail if we already have the data and the message id is the same 70 | if (this.state.data && id === this.state.currentId) { 71 | return; 72 | } 73 | 74 | // TODO: actually use the key from the message, this is my personal keybase 75 | // key fingerprint to test this with consistent results 76 | const encrypted = ['f3ac2d1dc4be59122aceb87d69adf8aeb6c8b5d1']; 77 | 78 | const promises = encrypted.map((x) => 79 | KeybaseRemote.userLookup({ 80 | key_fingerprint: [x], 81 | fields: ['basics', 'proofs_summary', 'cryptocurrency_addresses'], 82 | }).then((res) => { 83 | if (res && res.them) { 84 | return res.them[0]; 85 | } 86 | return null; 87 | }).catch((err) => { 88 | this.log.error('Ignoring error', err); 89 | }) 90 | ); 91 | 92 | Promise.all(promises).then((results) => { 93 | if (!results) { 94 | return; 95 | } 96 | 97 | const res = results[0]; 98 | this.log.info('Loaded info', res); 99 | 100 | if (this._mounted) { 101 | this.setState({ 102 | res, 103 | currentId: id, 104 | data: res.proofs_summary, 105 | name: res.basics.username, 106 | profile: res.profile, 107 | cryptoaddress: res.cryptocurrency_addresses, 108 | }); 109 | } 110 | }); 111 | } 112 | 113 | getInitialPGPState() { 114 | const msgId = MessageStore.items()[0].id; 115 | const pgpData = PGPStore.getState(msgId); 116 | if (pgpData) { 117 | this.onPGPStoreChange(msgId, pgpData); 118 | } 119 | } 120 | 121 | proofs() { 122 | return _.map(this.state.data.by_presentation_group, (proofs, site) => { 123 | let icon = 'globe'; 124 | if (site === 'twitter' || 125 | site === 'github' || 126 | site === 'reddit' 127 | ) { 128 | icon = site; 129 | } else if (site === 'coinbase') { 130 | icon = 'btc'; 131 | } else if (site === 'hackernews') { 132 | icon = 'hacker-news'; 133 | } 134 | 135 | const className = `social-icon fa fa-${icon}`; 136 | const style = { marginTop: 2, minWidth: '1em' }; 137 | const hasDnsTagPresent = proofs[1] && proofs[1].presentation_tag === 'dns'; 138 | 139 | return proofs.reduce((results, proof) => { 140 | if (!hasDnsTagPresent || proof.presentation_tag !== 'dns') { 141 | results.push( 142 |
143 | 144 | 145 |
146 | {proof.nametag} 147 |
148 |
149 | ); 150 | } 151 | return results; 152 | }, []); 153 | }); 154 | } 155 | 156 | cryptocoins() { 157 | return _.map(this.state.cryptoaddress, (data, type) => { 158 | let icon = 'question-circle'; 159 | if (type === 'bitcoin') { 160 | icon = 'btc'; 161 | } 162 | 163 | const className = `social-icon fa fa-${icon}`; 164 | const style = { marginTop: 2, minWidth: '1em' }; 165 | 166 | return data.map((address) => 167 |
168 | 169 | 170 |
171 | {address.address} 172 |
173 |
174 | ); 175 | }); 176 | } 177 | 178 | 179 | renderPlaceholder() { 180 | return ( 181 |
182 |

Keybase

183 |
Loading...
184 |
185 | ); 186 | } 187 | 188 | renderContent() { 189 | if (!this.state.data) { 190 | return this.renderPlaceholder(); 191 | } 192 | 193 | const proofs = this.proofs(); 194 | const coins = this.cryptocoins(); 195 | 196 | const href = `https://keybase.io/${this.state.name}`; 197 | const style = { textDecoration: 'none' }; 198 | 199 | return ( 200 |
201 |

202 | Keybase - link 203 |

204 | 205 |
206 | {this.state.name} 207 |
208 |
209 | {proofs} 210 | {coins} 211 |
212 |
213 | ); 214 | } 215 | 216 | render() { 217 | let content = null; 218 | 219 | if (this.state.data) { 220 | content = this.renderContent(); 221 | } else { 222 | content = this.renderPlaceholder(); 223 | } 224 | 225 | return ( 226 |
227 | {content} 228 |
229 | ); 230 | } 231 | } 232 | 233 | export default KeybaseSidebar; 234 | -------------------------------------------------------------------------------- /lib/keybase/keybase-integration.es6: -------------------------------------------------------------------------------- 1 | import Keybase from 'node-keybase'; 2 | import request from 'request'; 3 | 4 | import Logger from '../utils/Logger'; 5 | 6 | const API = 'https://keybase.io/_/api/1.0'; 7 | 8 | class KeybaseRemote { 9 | constructor() { 10 | this.loadPreviousLogin = this.loadPreviousLogin.bind(this); 11 | 12 | this.keybase = new Keybase(); 13 | 14 | this.login = Promise.promisify(this.keybase.login.bind(this.keybase)); 15 | this.userLookup = Promise.promisify(this.keybase.user_lookup); 16 | this.publicKeyForUsername = Promise.promisify(this.keybase.public_key_for_username); 17 | 18 | this.log = Logger.create('KeybaseRemote'); 19 | 20 | this.loadPreviousLogin(); 21 | } 22 | 23 | loadPreviousLogin() { 24 | const { 25 | username, 26 | uid, 27 | csrf_token: csrfToken, 28 | session_token: sessionToken, 29 | } = NylasEnv.config.get('cypher.keybase') || {}; 30 | 31 | if (username && uid && csrfToken && sessionToken) { 32 | this.log.info('Found Keybase stored login, loading into node-keybase'); 33 | this.keybase.usernameOrEmail = username; 34 | this.keybase.session = sessionToken; 35 | this.keybase.csrf_token = csrfToken; 36 | } else { 37 | this.log.info('Previous Keybase login not found'); 38 | } 39 | } 40 | 41 | sigChainForUid(uid) { 42 | const url = `${API}/sig/get.json?uid=${uid}`; 43 | 44 | return new Promise((resolve, reject) => { 45 | request.get({ url, json: true }, (err, res, body) => { 46 | if (err) { 47 | return reject(err); 48 | } else if (body.status.code === 200 && body.status.name === 'OK') { 49 | return reject(body.status); 50 | } 51 | return resolve(body); 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | export default new KeybaseRemote(); 58 | -------------------------------------------------------------------------------- /lib/keybase/store/keybase-actions.es6: -------------------------------------------------------------------------------- 1 | import { Reflux } from 'nylas-exports'; 2 | 3 | const Actions = [ 4 | 'login', 5 | 'fetchAndVerifySigChain', 6 | ]; 7 | 8 | Actions.forEach((key) => { 9 | Actions[key] = Reflux.createAction(name); 10 | Actions[key].sync = true; 11 | }); 12 | 13 | export default Actions; 14 | -------------------------------------------------------------------------------- /lib/keybase/store/keybase-store.es6: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import libkb from 'libkeybase'; 5 | import NylasStore from 'nylas-store'; 6 | 7 | import KeybaseActions from './keybase-actions'; 8 | import KeybaseRemote from '../keybase-integration'; 9 | 10 | import Logger from '../../utils/Logger'; 11 | 12 | class KeybaseStore extends NylasStore { 13 | _configurationDirPath = path.join(NylasEnv.getConfigDirPath(), 'cypher'); 14 | 15 | constructor() { 16 | super(); 17 | 18 | this._cachedPrimarySigChain = null; 19 | 20 | this.getPrimarySigChain = this.getPrimarySigChain.bind(this); 21 | this.getTrackedUsers = this.getTrackedUsers.bind(this); 22 | this._login = this._login.bind(this); 23 | this._fetchAndVerifySigChain = this._fetchAndVerifySigChain.bind(this); 24 | this.ensureConfigurationDirectoryExists = this.ensureConfigurationDirectoryExists.bind(this); 25 | this.loadSavedCredentials = this.loadSavedCredentials.bind(this); 26 | 27 | this.log = Logger.create('KeybaseStore'); 28 | 29 | this.listenTo(KeybaseActions.login, this._login); 30 | this.listenTo(KeybaseActions.fetchAndVerifySigChain, this._fetchAndVerifySigChain); 31 | 32 | this.ensureConfigurationDirectoryExists(); 33 | this.loadSavedCredentials(); 34 | 35 | global.$pgpKeybaseStore = this; 36 | } 37 | 38 | // Helper methods 39 | 40 | // SigChain for the stored login 41 | getPrimarySigChain() { 42 | return this._cachedPrimarySigChain; 43 | } 44 | 45 | getPrimaryTrackedUsers() { 46 | return this.getTrackedUsers(this._cachedPrimarySigChain); 47 | } 48 | 49 | getTrackedUsers(sigchain) { 50 | if (!sigchain) { 51 | throw new Error('No sigchain provided'); 52 | } 53 | 54 | const trackingStatus = sigchain 55 | .get_links() 56 | .filter((a) => a.type === 'track' || a.type === 'untrack') 57 | .reduce((origValue, value) => { 58 | const tracking = origValue; 59 | const { username } = value.payload.body[value.type].basics; 60 | 61 | tracking[username] = tracking[username] || 0; 62 | if (value.type === 'track') { 63 | tracking[username] += 1; 64 | } else if (value.type === 'untrack') { 65 | tracking[username] -= 1; 66 | } 67 | 68 | return tracking; 69 | }, {}); 70 | 71 | return Object.keys(trackingStatus).reduce((array, name) => { 72 | if (trackingStatus[name] % 2 === 0) { 73 | array.push(name); 74 | } 75 | }, []); 76 | } 77 | 78 | // Action Trigges 79 | 80 | _login(username, passphrase) { 81 | this.trigger({ type: 'LOGGING_IN' }); 82 | 83 | KeybaseRemote.login(username, passphrase).then((res) => { 84 | const { status: { name } } = res; 85 | let promise = Promise.resolve(true); 86 | 87 | if (name === 'BAD_LOGIN_PASSWORD') { 88 | this.log.error('Keybase login error: Bad Passphrase'); 89 | promise = Promise.resolve(false); 90 | } else if (name === 'BAD_LOGIN_USER_NOT_FOUND') { 91 | this.log.error('Keybase login error: Bad Username or Email'); 92 | promise = Promise.resolve(false); 93 | } else { 94 | NylasEnv.config.set('cypher.keybase.username', username); 95 | NylasEnv.config.set('cypher.keybase.uid', res.uid); 96 | NylasEnv.config.set('cypher.keybase.csrf_token', res.csrf_token); 97 | NylasEnv.config.set('cypher.keybase.session_token', res.session); 98 | 99 | const loginFilePath = path.join(this._configurationDirPath, 'keybase_login.json'); 100 | const string = JSON.stringify({ 101 | username, 102 | uid: res.uid, 103 | csrf_token: res.csrf_token, 104 | session_token: res.session, 105 | }); 106 | promise = fs.writeFileAsync(loginFilePath, string); 107 | 108 | this.loadSavedCredentials(); 109 | } 110 | 111 | this.trigger({ type: 'LOGIN', username, res }); 112 | 113 | return promise; 114 | }); 115 | } 116 | 117 | _fetchAndVerifySigChain(username, uid) { 118 | const parseAsync = Promise.promisify(libkb.ParsedKeys.parse); 119 | const replayAsync = Promise.promisify(libkb.SigChain.replay); 120 | 121 | const cachedPublicKeys = `${username}.${uid}.public_keys.json`; 122 | const cachedSigchain = `${username}.${uid}.sigchain.json`; 123 | 124 | return KeybaseRemote.userLookup({ 125 | usernames: [username], 126 | fields: ['public_keys'], 127 | }).then((result) => result.them[0].public_keys, (err) => { 128 | this.log.error('There was an error', err); 129 | this.log.info('Attempting to load from cache, if exists'); 130 | 131 | const cachedFile = path.join(this._configurationDirPath, cachedPublicKeys); 132 | return fs.accessAsync(cachedFile, fs.F_OK).then(() => 133 | fs.readFileAsync(cachedFile) 134 | ).then(JSON.parse); 135 | }).then((publicKeys) => { 136 | const cachedFile = path.join(this._configurationDirPath, cachedPublicKeys); 137 | fs.writeFileAsync(cachedFile, JSON.stringify(publicKeys)).then(() => { 138 | this.log.info('Wrote user public_keys to cache file successfully'); 139 | }, (err) => { 140 | this.log.error('Unable to write public_keys cache file', err); 141 | }); 142 | 143 | const keyBundles = publicKeys.all_bundles; 144 | return [ 145 | publicKeys.eldest_kid, 146 | parseAsync({ key_bundles: keyBundles }), 147 | ]; 148 | }).spread((eldestKid, [parsedKeys]) => { 149 | const log = (msg) => this.log.info(msg); 150 | 151 | return KeybaseRemote.sigChainForUid(uid).then(({ sigs }) => { 152 | const cachedFile = path.join(this._configurationDirPath, cachedSigchain); 153 | fs.writeFileAsync(cachedFile, JSON.stringify(sigs)).then(() => { 154 | this.log.info('Wrote user sigchain to cache file successfully'); 155 | }, (err) => { 156 | this.log.error('Unable to write sigchain cache file', err); 157 | }); 158 | 159 | return sigs; 160 | }, () => { 161 | const cachedFile = path.join(this._configurationDirPath, cachedSigchain); 162 | 163 | return fs.accessAsync(cachedFile, fs.F_OK) 164 | .then(() => fs.readFileAsync(cachedFile)) 165 | .then(JSON.parse); 166 | }).then((sigBlobs) => 167 | replayAsync({ 168 | sig_blobs: sigBlobs, 169 | parsed_keys: parsedKeys, 170 | username, 171 | uid, 172 | eldest_kid: eldestKid, 173 | log, 174 | }) 175 | ).then((res) => { 176 | if (username === this.username && uid === this.uid) { 177 | this._cachedPrimarySigChain = res; 178 | } 179 | 180 | this.trigger({ type: 'VERIFIED_SIGCHAIN', username, uid, res }); 181 | return res; 182 | }); 183 | }); 184 | } 185 | 186 | // Private methods 187 | 188 | ensureConfigurationDirectoryExists() { 189 | fs.access(this._configurationDirPath, fs.F_OK, (err) => { 190 | if (err) { 191 | this.log.info('Configuration directory missing, creating'); 192 | fs.mkdir(this._configurationDirPath, (err2) => { 193 | if (err) { 194 | this.log.error('Configuration directory creation unsuccessful', err2); 195 | } else { 196 | this.log.info('Configuration directory creation successful'); 197 | } 198 | }); 199 | } 200 | }); 201 | } 202 | 203 | loadSavedCredentials() { 204 | const { 205 | username, 206 | uid, 207 | csrf_token: csrfToken, 208 | session_token: sessionToken, 209 | } = NylasEnv.config.get('cypher.keybase') || {}; 210 | this.username = username; 211 | this.uid = uid; 212 | this.csrfToken = csrfToken; 213 | this.sessionToken = sessionToken; 214 | 215 | if (this.username && this.uid) { 216 | this._fetchAndVerifySigChain(this.username, this.uid); 217 | } 218 | } 219 | } 220 | 221 | export default new KeybaseStore(); 222 | -------------------------------------------------------------------------------- /lib/main.es6: -------------------------------------------------------------------------------- 1 | import { ComponentRegistry, ExtensionRegistry, PreferencesUIStore } from 'nylas-exports'; 2 | 3 | import PreferencesComponent from './settings/preferences-component'; 4 | import MessageLoaderExtension from './message-loader/message-loader-extension'; 5 | import MessageLoaderHeader from './message-loader/message-loader-header'; 6 | import WorkerFrontend from './worker-frontend'; 7 | import ComposerLoader from './composer/composer-loader'; 8 | import KeybaseSidebar from './keybase-sidebar'; 9 | 10 | class PGPMain { 11 | config = { 12 | keybase: { 13 | type: 'object', 14 | properties: { 15 | username: { 16 | type: 'string', 17 | default: '', 18 | }, 19 | uid: { 20 | type: 'string', 21 | default: '', 22 | }, 23 | csrf_token: { 24 | type: 'string', 25 | default: '', 26 | }, 27 | session_token: { 28 | type: 'string', 29 | default: '', 30 | }, 31 | }, 32 | }, 33 | }; 34 | 35 | _state = {}; 36 | _tab = null; 37 | 38 | constructor() { 39 | this.activate = this.activate.bind(this); 40 | this.serialize = this.serialize.bind(this); 41 | this.deactivate = this.deactivate.bind(this); 42 | } 43 | 44 | // Activate is called when the package is loaded. If your package previously 45 | // saved state using `serialize` it is provided. 46 | // 47 | activate(state) { 48 | const _loadSettings = NylasEnv.getLoadSettings(); 49 | const windowType = _loadSettings.windowType; 50 | 51 | if (windowType === 'default') { 52 | this._state = state; 53 | this._tab = new PreferencesUIStore.TabItem({ 54 | tabId: 'cypher', 55 | displayName: 'Cypher', 56 | component: PreferencesComponent, 57 | }); 58 | 59 | WorkerFrontend.initialize(); 60 | 61 | PreferencesUIStore.registerPreferencesTab(this._tab); 62 | ComponentRegistry.register(MessageLoaderHeader, { role: 'message:BodyHeader' }); 63 | ComponentRegistry.register(KeybaseSidebar, { role: 'MessageListSidebar:ContactCard' }); 64 | ExtensionRegistry.MessageView.register(MessageLoaderExtension); 65 | } 66 | 67 | if (windowType === 'default' || windowType === 'composer') { 68 | ComponentRegistry.register(ComposerLoader, { role: 'Composer:ActionButton' }); 69 | } 70 | } 71 | 72 | // Serialize is called when your package is about to be unmounted. 73 | // You can return a state object that will be passed back to your package 74 | // when it is re-activated. 75 | serialize() { 76 | } 77 | 78 | // This **optional** method is called when the window is shutting down, 79 | // or when your package is being updated or disabled. If your package is 80 | // watching any files, holding external resources, providing commands or 81 | // subscribing to events, release them here. 82 | deactivate() { 83 | const _loadSettings = NylasEnv.getLoadSettings(); 84 | const windowType = _loadSettings.windowType; 85 | 86 | if (windowType === 'default') { 87 | PreferencesUIStore.unregisterPreferencesTab(this._tab.tabId); 88 | ExtensionRegistry.MessageView.unregister(MessageLoaderExtension); 89 | ComponentRegistry.unregister(MessageLoaderHeader); 90 | } 91 | 92 | if (windowType === 'default' || windowType === 'composer') { 93 | ComponentRegistry.unregister(ComposerLoader); 94 | } 95 | } 96 | } 97 | 98 | const { activate, serialize, deactivate } = new PGPMain(); 99 | export { activate, serialize, deactivate }; 100 | -------------------------------------------------------------------------------- /lib/message-loader/message-loader-extension.es6: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: 0 */ 2 | 3 | import { MessageViewExtension } from 'nylas-exports'; 4 | 5 | import PGPStore from '../flux/stores/pgp-store'; 6 | import MessageCacheStore from '../flux/stores/message-cache-store'; 7 | import MessageActions from '../flux/actions/pgp-actions'; 8 | 9 | import Logger from '../utils/Logger'; 10 | 11 | const log = Logger.create('MessageLoaderExtension'); 12 | 13 | class MessageLoaderExtension extends MessageViewExtension { 14 | // CANNOT crash here. If we do, the whole app stops working 15 | // properly and the main screen is stuck with the message 16 | // viewer 17 | static formatMessageBody({ message }) { 18 | // Check for a cached message body for a decrypted message 19 | // If we have one we should return the cached message so the 20 | // proper message body is displayed 21 | const cached = MessageCacheStore.getCachedBody(message.id); 22 | if (cached) { 23 | log.info(`Have cached body for ${message.id}`); 24 | message.body = cached; 25 | 26 | return; 27 | } 28 | 29 | // If we don't have a cached copy and the message matches the parameters for 30 | // decryption, then signal the `EmailPGPStore` to decrypt the message and 31 | // pass on the cloned message 32 | if (PGPStore.shouldDecryptMessage(message)) { 33 | log.info(`MessageLoaderExtension formatting ${message.id}`); 34 | MessageActions.decrypt(message); 35 | } 36 | } 37 | } 38 | 39 | export default MessageLoaderExtension; 40 | -------------------------------------------------------------------------------- /lib/message-loader/message-loader-header.es6: -------------------------------------------------------------------------------- 1 | import { React } from 'nylas-exports'; 2 | 3 | import MessageActions from '../flux/actions/pgp-actions'; 4 | import PGPStore from '../flux/stores/pgp-store'; 5 | import FlowError from '../utils/flow-error'; 6 | 7 | /** 8 | * Header component to display the user-readable status of the decryption 9 | * decryption process from @class{PGPStore} 10 | * 11 | * @class MessageLoaderHeader 12 | */ 13 | class MessageLoaderHeader extends React.Component { 14 | static displayName = 'MessageLoader'; 15 | 16 | static propTypes = { 17 | message: React.PropTypes.object.isRequired, 18 | }; 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | // All the methods that depend on `this` instance 24 | this.componentDidMount = this.componentDidMount.bind(this); 25 | this.componentWillUnmount = this.componentWillUnmount.bind(this); 26 | this.render = this.render.bind(this); 27 | this.retryDecryption = this.retryDecryption.bind(this); 28 | this._onPGPStoreChange = this._onPGPStoreChange.bind(this); 29 | 30 | this.state = PGPStore.getState(this.props.message.id) || {}; 31 | } 32 | 33 | componentDidMount() { 34 | this._storeUnlisten = PGPStore.listen(this._onPGPStoreChange); 35 | 36 | global.$pgpLoaderHeader = this; 37 | } 38 | 39 | componentWillUnmount() { 40 | if (this._storeUnlisten) { 41 | this._storeUnlisten(); 42 | } 43 | } 44 | 45 | retryDecryption() { 46 | MessageActions.retry(this.props.message); 47 | } 48 | 49 | _onPGPStoreChange(messageId, state) { 50 | if (messageId === this.props.message.id) { 51 | this.state = state; 52 | this.forceUpdate(); 53 | } 54 | } 55 | 56 | render() { 57 | let display = true; 58 | let displayMessage = ''; 59 | let className = 'pgp-message-header'; 60 | 61 | if (this.state.decrypting && !this.state.statusMessage) { 62 | displayMessage = Decrypting message; 63 | } else if (this.state.decrypting && this.state.statusMessage) { 64 | className += ' pgp-message-header-info'; 65 | displayMessage = {this.state.statusMessage}; 66 | } else if (this.state.lastError && 67 | ((this.state.lastError instanceof FlowError && this.state.lastError.display) || 68 | !(this.state.lastError instanceof FlowError))) { 69 | className += ' pgp-message-header-error'; 70 | displayMessage = ( 71 |
72 | Error: {this.state.lastError.message} 73 | Retry Decryption 74 |
75 | ); 76 | } else { 77 | display = false; 78 | } 79 | 80 | if (display) { 81 | return
{displayMessage}
; 82 | } 83 | return
; 84 | } 85 | } 86 | 87 | export default MessageLoaderHeader; 88 | -------------------------------------------------------------------------------- /lib/settings/config-schema-item.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 Nylas 3 | * blah blah blah GPL-3.0 licensed 4 | */ 5 | 6 | import { React } from 'nylas-exports'; 7 | import _ from 'lodash'; 8 | 9 | /** 10 | * This component renders input controls for a subtree of the N1 config-schema 11 | * and reads/writes current values using the `config` prop, which is expected to 12 | * be an instance of the config provided by `ConfigPropContainer`. 13 | * 14 | * The config schema follows the JSON Schema standard: http://json-schema.org/ 15 | */ 16 | const ConfigSchemaItem = (props) => { 17 | const _appliesToPlatform = () => { 18 | if (!props.configSchema.platforms) { 19 | return true; 20 | } 21 | for (const platform of props.configSchema.platforms) { 22 | if (process.platform === platform) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | }; 28 | 29 | const _onChangeChecked = (event) => { 30 | props.config.toggle(props.keyPath); 31 | event.target.blur(); 32 | }; 33 | 34 | const _onChangeValue = (event) => { 35 | props.config.set(props.keyPath, event.target.value); 36 | event.target.blur(); 37 | }; 38 | 39 | // In the future, we may add an option to reveal "advanced settings" 40 | if (!_appliesToPlatform() || props.configSchema.advanced) { 41 | return false; 42 | } else if (props.configSchema.type === 'object') { 43 | return ( 44 |
45 |

{props.keyName}

46 | {_.pairs(props.configSchema.properties).map(([key, value]) => 47 | 54 | )} 55 |
56 | ); 57 | } else if (props.configSchema.enum) { 58 | const { config, keyPath, configSchema: { enumLabels, title } } = props; 59 | const selectValue = config.get(keyPath); 60 | return ( 61 |
62 | 63 | 68 |
69 | ); 70 | } else if (props.configSchema.type === 'boolean') { 71 | return ( 72 |
73 | 79 | 80 |
81 | ); 82 | } 83 | 84 | return ( 85 | 86 | ); 87 | }; 88 | 89 | ConfigSchemaItem.displayName = 'ConfigSchemaItem'; 90 | ConfigSchemaItem.propTypes = { 91 | config: React.PropTypes.object, 92 | configSchema: React.PropTypes.object, 93 | keyPath: React.PropTypes.string, 94 | keyName: React.PropTypes.string, 95 | }; 96 | 97 | export default ConfigSchemaItem; 98 | -------------------------------------------------------------------------------- /lib/settings/keybase-login-section.es6: -------------------------------------------------------------------------------- 1 | import { React } from 'nylas-exports'; 2 | 3 | import KeybaseActions from '../keybase/store/keybase-actions'; 4 | import KeybaseStore from '../keybase/store/keybase-store'; 5 | import SettingsField from './settings-field'; 6 | 7 | import Logger from '../utils/Logger'; 8 | 9 | class KeybaseLoginSection extends React.Component { 10 | static displayName = 'KeybaseLoginSection'; 11 | 12 | defaultState = { 13 | error: '', 14 | username: '', 15 | passphrase: '', 16 | uid: '', 17 | csrfToken: '', 18 | sessionToken: '', 19 | userInfo: '', 20 | }; 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | this.loginToKeybase = this.loginToKeybase.bind(this); 26 | this.fetchAndVerifySigChain = this.fetchAndVerifySigChain.bind(this); 27 | this.onKeybaseStore = this.onKeybaseStore.bind(this); 28 | this.onChangeUsername = this.onChangeUsername.bind(this); 29 | this.onChangePassphrase = this.onChangePassphrase.bind(this); 30 | 31 | this.log = Logger.create('KeybaseLoginSection'); 32 | this.state = this.loadPreviousLogin(); 33 | } 34 | 35 | componentDidMount() { 36 | this.unsubscribe = KeybaseStore.listen(this.onKeybaseStore); 37 | } 38 | 39 | componentWillUnmount() { 40 | if (this.unsubscribe) { 41 | this.unsubscribe(); 42 | this.unsubscribe = null; 43 | } 44 | } 45 | 46 | loadPreviousLogin() { 47 | const { 48 | username = '', 49 | uid = '', 50 | csrf_token: csrfToken = '', 51 | session_token: sessionToken = '', 52 | } = NylasEnv.config.get('cypher.keybase') || {}; 53 | 54 | return { 55 | error: '', 56 | username, 57 | passphrase: (csrfToken && sessionToken) ? '****' : '', 58 | uid, 59 | csrfToken, 60 | sessionToken, 61 | userInfo: null, 62 | }; 63 | } 64 | 65 | loginToKeybase() { 66 | const { username, passphrase } = this.state; 67 | 68 | if (username === '' || passphrase === '') { 69 | this.setState({ error: 'Please provide a username and passphrase!' }); 70 | return; 71 | } 72 | 73 | this.log.info('Keybase Login'); 74 | this.setState(Object.assign({}, this.defaultState, { 75 | username, 76 | })); 77 | 78 | KeybaseActions.login(username, passphrase); 79 | } 80 | 81 | fetchAndVerifySigChain() { 82 | const { username, uid } = this.state; 83 | KeybaseActions.fetchAndVerifySigChain(username, uid); 84 | } 85 | 86 | onKeybaseStore({ type, username, uid, res }) { 87 | if (type === 'LOGGING_IN') { 88 | this.setState({ status: 'LOGGING_IN' }); 89 | } else if (type === 'LOGIN') { 90 | const { status: { name } } = res; 91 | 92 | if (name === 'BAD_LOGIN_PASSWORD') { 93 | return this.setState({ 94 | status: 'ERRORED', 95 | error: 'Bad Passphrase', 96 | }); 97 | } else if (name === 'BAD_LOGIN_USER_NOT_FOUND') { 98 | return this.setState({ 99 | status: 'ERRORED', 100 | error: 'Bad Username or Email', 101 | }); 102 | } 103 | 104 | this.setState(Object.assign({}, this.loadPreviousLogin(), { 105 | status: 'SUCCESS_LOGIN', 106 | userInfo: res.me, 107 | })); 108 | } else { 109 | this.log.info(`listen: type=${type}, username=${username}, uid=${uid}, res=`, res); 110 | /* this.setState({ 111 | username, 112 | uid 113 | }); */ 114 | this.forceUpdate(); 115 | } 116 | } 117 | 118 | onChangeUsername(e) { 119 | this.setState({ username: e.target.value }); 120 | } 121 | 122 | onChangePassphrase(e) { 123 | this.setState({ passphrase: e.target.value }); 124 | } 125 | 126 | renderStatus() { 127 | const { error, status } = this.state; 128 | let type = ''; 129 | let msg = ''; 130 | 131 | if (error !== '') { 132 | type = 'error'; 133 | msg = `Error: ${error}`; 134 | } else if (status === 'LOGGING_IN') { 135 | type = 'info'; 136 | msg = 'Logging in...'; 137 | } else if (status === 'SUCCESS_LOGIN') { 138 | type = 'success'; 139 | msg = 'Successful login'; 140 | } else { 141 | return null; 142 | } 143 | 144 | const className = `pgp-message-header pgp-message-header-${type}`; 145 | return ( 146 |
{msg}
147 | ); 148 | } 149 | 150 | renderUserLoginInfo() { 151 | const { uid, sessionToken } = this.state; 152 | 153 | if (uid && sessionToken) { 154 | const body = `uid: ${uid}\nsessionToken: ${sessionToken}`; 155 | 156 | // Using substitution causes s to be used, causes incorrect line 157 | // breaks 158 | return ( 159 |
{body}
160 | ); 161 | } 162 | } 163 | 164 | render() { 165 | const { username, passphrase } = this.state; 166 | 167 | return ( 168 |
169 |

Keybase Login

170 | {this.renderStatus()} 171 | 180 | 189 | {this.renderUserLoginInfo()} 190 | 191 |
192 | ); 193 | } 194 | } 195 | 196 | export default KeybaseLoginSection; 197 | -------------------------------------------------------------------------------- /lib/settings/preferences-component.es6: -------------------------------------------------------------------------------- 1 | import { React } from 'nylas-exports'; 2 | 3 | import KeybaseLoginSection from './keybase-login-section'; 4 | import SigChainSection from './sig-chain-section'; 5 | 6 | // import { Logger } from '../utils/Logger'; 7 | 8 | class PreferencesComponent extends React.Component { 9 | static displayName = 'PreferencesComponent'; 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.render = this.render.bind(this); 15 | 16 | // this.log = Logger.create('PreferencesComponent'); 17 | 18 | global.$pgpPref = this; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | 25 | 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default PreferencesComponent; 32 | -------------------------------------------------------------------------------- /lib/settings/settings-field.es6: -------------------------------------------------------------------------------- 1 | import { React } from 'nylas-exports'; 2 | import { Flexbox } from 'nylas-component-kit'; 3 | 4 | function noop() {} 5 | 6 | export default class SettingsField extends React.Component { 7 | static displayName = 'SettingsField'; 8 | 9 | static propTypes = { 10 | className: React.PropTypes.string, 11 | inputId: React.PropTypes.string.isRequired, 12 | message: React.PropTypes.string.isRequired, 13 | type: React.PropTypes.string, 14 | placeholder: React.PropTypes.string, 15 | value: React.PropTypes.string, 16 | tabIndex: React.PropTypes.string, 17 | onChange: React.PropTypes.func.isRequired, 18 | }; 19 | 20 | render() { 21 | const { 22 | className = '', 23 | inputId, 24 | message, 25 | type = 'text', 26 | placeholder, 27 | value, 28 | tabIndex = '-1', 29 | onChange = noop, 30 | } = this.props; 31 | 32 | return ( 33 | 34 |
35 | 36 |
37 |
38 | 46 |
47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/settings/sig-chain-section.es6: -------------------------------------------------------------------------------- 1 | import { React } from 'nylas-exports'; 2 | import { Flexbox } from 'nylas-component-kit'; 3 | 4 | import KeybaseStore from '../keybase/store/keybase-store'; 5 | 6 | class SigChainSection extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.getStateFromStores = this.getStateFromStores.bind(this); 11 | this.setStateFromStores = this.setStateFromStores.bind(this); 12 | this.renderSigChain = this.renderSigChain.bind(this); 13 | this.render = this.render.bind(this); 14 | 15 | this.state = this.getStateFromStores(); 16 | } 17 | 18 | componentDidMount() { 19 | this.unsubscribe = KeybaseStore.listen(this.setStateFromStores); 20 | } 21 | 22 | componentWillUnmount() { 23 | if (this.unsubscribe) { 24 | this.unsubscribe(); 25 | this.unsubscribe = null; 26 | } 27 | } 28 | 29 | getStateFromStores() { 30 | return { 31 | sigchain: KeybaseStore.getPrimarySigChain(), 32 | }; 33 | } 34 | 35 | setStateFromStores() { 36 | this.setState(this.getStateFromStores()); 37 | } 38 | 39 | renderSigChain() { 40 | const { sigchain } = this.state; 41 | if (!sigchain) { 42 | return 'Not loaded yet.'; 43 | } 44 | 45 | const keytype = (kid) => { 46 | if (kid.startsWith('0101')) { 47 | return 'PGP'; 48 | } else if (kid.startsWith('0120')) { 49 | return 'NaCL'; 50 | } 51 | return 'Unknown'; 52 | }; 53 | 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {sigchain.get_links().map((link, i) => 66 | 67 | 68 | 69 | 70 | 71 | 72 | )} 73 | 74 |
#TypeSig Key TypeFingerprint or kid
{link.seqno}{link.type}{keytype(link.kid)}{link.fingerprint || link.kid}
75 | ); 76 | } 77 | 78 | render() { 79 | return ( 80 |
81 |

SigChain Status

82 | 83 | {this.renderSigChain()} 84 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | export default SigChainSection; 91 | -------------------------------------------------------------------------------- /lib/utils/Logger.es6: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const STYLE = ` 4 | font-weight: bold; 5 | color: purple; 6 | `; 7 | 8 | class Logger { 9 | constructor(name = 'default') { 10 | this.name = name; 11 | this.log = this.log.bind(this); 12 | this.info = this.info.bind(this); 13 | this.warn = this.warn.bind(this); 14 | this.error = this.error.bind(this); 15 | this.trace = this.trace.bind(this); 16 | } 17 | 18 | log(level, ...args) { 19 | console[level || 'log'](`%c[${this.name}]`, STYLE, ...args); 20 | } 21 | 22 | info(...args) { 23 | this.log('info', ...args); 24 | } 25 | 26 | warn(...args) { 27 | this.log('warn', ...args); 28 | } 29 | 30 | error(...args) { 31 | this.log('error', ...args); 32 | } 33 | 34 | trace(...args) { 35 | this.log('trace', ...args); 36 | } 37 | } 38 | 39 | const defaultLogger = new Logger(); 40 | 41 | export default { 42 | create: (...args) => new Logger(...args), 43 | log: defaultLogger.log, 44 | info: defaultLogger.info, 45 | warn: defaultLogger.warn, 46 | error: defaultLogger.error, 47 | trace: defaultLogger.trace, 48 | }; 49 | -------------------------------------------------------------------------------- /lib/utils/flow-error.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Error class that conditionally prints out the name to MessageLoaderHeader. 3 | * Useful for stopping a promise chain only on a fatal error and not a minor 4 | * error. 5 | */ 6 | class FlowError extends Error { 7 | name = 'FlowError'; 8 | 9 | /** 10 | * @param{string} message - title message for the error 11 | * @param{boolean} display - conditionally display the error in MessageLoaderHeader 12 | * @constructor 13 | */ 14 | constructor(message, display = false) { 15 | super(message); 16 | Error.captureStackTrace(this, this.constructor); 17 | 18 | this.message = message; 19 | this.display = display; 20 | 21 | this.title = this.message; 22 | } 23 | } 24 | 25 | export default FlowError; 26 | -------------------------------------------------------------------------------- /lib/utils/gpg-utils.es6: -------------------------------------------------------------------------------- 1 | /* eslint max-len: [2, 230, 2] */ 2 | 3 | import childProcess from 'child_process'; 4 | 5 | import WorkerFrontend from '../worker-frontend'; 6 | 7 | function getKeysUnix() { 8 | const keys = []; 9 | let currentKey = {}; 10 | 11 | if (!process.env.PATH.includes('/usr/local/bin')) { 12 | process.env.PATH += ':/usr/local/bin'; 13 | } 14 | 15 | const output = childProcess.execSync('gpg --list-secret-keys --fingerprint').toString(); 16 | 17 | for (let line of output.split('\n')) { 18 | line = line.trim(); 19 | 20 | if (line.startsWith('sec#')) { 21 | continue; 22 | } 23 | 24 | if (line.startsWith('sec') || line.startsWith('ssb')) { 25 | // One crazy line of regex for parsing GPG output 26 | const parsed = /^(sec|ssb) +(?:\w+)?([0-9]*)(?:[a-zA-Z]?)\/([a-zA-Z0-9]*) ((2[0-9]{3})-(1[0-2]|0[1-9])-(0[1-9]|[12]\d|3[01]))(?: \[expires\: )?((2[0-9]{3})-(1[0-2]|0[1-9])-(0[1-9]|[12]\d|3[01]))?/.exec(line).slice(1); 27 | 28 | currentKey = { 29 | type: parsed[0] === 'sec' ? 'master' : 'subkey', 30 | size: parsed[1], 31 | key: parsed[2], 32 | created: parsed[3], 33 | expires: parsed[7], 34 | }; 35 | } 36 | 37 | if (line.startsWith('Key fingerprint')) { 38 | currentKey.fpr = /^[ ]*Key fingerprint = ((([0-9a-fA-F]{4})[ ]*)+)$/.exec(line)[1]; 39 | keys.push(currentKey); 40 | } 41 | } 42 | 43 | return Promise.resolve(keys); 44 | } 45 | 46 | function getKeysKeybase() { 47 | return WorkerFrontend.getKeys(); 48 | } 49 | 50 | function getKeys() { 51 | if (process.platform === 'linux' || process.platform === 'darwin') { 52 | return getKeysUnix(); 53 | } 54 | 55 | return getKeysKeybase(); 56 | } 57 | 58 | export default { 59 | getKeys, 60 | }; 61 | -------------------------------------------------------------------------------- /lib/utils/html-parser.es6: -------------------------------------------------------------------------------- 1 | import MimeParser from 'emailjs-mime-parser'; 2 | 3 | import Logger from './Logger'; 4 | 5 | const regex = /\n--[^\n\r]*\r?\nContent-Type: text\/html[\s\S]*?\r?\n\r?\n([\s\S]*?)\n\r?\n--/gim; 6 | const log = Logger.create(`MimeParser`); 7 | 8 | // Uses regex to extract HTML component from a multipart message. Does not 9 | // contribute a significant amount of time to the decryption process. 10 | export function extractHTML({ text }) { 11 | return new Promise((resolve) => { 12 | const parser = new MimeParser(); 13 | 14 | // Use MIME parsing to extract possible body 15 | let matched = null; 16 | let start = process.hrtime(); 17 | 18 | parser.onbody = (node, chunk) => { 19 | if ((node.contentType.value === 'text/html') || // HTML body 20 | (node.contentType.value === 'text/plain' && !matched)) { // Plain text 21 | matched = new Buffer(chunk).toString('utf8'); 22 | } 23 | }; 24 | parser.onend = () => { 25 | const end = process.hrtime(start); 26 | log.info(`Parsed MIME in ${end[0] * 1e3 + end[1] / 1e6}ms`); 27 | }; 28 | 29 | parser.write(text); 30 | parser.end(); 31 | 32 | // Fallback to regular expressions method 33 | if (!matched) { 34 | start = process.hrtime(); 35 | const matches = regex.exec(text); 36 | const end = process.hrtime(start); 37 | if (matches) { 38 | log.info(`Regex found HTML in ${end[0] * 1e3 + end[1] / 1e6}ms`); 39 | matched = matches[1]; 40 | } 41 | } 42 | 43 | if (matched) { 44 | resolve(matched); 45 | } else { 46 | // REALLY FALLBACK TO RAW 47 | log.error('FALLBACK TO RAW DECRYPTED'); 48 | const formatted = `FALLBACK TO RAW:
${text}`; 49 | resolve(formatted); 50 | } 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/worker-frontend.es6: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | import path from 'path'; 3 | import readline from 'readline'; 4 | import nylasExports from 'nylas-exports'; 5 | 6 | import smalltalk from 'smalltalk'; 7 | import uuid from 'uuid'; 8 | 9 | import proto from './worker/worker-protocol'; 10 | import FlowError from './utils/flow-error'; 11 | import Logger from './utils/Logger'; 12 | 13 | class WorkerFrontend { 14 | constructor() { 15 | this._workerEntryScriptPath = path.join(__dirname, 'worker', 'worker-entry.js'); 16 | this._deferreds = new Map(); 17 | 18 | this.decrypt = this.decrypt.bind(this); 19 | this.getKeys = this.getKeys.bind(this); 20 | this.initialize = this.initialize.bind(this); 21 | this._forkProcess = this._forkProcess.bind(this); 22 | this._requestPassphrase = this._requestPassphrase.bind(this); 23 | 24 | this.log = Logger.create('WorkerFrontend'); 25 | 26 | global.$pgpWorkerFrontend = this; 27 | } 28 | 29 | decrypt(notify, armored, secretKey) { 30 | const id = uuid(); 31 | 32 | const deferred = Promise.defer(); 33 | deferred.notify = notify; 34 | this._deferreds.set(id, deferred); 35 | 36 | this._child.send({ method: proto.DECRYPT, id, armored, secretKey }); 37 | 38 | return deferred.promise; 39 | } 40 | 41 | getKeys() { 42 | const id = uuid(); 43 | 44 | const deferred = Promise.defer(); 45 | this._deferreds.set(id, deferred); 46 | 47 | this._child.send({ method: proto.GET_KEYS, id }); 48 | 49 | return deferred.promise; 50 | } 51 | 52 | // Called by `main.es6` when the `windowType` matches either `default` or 53 | // `composer` 54 | initialize() { 55 | this._forkProcess(); 56 | } 57 | 58 | _forkProcess() { 59 | // We need to find out the path of the compile-cache module so we can 60 | // pass it on to the worker process, use the hijacked require to ensure it 61 | // is in the module cache 62 | nylasExports.lazyLoad('CompileCache', 'compile-cache'); 63 | const compileCache = nylasExports.CompileCache; 64 | const compileCachePath = compileCache.getCacheDirectory(); 65 | 66 | let modulePath = ''; 67 | Object.keys(require.cache).some((module) => { 68 | if (module.match(/compile-cache/)) { 69 | modulePath = module; 70 | return true; 71 | } 72 | return false; 73 | }); 74 | 75 | this._child = childProcess.fork(this._workerEntryScriptPath, { 76 | env: Object.assign({}, process.env, { 77 | PGP_COMPILE_CACHE_MODULE_PATH: modulePath, 78 | PGP_COMPILE_CACHE_PATH: compileCachePath, 79 | PGP_CONFIG_DIR_PATH: NylasEnv.getConfigDirPath(), 80 | }), 81 | silent: true, 82 | }); 83 | 84 | const rlOut = readline.createInterface({ 85 | input: this._child.stdout, 86 | terminal: false, 87 | }); 88 | const rlErr = readline.createInterface({ 89 | input: this._child.stderr, 90 | terminal: false, 91 | }); 92 | 93 | rlOut.on('line', (data) => this.log.info('[child.stdout]', data)); 94 | rlErr.on('line', (data) => this.log.info('[child.stderr]', data)); 95 | 96 | this._child.on('message', (message) => { 97 | if (message.method === proto.ERROR_OCCURRED) { 98 | // ERROR_OCCURRED 99 | const errorTitle = message.errorMessage || 'unknown error, check error.childStackTrace'; 100 | const error = new FlowError(errorTitle, true); 101 | error.childStackTrace = message.errorStackTrace; 102 | this.log.error('Error from worker:', error, error.childStackTrace); 103 | } else if (message.method === proto.VERBOSE_OUT) { 104 | // VERBOSE_OUT 105 | this.log.info('[Verbose]', message.message); 106 | } else if (message.method === proto.REQUEST_PASSPHRASE) { 107 | // REQUEST_PASSPHRASE 108 | this._requestPassphrase(message.id, message.message); 109 | } else if (message.method === proto.PROMISE_RESOLVE && this._deferreds.has(message.id)) { 110 | // PROMISE_RESOLVE 111 | this._deferreds.get(message.id).resolve(message.result); 112 | this._deferreds.delete(message.id); 113 | } else if (message.method === proto.PROMISE_REJECT && this._deferreds.has(message.id)) { 114 | // PROMISE_REJECT 115 | this._deferreds.get(message.id).reject(new FlowError(message.result, true)); 116 | this._deferreds.delete(message.id); 117 | } else if (message.method === proto.PROMISE_NOTIFY && this._deferreds.has(message.id)) { 118 | // PROMISE_NOTIFY 119 | this._deferreds.get(message.id).notify(message.result); 120 | } else { 121 | this.log.error('Unknown Message Received From Worker:', message); 122 | } 123 | }); 124 | } 125 | 126 | _requestPassphrase(id, msg) { 127 | smalltalk.passphrase('PGP Passphrase', msg || '').then((passphrase) => { 128 | this._child.send({ method: proto.PROMISE_RESOLVE, id, result: passphrase }); 129 | this.log.info('Passphrase entered'); 130 | }, () => { 131 | this._child.send({ method: proto.PROMISE_REJECT, id }); 132 | this.log.info('Passphrase cancelled'); 133 | }); 134 | } 135 | } 136 | 137 | export default new WorkerFrontend(); 138 | -------------------------------------------------------------------------------- /lib/worker/event-processor.es6: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | 3 | import uuid from 'uuid'; 4 | 5 | import { log } from './logger'; 6 | import proto from './worker-protocol'; 7 | import KbpgpDecryptController from './kbpgp/kbpgp-decrypt'; 8 | // import KeyStore from './kbpgp/key-store'; 9 | 10 | class EventProcessor { 11 | constructor() { 12 | this._pendingPromises = {}; 13 | this._waitingForPassphrase = {}; 14 | 15 | this._kbpgpDecryptController = new KbpgpDecryptController(this); 16 | 17 | this.isWaitingForPassphrase = this.isWaitingForPassphrase.bind(this); 18 | this.completedPassphrasePromise = this.completedPassphrasePromise.bind(this); 19 | this.requestPassphrase = this.requestPassphrase.bind(this); 20 | this._sendError = this._sendError.bind(this); 21 | this._handleDecryptMessage = this._handleDecryptMessage.bind(this); 22 | this._handleGetKeys = this._handleGetKeys.bind(this); 23 | this._onFrontendMessage = this._onFrontendMessage.bind(this); 24 | 25 | process.on('message', this._onFrontendMessage); 26 | } 27 | 28 | isWaitingForPassphrase(keyId) { 29 | return this._waitingForPassphrase[keyId]; 30 | } 31 | 32 | completedPassphrasePromise(keyId, err) { 33 | if (!this._waitingForPassphrase[keyId]) { 34 | throw new Error('No pending promise for that keyId'); 35 | } 36 | 37 | if (err) { 38 | this._waitingForPassphrase[keyId].reject(err); 39 | return err; 40 | } 41 | 42 | this._waitingForPassphrase[keyId].resolve(); 43 | } 44 | 45 | requestPassphrase(keyId, message) { 46 | if (this._waitingForPassphrase[keyId]) { 47 | return this._waitingForPassphrase[keyId].promise; 48 | } 49 | 50 | const deferred = Promise.defer(); 51 | this._waitingForPassphrase[keyId] = deferred; 52 | this._waitingForPassphrase[keyId].promise = deferred.promise.then(() => { 53 | delete this._waitingForPassphrase[keyId]; 54 | }, err => { 55 | delete this._waitingForPassphrase[keyId]; 56 | return Promise.reject(err); 57 | }); 58 | 59 | const id = uuid(); 60 | 61 | return new Promise((resolve, reject) => { 62 | this._pendingPromises[id] = { resolve, reject }; 63 | process.send({ method: proto.REQUEST_PASSPHRASE, id, message }); 64 | }); 65 | } 66 | 67 | _sendError(err) { 68 | process.send({ 69 | method: proto.ERROR_OCCURRED, 70 | err, 71 | errorMessage: err.message, 72 | errorStackTrace: err.stack, 73 | }); 74 | } 75 | 76 | _handleDecryptMessage(message) { 77 | const { id } = message; 78 | const notify = (result) => { 79 | process.send({ method: proto.PROMISE_NOTIFY, id, result }); 80 | }; 81 | 82 | this._kbpgpDecryptController.decrypt(message, notify).then(({ 83 | literals = [], 84 | signedBy = '', 85 | elapsed, 86 | }) => { 87 | process.send({ 88 | method: proto.PROMISE_RESOLVE, 89 | id, 90 | elapsed, 91 | result: { 92 | text: literals[0].toString(), 93 | signedBy, 94 | }, 95 | }); 96 | }).catch((err) => { 97 | this._sendError(err); 98 | process.send({ method: proto.PROMISE_REJECT, id, result: err.message }); 99 | }); 100 | } 101 | 102 | _handleGetKeys(message) { 103 | const { id } = message; 104 | 105 | // This is a stopgap 106 | setImmediate(() => { 107 | process.send({ 108 | method: proto.PROMISE_RESOLVE, 109 | id, 110 | result: [ 111 | { 112 | type: 'master', 113 | size: '', 114 | fpr: 'F779 EF6C 34B4 63E8 AAE8 3A56 F4ED 3753 91FA D78F', 115 | key: '0xF4ED375391FAD78F', 116 | created: '2016-04-13', 117 | expires: null, 118 | }, 119 | ], 120 | }); 121 | }); 122 | } 123 | 124 | _onFrontendMessage(message) { 125 | if (message.method === proto.DECRYPT) { 126 | // DECRYPT 127 | this._handleDecryptMessage(message); 128 | } else if (message.method === proto.GET_KEYS) { 129 | // GET_KEYS 130 | this._handleGetKeys(message); 131 | } else if (message.method === proto.PROMISE_RESOLVE && this._pendingPromises[message.id]) { 132 | // PROMISE_RESOLVE 133 | this._pendingPromises[message.id].resolve(message.result); 134 | delete this._pendingPromises[message.id]; 135 | } else if (message.method === proto.PROMISE_REJECT && this._pendingPromises[message.id]) { 136 | // PROMISE_REJECT 137 | this._pendingPromises[message.id].reject(message.result); 138 | delete this._pendingPromises[message.id]; 139 | } else if (message.method === proto.LIST_PENDING_PROMISES) { 140 | // LIST_PENDING_PROMISES 141 | log(util.inspect(this._pendingPromises)); 142 | log(util.inspect(this._waitingForPassphrase)); 143 | } 144 | } 145 | } 146 | 147 | export default new EventProcessor(); 148 | -------------------------------------------------------------------------------- /lib/worker/kbpgp/hkp-cacher.es6: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { log, error } from '../logger'; 5 | 6 | // HKP Remote Cacher 7 | // 8 | // Caches the sucessful result from any HKP request in memory and on disk. While 9 | // the in-memory cache may not be entirely useful as KeyStore stores the decoded 10 | // KeyManager in memory, it is still nice to have around. 11 | class HKPCacher { 12 | constructor() { 13 | this._memCache = new Map(); 14 | this._cacheDirectory = path.join(NylasEnv.getConfigDirPath(), 'cypher', 'pubkey-cache'); 15 | 16 | this.cacheResult = this.cacheResult.bind(this); 17 | this.isCached = this.isCached.bind(this); 18 | this._getFilePath = this._getFilePath.bind(this); 19 | this._ensureCacheDirectoryExists = this._ensureCacheDirectoryExists.bind(this); 20 | 21 | this._ensureCacheDirectoryExists(); 22 | } 23 | 24 | cacheResult(keyId, result) { 25 | const filePath = this._getFilePath(keyId); 26 | this._memCache.set(keyId, result); 27 | 28 | return new Promise((resolve, reject) => { 29 | fs.writeFile(filePath, result, (err) => { 30 | if (err) { 31 | reject(err); 32 | } else { 33 | resolve(); 34 | } 35 | }); 36 | }); 37 | } 38 | 39 | isCached(keyId) { 40 | const memcached = this._memCache.get(keyId); 41 | if (memcached) { 42 | return Promise.resolve(memcached); 43 | } 44 | 45 | const filePath = this._getFilePath(keyId); 46 | return new Promise((resolve, reject) => { 47 | fs.readFile(filePath, 'utf8', (err, result) => { 48 | if (err) { 49 | reject(err); 50 | } else { 51 | resolve(result); 52 | } 53 | }); 54 | }).then((result) => { 55 | this._memCache.set(keyId, result); 56 | 57 | return result; 58 | }, (err) => { 59 | error('[HKPCacher] Error checking for cached pubkey, assuming false %s', err.stack); 60 | 61 | return false; 62 | }); 63 | } 64 | 65 | _getFilePath(keyId) { 66 | return path.join(this._cacheDirectory, `pubkey_${keyId}.asc`); 67 | } 68 | 69 | _ensureCacheDirectoryExists() { 70 | fs.access(this._cacheDirectory, fs.F_OK, (err) => { 71 | if (err) { 72 | log('[PGP - HKPCacher] Pubkey cache directory missing, creating'); 73 | fs.mkdir(this._cacheDirectory, (err2) => { 74 | if (err) { 75 | error('[PGP - HKPCacher] Pubkey cache directory creation unsuccessful', err2.stack); 76 | } else { 77 | log('[PGP - HKPCacher] Pubkey cache directory creation successful'); 78 | } 79 | }); 80 | } 81 | }); 82 | } 83 | } 84 | 85 | export default new HKPCacher(); 86 | -------------------------------------------------------------------------------- /lib/worker/kbpgp/hkp.es6: -------------------------------------------------------------------------------- 1 | import HKPCacher from './hkp-cacher'; 2 | 3 | let request = null; 4 | 5 | // HKP Public Key Fetcher 6 | export default class HKP { 7 | constructor(keyServerBaseUrl) { 8 | this.lookup = this.lookup.bind(this); 9 | this._makeFetch = this._makeFetch.bind(this); 10 | 11 | const isBrowserFetch = typeof window !== 'undefined' && window.fetch; 12 | 13 | this._baseUrl = keyServerBaseUrl || 'https://pgp.mit.edu'; 14 | this._fetch = isBrowserFetch ? window.fetch : this._makeFetch(); 15 | } 16 | 17 | lookup(keyId) { 18 | const uri = `${this._baseUrl}/pks/lookup?op=get&options=mr&search=0x${keyId}`; 19 | 20 | // Really obsure bug here. If we replace fetch(url) later, Electron throws 21 | // an "Illegal invocation error" unless we unwrap the variable here. 22 | const fetch = this._fetch; 23 | 24 | return HKPCacher.isCached(keyId).then((result) => { 25 | if (!result) { 26 | return fetch(uri).then((response) => response.text()).then((text) => { 27 | HKPCacher.cacheResult(keyId, text); 28 | return text; 29 | }); 30 | } 31 | 32 | return result; 33 | }).then((publicKeyArmored) => { 34 | if (publicKeyArmored && publicKeyArmored.indexOf('-----END PGP PUBLIC KEY BLOCK-----') > -1) { 35 | return publicKeyArmored.trim(); 36 | } 37 | }); 38 | } 39 | 40 | // For testing without Electron providing fetch API 41 | _makeFetch() { 42 | if (!request) { 43 | request = require('request'); 44 | } 45 | 46 | return (uri) => new Promise((resolve, reject) => { 47 | request(uri, (error, response, body) => { 48 | if (!error && response.statusCode === 200) { 49 | resolve({ text: () => body }); 50 | } else { 51 | reject(error); 52 | } 53 | }); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/worker/kbpgp/kbpgp-decrypt.es6: -------------------------------------------------------------------------------- 1 | import kbpgp from 'kbpgp'; 2 | import childProcess from 'child_process'; 3 | 4 | import EventProcessor from '../event-processor'; 5 | import { log } from '../logger'; 6 | import KeyStore from './key-store'; 7 | 8 | const unboxAsync = (options) => new Promise((resolve, reject) => { 9 | kbpgp.unbox(options, (err, literals) => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(literals); 14 | } 15 | }); 16 | }); 17 | 18 | class KbpgpDecryptRoutine { 19 | constructor(controller, notify) { 20 | this._controller = controller; 21 | this.notify = notify; 22 | 23 | this._importKey = this._importKey.bind(this); 24 | this._checkCache = this._checkCache.bind(this); 25 | this._decryptKey = this._decryptKey.bind(this); 26 | this.run = this.run.bind(this); 27 | } 28 | 29 | _importKey(armored) { 30 | return new Promise((resolve, reject) => { 31 | kbpgp.KeyManager.import_from_armored_pgp({ armored }, (err, secretKey) => { 32 | if (err) { 33 | reject(err, secretKey); 34 | } else { 35 | resolve(secretKey); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | _checkCache(secretKey) { 42 | const keyId = secretKey.get_pgp_key_id(); 43 | const keyIdHex = keyId.toString('hex'); 44 | const cachedKey = KeyStore.lookupKeyManager(keyId); 45 | const isLocked = EventProcessor.isWaitingForPassphrase(keyIdHex); 46 | 47 | if (cachedKey) { 48 | log('[InProcessDecrypter] Found cached key for %s', keyIdHex); 49 | 50 | return Promise.resolve(cachedKey); 51 | } else if (isLocked) { 52 | return isLocked.promise; 53 | } 54 | 55 | const isKeyLocked = secretKey.is_pgp_locked(); 56 | 57 | return this._decryptKey(secretKey).then(decryptedKey => { 58 | KeyStore.addKeyManager(decryptedKey); 59 | if (isKeyLocked) { 60 | EventProcessor.completedPassphrasePromise(keyIdHex); 61 | } 62 | }, err => { 63 | if (isKeyLocked) { 64 | EventProcessor.completedPassphrasePromise(keyIdHex, { err }); 65 | } 66 | return Promise.reject(err); 67 | }); 68 | } 69 | 70 | _decryptKey(secretKey) { 71 | if (!secretKey.is_pgp_locked()) { 72 | return Promise.resolve(secretKey); 73 | } 74 | 75 | this.notify('Waiting for passphrase...'); 76 | 77 | const keyId = secretKey.get_pgp_key_id().toString('hex'); 78 | const askString = `PGP Key with fingerprint ${keyId} needs to be decrypted`; 79 | return this._controller.requestPassphrase(keyId, askString).then(passphrase => 80 | new Promise((resolve, reject) => { 81 | this.notify('Unlocking secret key...'); 82 | 83 | const startTime = process.hrtime(); 84 | secretKey.unlock_pgp({ passphrase }, (err) => { 85 | if (err) { 86 | return reject(err); 87 | } 88 | 89 | const elapsed = process.hrtime(startTime); 90 | const msg = `Secret key unlocked secret key in ${elapsed[0] * 1e3 + elapsed[1] / 1e6}ms`; 91 | 92 | this.notify(msg); 93 | log('[KbpgpDecryptRoutine] %s', msg); 94 | 95 | resolve(secretKey); 96 | }); 97 | }) 98 | , () => 99 | // Since the first argument is undefined, the rejected promise does not 100 | // propagate to the `catch` receiver in `EventProcessor`. Create an Error 101 | // here to ensure the error is delivered to `EventProcessor` 102 | Promise.reject(new Error('Passphrase dialog cancelled')) 103 | ); 104 | } 105 | 106 | run(armored, identifier) { 107 | const platform = process.platform; 108 | const method = 'GPG_DECRYPT'; 109 | const startTime = process.hrtime(); 110 | 111 | if (method === 'GPG_DECRYPT' && (platform === 'linux' || platform === 'darwin')) { 112 | this.notify('Waiting for GPG...'); 113 | 114 | // var key = childProcess.execSync(`gpg --export-secret-keys -a ${identifier}`); 115 | const stdout = []; 116 | const stderr = []; 117 | 118 | const deferred = Promise.defer(); 119 | const child = childProcess.spawn('gpg', ['--decrypt']); 120 | child.stdout.on('data', (data) => stdout.push(data)); 121 | child.stderr.on('data', (data) => stderr.push(data)); 122 | child.on('close', (code) => { 123 | // GPG throws code 2 when it cannot verify one-pass signature packet 124 | // inside armored message 125 | if (code !== 0 && code !== 2) { 126 | return deferred.reject(new Error(`GPG decrypt failed with code ${code}`)); 127 | } 128 | 129 | this.notify(null); 130 | 131 | const elapsed = process.hrtime(startTime); 132 | const output = Buffer.concat(stdout); 133 | const error = Buffer.concat(stderr); 134 | const literals = [output]; 135 | 136 | log(error.toString('utf8')); 137 | 138 | deferred.resolve({ literals, elapsed }); 139 | }); 140 | child.stdin.write(armored); 141 | child.stdin.end(); 142 | 143 | return deferred.promise; 144 | } 145 | 146 | let startDecrypt = null; 147 | 148 | return this._importKey(identifier) 149 | .then(this._checkCache) 150 | .then(() => { 151 | this.notify(null); 152 | startDecrypt = process.hrtime(); 153 | }) 154 | .then(() => unboxAsync({ keyfetch: KeyStore, armored })) 155 | .then((literals) => { 156 | const decryptTime = process.hrtime(startDecrypt); 157 | const elapsed = process.hrtime(startTime); 158 | 159 | this.notify(`Message decrypted in ${decryptTime[0] * 1e3 + decryptTime[1] / 1e6}ms`); 160 | 161 | const ds = literals[0].get_data_signer(); 162 | let km = null; 163 | let signedBy = null; 164 | if (ds) { 165 | km = ds.get_key_manager(); 166 | } 167 | if (km) { 168 | signedBy = km.get_pgp_fingerprint().toString('hex'); 169 | log(`Signed by PGP fingerprint: ${signedBy}`); 170 | } 171 | 172 | return { literals, signedBy, elapsed }; 173 | }); 174 | } 175 | } 176 | 177 | // Singleton to manage each decryption session, converts stringified Buffers 178 | // back to Buffers for kbpgp 179 | class KbpgpDecryptController { 180 | constructor() { 181 | this.decrypt = this.decrypt.bind(this); 182 | } 183 | 184 | // TODO: figure out a way to prompt the user to pick which PGP key to use to 185 | // decrypt or add a config page to allow them to pick per-email account. 186 | decrypt({ armored, secretKey }, notify) { 187 | let bufferData = armored; 188 | if (armored && armored.type === 'Buffer') { 189 | bufferData = new Buffer(armored.data); 190 | } 191 | 192 | let keyToUse = secretKey; 193 | if (secretKey && secretKey.type === 'Buffer') { 194 | keyToUse = new Buffer(secretKey.data); 195 | } 196 | 197 | return new KbpgpDecryptRoutine(this, notify).run(bufferData, keyToUse); 198 | } 199 | } 200 | 201 | export default KbpgpDecryptController; 202 | -------------------------------------------------------------------------------- /lib/worker/kbpgp/key-store.es6: -------------------------------------------------------------------------------- 1 | import kbpgp from 'kbpgp'; 2 | 3 | import HKP from './hkp'; 4 | 5 | const hexkid = (k) => k.toString('hex'); 6 | 7 | // Adapted from PgpKeyRing in kbpgp 8 | class KeyStore { 9 | constructor() { 10 | this._keys = {}; 11 | this._kms = {}; 12 | 13 | this._hkp = new HKP(); 14 | 15 | this.addKeyManager = this.addKeyManager.bind(this); 16 | this.fetchRemotePublicKey = this.fetchRemotePublicKey.bind(this); 17 | this.fetch = this.fetch.bind(this); 18 | this.findBestKey = this.findBestKey.bind(this); 19 | this.lookup = this.lookup.bind(this); 20 | this.lookupKeyManager = this.lookupKeyManager.bind(this); 21 | 22 | global.$pgpKeyStore = this; 23 | } 24 | 25 | addKeyManager(km) { 26 | const keys = km.export_pgp_keys_to_keyring(); 27 | for (const k of keys) { 28 | const kid = hexkid(k.key_material.get_key_id()); 29 | this._keys[kid] = k; 30 | this._kms[kid] = km; 31 | } 32 | } 33 | 34 | fetchRemotePublicKey(keyId) { 35 | return this._hkp.lookup(keyId).then((armored) => new Promise((resolve, reject) => { 36 | kbpgp.KeyManager.import_from_armored_pgp({ armored }, (err, km, warn) => { 37 | if (err) { 38 | reject(err, km, warn); 39 | } else { 40 | resolve(km, warn); 41 | } 42 | }); 43 | })); 44 | } 45 | 46 | fetch(keyIds, ops, cb) { 47 | let err = null; 48 | let km = null; 49 | let returnValue = null; 50 | 51 | const hexKeyIds = keyIds.map((keyId) => hexkid(keyId)); 52 | 53 | const checkForKey = () => { 54 | for (let _i = 0, _len = hexKeyIds.length; _i < _len; _i++) { 55 | const id = hexKeyIds[_i]; 56 | const k = this._keys[id]; 57 | if (k && k.key) { 58 | if (k.key.can_perform(ops)) { 59 | returnValue = _i; 60 | km = this._kms[id]; 61 | } 62 | } 63 | } 64 | }; 65 | 66 | checkForKey(); 67 | 68 | if (!km) { 69 | const promises = hexKeyIds.map((k) => 70 | this.fetchRemotePublicKey(k).then((kmm) => this.addKeyManager(kmm)) 71 | ); 72 | 73 | Promise.all(promises).then(() => { 74 | checkForKey(); 75 | 76 | if (!km) { 77 | err = new Error(`key not found: ${JSON.stringify(hexKeyIds)}`); 78 | cb(err, km, returnValue); 79 | } else { 80 | cb(err, km, returnValue); 81 | } 82 | }).catch((err2) => { 83 | cb(err2, km, returnValue); 84 | }); 85 | } else { 86 | cb(err, km, returnValue); 87 | } 88 | } 89 | 90 | // Pick the best key to fill the flags asked for by the flags. 91 | // See C.openpgp.key_flags for ideas of what the flags might be. 92 | findBestKey({ key_id, flags }, cb) { 93 | const kid = hexkid(key_id); 94 | const km = this._kms[kid]; 95 | 96 | if (!km) { 97 | cb(new Error(`Could not find key for fingerprint ${kid}`), null); 98 | return; 99 | } 100 | 101 | const key = km.find_best_pgp_key(flags); 102 | if (!key) { 103 | cb(new Error(`no matching key for flags: ${flags}`), null); 104 | return; 105 | } 106 | 107 | cb(null, key); 108 | } 109 | 110 | lookup(keyId) { 111 | return this._keys[hexkid(keyId)]; 112 | } 113 | 114 | lookupKeyManager(keyId) { 115 | return this._kms[hexkid(keyId)]; 116 | } 117 | } 118 | 119 | export default new KeyStore(); 120 | -------------------------------------------------------------------------------- /lib/worker/logger.es6: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | import util from 'util'; 4 | 5 | import proto from './worker-protocol'; 6 | 7 | export function log(...args) { 8 | if (process.send) { 9 | process.send({ method: proto.VERBOSE_OUT, message: util.format.apply(this, args) }); 10 | } else { 11 | return console.log.apply(console, args); 12 | } 13 | } 14 | 15 | export function error(...args) { 16 | if (process.send) { 17 | process.send({ method: proto.ERROR_OCCURRED, err: util.format.apply(this, args) }); 18 | } else { 19 | return console.error.apply(console, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/worker/nylas-env-wrapper.js: -------------------------------------------------------------------------------- 1 | // Basic NylasEnv to fetch configuration directory 2 | function NylasEnvConstructor() { 3 | } 4 | 5 | NylasEnvConstructor.prototype.getConfigDirPath = function getConfigDirPath() { 6 | return process.env.PGP_CONFIG_DIR_PATH; 7 | }; 8 | 9 | module.exports = new NylasEnvConstructor(); 10 | -------------------------------------------------------------------------------- /lib/worker/worker-entry.js: -------------------------------------------------------------------------------- 1 | // This is the main entry-point for the worker process. The `compile-cache` is 2 | // used here to speed up the initialization part 3 | /* eslint strict: 0, no-console: 0 */ 4 | 'use strict'; 5 | 6 | const proto = require('./worker-protocol'); 7 | 8 | if (!process.send) { 9 | console.error('This is an IPC worker. Use as intended'); 10 | process.exit(1); 11 | } 12 | 13 | [ 14 | 'PGP_COMPILE_CACHE_MODULE_PATH', 15 | 'PGP_COMPILE_CACHE_PATH', 16 | 'PGP_CONFIG_DIR_PATH', 17 | ].forEach(envToCheck => { 18 | if (!process.env[envToCheck]) { 19 | const err = new Error(`Environment variable ${envToCheck} undefined`); 20 | console.error(err.message); 21 | console.error(err.stack); 22 | process.send({ 23 | method: proto.ERROR_OCCURRED, 24 | err, 25 | errorMessage: err.message, 26 | }); 27 | process.exit(1); 28 | } 29 | }); 30 | 31 | process.on('uncaughtException', (err) => { 32 | console.error(err); 33 | process.send({ 34 | method: proto.ERROR_OCCURRED, 35 | err, 36 | errorMessage: err.message, 37 | errorStackTrace: err.stack, 38 | }); 39 | process.exit(1); 40 | }); 41 | 42 | process.on('unhandledRejection', (err) => { 43 | console.error(err); 44 | process.send({ 45 | method: proto.ERROR_OCCURRED, 46 | err, 47 | errorMessage: err.message, 48 | errorStackTrace: err.stack, 49 | }); 50 | }); 51 | 52 | global.NylasEnv = require('./nylas-env-wrapper'); 53 | 54 | const compileCacheModulePath = process.env.PGP_COMPILE_CACHE_MODULE_PATH; 55 | const compileCachePath = process.env.PGP_COMPILE_CACHE_PATH; 56 | 57 | require(compileCacheModulePath).setCacheDirectory(compileCachePath); 58 | process.send({ method: proto.VERBOSE_OUT, message: 'Required the compile cache' }); 59 | 60 | // The `compile-cache` module handles initializing Babel so ES6 will work from 61 | // this point on. We now hand off the processing to the `event-processor` 62 | 63 | require('./event-processor'); 64 | -------------------------------------------------------------------------------- /lib/worker/worker-protocol.js: -------------------------------------------------------------------------------- 1 | // A file that specifies protocol types between the master and worker processes 2 | 3 | module.exports = { 4 | DECRYPT: 1, 5 | PROMISE_RESOLVE: 2, 6 | PROMISE_REJECT: 3, 7 | PROMISE_NOTIFY: 4, 8 | REQUEST_PASSPHRASE: 5, 9 | VERBOSE_OUT: 6, 10 | ERROR_OCCURRED: 7, 11 | LIST_PENDING_PROMISES: 8, 12 | GET_KEYS: 9, 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypher", 3 | "main": "./lib/main", 4 | "version": "0.2.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/mbilker/cypher.git" 8 | }, 9 | "engines": { 10 | "nylas": ">=0.4.10" 11 | }, 12 | "title": "Cypher", 13 | "icon": "./assets/icon.png", 14 | "description": "Privacy extensions for N1", 15 | "dependencies": { 16 | "emailjs-mime-parser": "^1.0.0", 17 | "kbpgp": "^2.0.53", 18 | "libkeybase": "^1.2.24", 19 | "lodash": "^4.0.0", 20 | "mimelib": "^0.2.19", 21 | "node-keybase": "0.0.5", 22 | "request": "^2.67.0", 23 | "rimraf": "^2.5.1", 24 | "smalltalk": "git+https://github.com/mbilker/smalltalk.git", 25 | "uuid": "^2.0.1" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^2.2.0", 29 | "eslint-config-airbnb": "^7.0.0", 30 | "eslint-plugin-import": "^1.8.1", 31 | "eslint-plugin-jsx-a11y": "^0.6.2", 32 | "eslint-plugin-react": "^4.3.0" 33 | }, 34 | "license": "GPL-3.0", 35 | "windowTypes": { 36 | "default": true, 37 | "composer": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/main-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { ComponentRegistry, ExtensionRegistry, PreferencesUIStore } from 'nylas-exports'; 4 | 5 | import PGPMain from '../lib/main'; 6 | import MessageLoaderExtension from '../lib/message-loader/message-loader-extension'; 7 | import MessageLoaderHeader from '../lib/message-loader/message-loader-header'; 8 | import WorkerFrontend from '../lib/worker-frontend'; 9 | import ComposerLoader from '../lib/composer/composer-loader'; 10 | 11 | describe("PGPMain", () => { 12 | describe("::activate(state)", () => { 13 | it("should register the preferences tab, message header, message loader, and composer button", () => { 14 | spyOn(PreferencesUIStore, 'registerPreferencesTab'); 15 | spyOn(ComponentRegistry, 'register'); 16 | spyOn(ExtensionRegistry.MessageView, 'register'); 17 | spyOn(WorkerFrontend, 'initialize'); 18 | 19 | PGPMain.activate(); 20 | 21 | expect(PGPMain._tab).not.toBeNull(); 22 | expect(PreferencesUIStore.registerPreferencesTab).toHaveBeenCalledWith(PGPMain._tab); 23 | expect(ComponentRegistry.register).toHaveBeenCalledWith(MessageLoaderHeader, {role: 'message:BodyHeader'}); 24 | expect(ExtensionRegistry.MessageView.register).toHaveBeenCalledWith(MessageLoaderExtension); 25 | expect(WorkerFrontend.initialize).toHaveBeenCalled(); 26 | expect(ComponentRegistry.register).toHaveBeenCalledWith(ComposerLoader, {role: 'Composer:ActionButton'}); 27 | }); 28 | }); 29 | 30 | describe("::deactivate()", () => { 31 | it("should unregister the preferences tab, message header, message loader, and composer button", () => { 32 | spyOn(PreferencesUIStore, 'unregisterPreferencesTab'); 33 | spyOn(ComponentRegistry, 'unregister'); 34 | spyOn(ExtensionRegistry.MessageView, 'unregister'); 35 | 36 | PGPMain.deactivate(); 37 | 38 | expect(PreferencesUIStore.unregisterPreferencesTab).toHaveBeenCalledWith(PGPMain._tab.tabId); 39 | expect(ExtensionRegistry.MessageView.unregister).toHaveBeenCalledWith(MessageLoaderExtension); 40 | expect(ComponentRegistry.unregister).toHaveBeenCalledWith(MessageLoaderHeader); 41 | expect(ComponentRegistry.unregister).toHaveBeenCalledWith(ComposerLoader); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/message-loader-header-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { Contact, File, Message, React, ReactDOM, ReactTestUtils } from 'nylas-exports'; 4 | 5 | import PGPStore from '../lib/flux/stores/pgp-store'; 6 | import MessageLoaderHeader from '../lib/message-loader/message-loader-header'; 7 | 8 | const me = new Contact({ 9 | name: TEST_ACCOUNT_NAME, 10 | email: TEST_ACCOUNT_EMAIL 11 | }); 12 | 13 | describe("MessageLoaderHeader", function() { 14 | beforeEach(function() { 15 | this.message = new Message({ 16 | from: [me], 17 | to: [me], 18 | cc: [], 19 | bcc: [] 20 | }); 21 | this.component = ReactTestUtils.renderIntoDocument( 22 | 23 | ); 24 | }); 25 | 26 | it("should render into the page", function() { 27 | expect(this.component).toBeDefined(); 28 | }); 29 | 30 | it("should have a displayName", function() { 31 | expect(MessageLoaderHeader.displayName).toBe('MessageLoader'); 32 | }); 33 | 34 | describe("when not decrypting", function() { 35 | beforeEach(function() { 36 | this.component.setState({decrypting: false}); 37 | }); 38 | 39 | it("should have no child elements", function() { 40 | expect(ReactDOM.findDOMNode(this.component).childElementCount).toEqual(0); 41 | }); 42 | }); 43 | 44 | describe("when decrypting", function() { 45 | beforeEach(function() { 46 | this.component.setState({ decrypting: true }); 47 | }); 48 | 49 | it("should have one single child element", function() { 50 | expect(ReactDOM.findDOMNode(this.component).childElementCount).toEqual(1); 51 | }); 52 | }); 53 | 54 | it("should throw when text input to multipart parser is null", function() { 55 | let text = null; 56 | expect(() => this.component._extractHTML(text)).toThrow(); 57 | }); 58 | 59 | //it "should show a dialog box when clicked", function() { 60 | //spyOn(@component, '_onClick'); 61 | //buttonNode = React.findDOMNode(this.component.refs.button); 62 | //ReactTestUtils.Simulate.click(buttonNode); 63 | //expect(@component._onClick).toHaveBeenCalled(); 64 | //}); 65 | }); 66 | -------------------------------------------------------------------------------- /stylesheets/main.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .pgp-message-header { 4 | display: block; 5 | background-color: mix(@background-primary, #f0ad4e, 80%); 6 | border: 1px solid darken(mix(@background-primary, #f0ad4e, 50%), 25%); 7 | color: fade(mix(@text-color-subtle, #f0ad4e, 80%), 70%); 8 | margin: @padding-base-vertical 0; 9 | padding: @padding-base-vertical @padding-base-horizontal; 10 | 11 | .option { 12 | color: fade(mix(@text-color-subtle, #f0ad4e, 80%), 70%); 13 | } 14 | .option:hover { 15 | color: mix(@text-color-subtle, #f0ad4e, 80%); 16 | } 17 | } 18 | .pgp-message-header-error { 19 | background-color: mix(@background-primary, #A03232, 80%); 20 | border: 1px solid darken(mix(@background-primary, #A03232, 50%), 25%); 21 | color: mix(@text-color-subtle, #e64d65, 80%); 22 | } 23 | .pgp-message-header-info { 24 | background-color: mix(@background-primary, @blue-dark, 80%); 25 | border: 1px solid darken(mix(@background-primary, @blue-dark, 50%), 25%); 26 | color: mix(@text-color-subtle, @blue-dark, 80%); 27 | } 28 | .pgp-message-header-success { 29 | background-color: mix(@background-primary, @color-success, 80%); 30 | border: 1px solid darken(mix(@background-primary, @color-success, 50%), 10%); 31 | color: mix(@text-color-subtle, @color-success, 80%); 32 | } 33 | 34 | .container-pgp-mail { 35 | width: 50%; 36 | min-width: 360px; 37 | margin: 0 auto; 38 | 39 | .setting-name { 40 | max-width: 110px; 41 | flex: 1; 42 | margin-right: 10px; 43 | } 44 | 45 | .setting-value { 46 | flex: 1; 47 | } 48 | 49 | .keybase-username { 50 | padding-bottom: 0.25em; 51 | } 52 | 53 | .keybase-username, .keybase-password { 54 | vertical-align: middle; 55 | } 56 | 57 | .keybase-sigchain { 58 | .btn-primary { 59 | color: @text-color-selected; 60 | background-color: @color-info; 61 | } 62 | 63 | .bg-green { 64 | background-color: #dff0d8 65 | } 66 | 67 | .sigchain-table { 68 | width: 100%; 69 | 70 | td { 71 | padding-left: 1em; 72 | } 73 | } 74 | } 75 | } 76 | 77 | .pgp-composer { 78 | .menu { 79 | width: 250px; 80 | 81 | .item { 82 | padding-top: 0; 83 | padding-right: @padding-base-horizontal; 84 | } 85 | } 86 | 87 | .divider { 88 | border-top: 1px solid @border-color-divider; 89 | } 90 | 91 | .submit-section { 92 | padding: @padding-base-vertical @padding-base-horizontal; 93 | padding-bottom: @padding-base-vertical * 1.2; 94 | .btn { 95 | width: 100%; 96 | } 97 | } 98 | 99 | .keybase-username { 100 | display: block; 101 | } 102 | } 103 | 104 | .sidebar-keybase { 105 | .social-profiles { 106 | border-top: 0; 107 | padding-top: 0; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /stylesheets/smalltalk.css: -------------------------------------------------------------------------------- 1 | .smalltalk { 2 | display: flex; 3 | 4 | align-items: center; 5 | flex-direction: column; 6 | justify-content: center; 7 | 8 | -webkit-transition: 200ms opacity; 9 | transition: 200ms opacity; 10 | 11 | bottom: 0; 12 | left: 0; 13 | overflow: auto; 14 | padding: 20px; 15 | position: fixed; 16 | right: 0; 17 | top: 0; 18 | 19 | z-index: 100; 20 | } 21 | 22 | .smalltalk .page { 23 | border-radius: 3px; 24 | background: white; 25 | box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0,0,0,0.15); 26 | color: #333; 27 | min-width: 400px; 28 | padding: 0; 29 | position: relative; 30 | z-index: 0; 31 | } 32 | 33 | @media only screen and (max-width: 500px) { 34 | .smalltalk .page { 35 | min-width: 0; 36 | } 37 | } 38 | 39 | .smalltalk .page > .close-button { 40 | background-image: url(nylas://cypher/assets/IDR_CLOSE_DIALOG.png); 41 | background-position: center; 42 | background-repeat: no-repeat; 43 | height: 14px; 44 | position: absolute; 45 | right: 7px; 46 | top: 7px; 47 | width: 14px; 48 | z-index: 1; 49 | } 50 | 51 | .smalltalk .page > .close-button:hover { 52 | background-image: url(nylas://cypher/assets/IDR_CLOSE_DIALOG_H.png); 53 | } 54 | 55 | .smalltalk .page header { 56 | overflow: hidden; 57 | text-overflow: ellipsis; 58 | white-space: nowrap; 59 | max-width: 500px; 60 | 61 | -webkit-user-select: none; 62 | user-select: none; 63 | color: #333; 64 | font-size: 120%; 65 | font-weight: bold; 66 | margin: 0; 67 | padding: 14px 17px 14px; 68 | text-shadow: white 0 1px 2px; 69 | } 70 | 71 | .smalltalk .page .content-area { 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | 75 | padding: 6px 17px 6px; 76 | position: relative; 77 | max-width: 500px; 78 | } 79 | 80 | .smalltalk .page .action-area { 81 | padding: 14px 17px; 82 | } 83 | 84 | .smalltalk .page .button-strip { 85 | display: -webkit-box; 86 | display: -moz-box; 87 | display: -ms-flexbox; 88 | display:flex; 89 | 90 | flex-direction: row; 91 | justify-content: flex-end; 92 | } 93 | 94 | .smalltalk .page .button-strip > button { 95 | -webkit-margin-start: 10px; 96 | } 97 | 98 | .smalltalk button:enabled:focus, 99 | .smalltalk input:enabled:focus { 100 | -webkit-transition: border-color 200ms; 101 | transition: border-color 200ms; 102 | border-color: rgb(77, 144, 254); 103 | outline: none; 104 | } 105 | 106 | .smalltalk button, .smalltalk .smalltalk { 107 | min-height: 2em; 108 | min-width: 4em; 109 | } 110 | 111 | .smalltalk input { 112 | width: 100%; 113 | border: 1px solid #bfbfbf; 114 | border-radius: 2px; 115 | box-sizing: border-box; 116 | color: #444; 117 | font: inherit; 118 | margin: 0; 119 | min-height: 2em; 120 | padding: 3px; 121 | outline: none; 122 | } 123 | -------------------------------------------------------------------------------- /test-kbpgp/README.md: -------------------------------------------------------------------------------- 1 | # kbpgp example files 2 | 3 | Here is a worker process implementation using the `kbpgp` module from Keybase. I had issues using it due to a misimplementation of signature verification, causing corruption to the `key_id` to be used in decryption. 4 | -------------------------------------------------------------------------------- /test-kbpgp/test-kbpgp.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var child = child_process.fork(path.join(__dirname, '..', 'lib', 'kbpgp', 'kbpgp-worker-decrypt.js')); 6 | 7 | child.on('message', function(message) { 8 | console.log('Child sent message:', message); 9 | }); 10 | 11 | var protocol = { 12 | SECRET_KEY: 1, 13 | PASSPHRASE: 2, 14 | ENCRYPTED_MESSAGE: 3, 15 | DECRYPT: 4, 16 | SECRET_KEY_DECRYPT_TIME: 5, 17 | MESSAGE_DECRYPT_TIME: 6, 18 | DECRYPTED_TEXT: 7 19 | } 20 | 21 | var key = fs.readFileSync(path.join(process.env.HOME, 'pgpkey'), 'utf8'); 22 | var message = fs.readFileSync(process.argv[3] || path.join(process.env.HOME, 'encrypted.asc'), 'utf8'); 23 | var passphrase = process.argv[2] || ''; 24 | 25 | child.send({ method: protocol.SECRET_KEY, secretKey: key }); 26 | child.send({ method: protocol.PASSPHRASE, passphrase: passphrase }); 27 | child.send({ method: protocol.ENCRYPTED_MESSAGE, encryptedMessage: message }); 28 | child.send({ method: protocol.DECRYPT }); 29 | --------------------------------------------------------------------------------