├── .babelrc ├── .gitignore ├── .vscode └── settings.json ├── INSTALL ├── LICENSE.md ├── README.md ├── encrypt-composer-button@2x.png ├── icon.png ├── key-present@2x.png ├── package-lock.json ├── package.json ├── spec ├── decrypt-buttons-spec.jsx ├── encrypt-button-spec.jsx ├── keybase-profile-spec.jsx ├── keybase-search-spec.jsx ├── keybase-spec.js ├── main-spec.js ├── pgp-key-store-spec.jsx └── recipient-key-chip-spec.jsx ├── src ├── decrypt-button.jsx ├── decryption-preprocess.js ├── email-popover.jsx ├── encrypt-button.jsx ├── identity.js ├── key-adder.jsx ├── key-manager.jsx ├── keybase-search.jsx ├── keybase-user.jsx ├── keybase.js ├── main.es6 ├── modal-key-recommender.jsx ├── passphrase-popover.jsx ├── pgp-key-store.jsx ├── preferences-keybase.jsx ├── private-key-popover.jsx └── recipient-key-chip.jsx └── stylesheets └── main.less /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | The following installation instruction reference to David's issue, "Add install instructions in Readme.md." 2 | 3 | 1. Clone repository into any location 4 | 2. Run the command `cd /path/to/mailspring-keybase` 5 | 3. Install babel and other dependencies: `npm install` 6 | 4. Compile source codes to vanilla js with babel: `./node_modules/.bin/babel src --out-dir lib` 7 | 3. Start Mailspring and add plugin - you should see a success notification 8 | 4. Close Mailspring and edit config.json and remove keybase from disabled plugins list (config.json can be found in different places depending on your OS. Search for the Mailspring folder in your filesystem) 9 | 5. Start Mailspring, key management can be found in preferences 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nylas, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Keybase Plugin 2 | 3 | TODO: 4 | ----- 5 | * final refactor 6 | * tests 7 | 8 | WISHLIST: 9 | ----- 10 | * message signing 11 | * encrypted file handling 12 | * integrate MIT PGP Keyserver search into Keybase searchbar 13 | * make the decrypt interface a message body overlay instead of a button in the header 14 | * improve search result deduping with keys on file 15 | 16 | How to install: 17 | ----- 18 | 1. Clone repository into any location 19 | 2. Run the command `cd /path/to/mailspring-keybase` 20 | 3. Install babel and other dependencies: `npm install` 21 | 4. Compile source codes to vanilla js with babel: `chmod +x ./node_modules/.bin/babel` and `./node_modules/.bin/babel src --out-dir lib` 22 | 3. Start Mailspring and add plugin - you should see a success notification 23 | 4. Close Mailspring and edit config.json and remove keybase from disabled plugins list (config.json can be found in different places depending on your OS. Search for the Mailspring folder in your filesystem). For linux you will find it in: `~/.config/Mailspring/config.json` 24 | 5. Start Mailspring, key management can be found in preferences 25 | -------------------------------------------------------------------------------- /encrypt-composer-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NgoHuy/mailspring-keybase/e316396b216c9750ed5cac799dfb413b9c8a1b8b/encrypt-composer-button@2x.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NgoHuy/mailspring-keybase/e316396b216c9750ed5cac799dfb413b9c8a1b8b/icon.png -------------------------------------------------------------------------------- /key-present@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NgoHuy/mailspring-keybase/e316396b216c9750ed5cac799dfb413b9c8a1b8b/key-present@2x.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keybase", 3 | "main": "./lib/main", 4 | "version": "0.1.0", 5 | "engines": { 6 | "mailspring": ">=0.3.0" 7 | }, 8 | "isOptional": true, 9 | "isHiddenOnPluginsPage": true, 10 | "title": "Encryption", 11 | "description": "Send and receive encrypted messages using Keybase for public key exchange.", 12 | "icon": "./icon.png", 13 | "license": "GPL-3.0", 14 | "windowTypes": { 15 | "default": true, 16 | "composer": true, 17 | "thread-popout": true 18 | }, 19 | "dependencies": { 20 | "@babel/cli": "^7.10.4", 21 | "@babel/core": "^7.10.4", 22 | "@babel/preset-react": "^7.10.4", 23 | "babel-plugin-transform-react-createelement-to-jsx": "^1.0.1", 24 | "coffee-react": "^5.0.1", 25 | "kbpgp": "^2.0.79", 26 | "lebab": "^3.0.2", 27 | "prop-types": "^15.6.2", 28 | "react": "^16.5.2", 29 | "request": "^2.88.0", 30 | "underscore": "^1.12.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/preset-env": "^7.10.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spec/decrypt-buttons-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | const {React, ReactTestUtils, DraftStore, Message} = require('mailspring-exports'); 7 | const pgp = require('kbpgp'); 8 | 9 | const DecryptMessageButton = require('../lib/decrypt-button'); 10 | const PGPKeyStore = require('../lib/pgp-key-store'); 11 | 12 | describe("DecryptMessageButton", function() { 13 | beforeEach(function() { 14 | this.unencryptedMsg = new Message({clientId: 'test', subject: 'Subject', body: '

Body

'}); 15 | const body = `-----BEGIN PGP MESSAGE----- 16 | Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto 17 | 18 | wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw= 19 | =1aPN 20 | -----END PGP MESSAGE-----`; 21 | this.encryptedMsg = new Message({clientId: 'test2', subject: 'Subject', body}); 22 | 23 | this.msg = new Message({subject: 'Subject', body: '

Body

