mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
Remove keybase plugin
This commit is contained in:
parent
9977c566df
commit
07428a1060
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
|
@ -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 {};
|
||||
}
|
|
@ -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>
|
|
@ -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()
|
|
@ -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(/+/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(/ /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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue