OTR.js

Off-the-Record Messaging Protocol implemented in JavaScript

View the Project on GitHub arlolra/otr

Build Status

Off-the Record Messaging Protocol in JavaScript

Warning

This library hasn't been properly vetted by security researchers. Do not use in life and death situations!

Install

Include the build files on the page,

<!-- Load dependencies -->
<script src="build/dep/bigint.js"></script>
<script src="build/dep/crypto.js"></script>
<script src="build/dep/eventemitter.js"></script>

<!-- Load otr.js or otr.min.js -->
<script src="build/otr.min.js"></script>

Here's an example use in the browser.

Although this is a client library, it can be used on the server.

npm install otr

And then,

var DSA = require('otr').DSA
var OTR = require('otr').OTR

Build

The contents of build/ are the result of calling make build and are only updated with releases. Please submit patches against lib/ and vendor/.

Release

The normal flow for making a release is as follows,

make test
// bump the version numbers in package.json / bower.json
make build
git changelog  // cleanup the changelog
git commit -m "bump version"
git tag -a vX.X.X -m "version X.X.X"
git push origin master
git push --tags
npm publish
// update github releases and pages

Usage

Initial setup: Compute your long-lived key beforehand. Currently this is expensive and can take several seconds.

// precompute your DSA key
var myKey = new DSA()

For each user you're communicating with, instantiate an OTR object.

// provide options
var options = {
    fragment_size: 140
  , send_interval: 200
  , priv: myKey
}

var buddy = new OTR(options)

buddy.on('ui', function (msg, encrypted, meta) {
  console.log("message to display to the user: " + msg)
  // encrypted === true, if the received msg was encrypted
  console.log("(optional) with receiveMsg attached meta data: " + meta)
})

buddy.on('io', function (msg, meta) {
  console.log("message to send to buddy: " + msg)
  console.log("(optional) with sendMsg attached meta data: " + meta)
})

buddy.on('error', function (err, severity) {
  if (severity === 'error')  // either 'error' or 'warn'
    console.error("error occurred: " + err)
})

New message from buddy received: Pass the received message to the receiveMsg method.

var rcvmsg = "Message from buddy."
var meta = "optional some meta data, like delay"
buddy.receiveMsg(rcvmsg, meta)

Send a message to buddy: Pass the message to the sendMsg method.

var newmsg = "Message to userA."
var meta = "optional some meta data, like message id"
buddy.sendMsg(newmsg, meta)

Going encrypted: Initially, messages are sent in plaintext. To manually initiate the authenticated key exchange.

buddy.sendQueryMsg()

Alternatively, one can set the policy REQUIRE_ENCRYPTION and send a plaintext message. This will store the message, initiate the authentication and then, upon success, send it out.

buddy.REQUIRE_ENCRYPTION = true
buddy.sendMsg('My plaintext message to be encrypted.')

Another policy, SEND_WHITESPACE_TAG, will append tags to plaintext messages, indicating a willingness to speak OTR. If the recipient in turn has set the policy WHITESPACE_START_AKE, the AKE will be initiated.

Close private connection: To end an encrypted communication session,

buddy.endOtr(function() {
  // calls back when the 'disconnect' message has been sent
})

will return the message state to plaintext and notify the correspondent.

Options: A dictionary of the current options accepted by the OTR constructor.

var options = {

  // long-lived private key
  priv: new DSA(),

  // turn on some debuggin logs
  debug: false,

  // fragment the message in case of char limits
  fragment_size: 140,

  // ms delay between sending fragmented msgs, avoid rate limits
  send_interval: 200

}

Status

A listener can be attached for status changes. These are non-standard codes, specific to this OTR library, indicating various things like the AKE success.

buddy.on('status', function (state) {
  switch (state) {
    case OTR.CONST.STATUS_AKE_SUCCESS:
      // sucessfully ake'd with buddy
      // check if buddy.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED
      break
    case OTR.CONST.STATUS_END_OTR:
      // if buddy.msgstate === OTR.CONST.MSGSTATE_FINISHED
      // inform the user that his correspondent has closed his end
      // of the private connection and the user should do the same
      break
  }
})

Policies

To be set on a per-correspondent basis. The defaults are as follows:

// Allow version 2 or 3 of the OTR protocol to be used.
ALLOW_V2 = true
ALLOW_V3 = true

// Refuse to send unencrypted messages.
REQUIRE_ENCRYPTION = false