'}); 24 | return this.component = ReactTestUtils.renderIntoDocument( 25 | React.createElement(DecryptMessageButton, {"message": (this.msg)}) 26 | ); 27 | }); 28 | 29 | xit("should try to decrypt the message whenever a new key is unlocked", function() { 30 | spyOn(PGPKeyStore, "decrypt"); 31 | spyOn(PGPKeyStore, "isDecrypted").andCallFake(message => { 32 | return false; 33 | }); 34 | spyOn(PGPKeyStore, "hasEncryptedComponent").andCallFake(message => { 35 | return true; 36 | }); 37 | 38 | PGPKeyStore.trigger(PGPKeyStore); 39 | return expect(PGPKeyStore.decrypt).toHaveBeenCalled(); 40 | }); 41 | 42 | xit(`should not try to decrypt the message whenever a new key is unlocked \ 43 | if the message is already decrypted`, function() { 44 | spyOn(PGPKeyStore, "decrypt"); 45 | spyOn(PGPKeyStore, "isDecrypted").andCallFake(message => { 46 | return true; 47 | }); 48 | spyOn(PGPKeyStore, "hasEncryptedComponent").andCallFake(message => { 49 | return true; 50 | }); 51 | 52 | // TODO for some reason the above spyOn calls aren't working and false is 53 | // being returned from isDecrypted, causing this test to fail 54 | PGPKeyStore.trigger(PGPKeyStore); 55 | 56 | return expect(PGPKeyStore.decrypt).not.toHaveBeenCalled(); 57 | }); 58 | 59 | it("should have a button to decrypt a message", function() { 60 | this.component = ReactTestUtils.renderIntoDocument( 61 | React.createElement(DecryptMessageButton, {"message": this.encryptedMsg}) 62 | ); 63 | 64 | return expect(this.component.refs.button).toBeDefined(); 65 | }); 66 | 67 | it("should not allow for the unlocking of a message with no encrypted component", function() { 68 | this.component = ReactTestUtils.renderIntoDocument( 69 | React.createElement(DecryptMessageButton, {"message": this.unencryptedMsg}) 70 | ); 71 | 72 | return expect(this.component.refs.button).not.toBeDefined(); 73 | }); 74 | 75 | it("should indicate when a message has been decrypted", function() { 76 | spyOn(PGPKeyStore, "isDecrypted").andCallFake(message => { 77 | return true; 78 | }); 79 | 80 | this.component = ReactTestUtils.renderIntoDocument( 81 | React.createElement(DecryptMessageButton, {"message": this.encryptedMsg}) 82 | ); 83 | 84 | return expect(this.component.refs.button).not.toBeDefined(); 85 | }); 86 | 87 | return it("should open a popover when clicked", function() { 88 | spyOn(DecryptMessageButton.prototype, "_onClickDecrypt"); 89 | 90 | const msg = this.encryptedMsg; 91 | msg.to = [{email: "test@example.com"}]; 92 | this.component = ReactTestUtils.renderIntoDocument( 93 | React.createElement(DecryptMessageButton, {"message": msg}) 94 | ); 95 | expect(this.component.refs.button).toBeDefined(); 96 | ReactTestUtils.Simulate.click(this.component.refs.button); 97 | return expect(DecryptMessageButton.prototype._onClickDecrypt).toHaveBeenCalled(); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /spec/encrypt-button-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS207: Consider shorter variations of null checks 5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 6 | */ 7 | const {React, ReactDOM, ReactTestUtils, DraftStore, Message} = require('mailspring-exports'); 8 | const pgp = require('kbpgp'); 9 | 10 | const EncryptMessageButton = require('../lib/encrypt-button'); 11 | const PGPKeyStore = require('../lib/pgp-key-store'); 12 | 13 | describe("EncryptMessageButton", function() { 14 | beforeEach(function() { 15 | const key = `-----BEGIN PGP PRIVATE KEY BLOCK----- 16 | Version: GnuPG v1 17 | 18 | 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 70 | 217s2OKjpJqtpHPf2vY= 71 | =UY7Y 72 | -----END PGP PRIVATE KEY BLOCK-----`; 73 | 74 | pgp.KeyManager.import_from_armored_pgp({ 75 | armored: key 76 | }, (err, km) => { 77 | return this.km = km; 78 | }); 79 | 80 | waitsFor((() => (this.km != null)), "getting a key took too long", 1000); 81 | 82 | this.msg = new Message({subject: 'Subject', body: '

Body

', draft: true}); 83 | this.session = { 84 | draft: () => { 85 | return this.msg; 86 | }, 87 | changes: { 88 | add: changes => { 89 | return this.output = changes; 90 | } 91 | } 92 | }; 93 | 94 | this.output = null; 95 | 96 | const add = jasmine.createSpy('add'); 97 | spyOn(DraftStore, 'sessionForClientId').andCallFake(draftClientId => { 98 | return Promise.resolve(this.session); 99 | }); 100 | 101 | return this.component = ReactTestUtils.renderIntoDocument( 102 | React.createElement(EncryptMessageButton, {"draft": (this.msg), "session": (this.session)}) 103 | ); 104 | }); 105 | 106 | it("should render into the page", function() { 107 | return expect(this.component).toBeDefined(); 108 | }); 109 | 110 | it("should have a displayName", () => expect(EncryptMessageButton.displayName).toBe('EncryptMessageButton')); 111 | 112 | it("should have an onClick behavior which encrypts the message", function() { 113 | spyOn(this.component, '_onClick'); 114 | const buttonNode = ReactDOM.findDOMNode(this.component.refs.button); 115 | ReactTestUtils.Simulate.click(buttonNode); 116 | return expect(this.component._onClick).toHaveBeenCalled(); 117 | }); 118 | 119 | it("should store the message body's plaintext on encryption", function() { 120 | spyOn(this.component, '_onClick'); 121 | const buttonNode = ReactDOM.findDOMNode(this.component.refs.button); 122 | ReactTestUtils.Simulate.click(buttonNode); 123 | return expect(this.component.plaintext === this.msg.body); 124 | }); 125 | 126 | it("should mark itself as encrypted", function() { 127 | spyOn(this.component, '_onClick'); 128 | const buttonNode = ReactDOM.findDOMNode(this.component.refs.button); 129 | ReactTestUtils.Simulate.click(buttonNode); 130 | return expect(this.component.currentlyEncrypted === true); 131 | }); 132 | 133 | return xit("should be able to encrypt messages", function() { 134 | // NOTE: this doesn't work. 135 | // As best I can tell, something is wrong with the pgp.box function - 136 | // nothing seems to get it to complete. Weird. 137 | 138 | runs( () => { 139 | console.log(this.km); 140 | this.component._encrypt("test text", [this.km]); 141 | 142 | this.flag = false; 143 | return pgp.box({encrypt_for: [this.km], msg: "test text"}, (err, result_string) => { 144 | expect((err == null)); 145 | this.err = err; 146 | this.result_string = result_string; 147 | return this.flag = true; 148 | }); 149 | }); 150 | 151 | waitsFor((() => { console.log(this.flag); return this.flag; }), "encryption took too long", 5000); 152 | 153 | return runs( () => { 154 | console.log(this.err); 155 | console.log(this.result_string); 156 | console.log(this.output); 157 | 158 | return expect(this.output === this.result_string); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /spec/keybase-profile-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | const {React, ReactTestUtils, Message} = require('mailspring-exports'); 7 | 8 | const KeybaseUser = require('../lib/keybase-user'); 9 | 10 | describe("KeybaseUserProfile", () => 11 | it("should have a displayName", () => expect(KeybaseUser.displayName).toBe('KeybaseUserProfile')) 12 | ); 13 | 14 | // behold, the most comprehensive test suite of all time 15 | -------------------------------------------------------------------------------- /spec/keybase-search-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | const {React, ReactTestUtils, Message} = require('mailspring-exports'); 7 | 8 | const KeybaseSearch = require('../lib/keybase-search'); 9 | 10 | describe("KeybaseSearch", function() { 11 | it("should have a displayName", () => expect(KeybaseSearch.displayName).toBe('KeybaseSearch')); 12 | 13 | return it("should have no results when rendered", function() { 14 | this.component = ReactTestUtils.renderIntoDocument( 15 | React.createElement(KeybaseSearch, null) 16 | ); 17 | 18 | return expect(this.component.state.results).toEqual([]); 19 | }); 20 | }); 21 | 22 | // behold, the most comprehensive test suite of all time 23 | -------------------------------------------------------------------------------- /spec/keybase-spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS207: Consider shorter variations of null checks 5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 6 | */ 7 | const kb = require("../lib/keybase"); 8 | 9 | xdescribe("keybase lib", function() { 10 | // TODO stub keybase calls? 11 | it("should be able to fetch an account by username", function() { 12 | this.them = null; 13 | runs(() => { 14 | return kb.getUser("dakota", "usernames", (err, them) => { 15 | return (this.them = them); 16 | }); 17 | }); 18 | waitsFor(() => this.them !== null, 2000); 19 | return runs(() => { 20 | return expect( 21 | this.them != null ? this.them[0].components.username.val : undefined 22 | ).toEqual("dakota"); 23 | }); 24 | }); 25 | 26 | it("should be able to fetch an account by key fingerprint", function() { 27 | this.them = null; 28 | runs(() => { 29 | return kb.getUser( 30 | "7FA5A43BBF2BAD1845C8D0E8145FCCD989968E3B", 31 | "key_fingerprint", 32 | (err, them) => { 33 | return (this.them = them); 34 | } 35 | ); 36 | }); 37 | waitsFor(() => this.them !== null, 2000); 38 | return runs(() => { 39 | return expect( 40 | this.them != null ? this.them[0].components.username.val : undefined 41 | ).toEqual("dakota"); 42 | }); 43 | }); 44 | 45 | it("should be able to fetch a user's key", function() { 46 | this.key = null; 47 | runs(() => { 48 | return kb.getKey("dakota", (error, key) => { 49 | return (this.key = key); 50 | }); 51 | }); 52 | waitsFor(() => this.key !== null, 2000); 53 | return runs(() => { 54 | return expect( 55 | this.key != null 56 | ? this.key.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----") 57 | : undefined 58 | ); 59 | }); 60 | }); 61 | 62 | return it("should be able to return an autocomplete query", function() { 63 | this.completions = null; 64 | runs(() => { 65 | return kb.autocomplete("dakota", (error, completions) => { 66 | return (this.completions = completions); 67 | }); 68 | }); 69 | waitsFor(() => this.completions !== null, 2000); 70 | return runs(() => { 71 | return expect(this.completions[0].components.username.val).toEqual( 72 | "dakota" 73 | ); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /spec/main-spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | const { ComponentRegistry, ExtensionRegistry } = require("mailspring-exports"); 7 | const { activate, deactivate } = require("../lib/main"); 8 | 9 | const EncryptMessageButton = require("../lib/encrypt-button"); 10 | const DecryptMessageButton = require("../lib/decrypt-button"); 11 | const DecryptPGPExtension = require("../lib/decryption-preprocess"); 12 | 13 | describe("activate", function() { 14 | it("should register the encryption button", function() { 15 | spyOn(ComponentRegistry, "register"); 16 | activate(); 17 | return expect(ComponentRegistry.register).toHaveBeenCalledWith( 18 | EncryptMessageButton, 19 | { role: "Composer:ActionButton" } 20 | ); 21 | }); 22 | 23 | it("should register the decryption button", function() { 24 | spyOn(ComponentRegistry, "register"); 25 | activate(); 26 | return expect(ComponentRegistry.register).toHaveBeenCalledWith( 27 | DecryptMessageButton, 28 | { role: "message:BodyHeader" } 29 | ); 30 | }); 31 | 32 | return it("should register the decryption processor", function() { 33 | spyOn(ExtensionRegistry.MessageView, "register"); 34 | activate(); 35 | return expect(ExtensionRegistry.MessageView.register).toHaveBeenCalledWith( 36 | DecryptPGPExtension 37 | ); 38 | }); 39 | }); 40 | 41 | describe("deactivate", function() { 42 | it("should unregister the encrypt button", function() { 43 | spyOn(ComponentRegistry, "unregister"); 44 | deactivate(); 45 | return expect(ComponentRegistry.unregister).toHaveBeenCalledWith( 46 | EncryptMessageButton 47 | ); 48 | }); 49 | 50 | it("should unregister the decryption button", function() { 51 | spyOn(ComponentRegistry, "unregister"); 52 | deactivate(); 53 | return expect(ComponentRegistry.unregister).toHaveBeenCalledWith( 54 | DecryptMessageButton 55 | ); 56 | }); 57 | 58 | return it("should unregister the decryption processor", function() { 59 | spyOn(ExtensionRegistry.MessageView, "unregister"); 60 | deactivate(); 61 | return expect( 62 | ExtensionRegistry.MessageView.unregister 63 | ).toHaveBeenCalledWith(DecryptPGPExtension); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /spec/pgp-key-store-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS207: Consider shorter variations of null checks 5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 6 | */ 7 | const {React, ReactTestUtils, DraftStore, Message} = require('mailspring-exports'); 8 | const pgp = require('kbpgp'); 9 | const _ = require('underscore'); 10 | const fs = require('fs'); 11 | 12 | const Identity = require('../lib/identity'); 13 | const PGPKeyStore = require('../lib/pgp-key-store'); 14 | 15 | describe("PGPKeyStore", function() { 16 | beforeEach(function() { 17 | this.TEST_KEY = `-----BEGIN PGP PRIVATE KEY BLOCK----- 18 | Version: GnuPG v1 19 | 20 | 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 72 | 217s2OKjpJqtpHPf2vY= 73 | =UY7Y 74 | -----END PGP PRIVATE KEY BLOCK-----`; 75 | 76 | // mock getKeyContents to get rid of all the fs.readFiles 77 | spyOn(PGPKeyStore, "getKeyContents").andCallFake( ({key, passphrase, callback}) => { 78 | const data = this.TEST_KEY; 79 | return pgp.KeyManager.import_from_armored_pgp({ 80 | armored: data 81 | }, (err, km) => { 82 | expect(err).toEqual(null); 83 | if (km.is_pgp_locked()) { 84 | expect(passphrase).toBeDefined(); 85 | km.unlock_pgp({ passphrase }, err => { 86 | return expect(err).toEqual(null); 87 | }); 88 | } 89 | key.key = km; 90 | key.setTimeout(); 91 | if (callback != null) { 92 | return callback(); 93 | } 94 | }); 95 | }); 96 | 97 | // define an encrypted and an unencrypted message 98 | this.unencryptedMsg = new Message({clientId: 'test', subject: 'Subject', body: '

Body

