Remove keybase plugin

This commit is contained in:
Ben Gotow 2017-06-23 18:23:12 -07:00
parent 9977c566df
commit 07428a1060
31 changed files with 0 additions and 3256 deletions

View file

@ -1,14 +0,0 @@
## Keybase Plugin
TODO:
-----
* final refactor
* tests
WISHLIST:
-----
* message signing
* encrypted file handling
* integrate MIT PGP Keyserver search into Keybase searchbar
* make the decrypt interface a message body overlay instead of a button in the header
* improve search result deduping with keys on file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,143 +0,0 @@
{MessageStore, React, ReactDOM, FileDownloadStore, MessageBodyProcessor, Actions} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
{remote} = require 'electron'
PassphrasePopover = require './passphrase-popover'
PrivateKeyPopover = require './private-key-popover'
pgp = require 'kbpgp'
_ = require 'underscore'
class DecryptMessageButton extends React.Component
@displayName: 'DecryptMessageButton'
@propTypes:
message: React.PropTypes.object.isRequired
constructor: (props) ->
super(props)
@state = @_getStateFromStores()
_getStateFromStores: ->
return {
isDecrypted: PGPKeyStore.isDecrypted(@props.message)
wasEncrypted: PGPKeyStore.hasEncryptedComponent(@props.message)
encryptedAttachments: PGPKeyStore.fetchEncryptedAttachments(@props.message)
status: PGPKeyStore.msgStatus(@props.message)
}
componentDidMount: ->
@unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)
componentWillUnmount: ->
@unlistenKeystore()
_onKeystoreChange: ->
# every time a new key gets unlocked/fetched, try to decrypt this message
if not @state.isDecrypted
PGPKeyStore.decrypt(@props.message)
@setState(@_getStateFromStores())
_onClickDecrypt: (event) =>
popoverTarget = event.target.getBoundingClientRect()
if @_noPrivateKeys()
Actions.openPopover(
<PrivateKeyPopover
addresses={_.pluck(@props.message.to, "email")}
callback={=> @_openPassphrasePopover(popoverTarget, @decryptPopoverDone)}/>,
{originRect: popoverTarget, direction: 'down'}
)
else
@_openPassphrasePopover(popoverTarget, @decryptPopoverDone)
_displayError: (err) ->
dialog = remote.dialog
dialog.showErrorBox('Decryption Error', err.toString())
_onClickDecryptAttachments: (event) =>
popoverTarget = event.target.getBoundingClientRect()
if @_noPrivateKeys()
Actions.openPopover(
<PrivateKeyPopover
addresses={_.pluck(@props.message.to, "email")}
callback={=> @_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)}/>,
{originRect: popoverTarget, direction: 'down'}
)
else
@_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)
decryptPopoverDone: (passphrase) =>
for recipient in @props.message.to
# right now, just try to unlock all possible keys
# (many will fail - TODO?)
privateKeys = PGPKeyStore.privKeys(address: recipient.email, timed: false)
for privateKey in privateKeys
PGPKeyStore.getKeyContents(key: privateKey, passphrase: passphrase)
decryptAttachmentsPopoverDone: (passphrase) =>
for recipient in @props.message.to
privateKeys = PGPKeyStore.privKeys(address: recipient.email, timed: false)
for privateKey in privateKeys
PGPKeyStore.getKeyContents(key: privateKey, passphrase: passphrase, callback: (identity) => PGPKeyStore.decryptAttachments(identity, @state.encryptedAttachments))
_openPassphrasePopover: (target, callback) =>
Actions.openPopover(
<PassphrasePopover addresses={_.pluck(@props.message.to, "email")} onPopoverDone={callback} />,
{originRect: target, direction: 'down'}
)
_noPrivateKeys: =>
numKeys = 0
for recipient in @props.message.to
numKeys = numKeys + PGPKeyStore.privKeys(address: recipient.email, timed: false).length
return numKeys < 1
render: =>
if not (@state.wasEncrypted or @state.encryptedAttachments.length > 0)
return false
title = "Message Encrypted"
decryptLabel = "Decrypt"
borderClass = "border"
decryptClass = "decrypt-bar"
if @state.status?
if @state.status.indexOf("Message decrypted") >= 0
title = @state.status
borderClass = "border done-border"
decryptClass = "decrypt-bar done-decrypt-bar"
else if @state.status.indexOf("Unable to decrypt message.") >= 0
title = @state.status
borderClass = "border error-border"
decryptClass = "decrypt-bar error-decrypt-bar"
decryptLabel = "Try Again"
decryptBody = false
if !@state.isDecrypted and !(@state.status?.indexOf("malformed") >= 0)
decryptBody = <button title="Decrypt email body" className="btn btn-toolbar" onClick={@_onClickDecrypt} ref="button">{decryptLabel}</button>
decryptAttachments = false
if @state.encryptedAttachments?.length >= 1
title = if @state.encryptedAttachments.length == 1 then "Attachment Encrypted" else "Attachments Encrypted"
buttonLabel = if @state.encryptedAttachments.length == 1 then "Decrypt Attachment" else "Decrypt Attachments"
decryptAttachments = <button onClick={ @_onClickDecryptAttachments } className="btn btn-toolbar">{buttonLabel}</button>
if decryptAttachments or decryptBody
decryptionInterface =
<div className="decryption-interface">
{decryptBody}
{decryptAttachments}
</div>
<div className="keybase-decrypt">
<div className="line-w-label">
<div className={borderClass}></div>
<div className={decryptClass}>
<div className="title-text">
{title}
</div>
{decryptionInterface}
</div>
<div className={borderClass}></div>
</div>
</div>
module.exports = DecryptMessageButton

View file

@ -1,15 +0,0 @@
{MessageViewExtension, Actions} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
class DecryptPGPExtension extends MessageViewExtension
@formatMessageBody: ({message}) =>
if not PGPKeyStore.hasEncryptedComponent(message)
return message
if PGPKeyStore.isDecrypted(message)
message.body = PGPKeyStore.getDecrypted(message)
else
# trigger a decryption
PGPKeyStore.decrypt(message)
message
module.exports = DecryptPGPExtension

View file

@ -1,31 +0,0 @@
{React, Actions} = require 'nylas-exports'
{ParticipantsTextField} = require 'nylas-component-kit'
Identity = require './identity'
_ = require 'underscore'
module.exports =
class EmailPopover extends React.Component
constructor: ->
@state = {to: [], cc: [], bcc: []}
@propTypes:
profile: React.PropTypes.instanceOf(Identity).isRequired
render: ->
participants = @state
<div className="keybase-import-popover">
<ParticipantsTextField
field="to"
className="keybase-participant-field"
participants={ participants }
change={ @_onRecipientFieldChange } />
<button className="btn btn-toolbar" onClick={ @_onDone }>Associate Emails with Key</button>
</div>
_onRecipientFieldChange: (contacts) =>
@setState(contacts)
_onDone: =>
@props.onPopoverDone(_.pluck(@state.to, 'email'), @props.profile)
Actions.closePopover()

View file

@ -1,160 +0,0 @@
{Utils, DraftStore, React, Actions, DatabaseStore, Contact, ReactDOM} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
Identity = require './identity'
ModalKeyRecommender = require './modal-key-recommender'
{RetinaImg} = require 'nylas-component-kit'
{remote} = require 'electron'
pgp = require 'kbpgp'
_ = require 'underscore'
class EncryptMessageButton extends React.Component
@displayName: 'EncryptMessageButton'
# require that we have a draft object available
@propTypes:
draft: React.PropTypes.object.isRequired
session: React.PropTypes.object.isRequired
constructor: (props) ->
super(props)
# plaintext: store the message's plaintext in case the user wants to edit
# further after hitting the "encrypt" button (i.e. so we can "undo" the
# encryption)
# cryptotext: store the message's body here, for comparison purposes (so
# that if the user edits an encrypted message, we can revert it)
@state = {plaintext: "", cryptotext: "", currentlyEncrypted: false}
componentDidMount: ->
@unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)
componentWillUnmount: ->
@unlistenKeystore()
componentWillReceiveProps: (nextProps) ->
if @state.currentlyEncrypted and nextProps.draft.body != @props.draft.body and nextProps.draft.body != @state.cryptotext
# A) we're encrypted
# B) someone changed something
# C) the change was AWAY from the "correct" cryptotext
body = @state.cryptotext
@props.session.changes.add({body: body})
_getKeys: ->
keys = []
for recipient in @props.draft.participants({includeFrom: false, includeBcc: true})
publicKeys = PGPKeyStore.pubKeys(recipient.email)
if publicKeys.length < 1
# no key for this user
keys.push(new Identity({addresses: [recipient.email]}))
else
# note: this, by default, encrypts using every public key associated
# with the address
for publicKey in publicKeys
if not publicKey.key?
PGPKeyStore.getKeyContents(key: publicKey)
else
keys.push(publicKey)
return keys
_onKeystoreChange: =>
# if something changes with the keys, check to make sure the recipients
# haven't changed (thus invalidating our encrypted message)
if @state.currentlyEncrypted
newKeys = _.map(@props.draft.participants(), (participant) ->
return PGPKeyStore.pubKeys(participant.email)
)
newKeys = _.flatten(newKeys)
oldKeys = _.map(@props.draft.participants(), (participant) ->
return PGPKeyStore.pubKeys(participant.email)
)
oldKeys = _.flatten(oldKeys)
if newKeys.length != oldKeys.length
# someone added/removed a key - our encrypted body is now out of date
@_toggleCrypt()
render: ->
classnames = "btn btn-toolbar"
if @state.currentlyEncrypted
classnames += " btn-enabled"
<div className="n1-keybase">
<button title="Encrypt email body" className={ classnames } onClick={ => @_onClick()} ref="button">
<RetinaImg url="nylas://keybase/encrypt-composer-button@2x.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
</div>
_onClick: =>
@_toggleCrypt()
_toggleCrypt: =>
# if decrypted, encrypt, and vice versa
# addresses which don't have a key
if @state.currentlyEncrypted
# if the message is already encrypted, place the stored plaintext back
# in the draft (i.e. un-encrypt)
@props.session.changes.add({body: @state.plaintext})
@setState({currentlyEncrypted: false})
else
# if not encrypted, save the plaintext, then encrypt
plaintext = @props.draft.body
identities = @_getKeys()
@_checkKeysAndEncrypt(plaintext, identities, (err, cryptotext) =>
if err
console.warn err
Actions.recordUserEvent("Email Encryption Errored", {error: err})
NylasEnv.showErrorDialog(err)
if cryptotext? and cryptotext != ""
Actions.recordUserEvent("Email Encrypted")
# <pre> tag prevents gross HTML formatting in-flight
cryptotext = "<pre>#{cryptotext}</pre>"
@setState({
currentlyEncrypted: true
plaintext: plaintext
cryptotext: cryptotext
})
@props.session.changes.add({body: cryptotext})
)
_encrypt: (text, identities, cb) =>
# get the actual key objects
keys = _.pluck(identities, "key")
# remove the nulls
kms = _.compact(keys)
if kms.length == 0
NylasEnv.showErrorDialog("There are no PGP public keys loaded, so the message cannot be
encrypted. Compose a message, add recipients in the To: field, and try again.")
return
params =
encrypt_for: kms
msg: text
pgp.box(params, cb)
_checkKeysAndEncrypt: (text, identities, cb) =>
emails = _.chain(identities)
.pluck("addresses")
.flatten()
.uniq()
.value()
if _.every(identities, (identity) -> identity.key?)
# every key is present and valid
@_encrypt(text, identities, cb)
else
# open a popover to correct null keys
DatabaseStore.findAll(Contact, {email: emails}).then((contacts) =>
component = (<ModalKeyRecommender contacts={contacts} emails={emails} callback={ (newIdentities) => @_encrypt(text, newIdentities, cb) }/>)
Actions.openPopover(
component,
{
originRect: ReactDOM.findDOMNode(@).getBoundingClientRect(),
direction: 'up',
closeOnAppBlur: false,
})
)
module.exports = EncryptMessageButton

View file

@ -1,53 +0,0 @@
# A single user identity: a key, a way to find that key, one or more email
# addresses, and a keybase profile
{Utils} = require 'nylas-exports'
path = require 'path'
module.exports =
class Identity
constructor: ({key, addresses, isPriv, keybase_profile}) ->
@id = Utils.generateTempId()
@key = key ? null # keybase keymanager object
@isPriv = isPriv ? false # is this a private key?
@timeout = null # the time after which this key (if private) needs to be unlocked again
@addresses = addresses ? [] # email addresses associated with this identity
@keybase_profile = keybase_profile ? null # a kb profile object associated with this identity
Object.defineProperty(@, 'keyPath', {
get: ->
if @addresses.length > 0
keyDir = path.join(NylasEnv.getConfigDirPath(), 'keys')
thisDir = if @isPriv then path.join(keyDir, 'private') else path.join(keyDir, 'public')
keyPath = path.join(thisDir, @addresses.join(" "))
else
keyPath = null
return keyPath
})
if @isPriv
@setTimeout()
fingerprint: ->
if @key?
return @key.get_pgp_fingerprint().toString('hex')
return null
setTimeout: ->
delay = 1000 * 60 * 30 # 30 minutes in ms
@timeout = Date.now() + delay
isTimedOut: ->
return @timeout < Date.now()
uid: ->
if @key?
uid = @key.get_pgp_fingerprint().toString('hex')
else if @keybase_profile?
uid = @keybase_profile.components.username.val
else if @addresses.length > 0
uid = @addresses.join('')
else
uid = @id
return uid

View file

@ -1,213 +0,0 @@
{Utils, React, RegExpUtils} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
PGPKeyStore = require './pgp-key-store'
Identity = require './identity'
kb = require './keybase'
pgp = require 'kbpgp'
_ = require 'underscore'
fs = require 'fs'
module.exports =
class KeyAdder extends React.Component
@displayName: 'KeyAdder'
constructor: (props) ->
@state =
address: ""
keyContents: ""
passphrase: ""
generate: false
paste: false
import: false
isPriv: false
loading: false
validAddress: false
validKeyBody: false
_onPasteButtonClick: (event) =>
@setState
generate: false
paste: !@state.paste
import: false
address: ""
validAddress: false
keyContents: ""
_onGenerateButtonClick: (event) =>
@setState
generate: !@state.generate
paste: false
import: false
address: ""
validAddress: false
keyContents: ""
passphrase: ""
_onImportButtonClick: (event) =>
NylasEnv.showOpenDialog({
title: "Import PGP Key",
buttonLabel: "Import",
properties: ['openFile']
}, (filepath) =>
if filepath?
@setState
generate: false
paste: false
import: true
address: ""
validAddress: false
passphrase: ""
fs.readFile(filepath[0], (err, data) =>
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
if err
PGPKeyStore._displayError("File is not a valid PGP key.")
return
else
privateStart = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
keyBody = if km.armored_pgp_private? then km.armored_pgp_private else km.armored_pgp_public
@setState
keyContents: keyBody
isPriv: keyBody.indexOf(privateStart) >= 0
validKeyBody: true
)
)
_onInnerGenerateButtonClick: (event) =>
@setState
loading: true
@_generateKeypair()
_generateKeypair: =>
pgp.KeyManager.generate_rsa { userid : @state.address }, (err, km) =>
km.sign {}, (err) =>
if err
console.warn(err)
km.export_pgp_private {passphrase: @state.passphrase}, (err, pgp_private) =>
ident = new Identity({
addresses: [@state.address]
isPriv: true
})
PGPKeyStore.saveNewKey(ident, pgp_private)
km.export_pgp_public {}, (err, pgp_public) =>
ident = new Identity({
addresses: [@state.address]
isPriv: false
})
PGPKeyStore.saveNewKey(ident, pgp_public)
@setState
keyContents: pgp_public
loading: false
_saveNewKey: =>
ident = new Identity({
addresses: [@state.address]
isPriv: @state.isPriv
})
PGPKeyStore.saveNewKey(ident, @state.keyContents)
_onAddressChange: (event) =>
address = event.target.value
valid = false
if (address and address.length > 0 and RegExpUtils.emailRegex().test(address))
valid = true
@setState
address: event.target.value
validAddress: valid
_onPassphraseChange: (event) =>
@setState
passphrase: event.target.value
_onKeyChange: (event) =>
privateStart = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
@setState
keyContents: event.target.value
isPriv: event.target.value.indexOf(privateStart) >= 0
pgp.KeyManager.import_from_armored_pgp {
armored: event.target.value
}, (err, km) =>
if err
valid = false
else
valid = true
@setState
validKeyBody: valid
_renderAddButtons: ->
<div>
Add a PGP Key:
<button className="btn key-creation-button" title="Paste" onClick={@_onPasteButtonClick}>Paste in a New Key</button>
<button className="btn key-creation-button" title="Import" onClick={@_onImportButtonClick}>Import a Key From File</button>
<button className="btn key-creation-button" title="Generate" onClick={@_onGenerateButtonClick}>Generate a New Keypair</button>
</div>
_renderManualKey: ->
if !@state.validAddress and @state.address.length > 0
invalidMsg = <span className="invalid-msg">Invalid email address</span>
else if !@state.validKeyBody and @state.keyContents.length > 0
invalidMsg = <span className="invalid-msg">Invalid key body</span>
else
invalidMsg = <span className="invalid-msg"> </span>
invalidInputs = !(@state.validAddress and @state.validKeyBody)
buttonClass = if invalidInputs then "btn key-add-btn btn-disabled" else "btn key-add-btn"
passphraseInput = <input type="password" value={@state.passphrase} placeholder="Private Key Password" className="key-passphrase-input" onChange={@_onPassphraseChange} />
<div className="key-adder">
<div className="key-text">
<textarea ref="key-input"
value={@state.keyContents || ""}
onChange={@_onKeyChange}
placeholder="Paste in your PGP key here!"/>
</div>
<div className="credentials">
<input type="text" value={@state.address} placeholder="Email Address" className="key-email-input" onChange={@_onAddressChange} />
{if @state.isPriv then passphraseInput}
{invalidMsg}
<button className={buttonClass} disabled={invalidInputs} title="Save" onClick={@_saveNewKey}>Save</button>
</div>
</div>
_renderGenerateKey: ->
if !@state.validAddress and @state.address.length > 0
invalidMsg = <span className="invalid-msg">Invalid email address</span>
else
invalidMsg = <span className="invalid-msg"> </span>
loading = <RetinaImg style={width: 20, height: 20} name="inline-loading-spinner.gif" mode={RetinaImg.Mode.ContentPreserve} />
if @state.loading
keyPlaceholder = "Generating your key now. This could take a while."
else
keyPlaceholder = "Your generated public key will appear here. Share it with your friends!"
buttonClass = if !@state.validAddress then "btn key-add-btn btn-disabled" else "btn key-add-btn"
<div className="key-adder">
<div className="credentials">
<input type="text" value={@state.address} placeholder="Email Address" className="key-email-input" onChange={@_onAddressChange} />
<input type="password" value={@state.passphrase} placeholder="Private Key Password" className="key-passphrase-input" onChange={@_onPassphraseChange} />
{invalidMsg}
<button className={buttonClass} disabled={!(@state.validAddress)} title="Generate" onClick={@_onInnerGenerateButtonClick}>Generate</button>
</div>
<div className="key-text">
<div className="loading">{if @state.loading then loading}</div>
<textarea ref="key-output"
value={@state.keyContents || ""}
disabled
placeholder={keyPlaceholder}/>
</div>
</div>
render: ->
<div>
{@_renderAddButtons()}
{if @state.generate then @_renderGenerateKey()}
{if @state.paste or @state.import then @_renderManualKey()}
</div>

View file

@ -1,97 +0,0 @@
{Utils, React, Actions} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
KeybaseUser = require './keybase-user'
PassphrasePopover = require './passphrase-popover'
kb = require './keybase'
_ = require 'underscore'
pgp = require 'kbpgp'
fs = require 'fs'
module.exports =
class KeyManager extends React.Component
@displayName: 'KeyManager'
@propTypes:
pubKeys: React.PropTypes.array.isRequired
privKeys: React.PropTypes.array.isRequired
constructor: (props) ->
super(props)
_exportPopoverDone: (passphrase, identity) =>
# check the passphrase before opening the save dialog
fs.readFile(identity.keyPath, (err, data) =>
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
if err
console.warn err
else
km.unlock_pgp { passphrase: passphrase }, (err) =>
if err
PGPKeyStore._displayError(err)
else
PGPKeyStore.exportKey({identity: identity, passphrase: passphrase})
)
_exportPrivateKey: (identity, event) =>
popoverTarget = event.target.getBoundingClientRect()
Actions.openPopover(
<PassphrasePopover identity={identity} addresses={identity.addresses} onPopoverDone={ @_exportPopoverDone } />,
{originRect: popoverTarget, direction: 'left'}
)
render: ->
{pubKeys, privKeys} = @props
pubKeys = pubKeys.map (identity) =>
deleteButton = (<button title="Delete Public" className="btn btn-toolbar btn-danger" onClick={ => PGPKeyStore.deleteKey(identity) } ref="button">
Delete Key
</button>
)
exportButton = (<button title="Export Public" className="btn btn-toolbar" onClick={ => PGPKeyStore.exportKey({identity: identity}) } ref="button">
Export Key
</button>
)
actionButton = (<div className="key-actions">
{exportButton}
{deleteButton}
</div>
)
return <KeybaseUser profile={identity} key={identity.clientId} actionButton={actionButton}/>
privKeys = privKeys.map (identity) =>
deleteButton = (<button title="Delete Private" className="btn btn-toolbar btn-danger" onClick={ => PGPKeyStore.deleteKey(identity) } ref="button">
Delete Key
</button>
)
exportButton = (<button title="Export Private" className="btn btn-toolbar" onClick={ (event) => @_exportPrivateKey(identity, event) } ref="button">
Export Key
</button>
)
actionButton = (<div className="key-actions">
{exportButton}
{deleteButton}
</div>
)
return <KeybaseUser profile={identity} key={identity.clientId} actionButton={actionButton}/>
<div className="key-manager">
<div className="line-w-label">
<div className="border"></div>
<div className="title-text">Saved Public Keys</div>
<div className="border"></div>
</div>
<div>
{ pubKeys }
</div>
<div className="line-w-label">
<div className="border"></div>
<div className="title-text">Saved Private Keys</div>
<div className="border"></div>
</div>
<div>
{ privKeys }
</div>
</div>

View file

@ -1,189 +0,0 @@
{Utils,
React,
ReactDOM,
Actions,
RegExpUtils,
IdentityStore,
AccountStore,
LegacyEdgehillAPI} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
EmailPopover = require './email-popover'
PGPKeyStore = require './pgp-key-store'
KeybaseUser = require '../lib/keybase-user'
Identity = require './identity'
kb = require './keybase'
_ = require 'underscore'
class KeybaseInviteButton extends React.Component
constructor: (@props) ->
@state = {
loading: false,
}
_onGetKeybaseInvite: =>
@setState({loading: true})
errorHandler = (err) =>
@setState({loading: false})
NylasEnv.showErrorDialog(err.message)
req = LegacyEdgehillAPI.makeRequest({
authWithNylasAPI: true
path: "/keybase-invite",
method: "POST",
body:
n1_id: IdentityStore.identityId(),
})
req.run()
.then((body) =>
@setState({loading: false})
try
if not (body instanceof Object) or not body.invite_url
throw new Error("We were unable to retrieve an invitation.")
catch err
errorHandler(err)
require('electron').shell.openExternal(body.invite_url)
)
.catch(errorHandler)
render: =>
if @state.loading
<a>Processing...</a>
else
<a onClick={@_onGetKeybaseInvite}>We've got an invite for you!</a>
module.exports =
class KeybaseSearch extends React.Component
@displayName: 'KeybaseSearch'
@propTypes:
initialSearch: React.PropTypes.string
# importFunc: a alternate function to execute when the "import" button is
# clicked instead of the "please specify an email" popover
importFunc: React.PropTypes.func
# TODO consider just passing in a pre-specified email instead of a func?
inPreferences: React.PropTypes.bool
@defaultProps:
initialSearch: ""
importFunc: null
inPreferences: false
constructor: (props) ->
super(props)
@state = {
query: props.initialSearch
results: []
loading: false
searchedByEmail: false
}
@debouncedSearch = _.debounce(@_search, 300)
componentDidMount: ->
@_search()
componentWillReceiveProps: (props) ->
@setState({query: props.initialSearch})
_search: ->
oldquery = @state.query
if @state.query != "" and @state.loading == false
@setState({loading: true})
kb.autocomplete(@state.query, (error, profiles) =>
if profiles?
profiles = _.map(profiles, (profile) ->
return new Identity({keybase_profile: profile, isPriv: false})
)
@setState({results: profiles, loading: false})
else
@setState({results: [], loading: false})
if @state.query != oldquery
@debouncedSearch()
)
else
# no query - empty out the results
@setState({results: []})
_importKey: (profile, event) =>
# opens a popover requesting user to enter 1+ emails to associate with a
# key - a button in the popover then calls _save to actually import the key
popoverTarget = event.target.getBoundingClientRect()
Actions.openPopover(
<EmailPopover profile={profile} onPopoverDone={ @_popoverDone } />,
{originRect: popoverTarget, direction: 'left'}
)
_popoverDone: (addresses, identity) =>
if addresses.length < 1
# no email addresses added, noop
return
else
identity.addresses = addresses
# TODO validate the addresses?
@_save(identity)
_save: (identity) =>
# save/import a key from keybase
keybaseUsername = identity.keybase_profile.components.username.val
kb.getKey(keybaseUsername, (error, key) =>
if error
console.error error
else
PGPKeyStore.saveNewKey(identity, key)
)
_queryChange: (event) =>
emailQuery = RegExpUtils.emailRegex().test(event.target.value)
@setState({query: event.target.value, searchedByEmail: emailQuery})
@debouncedSearch()
render: ->
profiles = _.map(@state.results, (profile) =>
# allow for overriding the import function
if typeof @props.importFunc is "function"
boundFunc = @props.importFunc
else
boundFunc = @_importKey
saveButton = (<button title="Import" className="btn btn-toolbar" onClick={ (event) => boundFunc(profile, event) } ref="button">
Import Key
</button>
)
# TODO improved deduping? tricky because of the kbprofile - email association
if not profile.keyPath?
return <KeybaseUser profile={profile} actionButton={ saveButton } />
)
if not profiles? or profiles.length < 1
profiles = []
badSearch = null
loading = null
empty = null
if profiles.length < 1 and @state.searchedByEmail
badSearch = <span className="bad-search-msg">Keybase cannot be searched by email address. <br/>Try entering a name, or a username from GitHub, Keybase or Twitter.</span>
if @state.loading
loading = <RetinaImg style={width: 20, height: 20, marginTop: 2} name="inline-loading-spinner.gif" mode={RetinaImg.Mode.ContentPreserve} />
if @props.inPreferences and not loading and not badSearch and profiles.length is 0
empty = <p className="empty">Not a Keybase user yet? <KeybaseInviteButton /></p>
<div className="keybase-search">
<div className="searchbar">
<input type="text" value={ @state.query } placeholder="Search for PGP public keys on Keybase" ref="searchbar" onChange={@_queryChange} />
{empty}
<div className="loading">{ loading }</div>
</div>
<div className="results" ref="results">
{ profiles }
{ badSearch }
</div>
</div>

View file

@ -1,135 +0,0 @@
{Utils, React, Actions} = require 'nylas-exports'
{ParticipantsTextField} = require 'nylas-component-kit'
PGPKeyStore = require './pgp-key-store'
EmailPopover = require './email-popover'
Identity = require './identity'
kb = require './keybase'
_ = require 'underscore'
module.exports =
class KeybaseUser extends React.Component
@displayName: 'KeybaseUserProfile'
@propTypes:
profile: React.PropTypes.instanceOf(Identity).isRequired
actionButton: React.PropTypes.node
displayEmailList: React.PropTypes.bool
@defaultProps:
actionButton: false
displayEmailList: true
constructor: (props) ->
super(props)
componentDidMount: ->
PGPKeyStore.getKeybaseData(@props.profile)
_addEmail: (email) =>
PGPKeyStore.addAddressToKey(@props.profile, email)
_addEmailClick: (event) =>
popoverTarget = event.target.getBoundingClientRect()
Actions.openPopover(
<EmailPopover profile={@props.profile} onPopoverDone={ @_popoverDone } />,
{originRect: popoverTarget, direction: 'left'}
)
_popoverDone: (addresses, identity) =>
if addresses.length < 1
# no email addresses added, noop
return
else
_.each(addresses, (address) =>
@_addEmail(address))
_removeEmail: (email) =>
PGPKeyStore.removeAddressFromKey(@props.profile, email)
render: =>
{profile} = @props
keybaseDetails = <div className="details"></div>
if profile.keybase_profile?
keybase = profile.keybase_profile
# profile picture
if keybase.thumbnail?
picture = <img className="user-picture" src={ keybase.thumbnail }/>
else
hue = Utils.hueForString("Keybase")
bgColor = "hsl(#{hue}, 50%, 45%)"
abv = "K"
picture = <div className="default-profile-image" style={{backgroundColor: bgColor}}>{abv}</div>
# full name
if keybase.components.full_name?.val?
fullname = keybase.components.full_name.val
else
fullname = username
username = false
# link to keybase profile
keybase_url = "keybase.io/#{keybase.components.username.val}"
if keybase_url.length > 25
keybase_string = keybase_url.slice(0, 23).concat('...')
else
keybase_string = keybase_url
username = <a href="https://#{keybase_url}">{keybase_string}</a>
# TODO: potentially display confirmation on keybase-user objects
###
possible_profiles = ["twitter", "github", "coinbase"]
profiles = _.map(possible_profiles, (possible) =>
if keybase.components[possible]?.val?
# TODO icon instead of weird "service: username" text
return (<span key={ possible }><b>{ possible }</b>: { keybase.components[possible].val }</span>)
)
profiles = _.reject(profiles, (profile) -> profile is undefined)
profiles = _.map(profiles, (profile) ->
return <span key={ profile.key }>{ profile } </span>)
profileList = (<span>{ profiles }</span>)
###
keybaseDetails = (<div className="details">
<div className="profile-name">
{ fullname }
</div>
<div className="profile-username">
{ username }
</div>
</div>)
else
# if no keybase profile, default image is based on email address
hue = Utils.hueForString(@props.profile.addresses[0])
bgColor = "hsl(#{hue}, 50%, 45%)"
abv = @props.profile.addresses[0][0].toUpperCase()
picture = <div className="default-profile-image" style={{backgroundColor: bgColor}}>{abv}</div>
# email addresses
if profile.addresses?.length > 0
emails = _.map(profile.addresses, (email) =>
# TODO make that remove button not terrible
return <li key={ email }>{ email } <small><a onClick={ => @_removeEmail(email) }>(X)</a></small></li>)
emailList = (<ul> { emails }
<a ref="addEmail" onClick={ @_addEmailClick }>+ Add Email</a>
</ul>)
emailListDiv = (<div className="email-list">
<ul>
{ emailList }
</ul>
</div>)
<div className="keybase-profile">
<div className="profile-photo-wrap">
<div className="profile-photo">
{ picture }
</div>
</div>
{ keybaseDetails }
{if @props.displayEmailList then emailListDiv}
{ @props.actionButton }
</div>

View file

@ -1,61 +0,0 @@
_ = require 'underscore'
request = require 'request'
class KeybaseAPI
constructor: ->
@baseUrl = "https://keybase.io"
getUser: (key, keyType, callback) =>
if not keyType in ['usernames', 'domain', 'twitter', 'github', 'reddit',
'hackernews', 'coinbase', 'key_fingerprint']
console.error 'keyType must be a supported Keybase query type.'
this._keybaseRequest("/_/api/1.0/user/lookup.json?#{keyType}=#{key}", (err, resp, obj) =>
return callback(err, null) if err
return callback(new Error("Empty response!"), null) if not obj? or not obj.them?
if obj.status?
return callback(new Error(obj.status.desc), null) if obj.status.name != "OK"
callback(null, _.map(obj.them, @_regularToAutocomplete))
)
getKey: (username, callback) =>
request({url: @baseUrl + "/#{username}/key.asc", headers: {'User-Agent': 'request'}}, (err, resp, obj) =>
return callback(err, null) if err
return callback(new Error("No key found for #{username}"), null) if not obj?
return callback(new Error("No key returned from keybase for #{username}"), null) if not obj.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")
callback(null, obj)
)
autocomplete: (query, callback) =>
url = "/_/api/1.0/user/autocomplete.json"
request({url: @baseUrl + url, form: {q: query}, headers: {'User-Agent': 'request'}, json: true}, (err, resp, obj) =>
return callback(err, null) if err
if obj.status?
return callback(new Error(obj.status.desc), null) if obj.status.name != "OK"
callback(null, obj.completions)
)
_keybaseRequest: (url, callback) =>
return request({url: @baseUrl + url, headers: {'User-Agent': 'request'}, json: true}, callback)
_regularToAutocomplete: (profile) ->
# converts a keybase profile to the weird format used in the autocomplete
# endpoint for backward compatability
# (does NOT translate accounts - e.g. twitter, github - yet)
# TODO this should be the other way around
cleanedProfile = {components: {}}
cleanedProfile.thumbnail = null
if profile.pictures?.primary?
cleanedProfile.thumbnail = profile.pictures.primary.url
safe_name = if profile.profile? then profile.profile.full_name else ""
cleanedProfile.components = {full_name: {val: safe_name }, username: {val: profile.basics.username}}
_.each(profile.proofs_summary.all, (connectedAccount) =>
component = {}
component[connectedAccount.proof_type] = {val: connectedAccount.nametag}
cleanedProfile.components = _.extend(cleanedProfile.components, component)
)
return cleanedProfile
module.exports = new KeybaseAPI()

View file

@ -1,34 +0,0 @@
import {PreferencesUIStore, ComponentRegistry, ExtensionRegistry} from 'nylas-exports';
import EncryptMessageButton from './encrypt-button';
import DecryptMessageButton from './decrypt-button';
import DecryptPGPExtension from './decryption-preprocess';
import RecipientKeyChip from './recipient-key-chip';
import PreferencesKeybase from './preferences-keybase';
const PREFERENCE_TAB_ID = 'Encryption'
export function activate() {
const preferencesTab = new PreferencesUIStore.TabItem({
tabId: PREFERENCE_TAB_ID,
displayName: 'Encryption',
component: PreferencesKeybase,
});
ComponentRegistry.register(EncryptMessageButton, {role: 'Composer:ActionButton'});
ComponentRegistry.register(DecryptMessageButton, {role: 'message:BodyHeader'});
ComponentRegistry.register(RecipientKeyChip, {role: 'Composer:RecipientChip'});
ExtensionRegistry.MessageView.register(DecryptPGPExtension);
PreferencesUIStore.registerPreferencesTab(preferencesTab);
}
export function deactivate() {
ComponentRegistry.unregister(EncryptMessageButton);
ComponentRegistry.unregister(DecryptMessageButton);
ComponentRegistry.unregister(RecipientKeyChip);
ExtensionRegistry.MessageView.unregister(DecryptPGPExtension);
PreferencesUIStore.unregisterPreferencesTab(PREFERENCE_TAB_ID);
}
export function serialize() {
return {};
}

View file

@ -1,157 +0,0 @@
{Utils, React, Actions} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
KeybaseSearch = require './keybase-search'
KeybaseUser = require './keybase-user'
kb = require './keybase'
_ = require 'underscore'
module.exports =
class ModalKeyRecommender extends React.Component
@displayName: 'ModalKeyRecommender'
@propTypes:
contacts: React.PropTypes.array.isRequired
emails: React.PropTypes.array
callback: React.PropTypes.func
@defaultProps:
callback: -> return # NOP
constructor: (props) ->
super(props)
@state = Object.assign({
currentContact: 0},
@_getStateFromStores())
componentDidMount: ->
@unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange)
componentWillUnmount: ->
@unlistenKeystore()
_onKeystoreChange: =>
@setState(@_getStateFromStores())
_getStateFromStores: =>
identities: PGPKeyStore.pubKeys(@props.emails)
_selectProfile: (address, identity) =>
# TODO this is an almost exact duplicate of keybase-search.cjsx:_save
keybaseUsername = identity.keybase_profile.components.username.val
identity.addresses.push(address)
kb.getKey(keybaseUsername, (error, key) =>
if error
console.error error
else
PGPKeyStore.saveNewKey(identity, key)
)
_onNext: =>
# NOTE: this doesn't do bounds checks! you must do that in render()!
@setState({currentContact: @state.currentContact + 1})
_onPrev: =>
# NOTE: this doesn't do bounds checks! you must do that in render()!
@setState({currentContact: @state.currentContact - 1})
_setPage: (page) =>
# NOTE: this doesn't do bounds checks! you must do that in render()!
@setState({currentContact: page})
# indexes from 0 because what kind of monster doesn't
_onDone: =>
if @state.identities.length < @props.emails.length
if !PGPKeyStore._displayDialog(
'Encrypt without keys for all recipients?',
'Some recipients are missing PGP public keys. They will not be able to decrypt this message.',
['Encrypt', 'Cancel']
)
return
emptyIdents = _.filter(@state.identities, (identity) -> !identity.key?)
if emptyIdents.length == 0
Actions.closePopover()
@props.callback(@state.identities)
else
newIdents = []
for idIndex of emptyIdents
identity = emptyIdents[idIndex]
if idIndex < emptyIdents.length - 1
PGPKeyStore.getKeyContents(key: identity, callback: (identity) => newIdents.push(identity))
else
PGPKeyStore.getKeyContents(key: identity, callback: (identity) =>
newIdents.push(identity)
@props.callback(newIdents)
Actions.closePopover()
)
_onManageKeys: =>
Actions.switchPreferencesTab('Encryption')
Actions.openPreferences()
render: ->
# find the email we're dealing with now
email = @props.emails[@state.currentContact]
# and a corresponding contact
contact = _.findWhere(@props.contacts, {'email': email})
contactString = if contact? then contact.toString() else email
# find the identity object that goes with this email (if any)
identity = _.find(@state.identities, (identity) ->
return email in identity.addresses
)
if @state.currentContact == (@props.emails.length - 1)
# last one
if @props.emails.length == 1
# only one
backButton = false
else
backButton = <button className="btn modal-back-button" onClick={ @_onPrev }>Back</button>
nextButton = <button className="btn modal-next-button" onClick={ @_onDone }>Done</button>
else if @state.currentContact == 0
# first one
backButton = false
nextButton = <button className="btn modal-next-button" onClick={ @_onNext }>Next</button>
else
# somewhere in the middle
backButton = <button className="btn modal-back-button" onClick={ @_onPrev }>Back</button>
nextButton = <button className="btn modal-next-button" onClick={ @_onNext }>Next</button>
if identity?
deleteButton = (<button title="Delete Public" className="btn btn-toolbar btn-danger" onClick={ => PGPKeyStore.deleteKey(identity) } ref="button">
Delete Key
</button>
)
body = [
<div key="title" className="picker-title">This PGP public key has been saved for <br/><b>{ contactString }.</b></div>
<div className="keybase-profile-solo">
<KeybaseUser key="keybase-user" profile={ identity }, displayEmailList={false}, actionButton={deleteButton}/>
</div>
]
else
if contact?
query = contact.fullName()
# don't search Keybase for emails, won't work anyways
if not query.match(/\s/)?
query = ""
else
query = ""
importFunc = ((identity) => @_selectProfile(email, identity))
body = [
<div key="title" className="picker-title">There is no PGP public key saved for <br/><b>{ contactString }.</b></div>
<KeybaseSearch key="keybase-search" initialSearch={ query }, importFunc={ importFunc } />
]
prefsButton = <button className="btn modal-prefs-button" onClick={@_onManageKeys}>Advanced Key Management</button>
<div className="key-picker-modal">
{ body }
<div style={{flex:1}}></div>
<div className="picker-controls">
<div style={{width: 60}}> { backButton } </div>
{ prefsButton }
<div style={{width: 60}}> { nextButton } </div>
</div>
</div>

View file

@ -1,80 +0,0 @@
{React, Actions} = require 'nylas-exports'
Identity = require './identity'
PGPKeyStore = require './pgp-key-store'
_ = require 'underscore'
fs = require 'fs'
pgp = require 'kbpgp'
module.exports =
class PassphrasePopover extends React.Component
constructor: ->
@state = {
passphrase: ""
placeholder: "PGP private key password"
error: false
mounted: true
}
componentDidMount: ->
@_mounted = true
componentWillUnmount: ->
@_mounted = false
@propTypes:
identity: React.PropTypes.instanceOf(Identity)
addresses: React.PropTypes.array
render: ->
classNames = if @state.error then "key-passphrase-input form-control bad-passphrase" else "key-passphrase-input form-control"
<div className="passphrase-popover">
<input type="password" value={@state.passphrase} placeholder={@state.placeholder} className={classNames} onChange={@_onPassphraseChange} onKeyUp={@_onKeyUp} />
<button className="btn btn-toolbar" onClick={@_validatePassphrase}>Done</button>
</div>
_onPassphraseChange: (event) =>
@setState
passphrase: event.target.value
placeholder: "PGP private key password"
error: false
_onKeyUp: (event) =>
if event.keyCode == 13
@_validatePassphrase()
_validatePassphrase: =>
passphrase = @state.passphrase
for emailIndex of @props.addresses
email = @props.addresses[emailIndex]
privateKeys = PGPKeyStore.privKeys(address: email, timed: false)
for keyIndex of privateKeys
# check to see if the password unlocks the key
key = privateKeys[keyIndex]
fs.readFile(key.keyPath, (err, data) =>
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
if err
console.warn err
else
km.unlock_pgp { passphrase: passphrase }, (err) =>
if err
if parseInt(keyIndex, 10) == privateKeys.length - 1
if parseInt(emailIndex, 10) == @props.addresses.length - 1
# every key has been tried, the password failed on all of them
if @_mounted
@setState
passphrase: ""
placeholder: "Incorrect password"
error: true
else
# the password unlocked a key; that key should be used
@_onDone()
)
_onDone: =>
if @props.identity?
@props.onPopoverDone(@state.passphrase, @props.identity)
else
@props.onPopoverDone(@state.passphrase)
Actions.closePopover()

View file

@ -1,498 +0,0 @@
NylasStore = require 'nylas-store'
{Actions, FileDownloadStore, DraftStore, MessageBodyProcessor, RegExpUtils} = require 'nylas-exports'
{remote, shell} = require 'electron'
Identity = require './identity'
kb = require './keybase'
pgp = require 'kbpgp'
_ = require 'underscore'
path = require 'path'
fs = require 'fs'
os = require 'os'
class PGPKeyStore extends NylasStore
constructor: ->
super()
@_identities = {}
@_msgCache = []
@_msgStatus = []
# Recursive subdir watching only works on OSX / Windows. annoying
@_pubWatcher = null
@_privWatcher = null
@_keyDir = path.join(NylasEnv.getConfigDirPath(), 'keys')
@_pubKeyDir = path.join(@_keyDir, 'public')
@_privKeyDir = path.join(@_keyDir, 'private')
# Create the key storage file system if it doesn't already exist
fs.access(@_keyDir, fs.R_OK | fs.W_OK, (err) =>
if err
fs.mkdir(@_keyDir, (err) =>
if err
console.warn err
else
fs.mkdir(@_pubKeyDir, (err) =>
if err
console.warn err
else
fs.mkdir(@_privKeyDir, (err) =>
if err
console.warn err
else
@watch())))
else
fs.access(@_pubKeyDir, fs.R_OK | fs.W_OK, (err) =>
if err
fs.mkdir(@_pubKeyDir, (err) =>
if err
console.warn err))
fs.access(@_privKeyDir, fs.R_OK | fs.W_OK, (err) =>
if err
fs.mkdir(@_privKeyDir, (err) =>
if err
console.warn err))
@_populate()
@watch())
validAddress: (address, isPub) =>
if (!address || address.length == 0)
@_displayError('You must provide an email address.')
return false
if not (RegExpUtils.emailRegex().test(address))
@_displayError('Invalid email address.')
return false
keys = if isPub then @pubKeys(address) else @privKeys({address: address, timed: false})
keystate = if isPub then 'public' else 'private'
if (keys.length > 0)
@_displayError("A PGP #{keystate} key for that email address already exists.")
return false
return true
### I/O and File Tracking ###
watch: =>
if (!@_pubWatcher)
@_pubWatcher = fs.watch(@_pubKeyDir, @_populate)
if (!@_privWatcher)
@_privWatcher = fs.watch(@_privKeyDir, @_populate)
unwatch: =>
if (@_pubWatcher)
@_pubWatcher.close()
@_pubWatcher = null
if (@_privWatcher)
@_privWatcher.close()
@_privWatcher = null
_populate: =>
# add identity elements to later be populated with keys from disk
# TODO if this function is called multiple times in quick succession it
# will duplicate keys - need to do deduplication on add
fs.readdir(@_pubKeyDir, (err, pubFilenames) =>
fs.readdir(@_privKeyDir, (err, privFilenames) =>
@_identities = {}
_.each([[pubFilenames, false], [privFilenames, true]], (readresults) =>
filenames = readresults[0]
i = 0
if filenames.length == 0
@trigger(@)
while i < filenames.length
filename = filenames[i]
if filename[0] == '.'
continue
ident = new Identity({
addresses: filename.split(" ")
isPriv: readresults[1]
})
@_identities[ident.id] = ident
@trigger(@)
i++)
)
)
getKeyContents: ({key, passphrase, callback}) =>
# Reads an actual PGP key from disk and adds it to the preexisting metadata
if not key.keyPath?
console.error "Identity has no path for key!", key
return
fs.readFile(key.keyPath, (err, data) =>
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
if err
console.warn err
else
if km.is_pgp_locked()
# private key - check passphrase
passphrase ?= ""
km.unlock_pgp { passphrase: passphrase }, (err) =>
if err
# decrypt checks all keys, so DON'T open an error dialog
console.warn err
return
else
key.key = km
key.setTimeout()
if callback?
callback(key)
else
# public key - get keybase data
key.key = km
key.setTimeout()
@getKeybaseData(key)
if callback?
callback(key)
@trigger(@)
)
getKeybaseData: (identity) =>
# Given a key, fetches metadata from keybase about that key
# TODO currently only works for public keys
if not identity.key? and not identity.isPriv and not identity.keybase_profile
@getKeyContents(key: identity)
else
fingerprint = identity.fingerprint()
if fingerprint?
kb.getUser(fingerprint, 'key_fingerprint', (err, user) =>
if err
console.error(err)
if user?.length == 1
identity.keybase_profile = user[0]
@trigger(@)
)
saveNewKey: (identity, contents) =>
# Validate the email address(es), then write to file.
if not identity instanceof Identity
console.error "saveNewKey requires an identity object"
return
addresses = identity.addresses
if addresses.length < 1
console.error "Identity must have at least one email address to save key"
return
if _.every(addresses, (address) => @validAddress(address, !identity.isPriv))
# Just say no to trailing whitespace.
if contents.charAt(contents.length - 1) != '-'
contents = contents.slice(0, -1)
fs.writeFile(identity.keyPath, contents, (err) =>
if (err)
@_displayError(err)
)
exportKey: ({identity, passphrase}) =>
atIndex = identity.addresses[0].indexOf("@")
suffix = if identity.isPriv then "-private.asc" else ".asc"
shortName = identity.addresses[0].slice(0, atIndex).concat(suffix)
NylasEnv.savedState.lastKeybaseDownloadDirectory ?= os.homedir()
savePath = path.join(NylasEnv.savedState.lastKeybaseDownloadDirectory, shortName)
@getKeyContents(key: identity, passphrase: passphrase, callback: ( (identity) =>
NylasEnv.showSaveDialog({
title: "Export PGP Key",
defaultPath: savePath,
}, (keyPath) =>
if (!keyPath)
return
NylasEnv.savedState.lastKeybaseDownloadDirectory = keyPath.slice(0, keyPath.length - shortName.length)
if passphrase?
identity.key.export_pgp_private {passphrase: passphrase}, (err, pgp_private) =>
if (err)
@_displayError(err)
fs.writeFile(keyPath, pgp_private, (err) =>
if (err)
@_displayError(err)
shell.showItemInFolder(keyPath)
)
else
identity.key.export_pgp_public {}, (err, pgp_public) =>
fs.writeFile(keyPath, pgp_public, (err) =>
if (err)
@_displayError(err)
shell.showItemInFolder(keyPath)
)
)
)
)
deleteKey: (key) =>
if this._displayDialog(
'Delete this key?',
'The key will be permanently deleted.',
['Delete', 'Cancel']
)
fs.unlink(key.keyPath, (err) =>
if (err)
@_displayError(err)
@_populate()
)
addAddressToKey: (profile, address) =>
if @validAddress(address, !profile.isPriv)
oldPath = profile.keyPath
profile.addresses.push(address)
fs.rename(oldPath, profile.keyPath, (err) =>
if (err)
@_displayError(err)
)
removeAddressFromKey: (profile, address) =>
if profile.addresses.length > 1
oldPath = profile.keyPath
profile.addresses = _.without(profile.addresses, address)
fs.rename(oldPath, profile.keyPath, (err) =>
if (err)
@_displayError(err)
)
else
@deleteKey(profile)
### Internal Key Management ###
pubKeys: (addresses) =>
# fetch public identity/ies for an address (synchronous)
# if no address, return them all
identities = _.where(_.values(@_identities), {isPriv: false})
if not addresses?
return identities
if typeof addresses is "string"
addresses = [addresses]
identities = _.filter(identities, (identity) ->
return _.intersection(addresses, identity.addresses).length > 0
)
return identities
privKeys: ({address, timed} = {timed: true}) =>
# fetch private identity/ies for an address (synchronous).
# by default, only return non-timed-out keys
# if no address, return them all
identities = _.where(_.values(@_identities), {isPriv: true})
if address?
identities = _.filter(identities, (identity) ->
return address in identity.addresses
)
if timed
identities = _.reject(identities, (identity) ->
return identity.isTimedOut()
)
return identities
_displayError: (err) ->
dialog = remote.dialog
dialog.showErrorBox('Key Management Error', err.toString())
_displayDialog: (title, message, buttons) ->
dialog = remote.dialog
return (dialog.showMessageBox({
title: title,
message: title,
detail: message,
buttons: buttons,
type: 'info',
}) == 0)
msgStatus: (msg) ->
# fetch the latest status of a message
if not msg?
return null
else
id = msg.id
statuses = _.filter @_msgStatus, (status) ->
return status.id == id
status = _.max statuses, (stat) ->
return stat.time
return status.message
isDecrypted: (message) ->
# if the message is already decrypted, return true
# if the message has no encrypted component, return true
# if the message has an encrypted component that is not yet decrypted, return false
if not @hasEncryptedComponent(message)
return true
else if @getDecrypted(message)?
return true
else
return false
getDecrypted: (message) =>
# Fetch a cached decrypted message
# (synchronous)
if message.id in _.pluck(@_msgCache, 'id')
msg = _.findWhere(@_msgCache, {id: message.id})
if msg.timeout > Date.now()
return msg.body
# otherwise
return null
hasEncryptedComponent: (message) ->
if not message.body?
return false
# find a PGP block
pgpStart = "-----BEGIN PGP MESSAGE-----"
pgpEnd = "-----END PGP MESSAGE-----"
blockStart = message.body.indexOf(pgpStart)
blockEnd = message.body.indexOf(pgpEnd)
# if they're both present, assume an encrypted block
return (blockStart >= 0 and blockEnd >= 0)
fetchEncryptedAttachments: (message) ->
encrypted = _.map(message.files, (file) =>
# calendars don't have filenames
if file.filename?
tokenized = file.filename.split('.')
extension = tokenized[tokenized.length - 1]
if extension == "asc" or extension == "pgp"
# something.asc or something.pgp -> assume encrypted attachment
return file
else
return null
else
return null
)
# NOTE for now we don't verify that the .asc/.pgp files actually have a PGP
# block inside
return _.compact(encrypted)
decrypt: (message) =>
# decrypt a message, cache the result
# (asynchronous)
# check to make sure we haven't already decrypted and cached the message
# note: could be a race condition here causing us to decrypt multiple times
# (not that that's a big deal other than minor resource wastage)
if @getDecrypted(message)?
return
if not @hasEncryptedComponent(message)
return
# fill our keyring with all possible private keys
ring = new pgp.keyring.KeyRing
# (the unbox function will use the right one)
for key in @privKeys({timed: true})
if key.key?
ring.add_key_manager(key.key)
# find a PGP block
pgpStart = "-----BEGIN PGP MESSAGE-----"
blockStart = message.body.indexOf(pgpStart)
pgpEnd = "-----END PGP MESSAGE-----"
blockEnd = message.body.indexOf(pgpEnd) + pgpEnd.length
# if we don't find those, it isn't encrypted
return unless (blockStart >= 0 and blockEnd >= 0)
pgpMsg = message.body.slice(blockStart, blockEnd)
# Some users may send messages from sources that pollute the encrypted block.
pgpMsg = pgpMsg.replace(/&#43;/gm,'+')
pgpMsg = pgpMsg.replace(/(<br>)/g, '\n')
pgpMsg = pgpMsg.replace(/<\/(blockquote|div|dl|dt|dd|form|h1|h2|h3|h4|h5|h6|hr|ol|p|pre|table|tr|td|ul|li|section|header|footer)>/g, '\n')
pgpMsg = pgpMsg.replace(/<(.+?)>/g, '')
pgpMsg = pgpMsg.replace(/&nbsp;/g, ' ')
pgp.unbox { keyfetch: ring, armored: pgpMsg }, (err, literals, warnings, subkey) =>
if err
console.warn err
errMsg = "Unable to decrypt message."
if err.toString().indexOf("tailer found") >= 0 or err.toString().indexOf("checksum mismatch") >= 0
errMsg = "Unable to decrypt message. Encrypted block is malformed."
else if err.toString().indexOf("key not found:") >= 0
errMsg = "Unable to decrypt message. Private key does not match encrypted block."
if !@msgStatus(message)?
errMsg = "Decryption preprocessing failed."
Actions.recordUserEvent("Email Decryption Errored", {error: errMsg})
@_msgStatus.push({"id": message.id, "time": Date.now(), "message": errMsg})
else
if warnings._w.length > 0
console.warn warnings._w
if literals.length > 0
plaintext = literals[0].toString('utf8')
# <pre> tag for consistent styling
if plaintext.indexOf("<pre>") == -1
plaintext = "<pre>\n" + plaintext + "\n</pre>"
# can't use _.template :(
body = message.body.slice(0, blockStart) + plaintext + message.body.slice(blockEnd)
# TODO if message is already in the cache, consider updating its TTL
timeout = 1000 * 60 * 30 # 30 minutes in ms
@_msgCache.push({id: message.id, body: body, timeout: Date.now() + timeout})
keyprint = subkey.get_fingerprint().toString('hex')
@_msgStatus.push({"id": message.id, "time": Date.now(), "message": "Message decrypted with key #{keyprint}"})
# re-render messages
Actions.recordUserEvent("Email Decrypted")
MessageBodyProcessor.resetCache()
@trigger(@)
else
console.warn "Unable to decrypt message."
@_msgStatus.push({"id": message.id, "time": Date.now(), "message": "Unable to decrypt message."})
decryptAttachments: (identity, files) =>
# fill our keyring with all possible private keys
keyring = new pgp.keyring.KeyRing
# (the unbox function will use the right one)
if identity.key?
keyring.add_key_manager(identity.key)
FileDownloadStore._fetchAndSaveAll(files).then((filepaths) ->
# open, decrypt, and resave each of the newly-downloaded files in place
_.each(filepaths, (filepath) =>
fs.readFile(filepath, (err, data) =>
# find a PGP block
pgpStart = "-----BEGIN PGP MESSAGE-----"
blockStart = data.indexOf(pgpStart)
pgpEnd = "-----END PGP MESSAGE-----"
blockEnd = data.indexOf(pgpEnd) + pgpEnd.length
# if we don't find those, it isn't encrypted
return unless (blockStart >= 0 and blockEnd >= 0)
pgpMsg = data.slice(blockStart, blockEnd)
# decrypt the file
pgp.unbox({ keyfetch: keyring, armored: pgpMsg }, (err, literals, warnings, subkey) =>
if err
console.warn err
else
if warnings._w.length > 0
console.warn warnings._w
literalLen = literals?.length
# if we have no literals, failed to decrypt and should abort
return unless literalLen?
if literalLen == 1
# success! replace old encrypted file with awesome decrypted file
filepath = filepath.slice(0, filepath.length-3).concat("txt")
fs.writeFile(filepath, literals[0].toBuffer(), (err) =>
if err
console.warn err
)
else
console.warn "Attempt to decrypt attachment failed: #{literalLen} literals found, expected 1."
)
)
)
)
module.exports = new PGPKeyStore()

View file

@ -1,50 +0,0 @@
{React, RegExpUtils} = require 'nylas-exports'
PGPKeyStore = require './pgp-key-store'
KeybaseSearch = require './keybase-search'
KeyManager = require './key-manager'
KeyAdder = require './key-adder'
class PreferencesKeybase extends React.Component
@displayName: 'PreferencesKeybase'
constructor: (@props) ->
@_keySaveQueue = {}
{pubKeys, privKeys} = @_getStateFromStores()
@state =
pubKeys: pubKeys
privKeys: privKeys
componentDidMount: =>
@unlistenKeystore = PGPKeyStore.listen(@_onChange, @)
componentWillUnmount: =>
@unlistenKeystore()
_onChange: =>
@setState @_getStateFromStores()
_getStateFromStores: ->
pubKeys = PGPKeyStore.pubKeys()
privKeys = PGPKeyStore.privKeys(timed: false)
return {pubKeys, privKeys}
render: =>
noKeysMessage =
<div className="key-status-bar no-keys-message">
You have no saved PGP keys!
</div>
keyManager = <KeyManager pubKeys={@state.pubKeys} privKeys={@state.privKeys}/>
<div className="container-keybase">
<section className="key-add">
<KeyAdder/>
</section>
<section className="keybase">
<KeybaseSearch inPreferences={true} />
{if @state.pubKeys.length == 0 and @state.privKeys.length == 0 then noKeysMessage else keyManager}
</section>
</div>
module.exports = PreferencesKeybase

View file

@ -1,138 +0,0 @@
{React, Actions, AccountStore} = require 'nylas-exports'
{remote} = require 'electron'
Identity = require './identity'
PGPKeyStore = require './pgp-key-store'
PassphrasePopover = require './passphrase-popover'
_ = require 'underscore'
fs = require 'fs'
pgp = require 'kbpgp'
module.exports =
class PrivateKeyPopover extends React.Component
constructor: ->
@state = {
selectedAddress: "0"
keyBody: ""
paste: false
import: false
validKeyBody: false
}
@propTypes:
addresses: React.PropTypes.array
render: =>
errorBar = <div className="invalid-key-body">Invalid key body.</div>
keyArea = <textarea value={@state.keyBody || ""} onChange={@_onKeyChange} placeholder="Paste in your PGP key here!"/>
saveBtnClass = if !(@state.validKeyBody) then "btn modal-done-button btn-disabled" else "btn modal-done-button"
saveButton = <button className={saveBtnClass} disabled={!(@state.validKeyBody)} onClick={@_onDone}>Save</button>
<div className="private-key-popover" tabIndex=0>
<span key="title" className="picker-title"><b>No PGP private key found.<br/>Add a key for {@_renderAddresses()}</b></span>
<div className="key-add-buttons">
<button className="btn btn-toolbar paste-btn" onClick={@_onClickPaste}>Paste in a Key</button>
<button className="btn btn-toolbar import-btn" onClick={@_onClickImport}>Import from File</button>
</div>
{if (@state.import or @state.paste) and !@state.validKeyBody and @state.keyBody != "" then errorBar}
{if @state.import or @state.paste then keyArea}
<div className="picker-controls">
<div style={{width: 80}}><button className="btn modal-cancel-button" onClick={=> Actions.closePopover()}>Cancel</button></div>
<button className="btn modal-prefs-button" onClick={@_onClickAdvanced}>Advanced</button>
<div style={{width: 80}}>{saveButton}</div>
</div>
</div>
_renderAddresses: =>
signedIn = _.pluck(AccountStore.accounts(), "emailAddress")
suggestions = _.intersection(signedIn, @props.addresses)
if suggestions.length == 1
addresses = <span>{suggestions[0]}.</span>
else if suggestions.length > 1
options = suggestions.map((address) => <option value={suggestions.indexOf(address)} key={suggestions.indexOf(address)}>{address}</option>)
addresses =
<select value={@state.selectedAddress} onChange={@_onSelectAddress} style={{minWidth: 150}}>
{options}
</select>
else
throw new Error("How did you receive a message that you're not in the TO field for?")
_onSelectAddress: (event) =>
@setState
selectedAddress: parseInt(event.target.value, 10)
_displayError: (err) ->
dialog = remote.dialog
dialog.showErrorBox('Private Key Error', err.toString())
_onClickAdvanced: =>
Actions.switchPreferencesTab('Encryption')
Actions.openPreferences()
_onClickImport: (event) =>
NylasEnv.showOpenDialog({
title: "Import PGP Key",
buttonLabel: "Import",
properties: ['openFile']
}, (filepath) =>
if filepath?
fs.readFile(filepath[0], (err, data) =>
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
if err
@_displayError("File is not a valid PGP private key.")
return
else
privateStart = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
if km.armored_pgp_public.indexOf(privateStart) >= 0
@setState
paste: false
import: true
keyBody: km.armored_pgp_public
validKeyBody: true
else
@_displayError("File is not a valid PGP private key.")
)
)
_onClickPaste: (event) =>
@setState
paste: !@state.paste
import: false
keyBody: ""
validKeyBody: false
_onKeyChange: (event) =>
@setState
keyBody: event.target.value
pgp.KeyManager.import_from_armored_pgp {
armored: event.target.value
}, (err, km) =>
if err
valid = false
else
privateStart = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
if km.armored_pgp_public.indexOf(privateStart) >= 0
valid = true
else
valid = false
@setState
validKeyBody: valid
_onDone: =>
signedIn = _.pluck(AccountStore.accounts(), "emailAddress")
suggestions = _.intersection(signedIn, @props.addresses)
selectedAddress = suggestions[@state.selectedAddress]
ident = new Identity({
addresses: [selectedAddress]
isPriv: true
})
@unlistenKeystore = PGPKeyStore.listen(@_onKeySaved, @)
PGPKeyStore.saveNewKey(ident, @state.keyBody)
_onKeySaved: =>
@unlistenKeystore()
Actions.closePopover()
@props.callback()

View file

@ -1,53 +0,0 @@
{MessageStore, React} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
PGPKeyStore = require './pgp-key-store'
pgp = require 'kbpgp'
_ = require 'underscore'
# Sits next to recipient chips in the composer and turns them green/red
# depending on whether or not there's a PGP key present for that user
class RecipientKeyChip extends React.Component
@displayName: 'RecipientKeyChip'
@propTypes:
contact: React.PropTypes.object.isRequired
constructor: (props) ->
super(props)
@state = @_getStateFromStores()
componentDidMount: ->
# fetch the actual key(s) from disk
keys = PGPKeyStore.pubKeys(@props.contact.email)
_.each(keys, (key) ->
PGPKeyStore.getKeyContents(key: key)
)
@unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)
componentWillUnmount: ->
@unlistenKeystore()
_getStateFromStores: ->
return {
# true if there is at least one loaded key for the account
keys: PGPKeyStore.pubKeys(@props.contact.email).some((cv, ind, arr) =>
cv.hasOwnProperty('key')
)
}
_onKeystoreChange: ->
@setState(@_getStateFromStores())
render: ->
if @state.keys
<div className="n1-keybase-recipient-key-chip">
<RetinaImg url="nylas://keybase/key-present@2x.png" mode={RetinaImg.Mode.ContentPreserve} ref="keyIcon" />
</div>
else
<div className="n1-keybase-recipient-key-chip">
<span ref="noKeyIcon"></span>
</div>
module.exports = RecipientKeyChip

