├── .gitignore ├── .npmignore ├── README.md ├── index.js ├── package.json ├── src ├── app.js ├── components │ ├── LoginForm │ │ ├── LoginForm.js │ │ └── package.json │ ├── Modal │ │ ├── Modal.js │ │ └── package.json │ └── Search │ │ ├── Search.js │ │ └── package.json ├── lib │ └── getAccounts.js ├── style.scss └── views │ └── Lastpass │ ├── Lastpass.js │ └── package.json └── tools ├── build.js ├── flags.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tools 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperTerm Lastpass 2 | 3 | > Lastpass plugin for autofilling passwords in HyperTerm. 4 | 5 | ## About 6 | 7 | I have admired [`hyperterm-1password`][1] because it seemed like such a cool 8 | idea, but I have always used Lastpass! This made me quite jealous, so I went 9 | ahead and created [`lastpass-node`][2] because there were no Node.js clients yet 10 | for Lastpass (_sigh_). Then I created this cool plugin! Finally, an easy way to 11 | fill in my passwords easily with HyperTerm 🎉 12 | 13 | Lastpass vaults are stored locally and are encrypted. The plugin will try to 14 | clear decrypted accounts from memory as soon as possible, too. 15 | 16 | 17 | **I am not a security expert, and I am not liable for any problems that may 18 | arise due to using this plugin.** 19 | 20 | 21 | ![alt demo](http://i.giphy.com/l3fQndLP7SVAIXgWc.gif) 22 | 23 | ### How to use 24 | 25 | 1. Either click the `Plugins > Lastpass` menu item or use `Cmd or Ctrl + L`. 26 | 2. Enter your Lastpass username and password. 27 | 3. If you need to enter a 2 factor authentication pin, enter it. 28 | 4. Search for the password you want to enter. 29 | 5. Click on the row you would like to autofill a password with. 30 | 6. ???? 31 | 7. PROFIT!!! 32 | 33 | ### Caveats 34 | 35 | There's currently no way of updating your Lastpass vault locally. The vaults are 36 | stored to `~/.lastpass-vault-${USERNAME}`, delete the vault you'd like to update 37 | and re-login to update the vault. 38 | 39 | [1]: https://github.com/sibartlett/hyperterm-1password 40 | [2]: https://github.com/dfrankland/lastpass-node 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { resolve: resolvePath } = require('path'); 2 | 3 | const MAIN = resolvePath(__dirname, './dist/main.js'); 4 | 5 | let createWindow; 6 | exports.onApp = app => { 7 | createWindow = app.createWindow; 8 | }; 9 | 10 | exports.decorateMenu = menu => 11 | menu.map( 12 | item => { 13 | if (item.label !== 'Plugins') return item; 14 | const newItem = Object.assign({}, item); 15 | newItem.submenu = newItem.submenu.concat( 16 | { 17 | label: 'Lastpass', 18 | accelerator: 'CmdOrCtrl+L', 19 | click: (clickedItem, focusedWindow) => { 20 | if (focusedWindow) { 21 | focusedWindow.rpc.emit('lastpass-open'); 22 | } else { 23 | createWindow(win => win.rpc.emit('lastpass-open')); 24 | } 25 | }, 26 | } 27 | ); 28 | return newItem; 29 | } 30 | ); 31 | 32 | exports.decorateTerm = (Term, { React }) => { 33 | const { Component, createElement } = React; 34 | 35 | /* eslint-disable react/prop-types */ 36 | /* eslint-disable no-underscore-dangle */ 37 | const LastpassTerm = class extends Component { 38 | constructor(props, context) { 39 | super(props, context); 40 | this._onTerminal = term => { 41 | if (props && props.onTerminal) props.onTerminal(term); 42 | 43 | const modal = require(MAIN).default(// eslint-disable-line global-require 44 | stringToSend => { 45 | if (window.store.getState().sessions.activeUid !== props.uid) return; 46 | term.io.sendString(stringToSend); 47 | } 48 | ); 49 | 50 | window.rpc.on('lastpass-open', () => { 51 | modal.handleToggle(); 52 | }); 53 | }; 54 | } 55 | 56 | render() { 57 | const { _onTerminal: onTerminal } = this; 58 | const newProps = Object.assign({}, this.props, { onTerminal }); 59 | return createElement(Term, newProps); 60 | } 61 | }; 62 | /* eslint-enable */ 63 | 64 | return LastpassTerm; 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperterm-lastpass", 3 | "version": "1.0.1", 4 | "description": "Lastpass plugin for autofilling passwords in HyperTerm.", 5 | "main": "index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "prepublish": "npm run build -- --release", 11 | "build": "babel-node ./tools/build.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dfrankland/hyperterm-lastpass.git" 16 | }, 17 | "keywords": [ 18 | "Lastpass", 19 | "hyperterm", 20 | "passwords", 21 | "autofill" 22 | ], 23 | "author": "Dylan Frankland ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/dfrankland/hyperterm-lastpass/issues" 27 | }, 28 | "homepage": "https://github.com/dfrankland/hyperterm-lastpass#readme", 29 | "devDependencies": { 30 | "babel-cli": "^6.11.4", 31 | "babel-loader": "^6.2.4", 32 | "babel-plugin-transform-runtime": "^6.12.0", 33 | "babel-preset-es2015": "^6.13.2", 34 | "babel-preset-modern-node": "^2.3.0", 35 | "babel-preset-react": "^6.11.1", 36 | "babel-preset-stage-0": "^6.5.0", 37 | "css-loader": "^0.23.1", 38 | "json-loader": "^0.5.4", 39 | "node-sass": "^3.8.0", 40 | "sass-loader": "^4.0.0", 41 | "style-loader": "^0.13.1", 42 | "webpack": "^1.13.1" 43 | }, 44 | "dependencies": { 45 | "babel-polyfill": "^6.13.0", 46 | "babel-runtime": "^6.11.6", 47 | "bootstrap": "^4.0.0-alpha.3", 48 | "fuzzaldrin": "^2.1.0", 49 | "lastpass": "^1.0.2", 50 | "react": "^15.3.0", 51 | "react-dom": "^15.3.0" 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "modern-node/6.0", 56 | "stage-0" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import style from './style.scss'; 4 | import Lastpass from './views/Lastpass'; 5 | 6 | let node; 7 | 8 | // Prevent attaching multiple modals ;) 9 | const styleRoot = style.root; 10 | try { 11 | node = document.querySelector(`.${styleRoot}`)[0]; 12 | } catch (err) { 13 | const element = document.createElement('div'); 14 | element.className = styleRoot; 15 | node = document.body.appendChild(element); 16 | } 17 | 18 | const root = sendString => render(, node); 19 | 20 | export default root; 21 | -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class LoginForm extends Component { 4 | static propTypes = { 5 | inputs: PropTypes.arrayOf( 6 | PropTypes.shape({ 7 | label: PropTypes.string, 8 | type: PropTypes.string, 9 | }) 10 | ), 11 | style: PropTypes.object, 12 | valid: PropTypes.bool, 13 | reset: PropTypes.func, 14 | submit: PropTypes.func, 15 | twoFactor: PropTypes.bool, 16 | }; 17 | 18 | getFormData() { 19 | return Object.keys(this.refs).map( 20 | ref => this.refs[ref].value 21 | ); 22 | } 23 | 24 | render() { 25 | const { style: s, valid, inputs, reset, submit } = this.props; 26 | return ( 27 |
28 | {inputs.map( 29 | (input, index) => { 30 | if (input.condition && !this.props[input.condition]) return null; 31 | return ( 32 |
42 | 45 | 58 |
59 | ); 60 | } 61 | )} 62 |
22 |