'}); 99 | const body = `-----BEGIN PGP MESSAGE----- 100 | Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto 101 | 102 | wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw= 103 | =1aPN 104 | -----END PGP MESSAGE-----`; 105 | this.encryptedMsg = new Message({clientId: 'test2', subject: 'Subject', body}); 106 | 107 | // blow away the saved identities and set up a test pub/priv keypair 108 | PGPKeyStore._identities = {}; 109 | const pubIdent = new Identity({ 110 | addresses: ["benbitdiddle@icloud.com"], 111 | isPriv: false 112 | }); 113 | PGPKeyStore._identities[pubIdent.clientId] = pubIdent; 114 | const privIdent = new Identity({ 115 | addresses: ["benbitdiddle@icloud.com"], 116 | isPriv: true 117 | }); 118 | return PGPKeyStore._identities[privIdent.clientId] = privIdent; 119 | }); 120 | 121 | describe("when handling private keys", function() { 122 | it('should be able to retrieve and unlock a private key', function() { 123 | expect(PGPKeyStore.privKeys().some((cv, index, array) => { 124 | return cv.hasOwnProperty("key"); 125 | })).toBeFalsey; 126 | const key = PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0]; 127 | return PGPKeyStore.getKeyContents({key, passphrase: "", callback: () => { 128 | return expect(PGPKeyStore.privKeys({timed: false}).some((cv, index, array) => { 129 | return cv.hasOwnProperty("key"); 130 | })).toBeTruthy; 131 | } 132 | }); 133 | }); 134 | 135 | it('should not return a private key after its timeout has passed', function() { 136 | expect(PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false}).length).toEqual(1); 137 | PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0].timeout = Date.now() - 5; 138 | expect(PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: true}).length).toEqual(0); 139 | return PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0].setTimeout(); 140 | }); 141 | 142 | it('should only return the key(s) corresponding to a supplied email address', () => expect(PGPKeyStore.privKeys({address: "wrong@example.com", timed: true}).length).toEqual(0)); 143 | 144 | it('should return all private keys when an address is not supplied', () => expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1)); 145 | 146 | return it('should update an existing key when it is unlocked, not add a new one', function() { 147 | const { timeout } = PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0]; 148 | return PGPKeyStore.getKeyContents({key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: () => { 149 | // expect no new keys to have been added 150 | expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1); 151 | // make sure the timeout is updated 152 | return expect(timeout < PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false}).timeout); 153 | } 154 | }); 155 | }); 156 | }); 157 | 158 | describe("when decrypting messages", function() { 159 | xit('should be able to decrypt a message', function() { 160 | // TODO for some reason, the pgp.unbox has a problem with the message body 161 | runs( () => { 162 | spyOn(PGPKeyStore, 'trigger'); 163 | return PGPKeyStore.getKeyContents({key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: () => { 164 | return PGPKeyStore.decrypt(this.encryptedMsg); 165 | } 166 | }); 167 | }); 168 | waitsFor((() => PGPKeyStore.trigger.callCount > 0), 'message to decrypt'); 169 | return runs( () => { 170 | return expect(_.findWhere(PGPKeyStore._msgCache, 171 | {clientId: this.encryptedMsg.clientId})).toExist(); 172 | }); 173 | }); 174 | 175 | it('should be able to handle an unencrypted message', function() { 176 | PGPKeyStore.decrypt(this.unencryptedMsg); 177 | return expect(_.findWhere(PGPKeyStore._msgCache, 178 | {clientId: this.unencryptedMsg.clientId})).not.toBeDefined(); 179 | }); 180 | 181 | it('should be able to tell when a message has no encrypted component', function() { 182 | expect(PGPKeyStore.hasEncryptedComponent(this.unencryptedMsg)).not; 183 | return expect(PGPKeyStore.hasEncryptedComponent(this.encryptedMsg)); 184 | }); 185 | 186 | it('should be able to handle a message with no BEGIN PGP MESSAGE block', function() { 187 | const body = `Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto 188 | 189 | wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw= 190 | =1aPN 191 | -----END PGP MESSAGE-----`; 192 | const badMsg = new Message({clientId: 'test2', subject: 'Subject', body}); 193 | 194 | return PGPKeyStore.getKeyContents({key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: () => { 195 | PGPKeyStore.decrypt(badMsg); 196 | return expect(_.findWhere(PGPKeyStore._msgCache, 197 | {clientId: badMsg.clientId})).not.toBeDefined(); 198 | } 199 | }); 200 | }); 201 | 202 | it('should be able to handle a message with no END PGP MESSAGE block', function() { 203 | const body = `-----BEGIN PGP MESSAGE----- 204 | Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto 205 | 206 | wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw= 207 | =1aPN`; 208 | const badMsg = new Message({clientId: 'test2', subject: 'Subject', body}); 209 | 210 | return PGPKeyStore.getKeyContents({key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: () => { 211 | PGPKeyStore.decrypt(badMsg); 212 | return expect(_.findWhere(PGPKeyStore._msgCache, 213 | {clientId: badMsg.clientId})).not.toBeDefined(); 214 | } 215 | }); 216 | }); 217 | 218 | it('should not return a decrypted message which has timed out', function() { 219 | PGPKeyStore._msgCache.push({clientId: "testID", body: "example body", timeout: Date.now()}); 220 | 221 | const msg = new Message({clientId: "testID"}); 222 | return expect(PGPKeyStore.getDecrypted(msg)).toEqual(null); 223 | }); 224 | 225 | return it('should return a decrypted message', function() { 226 | const timeout = Date.now() + (1000*60*60); 227 | PGPKeyStore._msgCache.push({clientId: "testID2", body: "example body", timeout}); 228 | 229 | const msg = new Message({clientId: "testID2", body: "example body"}); 230 | return expect(PGPKeyStore.getDecrypted(msg)).toEqual(msg.body); 231 | }); 232 | }); 233 | 234 | return describe("when handling public keys", () => 235 | 236 | it("should immediately return a pre-cached key", () => expect(PGPKeyStore.pubKeys('benbitdiddle@icloud.com').length).toEqual(1)) 237 | ); 238 | }); 239 | -------------------------------------------------------------------------------- /spec/recipient-key-chip-spec.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | const {React, ReactTestUtils, DraftStore, Contact} = require('mailspring-exports'); 7 | const pgp = require('kbpgp'); 8 | 9 | const RecipientKeyChip = require('../lib/recipient-key-chip'); 10 | const PGPKeyStore = require('../lib/pgp-key-store'); 11 | 12 | describe("DecryptMessageButton", function() { 13 | beforeEach(function() { 14 | this.contact = new Contact({email: "test@example.com"}); 15 | return this.component = ReactTestUtils.renderIntoDocument( 16 | React.createElement(RecipientKeyChip, {"contact": this.contact}) 17 | ); 18 | }); 19 | 20 | it("should render into the page", function() { 21 | return expect(this.component).toBeDefined(); 22 | }); 23 | 24 | it("should have a displayName", () => expect(RecipientKeyChip.displayName).toBe('RecipientKeyChip')); 25 | 26 | xit("should indicate when a recipient has a PGP key available", function() { 27 | spyOn(PGPKeyStore, "pubKeys").andCallFake(address => { 28 | return [{'key':0}]; 29 | }); 30 | const key = PGPKeyStore.pubKeys(this.contact.email); 31 | expect(key).toBeDefined(); 32 | 33 | // TODO these calls crash the tester because they require a call to getKeyContents 34 | expect(this.component.refs.keyIcon).toBeDefined(); 35 | return expect(this.component.refs.noKeyIcon).not.toBeDefined(); 36 | }); 37 | 38 | return xit("should indicate when a recipient does not have a PGP key available", function() { 39 | const component = ReactTestUtils.renderIntoDocument( 40 | React.createElement(RecipientKeyChip, {"contact": this.contact}) 41 | ); 42 | 43 | const key = PGPKeyStore.pubKeys(this.contact.email); 44 | expect(key).toEqual([]); 45 | 46 | // TODO these calls crash the tester because they require a call to getKeyContents 47 | expect(component.refs.keyIcon).not.toBeDefined(); 48 | return expect(component.refs.noKeyIcon).toBeDefined(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/decrypt-button.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS101: Remove unnecessary use of Array.from 5 | * DS102: Remove unnecessary code created because of implicit returns 6 | * DS205: Consider reworking code to avoid use of IIFEs 7 | * DS206: Consider reworking classes to avoid initClass 8 | * DS207: Consider shorter variations of null checks 9 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 10 | */ 11 | const { 12 | MessageStore, 13 | React, 14 | ReactDOM, 15 | AttachmentStore, 16 | MessageBodyProcessor, 17 | Actions 18 | } = require("mailspring-exports"); 19 | const PGPKeyStore = require("./pgp-key-store"); 20 | const { remote } = require("electron"); 21 | const PassphrasePopover = require("./passphrase-popover"); 22 | const PrivateKeyPopover = require("./private-key-popover"); 23 | const pgp = require("kbpgp"); 24 | const _ = require("underscore"); 25 | import PropTypes from 'prop-types'; 26 | 27 | class DecryptMessageButton extends React.Component { 28 | static displayName = "DecryptMessageButton"; 29 | static initClass() { 30 | this.propTypes = { message: PropTypes.object.isRequired }; 31 | } 32 | 33 | constructor(props) { 34 | super(props); 35 | this._onClickDecrypt = this._onClickDecrypt.bind(this); 36 | this._onClickDecryptAttachments = this._onClickDecryptAttachments.bind( 37 | this 38 | ); 39 | this.decryptPopoverDone = this.decryptPopoverDone.bind(this); 40 | this.decryptAttachmentsPopoverDone = this.decryptAttachmentsPopoverDone.bind( 41 | this 42 | ); 43 | this._openPassphrasePopover = this._openPassphrasePopover.bind(this); 44 | this._noPrivateKeys = this._noPrivateKeys.bind(this); 45 | this.render = this.render.bind(this); 46 | this.state = this._getStateFromStores(); 47 | } 48 | 49 | _getStateFromStores() { 50 | return { 51 | isDecrypted: PGPKeyStore.isDecrypted(this.props.message), 52 | wasEncrypted: PGPKeyStore.hasEncryptedComponent(this.props.message), 53 | encryptedAttachments: PGPKeyStore.fetchEncryptedAttachments( 54 | this.props.message 55 | ), 56 | status: PGPKeyStore.msgStatus(this.props.message) 57 | }; 58 | } 59 | 60 | componentDidMount() { 61 | return (this.unlistenKeystore = PGPKeyStore.listen( 62 | this._onKeystoreChange, 63 | this 64 | )); 65 | } 66 | 67 | componentWillUnmount() { 68 | return this.unlistenKeystore(); 69 | } 70 | 71 | _onKeystoreChange() { 72 | // every time a new key gets unlocked/fetched, try to decrypt this message 73 | if (!this.state.isDecrypted) { 74 | PGPKeyStore.decrypt(this.props.message); 75 | } 76 | return this.setState(this._getStateFromStores()); 77 | } 78 | 79 | _onClickDecrypt(event) { 80 | const popoverTarget = event.target.getBoundingClientRect(); 81 | if (this._noPrivateKeys()) { 82 | return Actions.openPopover( 83 | 86 | this._openPassphrasePopover(popoverTarget, this.decryptPopoverDone) 87 | } 88 | />, 89 | { originRect: popoverTarget, direction: "down" } 90 | ); 91 | } else { 92 | return this._openPassphrasePopover( 93 | popoverTarget, 94 | this.decryptPopoverDone 95 | ); 96 | } 97 | } 98 | 99 | _displayError(err) { 100 | const { dialog } = remote; 101 | return dialog.showErrorBox("Decryption Error", err.toString()); 102 | } 103 | 104 | _onClickDecryptAttachments(event) { 105 | const popoverTarget = event.target.getBoundingClientRect(); 106 | if (this._noPrivateKeys()) { 107 | return Actions.openPopover( 108 | 111 | this._openPassphrasePopover( 112 | popoverTarget, 113 | this.decryptAttachmentsPopoverDone 114 | ) 115 | } 116 | />, 117 | { originRect: popoverTarget, direction: "down" } 118 | ); 119 | } else { 120 | return this._openPassphrasePopover( 121 | popoverTarget, 122 | this.decryptAttachmentsPopoverDone 123 | ); 124 | } 125 | } 126 | 127 | decryptPopoverDone(passphrase) { 128 | return (() => { 129 | const result = []; 130 | for (let recipient of Array.from(this.props.message.to)) { 131 | // right now, just try to unlock all possible keys 132 | // (many will fail - TODO?) 133 | const privateKeys = PGPKeyStore.privKeys({ 134 | address: recipient.email, 135 | timed: false 136 | }); 137 | result.push( 138 | Array.from(privateKeys).map(privateKey => 139 | PGPKeyStore.getKeyContents({ key: privateKey, passphrase }) 140 | ) 141 | ); 142 | } 143 | return result; 144 | })(); 145 | } 146 | 147 | decryptAttachmentsPopoverDone(passphrase) { 148 | return (() => { 149 | const result = []; 150 | for (let recipient of Array.from(this.props.message.to)) { 151 | const privateKeys = PGPKeyStore.privKeys({ 152 | address: recipient.email, 153 | timed: false 154 | }); 155 | result.push( 156 | Array.from(privateKeys).map(privateKey => 157 | PGPKeyStore.getKeyContents({ 158 | key: privateKey, 159 | passphrase, 160 | callback: identity => 161 | PGPKeyStore.decryptAttachments( 162 | identity, 163 | this.state.encryptedAttachments 164 | ) 165 | }) 166 | ) 167 | ); 168 | } 169 | return result; 170 | })(); 171 | } 172 | 173 | _openPassphrasePopover(target, callback) { 174 | return Actions.openPopover( 175 | , 179 | { originRect: target, direction: "down" } 180 | ); 181 | } 182 | 183 | _noPrivateKeys() { 184 | let numKeys = 0; 185 | for (let recipient of Array.from(this.props.message.to)) { 186 | numKeys = 187 | numKeys + 188 | PGPKeyStore.privKeys({ address: recipient.email, timed: false }).length; 189 | } 190 | return numKeys < 1; 191 | } 192 | 193 | render() { 194 | let decryptionInterface; 195 | if ( 196 | !(this.state.wasEncrypted || this.state.encryptedAttachments.length > 0) 197 | ) { 198 | return false; 199 | } 200 | 201 | let title = "Message Encrypted"; 202 | let decryptLabel = "Decrypt"; 203 | let borderClass = "border"; 204 | let decryptClass = "decrypt-bar"; 205 | if (this.state.status != null) { 206 | if (this.state.status.indexOf("Message decrypted") >= 0) { 207 | title = this.state.status; 208 | borderClass = "border done-border"; 209 | decryptClass = "decrypt-bar done-decrypt-bar"; 210 | } else if (this.state.status.indexOf("Unable to decrypt message.") >= 0) { 211 | title = this.state.status; 212 | borderClass = "border error-border"; 213 | decryptClass = "decrypt-bar error-decrypt-bar"; 214 | decryptLabel = "Try Again"; 215 | } 216 | } 217 | 218 | let decryptBody = false; 219 | if ( 220 | !this.state.isDecrypted && 221 | !( 222 | (this.state.status != null 223 | ? this.state.status.indexOf("malformed") 224 | : undefined) >= 0 225 | ) 226 | ) { 227 | decryptBody = ( 228 | 236 | ); 237 | } 238 | 239 | let decryptAttachments = false; 240 | if ( 241 | (this.state.encryptedAttachments != null 242 | ? this.state.encryptedAttachments.length 243 | : undefined) >= 1 244 | ) { 245 | title = 246 | this.state.encryptedAttachments.length === 1 247 | ? "Attachment Encrypted" 248 | : "Attachments Encrypted"; 249 | const buttonLabel = 250 | this.state.encryptedAttachments.length === 1 251 | ? "Decrypt Attachment" 252 | : "Decrypt Attachments"; 253 | decryptAttachments = ( 254 | 260 | ); 261 | } 262 | 263 | if (decryptAttachments || decryptBody) { 264 | decryptionInterface = ( 265 |
266 | {decryptBody} 267 | {decryptAttachments} 268 |
269 | ); 270 | } 271 | 272 | return ( 273 |
274 |
275 |
276 |
277 |
{title}
278 | {decryptionInterface} 279 |
280 |
281 |
282 |
283 | ); 284 | } 285 | } 286 | DecryptMessageButton.initClass(); 287 | 288 | module.exports = DecryptMessageButton; 289 | -------------------------------------------------------------------------------- /src/decryption-preprocess.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.7 2 | var Actions, 3 | DecryptPGPExtension, 4 | MessageViewExtension, 5 | PGPKeyStore, 6 | ref, 7 | extend = function (child, parent) { 8 | for (var key in parent) { 9 | if (hasProp.call(parent, key)) child[key] = parent[key]; 10 | }function ctor() { 11 | this.constructor = child; 12 | }ctor.prototype = parent.prototype;child.prototype = new ctor();child.__super__ = parent.prototype;return child; 13 | }, 14 | hasProp = {}.hasOwnProperty; 15 | 16 | ref = require('mailspring-exports'), MessageViewExtension = ref.MessageViewExtension, Actions = ref.Actions; 17 | 18 | PGPKeyStore = require('./pgp-key-store'); 19 | 20 | DecryptPGPExtension = function (superClass) { 21 | extend(DecryptPGPExtension, superClass); 22 | 23 | function DecryptPGPExtension() { 24 | return DecryptPGPExtension.__super__.constructor.apply(this, arguments); 25 | } 26 | 27 | DecryptPGPExtension.formatMessageBody = function (arg) { 28 | var message; 29 | message = arg.message; 30 | if (!PGPKeyStore.hasEncryptedComponent(message)) { 31 | return message; 32 | } 33 | if (PGPKeyStore.isDecrypted(message)) { 34 | message.body = PGPKeyStore.getDecrypted(message); 35 | } else { 36 | PGPKeyStore.decrypt(message); 37 | } 38 | return message; 39 | }; 40 | 41 | return DecryptPGPExtension; 42 | }(MessageViewExtension); 43 | 44 | module.exports = DecryptPGPExtension; 45 | -------------------------------------------------------------------------------- /src/email-popover.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS102: Remove unnecessary code created because of implicit returns 5 | * DS206: Consider reworking classes to avoid initClass 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | let EmailPopover; 9 | const { React, Actions } = require("mailspring-exports"); 10 | const { ParticipantsTextField } = require("mailspring-component-kit"); 11 | const Identity = require("./identity"); 12 | const _ = require("underscore"); 13 | import PropTypes from 'prop-types'; 14 | 15 | module.exports = EmailPopover = (function() { 16 | EmailPopover = class EmailPopover extends React.Component { 17 | static initClass() { 18 | this.propTypes = { 19 | profile: PropTypes.instanceOf(Identity).isRequired 20 | }; 21 | } 22 | constructor() { 23 | super(); 24 | this._onRecipientFieldChange = this._onRecipientFieldChange.bind(this); 25 | this._onDone = this._onDone.bind(this); 26 | this.state = { to: [], cc: [], bcc: [] }; 27 | } 28 | 29 | render() { 30 | const participants = this.state; 31 | 32 | return ( 33 |
34 | 40 | 43 |
44 | ); 45 | } 46 | 47 | _onRecipientFieldChange(contacts) { 48 | return this.setState(contacts); 49 | } 50 | 51 | _onDone() { 52 | this.props.onPopoverDone( 53 | _.pluck(this.state.to, "email"), 54 | this.props.profile 55 | ); 56 | return Actions.closePopover(); 57 | } 58 | }; 59 | EmailPopover.initClass(); 60 | return EmailPopover; 61 | })(); 62 | -------------------------------------------------------------------------------- /src/encrypt-button.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS101: Remove unnecessary use of Array.from 5 | * DS102: Remove unnecessary code created because of implicit returns 6 | * DS206: Consider reworking classes to avoid initClass 7 | * DS207: Consider shorter variations of null checks 8 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 9 | */ 10 | import { 11 | Utils, 12 | DraftStore, 13 | React, 14 | Actions, 15 | DatabaseStore, 16 | Contact, 17 | ReactDOM 18 | } from "mailspring-exports"; 19 | const PGPKeyStore = require("./pgp-key-store"); 20 | const Identity = require("./identity"); 21 | const ModalKeyRecommender = require("./modal-key-recommender"); 22 | const { RetinaImg } = require("mailspring-component-kit"); 23 | const { remote } = require("electron"); 24 | const pgp = require("kbpgp"); 25 | const _ = require("underscore"); 26 | import PropTypes from 'prop-types'; 27 | 28 | class EncryptMessageButton extends React.Component { 29 | static displayName = "EncryptMessageButton"; 30 | static initClass() { 31 | 32 | // require that we have a draft object available 33 | this.propTypes = { 34 | draft: PropTypes.object.isRequired, 35 | session: PropTypes.object.isRequired 36 | }; 37 | } 38 | 39 | constructor(props) { 40 | super(props); 41 | this._onKeystoreChange = this._onKeystoreChange.bind(this); 42 | this._onClick = this._onClick.bind(this); 43 | this._toggleCrypt = this._toggleCrypt.bind(this); 44 | this._encrypt = this._encrypt.bind(this); 45 | this._checkKeysAndEncrypt = this._checkKeysAndEncrypt.bind(this); 46 | 47 | // plaintext: store the message's plaintext in case the user wants to edit 48 | // further after hitting the "encrypt" button (i.e. so we can "undo" the 49 | // encryption) 50 | 51 | // cryptotext: store the message's body here, for comparison purposes (so 52 | // that if the user edits an encrypted message, we can revert it) 53 | this.state = { plaintext: "", cryptotext: "", currentlyEncrypted: false }; 54 | } 55 | 56 | componentDidMount() { 57 | return (this.unlistenKeystore = PGPKeyStore.listen( 58 | this._onKeystoreChange, 59 | this 60 | )); 61 | } 62 | 63 | componentWillUnmount() { 64 | return this.unlistenKeystore(); 65 | } 66 | 67 | componentWillReceiveProps(nextProps) { 68 | if ( 69 | this.state.currentlyEncrypted && 70 | nextProps.draft.body !== this.props.draft.body && 71 | nextProps.draft.body !== this.state.cryptotext 72 | ) { 73 | // A) we're encrypted 74 | // B) someone changed something 75 | // C) the change was AWAY from the "correct" cryptotext 76 | const body = this.state.cryptotext; 77 | return this.props.session.changes.add({ body }); 78 | } 79 | } 80 | 81 | _getKeys() { 82 | const keys = []; 83 | for (let recipient of Array.from( 84 | this.props.draft.participants({ includeFrom: false, includeBcc: true }) 85 | )) { 86 | const publicKeys = PGPKeyStore.pubKeys(recipient.email); 87 | if (publicKeys.length < 1) { 88 | // no key for this user 89 | keys.push(new Identity({ addresses: [recipient.email] })); 90 | } else { 91 | // note: this, by default, encrypts using every public key associated 92 | // with the address 93 | for (let publicKey of Array.from(publicKeys)) { 94 | if (publicKey.key == null) { 95 | PGPKeyStore.getKeyContents({ key: publicKey }); 96 | } else { 97 | keys.push(publicKey); 98 | } 99 | } 100 | } 101 | } 102 | 103 | return keys; 104 | } 105 | 106 | _onKeystoreChange() { 107 | // if something changes with the keys, check to make sure the recipients 108 | // haven't changed (thus invalidating our encrypted message) 109 | if (this.state.currentlyEncrypted) { 110 | let newKeys = _.map(this.props.draft.participants(), participant => 111 | PGPKeyStore.pubKeys(participant.email) 112 | ); 113 | newKeys = _.flatten(newKeys); 114 | 115 | let oldKeys = _.map(this.props.draft.participants(), participant => 116 | PGPKeyStore.pubKeys(participant.email) 117 | ); 118 | oldKeys = _.flatten(oldKeys); 119 | 120 | if (newKeys.length !== oldKeys.length) { 121 | // someone added/removed a key - our encrypted body is now out of date 122 | return this._toggleCrypt(); 123 | } 124 | } 125 | } 126 | 127 | render() { 128 | let classnames = "btn btn-toolbar"; 129 | if (this.state.currentlyEncrypted) { 130 | classnames += " btn-enabled"; 131 | } 132 | 133 | return ( 134 |
135 | 146 |
147 | ); 148 | } 149 | 150 | _onClick() { 151 | return this._toggleCrypt(); 152 | } 153 | 154 | _toggleCrypt() { 155 | // if decrypted, encrypt, and vice versa 156 | // addresses which don't have a key 157 | if (this.state.currentlyEncrypted) { 158 | // if the message is already encrypted, place the stored plaintext back 159 | // in the draft (i.e. un-encrypt) 160 | this.props.session.changes.add({ body: this.state.plaintext }); 161 | return this.setState({ currentlyEncrypted: false }); 162 | } else { 163 | // if not encrypted, save the plaintext, then encrypt 164 | const plaintext = this.props.draft.body; 165 | const identities = this._getKeys(); 166 | return this._checkKeysAndEncrypt( 167 | plaintext, 168 | identities, 169 | (err, cryptotext) => { 170 | if (err) { 171 | console.warn(err); 172 | //Actions.recordUserEvent("Email Encryption Errored", { error: err }); 173 | AppEnv.showErrorDialog(err); 174 | } 175 | if (cryptotext != null && cryptotext !== "") { 176 | //
 tag prevents gross HTML formatting in-flight
177 |             cryptotext = `
${cryptotext}
`; 178 | this.setState({ 179 | currentlyEncrypted: true, 180 | plaintext, 181 | cryptotext 182 | }); 183 | return this.props.session.changes.add({ body: cryptotext }); 184 | } 185 | } 186 | ); 187 | } 188 | } 189 | 190 | _encrypt(text, identities, cb) { 191 | // get the actual key objects 192 | const keys = _.pluck(identities, "key"); 193 | // remove the nulls 194 | const kms = _.compact(keys); 195 | if (kms.length === 0) { 196 | AppEnv.showErrorDialog(`There are no PGP public keys loaded, so the message cannot be \ 197 | encrypted. Compose a message, add recipients in the To: field, and try again.`); 198 | return; 199 | } 200 | const params = { 201 | encrypt_for: kms, 202 | msg: text 203 | }; 204 | return pgp.box(params, cb); 205 | } 206 | 207 | _checkKeysAndEncrypt(text, identities, cb) { 208 | const emails = _.chain(identities) 209 | .pluck("addresses") 210 | .flatten() 211 | .uniq() 212 | .value(); 213 | 214 | if (_.every(identities, identity => identity.key != null)) { 215 | // every key is present and valid 216 | return this._encrypt(text, identities, cb); 217 | } else { 218 | // open a popover to correct null keys 219 | return DatabaseStore.findAll(Contact, { email: emails }).then( 220 | contacts => { 221 | const component = ( 222 | this._encrypt(text, newIdentities, cb)} 226 | /> 227 | ); 228 | return Actions.openPopover(component, { 229 | originRect: ReactDOM.findDOMNode(this).getBoundingClientRect(), 230 | direction: "up", 231 | closeOnAppBlur: false 232 | }); 233 | } 234 | ); 235 | } 236 | } 237 | } 238 | EncryptMessageButton.initClass(); 239 | 240 | module.exports = EncryptMessageButton; 241 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS207: Consider shorter variations of null checks 5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 6 | */ 7 | // A single user identity: a key, a way to find that key, one or more email 8 | // addresses, and a keybase profile 9 | 10 | let Identity; 11 | const { Utils } = require("mailspring-exports"); 12 | const path = require("path"); 13 | 14 | module.exports = Identity = class Identity { 15 | constructor({ key, addresses, isPriv, keybase_profile }) { 16 | this.clientId = Utils.generateTempId(); 17 | this.key = key != null ? key : null; // keybase keymanager object 18 | this.isPriv = isPriv != null ? isPriv : false; // is this a private key? 19 | this.timeout = null; // the time after which this key (if private) needs to be unlocked again 20 | this.addresses = addresses != null ? addresses : []; // email addresses associated with this identity 21 | this.keybase_profile = keybase_profile != null ? keybase_profile : null; // a kb profile object associated with this identity 22 | 23 | Object.defineProperty(this, "keyPath", { 24 | get() { 25 | let keyPath; 26 | if (this.addresses.length > 0) { 27 | const keyDir = path.join(AppEnv.getConfigDirPath(), "keys"); 28 | const thisDir = this.isPriv 29 | ? path.join(keyDir, "private") 30 | : path.join(keyDir, "public"); 31 | keyPath = path.join(thisDir, this.addresses.join(" ")); 32 | } else { 33 | keyPath = null; 34 | } 35 | return keyPath; 36 | } 37 | }); 38 | 39 | if (this.isPriv) { 40 | this.setTimeout(); 41 | } 42 | } 43 | 44 | fingerprint() { 45 | if (this.key != null) { 46 | return this.key.get_pgp_fingerprint().toString("hex"); 47 | } 48 | return null; 49 | } 50 | 51 | setTimeout() { 52 | const delay = 1000 * 60 * 30; // 30 minutes in ms 53 | return (this.timeout = Date.now() + delay); 54 | } 55 | 56 | isTimedOut() { 57 | return this.timeout < Date.now(); 58 | } 59 | 60 | uid() { 61 | let uid; 62 | if (this.key != null) { 63 | uid = this.key.get_pgp_fingerprint().toString("hex"); 64 | } else if (this.keybase_profile != null) { 65 | uid = this.keybase_profile.components.username.val; 66 | } else if (this.addresses.length > 0) { 67 | uid = this.addresses.join(""); 68 | } else { 69 | uid = this.clientId; 70 | } 71 | 72 | return uid; 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/key-adder.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS102: Remove unnecessary code created because of implicit returns 5 | * DS206: Consider reworking classes to avoid initClass 6 | * DS207: Consider shorter variations of null checks 7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 8 | */ 9 | let KeyAdder; 10 | const { Utils, React, RegExpUtils } = require("mailspring-exports"); 11 | const { RetinaImg } = require("mailspring-component-kit"); 12 | const PGPKeyStore = require("./pgp-key-store"); 13 | const Identity = require("./identity"); 14 | const kb = require("./keybase"); 15 | const pgp = require("kbpgp"); 16 | const _ = require("underscore"); 17 | const fs = require("fs"); 18 | 19 | module.exports = KeyAdder = (function() { 20 | KeyAdder = class KeyAdder extends React.Component { 21 | static displayName = "KeyAdder"; 22 | 23 | 24 | constructor(props) { 25 | super(); 26 | this._onPasteButtonClick = this._onPasteButtonClick.bind(this); 27 | this._onGenerateButtonClick = this._onGenerateButtonClick.bind(this); 28 | this._onImportButtonClick = this._onImportButtonClick.bind(this); 29 | this._onInnerGenerateButtonClick = this._onInnerGenerateButtonClick.bind( 30 | this 31 | ); 32 | this._generateKeypair = this._generateKeypair.bind(this); 33 | this._saveNewKey = this._saveNewKey.bind(this); 34 | this._onAddressChange = this._onAddressChange.bind(this); 35 | this._onPassphraseChange = this._onPassphraseChange.bind(this); 36 | this._onKeyChange = this._onKeyChange.bind(this); 37 | this.state = { 38 | address: "", 39 | keyContents: "", 40 | passphrase: "", 41 | 42 | generate: false, 43 | paste: false, 44 | import: false, 45 | 46 | isPriv: false, 47 | loading: false, 48 | 49 | validAddress: false, 50 | validKeyBody: false 51 | }; 52 | } 53 | 54 | _onPasteButtonClick(event) { 55 | return this.setState({ 56 | generate: false, 57 | paste: !this.state.paste, 58 | import: false, 59 | address: "", 60 | validAddress: false, 61 | keyContents: "" 62 | }); 63 | } 64 | 65 | _onGenerateButtonClick(event) { 66 | return this.setState({ 67 | generate: !this.state.generate, 68 | paste: false, 69 | import: false, 70 | address: "", 71 | validAddress: false, 72 | keyContents: "", 73 | passphrase: "" 74 | }); 75 | } 76 | 77 | _onImportButtonClick(event) { 78 | return AppEnv.showOpenDialog( 79 | { 80 | title: "Import PGP Key", 81 | buttonLabel: "Import", 82 | properties: ["openFile"] 83 | }, 84 | filepath => { 85 | if (filepath != null) { 86 | this.setState({ 87 | generate: false, 88 | paste: false, 89 | import: true, 90 | address: "", 91 | validAddress: false, 92 | passphrase: "" 93 | }); 94 | return fs.readFile(filepath[0], (err, data) => { 95 | return pgp.KeyManager.import_from_armored_pgp( 96 | { 97 | armored: data 98 | }, 99 | (err, km) => { 100 | if (err) { 101 | PGPKeyStore._displayError("File is not a valid PGP key."); 102 | return; 103 | } else { 104 | const privateStart = 105 | "-----BEGIN PGP PRIVATE KEY BLOCK-----"; 106 | const keyBody = 107 | km.armored_pgp_private != null 108 | ? km.armored_pgp_private 109 | : km.armored_pgp_public; 110 | return this.setState({ 111 | keyContents: keyBody, 112 | isPriv: keyBody.indexOf(privateStart) >= 0, 113 | validKeyBody: true 114 | }); 115 | } 116 | } 117 | ); 118 | }); 119 | } 120 | } 121 | ); 122 | } 123 | 124 | _onInnerGenerateButtonClick(event) { 125 | this.setState({ 126 | loading: true 127 | }); 128 | return this._generateKeypair(); 129 | } 130 | 131 | _generateKeypair() { 132 | return pgp.KeyManager.generate_rsa( 133 | { userid: this.state.address }, 134 | (err, km) => { 135 | return km.sign({}, err => { 136 | if (err) { 137 | console.warn(err); 138 | } 139 | km.export_pgp_private( 140 | { passphrase: this.state.passphrase }, 141 | (err, pgp_private) => { 142 | const ident = new Identity({ 143 | addresses: [this.state.address], 144 | isPriv: true 145 | }); 146 | return PGPKeyStore.saveNewKey(ident, pgp_private); 147 | } 148 | ); 149 | return km.export_pgp_public({}, (err, pgp_public) => { 150 | const ident = new Identity({ 151 | addresses: [this.state.address], 152 | isPriv: false 153 | }); 154 | PGPKeyStore.saveNewKey(ident, pgp_public); 155 | return this.setState({ 156 | keyContents: pgp_public, 157 | loading: false 158 | }); 159 | }); 160 | }); 161 | } 162 | ); 163 | } 164 | 165 | _saveNewKey() { 166 | const ident = new Identity({ 167 | addresses: [this.state.address], 168 | isPriv: this.state.isPriv 169 | }); 170 | return PGPKeyStore.saveNewKey(ident, this.state.keyContents); 171 | } 172 | 173 | _onAddressChange(event) { 174 | const address = event.target.value; 175 | let valid = false; 176 | if ( 177 | address && 178 | address.length > 0 && 179 | RegExpUtils.emailRegex().test(address) 180 | ) { 181 | valid = true; 182 | } 183 | return this.setState({ 184 | address: event.target.value, 185 | validAddress: valid 186 | }); 187 | } 188 | 189 | _onPassphraseChange(event) { 190 | return this.setState({ 191 | passphrase: event.target.value 192 | }); 193 | } 194 | 195 | _onKeyChange(event) { 196 | const privateStart = "-----BEGIN PGP PRIVATE KEY BLOCK-----"; 197 | this.setState({ 198 | keyContents: event.target.value, 199 | isPriv: event.target.value.indexOf(privateStart) >= 0 200 | }); 201 | return pgp.KeyManager.import_from_armored_pgp( 202 | { 203 | armored: event.target.value 204 | }, 205 | (err, km) => { 206 | let valid; 207 | if (err) { 208 | valid = false; 209 | } else { 210 | valid = true; 211 | } 212 | return this.setState({ 213 | validKeyBody: valid 214 | }); 215 | } 216 | ); 217 | } 218 | 219 | _renderAddButtons() { 220 | return ( 221 |
222 | {`\ 223 | Add a PGP Key:\ 224 | `} 225 | 232 | 239 | 246 |
247 | ); 248 | } 249 | 250 | _renderManualKey() { 251 | let invalidMsg; 252 | if (!this.state.validAddress && this.state.address.length > 0) { 253 | invalidMsg = Invalid email address; 254 | } else if ( 255 | !this.state.validKeyBody && 256 | this.state.keyContents.length > 0 257 | ) { 258 | invalidMsg = Invalid key body; 259 | } else { 260 | invalidMsg = ; 261 | } 262 | const invalidInputs = !( 263 | this.state.validAddress && this.state.validKeyBody 264 | ); 265 | 266 | const buttonClass = invalidInputs 267 | ? "btn key-add-btn btn-disabled" 268 | : "btn key-add-btn"; 269 | 270 | const passphraseInput = ( 271 | 278 | ); 279 | 280 | return ( 281 |
282 |
283 |