View file

@ -1,20 +0,0 @@
{
"name": "keybase",
"main": "./lib/main",
"version": "0.1.0",
"engines": {
"nylas": "*"
},
"isOptional": true,
"isHiddenOnPluginsPage": true,
"title": "Encryption",
"description": "Send and receive encrypted messages using Keybase for public key exchange.",
"icon": "./icon.png",
"license": "GPL-3.0",
"windowTypes": {
"default": true,
"composer": true,
"thread-popout": true
}
}

View file

@ -1,83 +0,0 @@
{React, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'
pgp = require 'kbpgp'
DecryptMessageButton = require '../lib/decrypt-button'
PGPKeyStore = require '../lib/pgp-key-store'
describe "DecryptMessageButton", ->
beforeEach ->
@unencryptedMsg = new Message({id: 'test', subject: 'Subject', body: '<p>Body</p>'})
body = """-----BEGIN PGP MESSAGE-----
Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto
wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=
=1aPN
-----END PGP MESSAGE-----"""
@encryptedMsg = new Message({id: 'test2', subject: 'Subject', body: body})
@msg = new Message({subject: 'Subject', body: '<p>Body</p>'})
@component = ReactTestUtils.renderIntoDocument(
<DecryptMessageButton message={@msg} />
)
xit "should try to decrypt the message whenever a new key is unlocked", ->
spyOn(PGPKeyStore, "decrypt")
spyOn(PGPKeyStore, "isDecrypted").andCallFake((message) =>
return false
)
spyOn(PGPKeyStore, "hasEncryptedComponent").andCallFake((message) =>
return true
)
PGPKeyStore.trigger(PGPKeyStore)
expect(PGPKeyStore.decrypt).toHaveBeenCalled()
xit "should not try to decrypt the message whenever a new key is unlocked
if the message is already decrypted", ->
spyOn(PGPKeyStore, "decrypt")
spyOn(PGPKeyStore, "isDecrypted").andCallFake((message) =>
return true)
spyOn(PGPKeyStore, "hasEncryptedComponent").andCallFake((message) =>
return true)
# TODO for some reason the above spyOn calls aren't working and false is
# being returned from isDecrypted, causing this test to fail
PGPKeyStore.trigger(PGPKeyStore)
expect(PGPKeyStore.decrypt).not.toHaveBeenCalled()
it "should have a button to decrypt a message", ->
@component = ReactTestUtils.renderIntoDocument(
<DecryptMessageButton message=@encryptedMsg />
)
expect(@component.refs.button).toBeDefined()
it "should not allow for the unlocking of a message with no encrypted component", ->
@component = ReactTestUtils.renderIntoDocument(
<DecryptMessageButton message=@unencryptedMsg />
)
expect(@component.refs.button).not.toBeDefined()
it "should indicate when a message has been decrypted", ->
spyOn(PGPKeyStore, "isDecrypted").andCallFake((message) =>
return true)
@component = ReactTestUtils.renderIntoDocument(
<DecryptMessageButton message=@encryptedMsg />
)
expect(@component.refs.button).not.toBeDefined()
it "should open a popover when clicked", ->
spyOn(DecryptMessageButton.prototype, "_onClickDecrypt")
msg = @encryptedMsg
msg.to = [{email: "test@example.com"}]
@component = ReactTestUtils.renderIntoDocument(
<DecryptMessageButton message=msg />
)
expect(@component.refs.button).toBeDefined()
ReactTestUtils.Simulate.click(@component.refs.button)
expect(DecryptMessageButton.prototype._onClickDecrypt).toHaveBeenCalled()

View file

@ -1,142 +0,0 @@
{React, ReactDOM, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'
pgp = require 'kbpgp'
EncryptMessageButton = require '../lib/encrypt-button'
PGPKeyStore = require '../lib/pgp-key-store'
describe "EncryptMessageButton", ->
beforeEach ->
key = """-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1
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
217s2OKjpJqtpHPf2vY=
=UY7Y
-----END PGP PRIVATE KEY BLOCK-----"""
pgp.KeyManager.import_from_armored_pgp {
armored: key
}, (err, km) =>
@km = km
waitsFor (=> @km?), "getting a key took too long", 1000
@msg = new Message({subject: 'Subject', body: '<p>Body</p>', draft: true})
@session =
draft: =>
return @msg
changes:
add: (changes) =>
@output = changes
@output = null
add = jasmine.createSpy('add')
spyOn(DraftStore, 'sessionForClientId').andCallFake((draftClientId) =>
return Promise.resolve(@session)
)
@component = ReactTestUtils.renderIntoDocument(
<EncryptMessageButton draft={@msg} session={@session} />
)
it "should render into the page", ->
expect(@component).toBeDefined()
it "should have a displayName", ->
expect(EncryptMessageButton.displayName).toBe('EncryptMessageButton')
it "should have an onClick behavior which encrypts the message", ->
spyOn(@component, '_onClick')
buttonNode = ReactDOM.findDOMNode(@component.refs.button)
ReactTestUtils.Simulate.click(buttonNode)
expect(@component._onClick).toHaveBeenCalled()
it "should store the message body's plaintext on encryption", ->
spyOn(@component, '_onClick')
buttonNode = ReactDOM.findDOMNode(@component.refs.button)
ReactTestUtils.Simulate.click(buttonNode)
expect(@component.plaintext is @msg.body)
it "should mark itself as encrypted", ->
spyOn(@component, '_onClick')
buttonNode = ReactDOM.findDOMNode(@component.refs.button)
ReactTestUtils.Simulate.click(buttonNode)
expect(@component.currentlyEncrypted is true)
xit "should be able to encrypt messages", ->
# NOTE: this doesn't work.
# As best I can tell, something is wrong with the pgp.box function -
# nothing seems to get it to complete. Weird.
runs( =>
console.log @km
@component._encrypt("test text", [@km])
@flag = false
pgp.box {encrypt_for: [@km], msg: "test text"}, (err, result_string) =>
expect(not err?)
@err = err
@result_string = result_string
@flag = true
)
waitsFor (=> console.log @flag; @flag), "encryption took too long", 5000
runs( =>
console.log @err
console.log @result_string
console.log @output
expect(@output is @result_string))

View file

@ -1,9 +0,0 @@
{React, ReactTestUtils, Message} = require 'nylas-exports'
KeybaseUser = require '../lib/keybase-user'
describe "KeybaseUserProfile", ->
it "should have a displayName", ->
expect(KeybaseUser.displayName).toBe('KeybaseUserProfile')
# behold, the most comprehensive test suite of all time

View file

@ -1,16 +0,0 @@
{React, ReactTestUtils, Message} = require 'nylas-exports'
KeybaseSearch = require '../lib/keybase-search'
describe "KeybaseSearch", ->
it "should have a displayName", ->
expect(KeybaseSearch.displayName).toBe('KeybaseSearch')
it "should have no results when rendered", ->
@component = ReactTestUtils.renderIntoDocument(
<KeybaseSearch />
)
expect(@component.state.results).toEqual([])
# behold, the most comprehensive test suite of all time

View file

@ -1,51 +0,0 @@
kb = require '../lib/keybase'
xdescribe "keybase lib", ->
# TODO stub keybase calls?
it "should be able to fetch an account by username", ->
@them = null
runs( =>
kb.getUser('dakota', 'usernames', (err, them) =>
@them = them
)
)
waitsFor((=> @them != null), 2000)
runs( =>
expect(@them?[0].components.username.val).toEqual("dakota")
)
it "should be able to fetch an account by key fingerprint", ->
@them = null
runs( =>
kb.getUser('7FA5A43BBF2BAD1845C8D0E8145FCCD989968E3B', 'key_fingerprint', (err, them) =>
@them = them
)
)
waitsFor((=> @them != null), 2000)
runs( =>
expect(@them?[0].components.username.val).toEqual("dakota")
)
it "should be able to fetch a user's key", ->
@key = null
runs( =>
kb.getKey('dakota', (error, key) =>
@key = key
)
)
waitsFor((=> @key != null), 2000)
runs( =>
expect(@key?.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'))
)
it "should be able to return an autocomplete query", ->
@completions = null
runs( =>
kb.autocomplete('dakota', (error, completions) =>
@completions = completions
)
)
waitsFor((=> @completions != null), 2000)
runs( =>
expect(@completions[0].components.username.val).toEqual("dakota")
)

View file

@ -1,39 +0,0 @@
{ComponentRegistry, ExtensionRegistry} = require 'nylas-exports'
{activate, deactivate} = require '../lib/main'
EncryptMessageButton = require '../lib/encrypt-button'
DecryptMessageButton = require '../lib/decrypt-button'
DecryptPGPExtension = require '../lib/decryption-preprocess'
describe "activate", ->
it "should register the encryption button", ->
spyOn(ComponentRegistry, 'register')
activate()
expect(ComponentRegistry.register).toHaveBeenCalledWith(EncryptMessageButton, {role: 'Composer:ActionButton'})
it "should register the decryption button", ->
spyOn(ComponentRegistry, 'register')
activate()
expect(ComponentRegistry.register).toHaveBeenCalledWith(DecryptMessageButton, {role: 'message:BodyHeader'})
it "should register the decryption processor", ->
spyOn(ExtensionRegistry.MessageView, 'register')
activate()
expect(ExtensionRegistry.MessageView.register).toHaveBeenCalledWith(DecryptPGPExtension)
describe "deactivate", ->
it "should unregister the encrypt button", ->
spyOn(ComponentRegistry, 'unregister')
deactivate()
expect(ComponentRegistry.unregister).toHaveBeenCalledWith(EncryptMessageButton)
it "should unregister the decryption button", ->
spyOn(ComponentRegistry, 'unregister')
deactivate()
expect(ComponentRegistry.unregister).toHaveBeenCalledWith(DecryptMessageButton)
it "should unregister the decryption processor", ->
spyOn(ExtensionRegistry.MessageView, 'unregister')
deactivate()
expect(ExtensionRegistry.MessageView.unregister).toHaveBeenCalledWith(DecryptPGPExtension)

View file

@ -1,209 +0,0 @@
{React, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'
pgp = require 'kbpgp'
_ = require 'underscore'
fs = require 'fs'
Identity = require '../lib/identity'
PGPKeyStore = require '../lib/pgp-key-store'
describe "PGPKeyStore", ->
beforeEach ->
@TEST_KEY = """-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1
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
217s2OKjpJqtpHPf2vY=
=UY7Y
-----END PGP PRIVATE KEY BLOCK-----"""
# mock getKeyContents to get rid of all the fs.readFiles
spyOn(PGPKeyStore, "getKeyContents").andCallFake( ({key, passphrase, callback}) =>
data = @TEST_KEY
pgp.KeyManager.import_from_armored_pgp {
armored: data
}, (err, km) =>
expect(err).toEqual(null)
if km.is_pgp_locked()
expect(passphrase).toBeDefined()
km.unlock_pgp { passphrase: passphrase }, (err) =>
expect(err).toEqual(null)
key.key = km
key.setTimeout()
if callback?
callback()
)
# define an encrypted and an unencrypted message
@unencryptedMsg = new Message({clientId: 'test', subject: 'Subject', body: '<p>Body</p>'})
body = """-----BEGIN PGP MESSAGE-----
Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto
wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=
=1aPN
-----END PGP MESSAGE-----"""
@encryptedMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})
# blow away the saved identities and set up a test pub/priv keypair
PGPKeyStore._identities = {}
pubIdent = new Identity({
addresses: ["benbitdiddle@icloud.com"]
isPriv: false
})
PGPKeyStore._identities[pubIdent.clientId] = pubIdent
privIdent = new Identity({
addresses: ["benbitdiddle@icloud.com"]
isPriv: true
})
PGPKeyStore._identities[privIdent.clientId] = privIdent
describe "when handling private keys", ->
it 'should be able to retrieve and unlock a private key', ->
expect(PGPKeyStore.privKeys().some((cv, index, array) =>
cv.hasOwnProperty("key"))).toBeFalsey
key = PGPKeyStore.privKeys(address: "benbitdiddle@icloud.com", timed: false)[0]
PGPKeyStore.getKeyContents(key: key, passphrase: "", callback: =>
expect(PGPKeyStore.privKeys({timed: false}).some((cv, index, array) =>
cv.hasOwnProperty("key"))).toBeTruthy
)
it 'should not return a private key after its timeout has passed', ->
expect(PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false}).length).toEqual(1)
PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0].timeout = Date.now() - 5
expect(PGPKeyStore.privKeys(address: "benbitdiddle@icloud.com", timed: true).length).toEqual(0)
PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0].setTimeout()
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)
it 'should return all private keys when an address is not supplied', ->
expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1)
it 'should update an existing key when it is unlocked, not add a new one', ->
timeout = PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false})[0].timeout
PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: =>
# expect no new keys to have been added
expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1)
# make sure the timeout is updated
expect(timeout < PGPKeyStore.privKeys({address: "benbitdiddle@icloud.com", timed: false}).timeout)
)
describe "when decrypting messages", ->
xit 'should be able to decrypt a message', ->
# TODO for some reason, the pgp.unbox has a problem with the message body
runs( =>
spyOn(PGPKeyStore, 'trigger')
PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: =>
PGPKeyStore.decrypt(@encryptedMsg)
)
)
waitsFor((=> PGPKeyStore.trigger.callCount > 0), 'message to decrypt')
runs( =>
expect(_.findWhere(PGPKeyStore._msgCache,
{clientId: @encryptedMsg.clientId})).toExist()
)
it 'should be able to handle an unencrypted message', ->
PGPKeyStore.decrypt(@unencryptedMsg)
expect(_.findWhere(PGPKeyStore._msgCache,
{clientId: @unencryptedMsg.clientId})).not.toBeDefined()
it 'should be able to tell when a message has no encrypted component', ->
expect(PGPKeyStore.hasEncryptedComponent(@unencryptedMsg)).not
expect(PGPKeyStore.hasEncryptedComponent(@encryptedMsg))
it 'should be able to handle a message with no BEGIN PGP MESSAGE block', ->
body = """Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto
wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=
=1aPN
-----END PGP MESSAGE-----"""
badMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})
PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: =>
PGPKeyStore.decrypt(badMsg)
expect(_.findWhere(PGPKeyStore._msgCache,
{clientId: badMsg.clientId})).not.toBeDefined()
)
it 'should be able to handle a message with no END PGP MESSAGE block', ->
body = """-----BEGIN PGP MESSAGE-----
Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto
wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=
=1aPN"""
badMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})
PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: "", callback: =>
PGPKeyStore.decrypt(badMsg)
expect(_.findWhere(PGPKeyStore._msgCache,
{clientId: badMsg.clientId})).not.toBeDefined()
)
it 'should not return a decrypted message which has timed out', ->
PGPKeyStore._msgCache.push({clientId: "testID", body: "example body", timeout: Date.now()})
msg = new Message({clientId: "testID"})
expect(PGPKeyStore.getDecrypted(msg)).toEqual(null)
it 'should return a decrypted message', ->
timeout = Date.now() + (1000*60*60)
PGPKeyStore._msgCache.push({clientId: "testID2", body: "example body", timeout: timeout})
msg = new Message({clientId: "testID2", body: "example body"})
expect(PGPKeyStore.getDecrypted(msg)).toEqual(msg.body)
describe "when handling public keys", ->
it "should immediately return a pre-cached key", ->
expect(PGPKeyStore.pubKeys('benbitdiddle@icloud.com').length).toEqual(1)