// Advertise your support of OTR using the whitespace tag.
SEND_WHITESPACE_TAG = false

// Start the OTR AKE when you receive a whitespace tag.
WHITESPACE_START_AKE = false

// Start the OTR AKE when you receive an OTR Error Message.
ERROR_START_AKE = false

Instance Tags

These are intended to be persistent and can be precomputed.

var myTag = OTR.makeInstanceTag()
var options = { instance_tag: myTag }

var buddy = new OTR(options)

Fingerprints

OTR public key fingerprints can be obtained as follows:

// assume you've gone through the ake with buddy
var buddy = new OTR({ priv: myKey })
// buddy.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED

// for my key, either one of the following
myKey.fingerprint()
// or,
buddy.priv.fingerprint()

// for their key
buddy.their_priv_pk.fingerprint()

Socialist Millionaire Protocol

At any time after establishing encryption, either party can initiate SMP to detect impersonation or man-in-the-middle attacks. A shared secret, exchanged through an out-of-band channel prior to starting the conversation, is required.

var secret = "ghostbusters"
buddy.smpSecret(secret)

A question can be supplied, as a reminder of the shared secret.

var question = "who are you going to call?"
buddy.smpSecret(secret, question)

If you plan on using SMP, as opposed to just allowing fingerprints for verification, provide on optional callback when initiating OTR, otherwise a no-opt is fired.

var buddy = new OTR()

buddy.on('smp', function (type, data, act) {
  switch (type) {
    case 'question':
      // call(data) some function with question?
      // return the user supplied data to
      // userA.smpSecret(secret)
      break
    case 'trust':
      // smp completed
      // check data (true|false) and update ui accordingly
      // act ("asked"|"answered") provides info one who initiated the smp
      break
    case 'abort':
      // smp was aborted. notify the user or update ui
    default:
      throw new Error('Unknown type.')
  }
})

Both users should run the SMP to establish trust. Further, it should be run each time a partner presents a fresh long-lived key.

Private Keys

To export a private, long-lived key:

var myKey = new DSA()
var string = myKey.packPrivate()  // returns a Base64 encoded string

It can then be imported as follows,

string = "AAAAAACA4COdKHpU/np9F8EDdnGiJJmc89p ... I9BzTkQduFA7ovXAMY="
myKey = DSA.parsePrivate(string)

Importing the (somewhat) standard libotr s-expression format works as well,

// in node.js
var fs = require('fs')
string = fs.readFileSync("~/.purple/otr.private_key", 'utf8')

// leaving out the terminal backslashes needed for multiline strings in js
string = "(privkeys
  (account
    (name "foo@example.com")
    (protocol prpl-jabber)
    (private-key
      (dsa
        (p #00FC07 ... 2AEFD07A2081#)
        (q #ASD5FF ... LKJDF898DK12#)
        (g #535E3E ... 1E3BC1FC6F26#)
        (y #0AC867 ... 8969009B6ECF#)
        (x #14D034 ... F72D79043216#)
      )
    )
  )
)"

myKey = DSA.parsePrivate(string, true)

Extra Symmetric Key

In version 3 of the protocol, an extra symmetric key is derived during the AKE. This may be used for secure communication over a different channel (e.g., file transfer, voice chat).

var filename = "test.zip"
var buddy = new OTR()
buddy.sendFile(filename)
buddy.on('file', function (type, key, filename) {
  // type === 'send'
  // key should be used to encrypt filename
  // and sent through a different channel
})

On the other end,

var friend = new OTR()
friend.on('file', function (type, key, filename) {
  // type === 'receive'
  // decrypt filename with key, once received
})

WebWorkers

Some support exists for calling computationally expensive work off the main thread. However, some feedback on these APIs would be appreciated.

// generate a DSA key in a web worker
DSA.createInWebWorker(null, function (key) {
        var buddy = new OTR({
            priv: key,
            // setting `smw` to a truthy value will perform the socialist
            // millionaire protocol in a webworker.
            smw: {}
        })
  })

WebWorkers don't have access to window.crypto.getRandomValues(), so they will need to include Salsa20.

<script src="build/dep/salsa20.js"></script>

Links

Spec:

Using:

In The Wild

A sampling of projects that use this library:

Donate

Bitcoins: 1BWLnnig89fpn8hCcASd2B1YbfK6j1vtX3

License

MPL v2.0