23 | {header} 24 |

25 | 26 | )} 27 | 28 | {children && ( 29 |
30 | {children} 31 |
32 | )} 33 | 34 | {footer && ( 35 |
36 | {footer.status} 37 | {footer.button && ( 38 | 45 | )} 46 |
47 | )} 48 | 49 | 50 | 51 | 52 | )} 53 | {display && ( 54 |
58 | )} 59 | 60 | ); 61 | 62 | Modal.propTypes = { 63 | header: PropTypes.string, 64 | children: PropTypes.node, 65 | footer: PropTypes.shape({ 66 | button: PropTypes.oneOfType([ 67 | PropTypes.string, 68 | PropTypes.bool, 69 | ]), 70 | click: PropTypes.func, 71 | status: PropTypes.string, 72 | }), 73 | style: PropTypes.object, 74 | fadeIn: PropTypes.bool, 75 | display: PropTypes.bool, 76 | toggle: PropTypes.func, 77 | }; 78 | 79 | export default Modal; 80 | -------------------------------------------------------------------------------- /src/components/Modal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Modal", 3 | "version": "1.0.0", 4 | "main": "./Modal.js" 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { filter } from 'fuzzaldrin'; 3 | 4 | class Search extends Component { 5 | static propTypes = { 6 | style: PropTypes.object, 7 | accounts: PropTypes.array, 8 | done: PropTypes.func, 9 | sendString: PropTypes.func, 10 | }; 11 | 12 | componentWillMount() { 13 | this.setState({ 14 | search: '', 15 | }); 16 | } 17 | 18 | updateSearch = event => { 19 | const { value: search } = event.target; 20 | this.setState({ search }); 21 | } 22 | 23 | autofill = password => event => { 24 | if (event) event.preventDefault(); 25 | this.props.sendString(password); 26 | this.props.done(true); 27 | } 28 | 29 | render() { 30 | const { style: s, accounts } = this.props; 31 | const { search } = this.state; 32 | return ( 33 |
34 |
35 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {filter(accounts, search, { key: 'name' }).map( 54 | (account, index) => { 55 | const { id, name, username, password } = account; 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | )} 65 | 66 |
#AccountUsername
{index}{name}{username}
67 |
68 |
69 | ); 70 | } 71 | } 72 | 73 | export default Search; 74 | -------------------------------------------------------------------------------- /src/components/Search/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Search", 3 | "version": "1.0.0", 4 | "main": "./Search.js" 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/getAccounts.js: -------------------------------------------------------------------------------- 1 | import Lastpass from 'lastpass'; 2 | 3 | export default async ({ updateStatus, username, password, twoFactor } = {}) => { 4 | let log = () => undefined; 5 | if (updateStatus) log = updateStatus; 6 | 7 | let accounts; 8 | 9 | const lpass = new Lastpass(username); 10 | try { 11 | log('Trying to load a vault file.'); 12 | await lpass.loadVaultFile(); 13 | log('Loaded vault file successfully!'); 14 | } catch (err) { 15 | log('Could not load vault file...'); 16 | try { 17 | log('Trying to get a new vault from Lastpass.'); 18 | await lpass.loadVault(username, password, twoFactor); 19 | log('Got a new vault from Lastpass successfully!'); 20 | } catch (errLoad) { 21 | const twoFactorError = ( 22 | errLoad && 23 | errLoad.body && 24 | errLoad.body.cause === 'googleauthrequired' 25 | ); 26 | 27 | log( 28 | `Couldn\'t get new vault. ${ 29 | twoFactorError ? 30 | 'Maybe you need to enter your 2 factor authentication?' : 31 | 'Try again?' 32 | }` 33 | ); 34 | 35 | return { 36 | success: false, 37 | error: err, 38 | twoFactorError, 39 | }; 40 | } 41 | } 42 | 43 | try { 44 | log('Double checking that the vault is loaded.'); 45 | lpass.getVault(); 46 | log('Vault is indeed loaded!'); 47 | } catch (err) { 48 | log('Vault is still not loaded. Try again?'); 49 | return { 50 | success: false, 51 | error: err, 52 | }; 53 | } 54 | 55 | try { 56 | log('Trying to decrypt accounts.'); 57 | accounts = await lpass.getAccounts(username, password); 58 | log('Accounts decrypted successfully!'); 59 | } catch (err) { 60 | log('Accounts could not be decrypted...'); 61 | return { 62 | success: false, 63 | error: err, 64 | }; 65 | } 66 | 67 | if (accounts) { 68 | setTimeout( 69 | async () => { 70 | try { 71 | log('Trying to save vault file.'); 72 | await lpass.saveVaultFile(); 73 | log('Vault file saved!'); 74 | } catch (err) { 75 | log('Could not save vault file...'); 76 | } 77 | }, 2000 78 | ); 79 | } 80 | 81 | return { 82 | success: true, 83 | accounts, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | @import "~bootstrap/scss/bootstrap"; 3 | font-family: $font-family-base; 4 | font-size: $font-size-base; 5 | line-height: $line-height-base; 6 | color: $body-color; 7 | background-color: $body-bg; 8 | } 9 | -------------------------------------------------------------------------------- /src/views/Lastpass/Lastpass.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Modal from '../../components/Modal'; 3 | import LoginForm from '../../components/LoginForm'; 4 | import Search from '../../components/Search'; 5 | import getAccounts from '../../lib/getAccounts'; 6 | 7 | let loginForm; 8 | let decryptedAccounts; 9 | 10 | class Lastpass extends Component { 11 | static propTypes = { 12 | style: PropTypes.object, 13 | sendString: PropTypes.func, 14 | }; 15 | 16 | componentWillMount() { 17 | this.setState({ 18 | fadeIn: false, 19 | display: false, 20 | valid: undefined, 21 | status: '', 22 | twoFactor: false, 23 | loggedIn: false, 24 | }); 25 | } 26 | 27 | setValidity = (event, validity) => { 28 | if (event) event.preventDefault(); 29 | if (this.state.valid === validity) return; 30 | let valid; 31 | if (typeof validity !== 'undefined') valid = !!validity; 32 | this.setState({ valid }); 33 | } 34 | 35 | handleToggle = event => { 36 | if (event) event.preventDefault(); 37 | 38 | // Reset everything if toggling the modal 39 | this.setValidity(); 40 | this.updateStatus(''); 41 | this.logOut(); 42 | 43 | const { fadeIn, display } = this.state; 44 | 45 | if (!fadeIn && !display) { 46 | this.setState({ display: !display }); 47 | setTimeout( 48 | () => this.setState({ fadeIn: !fadeIn }), 49 | 200 50 | ); 51 | } else if (fadeIn && display) { 52 | this.setState({ fadeIn: !fadeIn }); 53 | setTimeout( 54 | () => this.setState({ display: !display }), 55 | 200 56 | ); 57 | } 58 | } 59 | 60 | updateStatus = status => { 61 | if (this.state.status === status) return; 62 | this.setState({ status }); 63 | }; 64 | 65 | handleFormSubmit = event => { 66 | if (event) event.preventDefault(); 67 | const values = loginForm.getFormData(); 68 | const username = values[0]; 69 | const password = values[1]; 70 | const twoFactor = values[2] && values[2].length > 1 ? values[2] : undefined; 71 | (async () => { 72 | const { accounts, success, error, twoFactorError } = await getAccounts({ 73 | updateStatus: this.updateStatus, 74 | username, 75 | password, 76 | twoFactor, 77 | }); 78 | if (success) { 79 | this.setValidity(null, true); 80 | decryptedAccounts = accounts; 81 | this.setState({ twoFactor: false, loggedIn: true }); 82 | } else { 83 | this.setValidity(null, false); 84 | if (twoFactorError) this.setState({ twoFactor: true }); 85 | console.error(error); 86 | } 87 | })(); 88 | } 89 | 90 | logOut = close => { 91 | decryptedAccounts = undefined; 92 | this.setState({ loggedIn: false }); 93 | if (close) this.handleToggle(); 94 | } 95 | 96 | render() { 97 | const { style, sendString } = this.props; 98 | const { status, fadeIn, display, valid, twoFactor, loggedIn } = this.state; 99 | return ( 100 | 112 | {loggedIn && ( 113 |
114 | Warning!  115 | 116 | At this time your account is unclocked and passwords are decrypted in memory. 117 | 118 |
119 | )} 120 | {loggedIn ? ( 121 | 127 | ) : ( 128 | { 131 | if (c === null) return; 132 | loginForm = c; 133 | } 134 | } 135 | style={style} 136 | valid={valid} 137 | inputs={[ 138 | { 139 | label: 'Username:', 140 | type: 'text', 141 | placeholder: 'Enter your email', 142 | }, 143 | { 144 | label: 'Password:', 145 | type: 'password', 146 | placeholder: 'Enter your password', 147 | }, 148 | { 149 | label: 'Two Factor Authentication PIN (optional):', 150 | type: 'text', 151 | condition: 'twoFactor', 152 | placeholder: 'Enter your PIN', 153 | }, 154 | ]} 155 | reset={this.setValidity} 156 | submit={this.handleFormSubmit} 157 | twoFactor={twoFactor} 158 | /> 159 | )} 160 |
161 | ); 162 | } 163 | } 164 | 165 | export default Lastpass; 166 | -------------------------------------------------------------------------------- /src/views/Lastpass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lastpass", 3 | "version": "1.0.0", 4 | "main": "./Lastpass.js" 5 | } 6 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import webpackConfig from './webpack.config'; 3 | import { WATCH } from './flags'; 4 | 5 | const compiler = webpack(webpackConfig); 6 | const done = (error, stats) => { 7 | if (error) throw error; 8 | console.log(stats.toString(webpackConfig.stats)); 9 | }; 10 | 11 | if (WATCH) { 12 | compiler.watch({}, done); 13 | } else { 14 | compiler.run(done); 15 | } 16 | -------------------------------------------------------------------------------- /tools/flags.js: -------------------------------------------------------------------------------- 1 | export const VERBOSE = process.argv.includes('--verbose'); 2 | export const DEBUG = !process.argv.includes('--release'); 3 | export const WATCH = process.argv.includes('--watch'); 4 | -------------------------------------------------------------------------------- /tools/webpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve as resolvePath } from 'path'; 2 | import webpack from 'webpack'; 3 | import { DEBUG, VERBOSE } from './flags'; 4 | 5 | const SRC = resolvePath(__dirname, '../src'); 6 | const DIST = resolvePath(__dirname, '../dist'); 7 | 8 | export default { 9 | 10 | target: 'node', 11 | 12 | context: SRC, 13 | 14 | entry: [ 15 | './app.js', 16 | ], 17 | 18 | output: { 19 | path: DIST, 20 | libraryTarget: 'commonjs2', 21 | filename: './main.js', 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.js$/, 28 | loader: 'babel-loader', 29 | include: [ 30 | resolvePath(__dirname, '../src'), 31 | ], 32 | query: { 33 | cacheDirectory: DEBUG, 34 | babelrc: false, 35 | presets: [ 36 | 'react', 37 | 'es2015', 38 | 'stage-0', 39 | ], 40 | plugins: [ 41 | 'transform-runtime', 42 | ], 43 | }, 44 | }, 45 | { 46 | test: /\.json$/, 47 | loader: 'json-loader', 48 | }, 49 | { 50 | test: /\.scss$/, 51 | loaders: [ 52 | 'style-loader', 53 | `css-loader?${JSON.stringify({ 54 | sourceMap: DEBUG, 55 | modules: true, 56 | localIdentName: `lastpass_${ 57 | DEBUG ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]' 58 | }`, 59 | minimize: !DEBUG, 60 | })}`, 61 | 'sass-loader', 62 | ], 63 | }, 64 | ], 65 | }, 66 | 67 | resolve: { 68 | root: SRC, 69 | modulesDirectories: ['node_modules'], 70 | extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx', '.json'], 71 | }, 72 | 73 | cache: DEBUG, 74 | debug: DEBUG, 75 | 76 | stats: { 77 | colors: true, 78 | reasons: DEBUG, 79 | hash: VERBOSE, 80 | version: VERBOSE, 81 | timings: true, 82 | chunks: VERBOSE, 83 | chunkModules: VERBOSE, 84 | cached: VERBOSE, 85 | cachedAssets: VERBOSE, 86 | }, 87 | 88 | plugins: DEBUG ? [] : [ 89 | new webpack.DefinePlugin({ 90 | 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"', 91 | __DEV__: DEBUG, 92 | 'process.env.BROWSER': true, 93 | }), 94 | new webpack.optimize.DedupePlugin(), 95 | new webpack.optimize.UglifyJsPlugin({ 96 | compress: { screw_ie8: true, warnings: false }, 97 | }), 98 | new webpack.optimize.AggressiveMergingPlugin(), 99 | ], 100 | 101 | node: { 102 | console: false, 103 | global: false, 104 | process: false, 105 | Buffer: false, 106 | __filename: false, 107 | __dirname: false, 108 | setImmediate: false, 109 | crypto: false, 110 | fs: false, 111 | }, 112 | 113 | devtool: DEBUG ? 'cheap-module-eval-source-map' : false, 114 | }; 115 | --------------------------------------------------------------------------------