View file

@ -1,40 +0,0 @@
{React, ReactTestUtils, DraftStore, Contact} = require 'nylas-exports'
pgp = require 'kbpgp'
RecipientKeyChip = require '../lib/recipient-key-chip'
PGPKeyStore = require '../lib/pgp-key-store'
describe "DecryptMessageButton", ->
beforeEach ->
@contact = new Contact({email: "test@example.com"})
@component = ReactTestUtils.renderIntoDocument(
<RecipientKeyChip contact=@contact />
)
it "should render into the page", ->
expect(@component).toBeDefined()
it "should have a displayName", ->
expect(RecipientKeyChip.displayName).toBe('RecipientKeyChip')
xit "should indicate when a recipient has a PGP key available", ->
spyOn(PGPKeyStore, "pubKeys").andCallFake((address) =>
return [{'key':0}])
key = PGPKeyStore.pubKeys(@contact.email)
expect(key).toBeDefined()
# TODO these calls crash the tester because they require a call to getKeyContents
expect(@component.refs.keyIcon).toBeDefined()
expect(@component.refs.noKeyIcon).not.toBeDefined()
xit "should indicate when a recipient does not have a PGP key available", ->
component = ReactTestUtils.renderIntoDocument(
<RecipientKeyChip contact=@contact />
)
key = PGPKeyStore.pubKeys(@contact.email)
expect(key).toEqual([])
# TODO these calls crash the tester because they require a call to getKeyContents
expect(component.refs.keyIcon).not.toBeDefined()
expect(component.refs.noKeyIcon).toBeDefined()

View file

@ -1,526 +0,0 @@
@import "ui-variables";
@import "ui-mixins";
@code-bg-color: #fcf4db;
.keybase {
.no-keys-message {
text-align: center;
}
}
.container-keybase {
max-width: 640px;
margin: 0 auto;
}
.keybase-profile {
border: 1px solid @border-color-primary;
border-top: 0;
background: @background-primary;
padding: 10px;
overflow: auto;
display: flex;
.profile-photo-wrap {
width: 50px;
height: 50px;
border-radius: @border-radius-base;
padding: 3px;
box-shadow: 0 0 1px rgba(0,0,0,0.5);
background: @background-primary;
.profile-photo {
border-radius: @border-radius-small;
overflow: hidden;
text-align: center;
width: 44px;
height: 44px;
img, .default-profile-image {
width: 44px;
height: 44px;
}
.default-profile-image {
line-height: 44px;
font-size: 18px;
font-weight: 500;
color: white;
box-shadow: inset 0 0 1px rgba(0,0,0,0.18);
background-image: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);
}
.user-picture {
background: @background-secondary;
width: 44px;
height: 44px;
}
}
}
.key-actions {
display: flex;
flex-direction: column;
button {
margin: 2px 0 2px 10px;
white-space: nowrap;
display: inline-block;
float: right;
}
}
.details {
margin-left: 10px;
flex: 1;
}
button {
margin: 10px 0 10px 10px;
white-space: nowrap;
display: inline-block;
float: right;
}
keybase-participant-field {
float: right;
}
ul {
list-style-type: none;
}
.email-list {
padding-left: 10px;
word-break: break-all;
flex-grow: 3;
text-align: right;
}
}
.keybase-profile:first-child {
border-top: 1px solid @border-color-primary;
}
.fixed-popover-container, .email-list {
.keybase-participant-field {
margin-bottom: 10px;
.n1-keybase-recipient-key-chip {
display: none;
}
.tokenizing-field-label {
display: none;
padding-top: 0;
}
.tokenizing-field-input {
padding-left: 0;
padding-top: 0;
input {
border: none;
}
}
}
}
.fixed-popover-container {
.keybase-participant-field {
width: 300px;
background: @input-bg;
border: 1px solid @input-border-color;
.menu .content-container {
background: @background-secondary;
}
}
.passphrase-popover {
margin: 10px;
display: flex;
button {
margin-left: 5px;
flex: 0;
}
input {
min-width: 180px;
flex: 1;
}
.bad-passphrase {
border-color: @color-error;
}
}
.keybase-import-popover {
margin: 10px;
button {
width: 100%;
}
.title {
margin: 0 auto;
white-space: nowrap;
}
}
.private-key-popover {
display: flex;
flex-direction: column;
width: 300px;
margin: 5px 10px;
.picker-title {
margin-left: auto;
margin-right: auto;
text-align: center;
}
textarea {
margin-top: 5px;
}
.invalid-key-body {
background-color: @code-bg-color;
color: darken(@code-bg-color, 70%);
border: 1.5px solid darken(@code-bg-color, 10%);
border-radius: @border-radius-small;
font-size: @font-size-small;
margin: 5px 0 0 0;
text-align: center;
}
.key-add-buttons {
display: flex;
flex-direction: row;
button {
width: 147px;
margin: 5px 0 0 0;
}
.paste-btn {
margin-right: 6px;
}
}
.picker-controls {
width: 100%;
margin: 5px auto;
display: flex;
flex-shrink: 0;
flex-direction: row;
.modal-cancel-button {
float: left;
}
.modal-prefs-button {
flex: 1;
margin: 0 35px;
}
.modal-done-button {
float: right;
}
}
}
}
.email-list {
.keybase-participant-field {
width: 200px;
border-bottom: 1px solid @gray-light;
}
}
.keybase-decrypt {
div.line-w-label {
display: flex;
align-items: center;
color: rgba(128, 128, 128, 0.5);
}
div.decrypt-bar {
padding: 5px;
border: 1.5px solid rgba(128, 128, 128, 0.5);
border-radius: @border-radius-large;
align-items: center;
display: flex;
.title-text {
flex: 1;
margin: auto 0;
}
.decryption-interface {
button {
margin-left: 5px;
}
}
}
div.error-decrypt-bar {
border: 1.5px solid @color-error;
.title-text {
color: @color-error;
}
}
div.done-decrypt-bar {
border: 1.5px solid @color-success;
.title-text {
color: @color-success;
}
}
div.border {
height: 1px;
background: rgba(128, 128, 128, 0.5);
flex: 1;
}
div.error-border {
background: @color-error;
}
div.done-border {
background: @color-success;
}
}
.key-manager {
div.line-w-label {
display: flex;
align-items: center;
color: rgba(128, 128, 128, 0.5);
margin: 10px 0;
}
div.title-text {
padding: 0 10px;
}
div.border {
height: 1px;
background: rgba(128, 128, 128, 0.5);
flex: 1;
}
}
.key-status-bar {
background-color: @code-bg-color;
color: darken(@code-bg-color, 70%);
border: 1.5px solid darken(@code-bg-color, 10%);
border-radius: @border-radius-small;
font-size: @font-size-small;
margin-bottom: 10px;
}
.key-add {
padding-top:10px;
.no-keys-message {
text-align: center;
}
.key-adder {
position: relative;
border: 1px solid @input-border-color;
padding: 10px;
padding-top: 0;
margin-bottom: 10px;
.key-text {
margin-top: 10px;
min-height: 200px;
display: flex;
.loading {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, 50%);
}
textarea {
border: 0;
padding: 0;
font-size: 0.9em;
flex: 1;
}
}
}
.credentials {
display: flex;
flex-direction: row;
.key-add-btn {
margin: 10px 5px 0 0;
flex: 0;
}
.key-email-input {
margin: 10px 5px 0 0;
flex: 1;
}
.key-passphrase-input {
margin: 10px 5px 0 0;
flex: 1;
}
.invalid-msg {
color: #AAA;
white-space: nowrap;
text-align: right;
margin: 12px 5px 0 0;
flex: 1;
}
}
.key-creation-button {
display: inline-block;
margin: 0 5px 10px 5px;
}
.editor-note {
color: #AAA;
}
}
.key-instructions {
color: #333;
font-size: small;
margin-top: 20px;
}
.keybase-search {
margin-top: 15px;
margin-bottom: 15px;
overflow: scroll;
position: relative;
input {
padding: 10px;
margin-bottom: 10px;
}
.empty {
text-align: center;
}
.loading {
position: absolute;
right: 10px;
top: 8px; // lol I wonder how long until this is a problem
}
.bad-search-msg {
display: inline-block;
width: 100%;
text-align: center;
color: rgba(128, 128, 128, 0.5);
br {
display: none;
}
}
}
.key-picker-modal {
width: 400px;
height: 400px;
display: flex;
flex-direction: column;
.keybase-search {
display: flex;
flex-direction: column;
height: 100%;
max-width: 400px;
overflow: hidden;
margin-bottom: 0;
margin-top: 10px;
.searchbar {
width: 380px;
margin-left: auto;
margin-right: auto;
}
.loading {
right: 20px;
}
.results {
overflow: auto;
height: 100%;
width: 100%;
}
.bad-search-msg {
br {
display: inline;
}
}
}
.picker-controls {
width: 380px;
margin: 5px auto 10px auto;
display: flex;
flex-shrink: 0;
flex-direction: row;
.modal-back-button {
float: left;
}
.modal-prefs-button {
flex: 1;
margin: 0 35px;
}
.modal-next-button {
float: right;
}
}
.keybase-profile-solo {
border: 1px solid @border-color-primary;
margin-top: 10px;
}
.picker-title {
margin-top: 10px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
}
.decrypted {
display: block;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
padding: 8px 12px;
margin-bottom: 5px;
border: 1px solid rgb(235, 204, 209);
border-radius: 4px;
background-color: rgb(121, 212, 91);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}