[Pkg-mozext-commits] [cryptocat] 01/01: Initial import of upstream source 2.2.2

Ulrike Uhlig u-guest at moszumanska.debian.org
Fri Oct 17 10:15:55 UTC 2014


This is an automated email from the git hooks/post-receive script.

u-guest pushed a commit to branch master
in repository cryptocat.

commit 2696877a539c232e67b1dd0f3259b80c2e0039bc
Author: Ulrike Uhlig <u at 451f.org>
Date:   Fri Oct 17 12:11:13 2014 +0200

    Initial import of upstream source 2.2.2
---
 CHANGELOG.md                                       |  509 +++
 CONTRIBUTING.md                                    |   50 +
 Gruntfile.js                                       |  165 +
 LICENSE.txt                                        |  619 +++
 Makefile                                           |   93 +
 README.md                                          |   60 +
 RESOURCES.md                                       |   49 +
 SECURITY.md                                        |   10 +
 package.json                                       |   44 +
 src/core/chrome.js                                 |   15 +
 src/core/css/firstRun.css                          |   54 +
 src/core/css/jquery.basicslider.css                |   43 +
 src/core/css/jquery.utip.css                       |  118 +
 src/core/css/style.css                             | 1323 ++++++
 src/core/firstRun.html                             |   70 +
 src/core/fonts/logotype.woff                       |  Bin 0 -> 3440 bytes
 src/core/fonts/monda.woff                          |  Bin 0 -> 13680 bytes
 src/core/img/authStatusNo.png                      |  Bin 0 -> 90 bytes
 src/core/img/authTutorial/1.png                    |  Bin 0 -> 9641 bytes
 src/core/img/authTutorial/2.png                    |  Bin 0 -> 11563 bytes
 src/core/img/authTutorial/3.png                    |  Bin 0 -> 10053 bytes
 src/core/img/authTutorial/4.png                    |  Bin 0 -> 11778 bytes
 src/core/img/available.png                         |  Bin 0 -> 218 bytes
 src/core/img/away.png                              |  Bin 0 -> 210 bytes
 src/core/img/balloon.gif                           |  Bin 0 -> 36012 bytes
 src/core/img/bg.png                                |  Bin 0 -> 111 bytes
 src/core/img/composing.png                         |  Bin 0 -> 115 bytes
 src/core/img/cryptocat.png                         |  Bin 0 -> 7822 bytes
 src/core/img/down.png                              |  Bin 0 -> 109 bytes
 src/core/img/emoticon/cat.png                      |  Bin 0 -> 163 bytes
 src/core/img/emoticon/cry.png                      |  Bin 0 -> 161 bytes
 src/core/img/emoticon/gasp.png                     |  Bin 0 -> 145 bytes
 src/core/img/emoticon/grin.png                     |  Bin 0 -> 147 bytes
 src/core/img/emoticon/happy.png                    |  Bin 0 -> 157 bytes
 src/core/img/emoticon/sad.png                      |  Bin 0 -> 151 bytes
 src/core/img/emoticon/shut.png                     |  Bin 0 -> 151 bytes
 src/core/img/emoticon/smile.png                    |  Bin 0 -> 159 bytes
 src/core/img/emoticon/squint.png                   |  Bin 0 -> 145 bytes
 src/core/img/emoticon/tongue.png                   |  Bin 0 -> 162 bytes
 src/core/img/emoticon/unsure.png                   |  Bin 0 -> 153 bytes
 src/core/img/emoticon/wink.png                     |  Bin 0 -> 150 bytes
 src/core/img/emoticon/winkTongue.png               |  Bin 0 -> 154 bytes
 src/core/img/facebook.png                          |  Bin 0 -> 1637 bytes
 src/core/img/favicon.gif                           |  Bin 0 -> 802 bytes
 src/core/img/file.png                              |  Bin 0 -> 376 bytes
 src/core/img/firstRun/chrome.png                   |  Bin 0 -> 6968 bytes
 src/core/img/firstRun/firefox.png                  |  Bin 0 -> 6767 bytes
 src/core/img/firstRun/opera.png                    |  Bin 0 -> 8290 bytes
 src/core/img/firstRun/safari.png                   |  Bin 0 -> 18653 bytes
 src/core/img/groupChat.png                         |  Bin 0 -> 149 bytes
 src/core/img/icon-128.png                          |  Bin 0 -> 434 bytes
 src/core/img/icon-16.png                           |  Bin 0 -> 286 bytes
 src/core/img/icon-48.png                           |  Bin 0 -> 364 bytes
 src/core/img/key.png                               |  Bin 0 -> 215 bytes
 src/core/img/keygen.gif                            |  Bin 0 -> 2857 bytes
 src/core/img/logout.png                            |  Bin 0 -> 205 bytes
 src/core/img/newMessage.png                        |  Bin 0 -> 122 bytes
 src/core/img/noNotifications.png                   |  Bin 0 -> 252 bytes
 src/core/img/noSound.png                           |  Bin 0 -> 301 bytes
 src/core/img/notifications.png                     |  Bin 0 -> 272 bytes
 src/core/img/sending.gif                           |  Bin 0 -> 404 bytes
 src/core/img/sound.png                             |  Bin 0 -> 314 bytes
 src/core/img/up.png                                |  Bin 0 -> 116 bytes
 src/core/index.html                                |  168 +
 src/core/js/cryptocat.js                           | 1515 +++++++
 src/core/js/etc/catFacts.js                        |  116 +
 src/core/js/etc/customServers.js                   |  146 +
 src/core/js/etc/facebook.js                        |  578 +++
 src/core/js/etc/fileTransfer.js                    |  276 ++
 src/core/js/etc/firstRun.js                        |   27 +
 src/core/js/etc/locale.js                          |  239 ++
 src/core/js/etc/multiParty.js                      |  336 ++
 src/core/js/etc/otr.js                             |  206 +
 src/core/js/etc/random.js                          |   99 +
 src/core/js/etc/storage.js                         |  158 +
 src/core/js/etc/templates.js                       |  105 +
 src/core/js/etc/xmpp.js                            |  330 ++
 src/core/js/lib/bigint.js                          | 1705 ++++++++
 src/core/js/lib/crypto-js.js                       | 3163 +++++++++++++++
 src/core/js/lib/crypto-js/aes.js                   |  213 +
 src/core/js/lib/crypto-js/cipher-core.js           |  863 ++++
 src/core/js/lib/crypto-js/core.js                  |  712 ++++
 src/core/js/lib/crypto-js/enc-base64.js            |  109 +
 src/core/js/lib/crypto-js/footer.js                |   11 +
 src/core/js/lib/crypto-js/header.js                |   13 +
 src/core/js/lib/crypto-js/hmac.js                  |  131 +
 src/core/js/lib/crypto-js/mode-ctr.js              |   44 +
 src/core/js/lib/crypto-js/pad-nopadding.js         |   16 +
 src/core/js/lib/crypto-js/pbkdf2.js                |  131 +
 src/core/js/lib/crypto-js/sha1.js                  |  136 +
 src/core/js/lib/crypto-js/sha256.js                |  185 +
 src/core/js/lib/crypto-js/sha512.js                |  309 ++
 src/core/js/lib/crypto-js/x64-core.js              |  290 ++
 src/core/js/lib/elliptic.js                        |  208 +
 src/core/js/lib/eventemitter.js                    |  472 +++
 src/core/js/lib/jquery/jquery.basicslider.js       |  719 ++++
 src/core/js/lib/jquery/jquery.color.js             |    2 +
 src/core/js/lib/jquery/jquery.filterbydata.js      |   13 +
 src/core/js/lib/jquery/jquery.js                   |    4 +
 src/core/js/lib/jquery/jquery.utip.js              |  110 +
 src/core/js/lib/mousetrap.js                       |    9 +
 src/core/js/lib/mustache.js                        |  570 +++
 src/core/js/lib/otr.js                             | 2601 ++++++++++++
 src/core/js/lib/salsa20.js                         |  284 ++
 src/core/js/lib/strophe/strophe.ibb.js             |  166 +
 src/core/js/lib/strophe/strophe.js                 | 4201 ++++++++++++++++++++
 src/core/js/lib/strophe/strophe.muc.js             | 1013 +++++
 src/core/js/lib/strophe/strophe.ping.js            |   53 +
 src/core/js/lib/strophe/strophe.si-filetransfer.js |  159 +
 src/core/js/lib/tinycon.js                         |    8 +
 src/core/js/workers/dsa.js                         |   32 +
 src/core/js/workers/smp.js                         |   44 +
 src/core/locale/ar.txt                             |   87 +
 src/core/locale/bg.txt                             |   87 +
 src/core/locale/bo.txt                             |   68 +
 src/core/locale/ca.txt                             |   87 +
 src/core/locale/cs.txt                             |   87 +
 src/core/locale/da.txt                             |   87 +
 src/core/locale/de.txt                             |   87 +
 src/core/locale/el.txt                             |   87 +
 src/core/locale/en.txt                             |   87 +
 src/core/locale/es.txt                             |   87 +
 src/core/locale/et.txt                             |   68 +
 src/core/locale/eu.txt                             |   79 +
 src/core/locale/fa.txt                             |   87 +
 src/core/locale/fr.txt                             |   87 +
 src/core/locale/he.txt                             |   87 +
 src/core/locale/in.txt                             |   79 +
 src/core/locale/it.txt                             |   87 +
 src/core/locale/ja.txt                             |   87 +
 src/core/locale/km.txt                             |   79 +
 src/core/locale/kn.txt                             |   69 +
 src/core/locale/ko.txt                             |   87 +
 src/core/locale/lol.txt                            |   87 +
 src/core/locale/lv.txt                             |   68 +
 src/core/locale/nl.txt                             |   87 +
 src/core/locale/no.txt                             |   87 +
 src/core/locale/old/ug.txt                         |   53 +
 src/core/locale/old/ur.txt                         |   53 +
 src/core/locale/pl.txt                             |   87 +
 src/core/locale/pt.txt                             |   87 +
 src/core/locale/ru.txt                             |   87 +
 src/core/locale/sk.txt                             |   87 +
 src/core/locale/sv.txt                             |   87 +
 src/core/locale/tr.txt                             |   87 +
 src/core/locale/uk.txt                             |   87 +
 src/core/locale/vi.txt                             |   87 +
 src/core/locale/zh-cn.txt                          |   87 +
 src/core/locale/zh-hk.txt                          |   87 +
 src/core/manifest.json                             |   26 +
 src/core/snd/balloon.mp3                           |  Bin 0 -> 192611 bytes
 src/core/snd/balloon.ogg                           |  Bin 0 -> 185456 bytes
 src/core/snd/keygenEnd.mp3                         |  Bin 0 -> 50300 bytes
 src/core/snd/keygenEnd.ogg                         |  Bin 0 -> 39483 bytes
 src/core/snd/keygenLoop.mp3                        |  Bin 0 -> 88544 bytes
 src/core/snd/keygenLoop.ogg                        |  Bin 0 -> 97532 bytes
 src/core/snd/keygenStart.mp3                       |  Bin 0 -> 45286 bytes
 src/core/snd/keygenStart.ogg                       |  Bin 0 -> 32739 bytes
 src/core/snd/msgGet.mp3                            |  Bin 0 -> 12680 bytes
 src/core/snd/msgGet.ogg                            |  Bin 0 -> 23039 bytes
 src/core/snd/userJoin.mp3                          |  Bin 0 -> 25840 bytes
 src/core/snd/userJoin.ogg                          |  Bin 0 -> 38706 bytes
 src/core/snd/userLeave.mp3                         |  Bin 0 -> 25841 bytes
 src/core/snd/userLeave.ogg                         |  Bin 0 -> 38175 bytes
 src/firefox/chrome.manifest                        |    5 +
 src/firefox/chrome/content/browser.xul             |   32 +
 src/firefox/chrome/content/cryptocat.js            |   50 +
 src/firefox/defaults/preferences/prefs.js          |    2 +
 src/firefox/install.rdf                            |   22 +
 src/firefox/locale/en-US/translations.dtd          |    1 +
 src/firefox/skin/bar.png                           |  Bin 0 -> 320 bytes
 src/firefox/skin/menu.png                          |  Bin 0 -> 286 bytes
 src/firefox/skin/skin.css                          |    3 +
 src/mac/Cryptocat-Info.plist                       |   36 +
 src/mac/Cryptocat-Prefix.pch                       |    8 +
 src/mac/Cryptocat.entitlements                     |   14 +
 src/mac/Cryptocat.icns                             |  Bin 0 -> 141344 bytes
 src/mac/Cryptocat.xcodeproj/project.pbxproj        |  379 ++
 .../project.xcworkspace/contents.xcworkspacedata   |    7 +
 .../xcshareddata/Cryptocat.xccheckout              |   77 +
 .../dev.xcuserdatad/xcschemes/Cryptocat.xcscheme   |   86 +
 .../xcschemes/xcschememanagement.plist             |   22 +
 .../fred.xcuserdatad/xcschemes/Cryptocat.xcscheme  |   86 +
 .../xcschemes/xcschememanagement.plist             |   22 +
 .../xcschemes/Cryptocat.xcscheme                   |   86 +
 .../xcschemes/xcschememanagement.plist             |   22 +
 src/mac/CryptocatAppDelegate.h                     |   13 +
 src/mac/CryptocatAppDelegate.m                     |   43 +
 src/mac/CryptocatWindowController.h                |   15 +
 src/mac/CryptocatWindowController.m                |  119 +
 src/mac/CryptocatWindowController.xib              |   51 +
 src/mac/CryptocatWindowManager.h                   |   25 +
 src/mac/CryptocatWindowManager.m                   |   78 +
 src/mac/en.lproj/Credits.rtf                       |   10 +
 src/mac/en.lproj/InfoPlist.strings                 |    2 +
 src/mac/en.lproj/MainMenu.xib                      |  329 ++
 src/mac/fileUtils.h                                |   83 +
 src/mac/htdocs/placeholder.txt                     |    1 +
 src/mac/kConstants.h                               |   15 +
 src/mac/main.m                                     |   13 +
 src/opera/manifest.json                            |   27 +
 src/opera/opera.js                                 |    9 +
 src/safari/Info.plist                              |   63 +
 src/safari/Settings.plist                          |    5 +
 src/safari/icon-16.png                             |  Bin 0 -> 286 bytes
 src/safari/icon-32.png                             |  Bin 0 -> 2080 bytes
 src/safari/icon-64.png                             |  Bin 0 -> 3394 bytes
 src/safari/safari.html                             |   19 +
 src/standaloneServer.js                            |   11 +
 test/core/js/aes.test.js                           |  103 +
 test/core/js/bigint.test.js                        |   25 +
 test/core/js/catFacts.test.js                      |   31 +
 test/core/js/elliptic.test.js                      |  105 +
 test/core/js/encoding.test.js                      |   93 +
 test/core/js/hmac.test.js                          |  200 +
 test/core/js/locale.test.js                        |   94 +
 test/core/js/mustache.test.js                      |   27 +
 test/core/js/salsa20.test.js                       |  131 +
 test/core/js/sha.test.js                           |   99 +
 test/testBase.js                                   |   43 +
 220 files changed, 32140 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..c17df55
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,509 @@
+#Changelog
+
+##Cryptocat 2.2.2
+**June 12 2014**
+
+- Encrypted file transfer is now back in Cryptocat after being temporarily disabled pending review. Note that encrypted file transfer is not available over the Encrypted Facebook Chat feature.  
+- Updated Ukrainian translation.  
+- Small bug fixes.  
+
+##Cryptocat 2.2.1
+**May 13 2014**
+
+- The "Logout" button now makes Cryptocat forget Facebook login information by default.  
+- Fixed a bug that would cause single quotes and apostrophes to render inaccurately in Facebook conversations.  
+- User interface bug fixes and improvements.  
+- Updated Catalan and Slovak translations.  
+
+##Cryptocat 2.2
+**May 10 2014**
+
+Cryptocat 2.2 brings a major new feature: Encrypted Facebook Chat!
+Load your Facebook Chat contacts within Cryptocat. Cryptocat will detect other Facebook friends using Cryptocat and will allow you to immediately set up encrypted chat.
+
+You can still chat with regular Facebook contacts from within Cryptocat, but the conversations won't be encrypted. If a Facebook friend also logs in via Cryptocat, your chat will be immediately upgraded to an encrypted Cryptocat chat. Your encrypted chats cannot be viewed by Facebook or by Cryptocat since they are encrypted via the OTR protocol. 
+
+Both individuals must be using Cryptocat (or an OTR-enabled client) for this feature to work. A blog post will be published upon release clarifying this feature further.
+
+Cryptocat 2.2 also brings:
+- Typing notifications have been redesigned to be less intrusive, after complaints from users.  
+- Cryptocat's buddy list now maintains automatic alphabetic ordering.  
+- Fixed a bug that could render the authentication status inaccurately in rare circumstances.  
+- Updates to many of Cryptocat's translations.  
+- Tons of small fixes and improvements.  
+
+##Cryptocat 2.1.23
+**Apr. 10 2014**
+
+- Since we are currently unable to actively maintain it, Tor integration has been removed from Cryptocat for Mac.  
+- Fixed a bug that could, in limited circumstances, mistakenly show unauthenticated participants as authenticated.  
+- Fixed two non-time-invariant MAC comparisons.  
+- Fixed a bug that could allow an attacker to stall a Cryptocat conversation.
+- Fixed a bug that prevented audio notifications from playing in Firefox on Linux.  
+- Many small improvements and tweaks to the code and user interface.  
+- Many translations have been updated.  
+
+##Cryptocat 2.1.22
+**Apr. 2 2014**
+
+*Note: this release is currently in the process of being rolled out to production. The delay is that so we can launch the update across all platforms simultaneously.*  
+
+- Authentication is completely redone. Now, Cryptocat has a new, better and more useful authentication interface that reminds users to authenticate, shows authentication status, and even includes a slideshow tutorial on what authentication is and why it's important. Learn more about the authentication improvements on our blog: https://blog.crypto.cat/2014/04/recent-audits-and-coming-improvements/  
+- Authenticating via secret questions (SMP) now authenticates both group conversations and private conversations.  
+- Keyboard shortcuts! Press `ctrl+2` to switch to the next buddy, and `ctrl+1` to move back to the previous buddy.  
+- Important user interface change: the active conversation no longer switches to the top of the buddy list. This is done so that keyboard shortcuts can work better.  
+- Fixed a bug that made conversation content "jump" when a user finished sending a message for which there was a "typing" icon.  
+- Many improvements to the user interface.  
+- Many small improvements and tweaks.  
+- Many translations have been updated.  
+
+##Cryptocat 2.1.21
+**Mar. 13 2014**
+
+- Cryptocat for Firefox now only supports Firefox 21 and higher. This allows us to remove some outdated hacks that were necessary to allow older versions of Firefox to support Cryptocat.  
+- Added Ukrainian translation.  
+- Small fixes were made to the Spanish translation.  
+- The authentication interface has been updated to clarify that question-based authentication is for private OTR chats only.  
+- Fixed a bug that would allow clients with malformed nicknames to cause a crash in other Cryptocat clients in a conversation.  
+- Fixed a bug that would allow clients with malformed nicknames to secretly join a Cryptocat conversation and obtain ciphertext. These clients would not be part of the conversation however and would not be able to obtain plaintext.  
+- Fixed a bug that wouldn't allow non-latin characters in question-based authentication.  
+- Fixed a bug in Cryptocat for Firefox that would not add a Cryptocat button to the Firefox toolbar.  
+- Fixed a bug that would not allow the user to scroll up for a few seconds if they receive too many new messages in a short period of time.  
+- Many small improvements were made to the user interface.  
+- Many improvements and optimizations were made to the Cryptocat code.  
+- Updated Cryptocat dependencies to their latest versions.  
+
+
+##Cryptocat 2.1.20
+**Feb. 4 2014**
+
+**This is an important update with fixes for both usability and security.**
+
+- Fixed a bug introduced in Cryptocat 2.1.19 that prevented group conversations with exactly two participants from working properly.  
+- Addressed a security risk that could allow an attacker to divert the destination of outgoing chat messages.  
+- Added join/part notifications to one-on-one conversation windows.  
+
+##Cryptocat 2.1.19
+**Jan. 30 2014**
+
+- Cryptocat now supports anonymous SASL authentication.  
+- Fixed a bug that prevented tooltips for chat settings from displaying properly.  
+- Fixed a bug that would prevent the sending of messages if the user was the only person in a conversation.  
+- Fixed a bug that prevented text from showing in desktop notifications.  
+- Updated OTR.js and jQuery libraries to their latest versions.  
+- Other bug fixes.  
+
+##Cryptocat 2.1.18
+**Jan. 4 2014**
+
+This update contains fixes specific only to the Firefox and Opera versions of Cryptocat. It will not be released for any other platform.  
+
+- Firefox: Fixed a bug that could break compatibility with the latest Tor Browser Bundle.  
+- Opera: Fixed a bug that could prevent audio notifications from playing.  
+
+##Cryptocat 2.1.17
+**Dec. 5 2013**
+
+- **Security fix**: A bug which moderately weakened the keys used for group chat was fixed. The reduction in security is not thought to be enough to allow for the easy brute-forcing of keys. The buggy update (2.1.16) was available for less than six days before this bug was detected, and was never released for Firefox. We thank security researcher Steve Thomas for his incredibly valuable contributions to Cryptocat.  
+- Fixed a sometimes-incorrect alignment of warning messages in the user interface.  
+- The "message received" audio notification sound was redone. The sound was composed by Rich Vreeland.  
+- Usability improvements were made to the Authentication Question (SMP) feature.  
+- Improvements were made to the German translation.  
+
+##Cryptocat 2.1.16
+**Nov. 29 2013**
+
+- Cryptocat is now available for Opera 15 and higher.  
+- Big improvements to group chat messaging reliability.  
+- Added a "Getting Started" guide that appears for first-time Cryptocat users.  
+- New feature: Save custom server configurations to quickly access later.  
+- Cryptocat for Firefox now also remembers settings and preferences.  
+- Key generation speed improved by up to 40%!  
+- Encrypted file sharing is temporarily disabled in order to investigate a potential issue.  
+- Implemented some preparations for compatibility with upcoming Cryptocat for iPhone and Android.  
+- Buddy context menus now have an improved look.  
+- Audio notifications now work in Safari (only versions 7 and higher).  
+- Highly substantial reduction in Cryptocat's file size due to code optimization and audio file encoding improvements.  
+- Add new Kannada translation.  
+- Fix inconsistencies in some translations.  
+- Many other bug fixes, improvements, optimizations and tweaks.  
+
+##Cryptocat 2.1.15
+**Oct. 13 2013**  
+- Improve connection stability.  
+- Fixed a bug where messages with URLs would only appear as URL and without accompanying message text.  
+- Small bug fixes and tweaks.  
+
+##Cryptocat 2.1.14
+**Sep. 16 2013**
+- Major new feature: Cryptocat now automatically reconnects to conversations when disconnected, without troubling the user. Cryptocat will automatically detect accidental disconnections and wait for the Internet connection to be re-established before reconnecting.  
+- Major new feature: Cryptocat for Mac now includes built-in Tor support. When enabled, Cryptocat for Mac will automatically route all connections through the Tor anonymity network. You do not need to have Tor installed.  
+- Cryptocat for Chrome now uses the new "packaged app API."  
+- Authentication via questions has been greatly improved.  
+- Fixed a confusing user interface bug that would allow you to ask yourself an authentication question.  
+- New audio notifications have been added for key generation and user connection.  
+- The audio notifications for user join and leave have been replaced with new ones.  
+- Browser window title now displays conversation names, for increased usability when running multiple Cryptocat tabs.  
+- Many small user interface fixes and improvements.  
+
+##Cryptocat 2.1.13
+**Aug. 26 2013**  
+**Pending public release in early September.**  
+Group chat in this version of Cryptocat is not compatible with previous versions. In order to be able to use group chat, all participants must update to Cryptocat 2.1.13 or higher. Due to this, this release will be delayed until it can be rolled out simultaneously across all platforms. Updating is highly recommended.  
+
+- Important change: the "Block" feature has been changed to an "Ignore" feature. You may ignore messages from users, but you can no longer prevent them from receiving your messages. Ignored users will have a strikethrough through their nickname, displayed in the buddy list.  
+- New feature: SMP authentication. You can now authenticate a buddy's identity by asking them a secret question.  
+- New feature: Message previews! Private messages for conversations not in focus are now previewed via small speech bubbles that appear next to the buddy list.  
+- New feature: Cryptocat for Chrome, Firefox and Safari now displays new message notifications inside the favicon, in the browser tab.  
+- Fixed an issue where users may be able to send group chat messages to certain participants, but not to others.  
+- Fixed a bug where Cryptocat would not allow the file transfer of certain types of ZIP files.  
+- Fixed an issue where Cryptocat would display private messages if they were received without encryption. Now, all unencrypted messages are dropped.  
+- Cryptocat's user interface has been improved in numerous areas.  
+- Add warnings for decryption failures and for situations where Cryptocat thinks a user in the conversation is sending cryptographically suspicious messages.  
+- Fixed a bug that would prevent buddy submenus from working if more than one was open at the same time.  
+- Some work was done to resolve some potential openings for cryptographic timing attacks.  
+- Fixed a bug that would prevent disconnection and logout messages from displaying properly to the user disconnecting or logging out.  
+- Fixed a bug in Cryptocat for Firefox that would cause Cryptocat to freeze for around one second when switching tabs.  
+- User join and part notifications and messages now include timestamps.  
+- Fixed a bug in Cryptocat for Mac that would add an unnecessary scroll bar to the application window.  
+- Fixed a bug that would prevent more than two cat facts from being displayed in one session.  
+- Cryptocat for Mac now bounces the dock when new messages are received.  
+- Many translation mistakes and typos have been fixed.  
+- Many other small improvements and bug fixes.  
+
+##Cryptocat 2.1.12
+**Jul. 12 2013**
+- Fixed some non-critical security issues reported by Steve Thomas that slightly reduce the bits of entropy in OTR authentication. Updating is recommended.  
+- Fixed a pseudo-random number generator bug that causes some bias and wastes entropy. Updating is recommended.  
+- Fixed a user interface bug that would make nicknames hard to read under certain circumstances.  
+- Fixed a user interface bug that would sometimes show jagged-looking login text.  
+
+##Cryptocat 2.1.11
+**Jul. 2 2013**
+- Fixed a bug in Cryptocat for Mac that would cause problems when users tried to open links.
+
+##Cryptocat 2.1.10
+**Jun. 28 2013**
+- Fixed a bug that prevented desktop notifications from working in Chrome and Safari.  
+- Many usability bug fixes for Firefox.  
+- Fixed a typo.  
+
+##Cryptocat 2.1.9
+**Jun. 27 2013**
+- Web notifications are now available for Firefox 22 and higher!  
+- Fixed a bug that would cause Cryptocat to not launch in browsers set to certain languages and locales.  
+
+##Cryptocat 2.1.8
+**Jun. 26 2013**  
+- Fixed a bug that would sometimes stall group conversations after some activity, or if a user switched their status to "away".  
+
+##Cryptocat 2.1.6
+**Jun. 24 2013**  
+- Big fixes to file transfer! If you've been experiencing problems, this update should help.  
+- File transfer now works fully in Cryptocat for Safari and Mac, as well as Chrome and Firefox.  
+- Cryptocat for Mac has been rewritten from scratch.  
+- Cryptocat for Mac now supports creating multiple windows for multiple conversations.  
+- Some internal optimizations and updates.  
+- Bug fixes.  
+- Updated Russian and Farsi translations.  
+
+##Cryptocat 2.1.5
+**Jun. 15 2013**  
+- Fixed a series of user experience incompatibilities with previous versions.  
+
+##Cryptocat 2.1.3/2.1.4
+**Jun. 14 2013**  
+- New feature: Block users! Prevents annoying users from sending you messages/from seeing your messages.  
+- New feature: Typing notification. See when a user is composing a message (both users need to be using Cryptocat 2.1.3 or higher.)  
+- Fixed a bug that would prevent conversations from scrolling down when tabs are switched.  
+- Fixed a bug that prevented nickname tab completion from working.  
+- Fixed a bug that would prevent some ZIP files from being transferred.  
+- Fixed a bug that would prevent audio and desktop notification settings from being saved.  
+- Fixed a bug that would prevent a user from logging back in after logging out.  
+- Fixed a bug that would show a vertical scroll bar on logout for no reason.  
+- Additional small bug fixes.  
+- Uighur translation has returned.  
+
+##Cryptocat 2.1.1/2.1.2
+**Jun. 10 2013**  
+- Fix a connectivity bug introduced in the previous update that could lead to connectivity problems as well as an erroneous mismatch in OTR fingeprint displays in rare circumstances.  
+- Many small user interface and usability bug fixes.  
+
+##Cryptocat 2.1
+**Jun. 7 2013**  
+This is a major update to Cryptocat with many improvements and bug fixes.  
+
+- User Interface redesign: Cryptocat's user interface has been redesigned to be brighter, friendlier, and faster. Existing users will find the new design familiar enough use, while new users will benefit from a friendlier user experience.  
+- Encrypted file sharing: Send files via Cryptocat. ZIP files as well as images can now be shared with people inside a chatroom.  
+- Security enhancements and bug fixes. Updating is recommended.  
+- Major code cleanup and optimizations, including many bug fixes.  
+- Added 41 new interesting cat facts.  
+- Updated jQuery to 2.0.2.  
+- Updated OTR libraries to version 0.1.5.  
+
+**Known issues**: Safari users are currently able to send but not receive files. The Uighur translation is also currently unavailable and will return in a future version.  
+
+##Cryptocat 2.0.42
+**Apr. 19 2013**
+- IMPORTANT: Due to changes to multiparty key generation (in order to be compatible with the upcoming mobile apps), this version of Cryptocat cannot have multiparty conversations with previous versions. However private conversations still work. Due to this, this release will not be pushed until it is both reviewed by Mozilla (for the Firefox Addons Website) and Apple (for the Mac App Store).
+- Fixed a bug found in the encryption libraries that could partially weaken the security of multiparty Cryptocat messages.
+
+##Cryptocat 2.0.41
+**Mar. 14 2013**
+- Fixed a bug in Cryptocat for Mac that prevented text selection, copy and paste from working.
+- Updated OTR libraries to version 0.1.3.
+- Notification settings are now saved from session to session.
+- Improved Italian translation.
+- Improved Japanese translation.
+- Some small tweaks and adjustments.
+
+##Cryptocat 2.0.40
+**Mar. 3 2013**
+- More substantial color scheme and UI improvements.  
+
+##Cryptocat 2.0.39
+**Mar. 3 2013**
+- User interface bug fixes.  
+
+##Cryptocat 2.0.38
+**Mar. 2 2013**
+- Cryptocat will now save your language, server and nickname preferences automatically. This feature does not work yet on Firefox.
+- Cryptocat will now automatically log out on connection failure.
+- Improved message input interface.
+- Many small user interface tweaks and improvements.
+- Removed unused libraries, replaced some libraries.
+- Updated jQuery to version 1.9.1.
+- Minor bug fix in elliptic curve cryptography library.
+- Russian translation improvements.
+- Czech translation improvements.
+
+##Cryptocat 2.0.37
+**Feb. 11 2013**
+- Added Bengali and Bulgarian translations.
+- Minor German translation corrections.
+- Small usability improvements.
+
+##Cryptocat 2.0.36
+**Feb. 8 2013**
+- New and improved language selection interface.
+
+##Cryptocat 2.0.35
+**Feb. 6 2013**
+- Update Tibetan translation and improve language display semantics.
+
+##Cryptocat 2.0.34
+**Feb. 5 2013**
+- Updated Korean, Latvian and Urdu translations.
+- User interface minor bug fixes and improvements.
+
+##Cryptocat 2.0.33
+**Feb. 2 2013**
+- Redesigned message display: Now with speech bubbles, better colours and more!
+
+##Cryptocat 2.0.32
+**Feb. 2 2013**
+- "Retina-ready" graphics — high definition interface graphics for high resolution displays.
+- Prevent desktop notifications from lasting more than 5 seconds.
+- Improved Catalan translation.
+
+##Cryptocat 2.0.31
+**Jan. 30 2013**
+- More than a dozen bug fixes and tweaks related to the redesigned UI which was pushed last version.
+- Logging in/logging out is now faster.
+- Fixed a bug that could prohibit desktop notifications from appearing in Google Chrome.
+- Fixed a bug that could prevent notification sounds from being played in Google Chrome.
+- Warnings have been added to all translations except Tibetan, Korean, Latvian and Urdu (will be updated in an upcoming version.)
+- New: Uyghur translation.
+- Esperanto translation removed.
+- Font family tweaks.
+
+##Cryptocat 2.0.30
+**Jan. 23 2013**
+- Comprehensive user interface overhaul with:
+	- A full-screen, fluid interface.
+	- New fonts.
+	- An improved color scheme.
+- Usage tips and warnings now included on sign-in screen.
+
+##Cryptocat 2.0.29
+**Jan. 22 2013**
+- Removed unnecessary permission demands from Chrome.
+- Improved random number generation mechanics (thanks to Dmitry Chestnykh).
+- Some aesthetic and other bug fixes.
+
+##Cryptocat 2.0.28
+**Jan. 17 2013**
+- Key generation speed improved by over 33%.
+- Desktop notifications are now available in Safari.
+- Korean translation fixes.
+- Tibetan text is now more readable.
+- Added a cat fact.
+- Some aesthetic bug fixes.
+
+##Cryptocat 2.0.27
+**Dec. 12 2012**
+- Added Estonian, Esperanto, Japanese, Korean, Latvian and Khmer
+translations.
+
+##Cryptocat 2.0.26
+**Nov. 20 2012**
+- Fixed bug where fingerprints would sometimes not appear.
+- Version number is now displayed on login screen.
+- Fix French translation typos.
+- Update jQuery to 1.8.3.
+- Replaced call to nsILocalFile with nsIFile (Firefox-specific).
+
+##Cryptocat 2.0.25
+**Nov. 15 2012**
+- Fixed inaccuracies in Turkish, Norwegian and Polish.
+- Updated OTR.js library to version 0.0.11.
+
+##Cryptocat 2.0.24
+**Nov. 14 2012**
+- Added Urdu, Turkish and Norwegian translations.
+- Added a tooltip-based tutorial.
+- Multiparty protocol improvements:
+	* Added replay attack prevention.
+	* Added message tags to ensure all participants receive same plaintext per message.
+	* Improved multiparty protocol specification details.
+- Firefox performance improvements.
+- Fixed bug that would cause "nickname in use" error to show improperly.
+- Updated source code repository links.
+- Updated OTR.js library to version 0.0.10.
+- Fixed a buddy notification aesthetic bug.
+
+##Cryptocat 2.0.23
+**Nov. 8 2012**
+- Fixed a bug where URLs would not display properly in messages.
+
+##Cryptocat 2.0.22
+**Nov. 7 2012**
+- This version pushes many important security fixes, detailed here on the Cryptocat Development Blog: https://blog.crypto.cat/2012/11/security-update-our-first-full-audit/
+- Added a user counter.
+
+##Cryptocat 2.0.21
+**Nov. 4 2012**
+- Join/part notifications.
+- Improved nickname completion.
+- Small UX tweaks.
+
+##Cryptocat 2.0.20
+**Nov. 3 2012**
+- Bug fixes.
+- Updated Chinese translation.
+
+##Cryptocat 2.0.19
+**Nov. 1 2012**
+- Language detection bug fixes.
+- Minor security fixes.
+- Other minor bug fixes.
+
+##Cryptocat 2.0.18
+**Oct. 29 2012**
+- Fixed a bug where the fingerprint dialog would sometimes not show.
+
+##Cryptocat 2.0.17
+**Oct. 23 2012**
+- A bug fix was not pushed in the previous update by mistake.  
+
+##Cryptocat 2.0.16
+**Oct. 23 2012**
+- Fixed major multiparty messaging bug.  
+
+##Cryptocat 2.0.15
+**Oct. 22 2012**
+- Fix bug where login would never complete (in extremely rare cases.)
+- Minor UI bug fix.
+
+##Cryptocat 2.0.14
+**Oct. 22 2012**
+- Refactoring of random number generation.
+- Fix bug where buddy context menu would not open under certain circumstances.
+- Numerous other bug fixes.
+
+##Cryptocat 2.0.13
+**Oct. 20 2012**  
+- Various bug fixes and improvements related to fingerprint generation and display.  
+
+##Cryptocat 2.0.12
+**Oct. 19 2012**  
+- Colorprints
+(https://blog.crypto.cat/2012/10/colorprints-an-easier-way-to-authenticate/)  
+- Fix a bug that would display certain characters incorrectly in desktop notifications.  
+- Fix a bug that would not display a certain emoticon.  
+
+##Cryptocat 2.0.11
+**Oct. 17 2012**  
+- Nickname tab completion.  
+- Messages containing your nickname are highlighted.  
+- Prettier message display.  
+- Small bug fixes related to group message delivery.  
+
+##Cryptocat 2.0.10
+**Oct. 17 2012**  
+- Numerous user interface improvements.  
+- Tibetan translation.  
+- Improved existing translations (notably Chinese.)  
+- Better, more efficient translation handling.  
+- Added 12 new cat facts.  
+- Fixed a bug where desktop notifications would appear unnecessarily.  
+- Fixed a bug where Chrome extension would not ask for the proper permissions needed to connect to custom servers.  
+
+##Cryptocat 2.0.9
+**Oct. 1 2012**  
+- Fix bug that would attempt to enter conversation even if conversation name was invalid.  
+- Fix permissions for Chrome version so that Cryptocat for Chrome can connect to custom XMPP-BOSH servers.  
+
+##Cryptocat 2.0.8
+**Sep. 30 2012**  
+- Fixed bug which prevented Cryptocat from working specifically in Firefox on Fedora Linux.  
+
+##Cryptocat 2.0.7
+**Sep. 29 2012**  
+- Switch completely to browser native randomness seeding, remove Fortuna RNG.  
+
+##Cryptocat 2.0.6
+**Sep. 29 2012**  
+- Fixed a bug where some characters would be displayed incorrectly/in rare occasions cause the client to crash.  
+- Fixed UI bugs where some buttons appeared too large.  
+
+##Cryptocat 2.0.5
+**Sep. 29 2012**  
+- Many substantial Firefox-specific code improvements.  
+- Many small tweaks, bug fixes, cleanups and improvements.  
+- Update jQuery and jQuery plugin libraries.  
+- Update Strophe.js library.  
+
+```Note: This version has many localStorage features (such as persistent
+keys and settings) fully implemented, but all are disabled (in all
+browsers) due to Firefox bug #795615. Will be enabled as soon as that's
+fixed.```  
+
+##Cryptocat 2.0.4
+**Sep. 27 2012**  
+- Fixed bug where user would not be able to join any conversation again after logging out from first conversation (until restarting Cryptocat).  
+- Added Chinese (Hong Kong), Chinese (Traditional), Vietnamese and Hebrew translations.  
+- Minor aesthetic improvements.  
+- Minor code cleanup.  
+
+##Cryptocat 2.0.3
+**Sep. 24 2012**  
+- Fixed bug where conversation names with capital letters would not function properly.  
+- Fixed bug where away contacts were not arranged properly on buddy list.  
+
+##Cryptocat 2.0.2
+**Sep. 24 2012**  
+- Faster OTR key generation.  
+- Native CSPRNG support now implemented in Firefox, making all Cryptocat extensions have native CSPRNG support.  
+- Translation fixes.  
+- Misc. tweaks.  
+
+##Cryptocat 2.0.1
+**Sep. 23 2012**  
+- Add Lolcat translation.  
+- Czech translation fixes.  
+- Lower minimum Firefox version to Firefox 9.  
+
+##Cryptocat 2.0.0
+**Sep. 22 2012**  
+- Initial release.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1c831f7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+#Contributing to Cryptocat
+
+##Coding style for contributions
+Please be mindful that you are contributing to security and encryption software. We expect all code to be pre-reviewed for security and to be of high quality.  
+
+All contributed code, written in JavaScript, must adhere to the following coding style:  
+	1. Tabs are used for indentation, **not** spaces.  
+	2. Lines are **not** ended with semicolons.  
+	3. Use camel case for variables, filenames, and so on.  
+	4. As a rule, use single quotes, (such as 'string'), not double quotes ("string").  
+	5. As a rule, strict-type isEqual is preferred (1 **===** 1 instead of 1 **==** 1).  
+	6. Please comment your code sufficiently.  
+	7. Anonymous closures should be used wherever they are useful.  
+	8. Brackets are not on new lines.  
+	9. [Strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode) must be enforced at all times.
+
+##Contributing or improving translations
+Please **do not send pull requests for translations**. Instead, use [Transifex](https://www.transifex.com/projects/p/Cryptocat/resource/cryptocat/). Notify a project manager for Cryptocat on Transifex in case you need assistance.  
+**Note**: In translations, please do **not**:
+* Insert newlines in strings you are translating.  
+* Translate wildcard strings such as `(NICKNAME)` or `(SIZE)`. Leave those as they are.  
+
+##Tests
+* Run tests using `make tests`.
+* Make sure your code conforms by running `make lint`.
+
+##License
+All contributed code will automatically be licensed under the [GNU Affero General Public License (AGPL3)](https://www.gnu.org/licenses/agpl-3.0.html).  
+The full license text is included in `LICENSE.txt`.  
+
+##Discussion & Blog
+* [Issue tracker](https://github.com/cryptocat/cryptocat/issues)
+* [Development Blog](https://blog.crypto.cat)  
+
+##Contributors
+* **Arlo Breault**: OTR library maintainer, bug reporter, all-around helper.  
+* **Dmitry Chestnykh**: Salsa20 CSPRNG implementation.  
+* **David Dahl**: window.crypto.getRandomValues() implementation for Firefox.  
+* **Daniel "koolfy" Faucon**: Documentation maintainer, bug reporter.  
+* **Andreas "Gordin" Guth**: StropheJS maintainer, WebSocket & anonymous auth implementation.  
+* **Frederic Jacobs**: Substantial contributions to Cryptocat for Mac.  
+* **Nadim Kobeissi**: Lead developer. Created Cryptocat.  
+
+###Multimedia
+* **Ingrid Burrington**: Some icons and graphics.  
+* **A.J. Korkidakis**: Promotional video.  
+* **P.J. Onori**: Some of the icons.  
+* **Rich Vreeland**: Audio notifications.  
+  
+**With warm thanks to a contributor who has asked to remain anonymous.**
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..8e62fab
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,165 @@
+// WARNING:
+// This Gruntfile is not being maintained and is out of date.
+// Please use the Makefile instead.
+// — Nadim, March 31 2014
+
+'use strict';
+
+module.exports = function (grunt) {
+
+	var srcFolder = __dirname + '/src/core',
+		firefoxFolder = __dirname + '/src/firefox',
+		firefoxExtensionFolder = firefoxFolder + '/chrome/content/data',
+		safariFolder = __dirname + '/src/safari',
+		safariExtensionFolder = __dirname + '/src/cryptocat.safariextension',
+		releaseFolder = __dirname + '/release';
+
+
+	grunt.initConfig({
+		pkg:grunt.file.readJSON('package.json'),
+
+		mochaTest:{
+			files:['test/core/js/*.js']
+		},
+		mochaTestConfig:{
+			options:{
+				reporter:'spec',
+				ui:'exports'
+			}
+		},
+
+		clean: {
+			'copy': [firefoxExtensionFolder, safariExtensionFolder],
+			'release': [releaseFolder],
+		},
+
+		mkdir: {
+			'copy':{
+				options: {
+					create:[firefoxExtensionFolder, safariExtensionFolder]
+				}
+			},
+			'release': {
+				options: {
+					create:[releaseFolder]
+				}
+			}
+		},
+
+		copy:{
+			'firefox':{
+				files:[
+					{
+						expand:true,
+						src:[ 'css/**', 'img/**', 'js/**', 'locale/**', 'snd/**', 'locale/**', 'index.html' ],
+						dest:firefoxExtensionFolder,
+						cwd:srcFolder
+					}
+				]
+			},
+			'safari':{
+				files:[
+					{
+						expand:true,
+						src:[ 'css/**', 'img/**', 'js/**', 'locale/**', 'snd/**', 'locale/**', 'index.html' ],
+						dest:safariExtensionFolder,
+						cwd:srcFolder
+					},
+					{
+						expand:true,
+						src:[ '*' ],
+						dest:safariExtensionFolder,
+						cwd:safariFolder
+					}
+				]
+			}
+		},
+
+		jshint:{
+			options:{
+				jshintrc: '.jshintrc'
+			},
+			files:[
+				'Gruntfile.js',
+				'src/core/js/cryptocat.js',
+				'src/core/js/lib/elliptic.js',
+				'src/core/js/lib/salsa20.js',
+				'src/core/js/etc/*.js',
+				'src/standaloneServer.js',
+				'src/firefox/chrome/content/cryptocat.js',
+				'test/testBase.js',
+				'test/core/js/*.js'
+			]
+		}
+	});
+
+
+	grunt.loadNpmTasks('grunt-contrib-copy');
+	grunt.loadNpmTasks('grunt-contrib-jshint');
+	grunt.loadNpmTasks('grunt-contrib-clean');
+	grunt.loadNpmTasks('grunt-mocha-test');
+	grunt.loadNpmTasks('grunt-mkdir');
+
+	grunt.registerTask('tests', 'Run tests', ['mochaTest']);
+
+	grunt.registerTask('pre-build', 'Pre-build tasks', ['tests', 'jshint']);
+
+
+	/**
+	 * Create zip file.
+	 * @param inputFolder String folder containing input files.
+	 * @param outputFile String output file.
+	 * @param cb Function result callback with signature: (Boolean)
+	 */
+	var createZipFile = function (inputFolder, outputFile, cb) {
+		grunt.util.spawn({
+			cmd:'zip',
+			opts:{
+				cwd:inputFolder
+			},
+			args:['-q', '-r9', outputFile, '.', '-x', '*/\\.*', '-x', '\\.*'],
+			fallback:-255
+		}, function (error, result, code) {
+			if (-255 === code) {
+				grunt.log.errorlns(result.stderr);
+				cb(false);
+			} else {
+				grunt.log.writeln('...created ZIP [' + outputFile + ']');
+				cb(true);
+			}
+		});
+	};
+
+
+	grunt.registerTask('build-chrome', 'Build Chrome browser extension', function () {
+		var done = this.async();
+
+		grunt.log.write('Creating Chrome extension...');
+
+		var outputFile = releaseFolder + '/cryptocat-chrome.zip';
+
+		createZipFile(srcFolder, outputFile, done);
+	});
+
+	grunt.registerTask('build-firefox', 'Build Firefox browser extension', function () {
+		var done = this.async();
+
+		grunt.log.write('Creating Firefox add-on...');
+
+		var outputFile = releaseFolder + '/cryptocat-firefox.xpi';
+
+		createZipFile(firefoxFolder, outputFile, done);
+	});
+
+	grunt.registerTask('build-safari', 'Build Safari browser extension', function () {
+		grunt.log.write('Creating Safari extension...');
+
+		grunt.task.run([ 'copy:safari' ]);
+	});
+
+
+	grunt.registerTask('build', 'Build project', ['pre-build', 'mkdir', 'copy',
+												  'build-chrome', 'build-firefox', 'build-safari']);
+
+	grunt.registerTask('default', ['build']);
+};
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100755
index 0000000..faf7bf4
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,619 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100755
index 0000000..f90e0fe
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,93 @@
+chrome:
+	@make cryptojs
+	@mkdir -p release
+	@rm -f release/cryptocat.chrome.zip
+	@cd src/core/ && zip -q -r9 ../../release/cryptocat.chrome.zip * -x "*/\.*" -x "\.*" -x "*.ogg"
+	@/bin/echo "[Cryptocat] Chrome build available in release/"
+
+firefox:
+	@make cryptojs
+	@mkdir -p release
+	@rm -f release/cryptocat.firefox.xpi
+	@mkdir src/firefox/chrome/content/data/
+	@cp -R src/core/css src/core/firstRun.html src/core/fonts src/core/img src/core/index.html src/core/js src/core/locale src/core/snd src/firefox/chrome/content/data/
+	@rm -f src/firefox/chrome/content/data/snd/*.mp3
+	@cd src/firefox/ && zip -q -r9 ../../release/cryptocat.firefox.xpi * -x "*/\.*" -x "\.*"
+	@rm -r src/firefox/chrome/content/data/
+	@/bin/echo "[Cryptocat] Firefox build available in release/"
+
+safari:
+	@make cryptojs
+	@rm -rf src/cryptocat.safariextension
+	@mkdir src/cryptocat.safariextension
+	@cp -R src/core/css src/core/firstRun.html src/core/fonts src/core/img src/core/index.html src/core/js src/core/locale src/core/snd src/cryptocat.safariextension
+	@rm -f src/cryptocat.safariextension/snd/*.ogg
+	@cp -R src/safari/* src/cryptocat.safariextension
+	@/bin/echo "[Cryptocat] Safari extension packaged for testing."
+
+opera:
+	@make cryptojs
+	@mkdir -p release
+	@rm -f release/cryptocat.opera.nex
+	@cp -R src/core/css src/core/firstRun.html src/core/fonts src/core/img src/core/index.html src/core/js src/core/locale src/core/snd src/opera
+	@rm -f src/opera/snd/*.mp3
+	@cd src/opera/ && zip -q -r9 ../../release/cryptocat.opera.nex * -x "*/\.*" -x "\.*"
+	@rm -rf src/opera/css src/opera/firstRun.html src/opera/fonts src/opera/img src/opera/index.html src/opera/js src/opera/locale src/opera/snd
+	@/bin/echo "[Cryptocat] Opera build available in release/"
+
+mac:
+	@make cryptojs
+	@rm -rf release/Cryptocat.app
+	@rm -rf release/cryptocat.mac.zip
+	@cp -R src/core/css src/core/firstRun.html src/core/fonts src/core/img src/core/index.html src/core/js src/core/locale src/core/snd src/mac/htdocs
+	@rm -f src/mac/htdocs/snd/*.ogg
+	@xcodebuild -project src/mac/Cryptocat.xcodeproj -configuration 'Release' clean
+	@xcodebuild CONFIGURATION_BUILD_DIR="${PWD}/release" -project src/mac/Cryptocat.xcodeproj -configuration 'Release' build
+	@rm -rf release/Cryptocat.app.dSYM
+	@rm -rf src/mac/htdocs/*
+	@echo "." >> src/mac/htdocs/placeholder.txt
+	@cd release && zip -q -r9 cryptocat.mac.zip Cryptocat.app
+	@/bin/echo "[Cryptocat] Mac app available in release/"
+
+tests:
+	@/bin/echo -n "[Cryptocat] Running tests... "
+	@`/usr/bin/which npm` install
+	@node_modules/.bin/mocha --ui exports --reporter spec test/core/js/*.test.js
+
+lint:
+	@/bin/echo -n "[Cryptocat] Linting code... "
+	@node_modules/.bin/jshint --verbose --config .jshintrc \
+		src/core/js/cryptocat.js \
+		src/core/js/lib/elliptic.js \
+		src/core/js/lib/salsa20.js \
+		src/core/js/etc/*.js \
+		src/standaloneServer.js \
+		src/firefox/chrome/content/cryptocat.js \
+		src/opera/opera.js \
+		src/core/chrome.js \
+		test/testBase.js \
+		test/core/js/*.js \
+		Gruntfile.js
+	@/bin/echo ""
+
+vendor=src/core/js/lib
+cryptojs_path=$(vendor)/crypto-js
+cryptojs_js=$(vendor)/crypto-js.js
+cryptojs:  # order matters
+	@/bin/echo "[Cryptocat] Concatenating crypto-js files..."
+	@cat $(cryptojs_path)/header.js > $(cryptojs_js)
+	@cat $(cryptojs_path)/core.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/enc-base64.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/cipher-core.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/x64-core.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/aes.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/sha1.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/sha256.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/sha512.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/hmac.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/pbkdf2.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/pad-nopadding.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/mode-ctr.js >> $(cryptojs_js)
+	@cat $(cryptojs_path)/footer.js >> $(cryptojs_js)
+
+all: lint tests
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..8ca19ce
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+![Cryptocat](https://raw.github.com/cryptocat/cryptocat/master/src/core/img/icon-128.png)  
+[![Build Status](https://secure.travis-ci.org/cryptocat/cryptocat.png?branch=master)](http://travis-ci.org/cryptocat/cryptocat)  
+
+###Browser-based app for easy to use, accessible encrypted chat.
+Cryptocat is an experimental browser-based chat client for easy to use, encrypted conversations. It aims to make encrypted, private chat easy to use and accessible. We want to break down the barrier that prevents the general public from having an accessible privacy alternative that they already know how to use. Cryptocat is currently available for Chrome, Firefox, Safari, Opera, OS X and iPhone. It uses the [OTR](http://www.cypherpunks.ca/otr/) protocol over XMPP for encrypted two-party  [...]
+
+Git repository branches are in-development, nightly builds. For stable usable builds, check the [releases](https://github.com/cryptocat/cryptocat/releases) section.  
+
+##Platforms
+
+###Google Chrome
+Run `make chrome` to build a Google Chrome-loadable .zip extension (or just .zip the directory.)  
+Also available from the [Chrome Web Store](https://chrome.google.com/webstore/detail/cryptocat/gonbigodpnfghidmnphnadhepmbabhij).  
+
+###Mozilla Firefox
+Run `make firefox` to build a Mozilla Firefox-loadable .xpi extension (or just .zip the directory and change the extension to .xpi.)  
+Also available from [Mozilla Firefox Addons](https://addons.mozilla.org/en-US/firefox/addon/cryptocat/).  
+
+###Opera
+Run `make opera` to build an Opera-compatible .nex extension (or just .zip the directory and change the extension to .nex)  
+Also available from [Opera Addons](https://addons.opera.com/en/extensions/details/cryptocat/).
+
+###Apple Safari
+Apple's model makes an automated build process difficult.  
+Also available from [Cryptocat](https://crypto.cat/get/cryptocat.safariextz).
+
+### Apple Mac OS X
+Run `make mac` to build as a standalone Mac application. (After running `make mac`, you can also `open src/mac/Cryptocat.xcodeproj` to edit & build the project in Xcode normally.)  Building the Mac version requires [HomeBrew](http://brew.sh) to be installed to fetch a Tor dependency on `libevent` and `openssl`.
+Also available from the [Mac App Store](https://itunes.apple.com/app/cryptocat/id613116229?mt=12).
+
+##Goals
+* XMPP **[DONE]** | [Discussion](https://github.com/cryptocat/cryptocat/issues/83), [Library](http://strophe.im)
+* OTR **[DONE]** | [Discussion](https://github.com/cryptocat/cryptocat/issues/84), [Library](https://github.com/arlolra/otr)
+* mpOTR | [Discussion](https://github.com/cryptocat/cryptocat/issues/82), Spec in progress. Currently relying on the [Cryptocat Multiparty Protocol](https://github.com/cryptocat/cryptocat/wiki/Multiparty-Protocol-Specification)  
+
+##Contributing
+Please see `CONTRIBUTING.md` for guidelines on how to contribute.  
+Please see `SECURITY.md` for guidelines on reporting security issues.
+
+##Documentation & Wiki
+* [Multiparty Protocol Specification](https://github.com/cryptocat/cryptocat/wiki/Multiparty-Protocol-Specification)  
+* [OTR Encrypted File Transfer Specification](https://github.com/cryptocat/cryptocat/wiki/OTR-Encrypted-File-Transfer-Specification)  
+* [Server Deployment Instructions](https://github.com/cryptocat/cryptocat/wiki/Server-Deployment-Instructions)  
+* [Threat Model](https://github.com/cryptocat/cryptocat/wiki/Threat-Model)  
+* [Design and Functionality Overview](https://github.com/cryptocat/cryptocat/wiki/Design-and-Functionality)  
+* [Architecture and Lifecycle](https://crypto.cat/documents/a&l.pdf)  
+
+##Discussion & Blog
+* [Issue tracker](https://github.com/cryptocat/cryptocat/issues)  
+* [Development Blog](https://blog.crypto.cat)  
+
+##Builds
+Builds for all platforms are available for download from [GitHub Releases](https://github.com/cryptocat/cryptocat/releases), in addition to the sources above. Please be mindful that GitHub builds do not auto-update.
+
+##Changelog
+Please review `CHANGELOG.md` for an account of the changes made with each version update.  
+
+##License
+##### Cryptocat is released under the [GNU Affero General Public License (AGPL3)](https://www.gnu.org/licenses/agpl-3.0.html).
+The full license text is included in `LICENSE.txt`.  
diff --git a/RESOURCES.md b/RESOURCES.md
new file mode 100755
index 0000000..b84dfef
--- /dev/null
+++ b/RESOURCES.md
@@ -0,0 +1,49 @@
+#External Resources
+
+##BigInt
+Origin: http://leemon.com/crypto/BigInt.html
+License: PD
+
+##Crypto-JS
+Origin: http://code.google.com/p/crypto-js/
+License: BSD-2-Clause
+
+##EventEmitter
+Origin: https://github.com/Wolfy87/EventEmitter/
+License: MIT
+
+##jQuery
+Origin: http://jquery.org/
+License: MIT, GPL-2+
+
+##jQuery.basic-slider
+Origin: https://github.com/jcobb/basic-jquery-slider/
+License: GPL-3
+
+##jQuery.color
+Origin: https://github.com/jquery/jquery-color/
+License: MIT
+
+##jQuery.utip
+Origin: https://github.com/symeapp/utip
+License: MIT
+
+##Mustache.js
+Origin: https://github.com/janl/mustache.js/
+License: MIT
+
+##Mousetrap
+Origin: https://github.com/ccampbell/mousetrap
+License: Apache 2.0
+
+##OTR.js
+Origin: https://github.com/arlolra/otr/
+License: MPL 2.0
+
+##Strophe.js
+Origin: http://strophe.im
+License: MIT
+
+##Tinycon
+Origin: https://github.com/tommoor/tinycon
+License: MIT
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ba3cafa
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,10 @@
+#Security
+
+##How to report security issues
+Any security issue, regardless of severity, should be reported via full disclosure. Please open an issue on our GitHub tracker. If you are uncomfortable opening an issue on our GitHub tracker, please send an email to nadim at crypto.cat.
+ 
+##Communication expectations
+Depending on the severity of the security issue, response times will vary. Medium to severe security issues are almost guaranteed a same-day response.
+ 
+##Credit
+Depending on the nature and severity of the security issue, the reporter will receive due credit on the [Cryptocat Wall of Eternal Greatness](https://crypto.cat/bughunt/), the [Development Blog](https://blog.crypto.cat), and on our Twitter, GitHub and Changelog.
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..788f69c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,44 @@
+{
+	"name": "cryptocat",
+	"version": "2.2.2",
+	"description": "Web Instant Messaging App With Accessible Encryption",
+	"main": "index.js",
+	"directories": {
+		"test": "test"
+	},
+	"dependencies": {
+		"express": "3.4.4"
+	},
+	"devDependencies": {
+		"lodash": "2.3.0",
+		"mocha": "1.14.0",
+		"chai": "1.8.1",
+		"sinon": "1.7.3",
+		"jshint": "2.3.0",
+		"grunt": "0.4.1",
+		"grunt-contrib-jshint": "0.7.1",
+		"grunt-mocha-test": "0.7.0",
+		"grunt-contrib-copy": "0.4.1",
+		"grunt-contrib-clean": "~0.5.0",
+		"grunt-mkdir": "~0.1.1"
+	},
+	"bin": {
+		"cryptocat": "src/standaloneServer.js"
+	},
+	"scripts": {
+		"test": "mocha --ui exports --reporter spec test/chrome/js/*.test.js",
+		"start": "node src/standaloneServer.js"
+	},
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/cryptocat/cryptocat.git"
+	},
+	"keywords": [
+		"encryption",
+		"crypto",
+		"js",
+		"web"
+	],
+	"author": "Nadim Kobeissi <nadim at crypto.cat>",
+	"license": "AGPL3"
+}
diff --git a/src/core/chrome.js b/src/core/chrome.js
new file mode 100644
index 0000000..97a596d
--- /dev/null
+++ b/src/core/chrome.js
@@ -0,0 +1,15 @@
+chrome.runtime.onInstalled.addListener(function(details) {
+	if (details.reason === 'install') {
+		chrome.tabs.create({'url': chrome.extension.getURL('firstRun.html')})
+	}
+})
+
+/* chrome.app.runtime.onLaunched.addListener(function() {
+	chrome.app.window.create('index.html', {
+		'bounds': {
+			'width': 755,
+			'height': 625,
+		},
+		'resizable': false
+	})
+}) */
\ No newline at end of file
diff --git a/src/core/css/firstRun.css b/src/core/css/firstRun.css
new file mode 100644
index 0000000..fbdd7dd
--- /dev/null
+++ b/src/core/css/firstRun.css
@@ -0,0 +1,54 @@
+ at font-face {
+	font-family: 'Monda';
+	font-style: normal;
+	font-weight: 400;
+	src: url('../fonts/monda.woff') format('woff');
+}
+body {
+	height: 100%;
+	background: #C7E5F5;
+	margin: 0;
+	font-family: 'Helvetica Neue', Helvetica, Arial, Verdana;
+	line-height: 1.75em;
+	font-size: 14px;
+	font-family: 'Monda';
+}
+div#main {
+	width: 680px;
+	height: 550px;
+	padding: 20px;
+	background: #FFF;
+	border-radius: 18px 0 18px 18px;
+	margin: 2% auto 0 auto;
+	box-shadow: 8px 8px #97CEEC;
+	border: 8px solid #76BDE5;
+}
+img#logo {
+	width: 64px;
+}
+h1 {
+	display: inline;
+	vertical-align: 20px;
+	font-family: 'Monda';
+	font-weight: normal;
+	margin-left: 20px;
+	color: #76BDE5;
+	font-size: 36px;
+}
+.browser {
+	font-weight: bold;
+}
+div.instructions {
+	margin: 25px auto 20px; auto;
+	padding: 10px;
+	background: #F1F1F1;
+	width: 600px;
+	border-radius: 10px 0 10px 10px;
+	display: none;
+}
+div.instructions img {
+	display: block;
+}
+img.emoticon {
+	width: 16px;
+}
\ No newline at end of file
diff --git a/src/core/css/jquery.basicslider.css b/src/core/css/jquery.basicslider.css
new file mode 100755
index 0000000..897b5f1
--- /dev/null
+++ b/src/core/css/jquery.basicslider.css
@@ -0,0 +1,43 @@
+/* Basic jQuery Slider essential styles */
+
+ul.bjqs{position:relative; list-style:none;padding:0;margin:0;overflow:hidden; display:none;border-radius: 0 0 10px 10px;}
+li.bjqs-slide{position:absolute; display:none;}
+ul.bjqs-controls{list-style:none;margin:0;padding:0;z-index:9999;display:none;}
+ul.bjqs-controls.v-centered li a{position:absolute;}
+ul.bjqs-controls.v-centered li.bjqs-next a{right:0;}
+ul.bjqs-controls.v-centered li.bjqs-prev a{left:0;}
+ol.bjqs-markers{list-style: none; padding: 0; margin: -3px 0; width:100%;}
+ol.bjqs-markers.h-centered{text-align: center;}
+ol.bjqs-markers li{display:inline;}
+ol.bjqs-markers li a{display:inline-block;}
+p.bjqs-caption{display:block;width:96%;margin:0;padding:2%;position:absolute;bottom:0;}
+
+ul.bjqs-controls.v-centered li a{
+	display:block;
+	padding:1px 4px;
+	background:rgba(150, 200, 255, 0.6);
+	color:#000;
+	margin-top: -20px;
+	text-decoration: none;
+}
+
+ul.bjqs-controls.v-centered li a:hover{
+	background:#000;
+	color:#fff;
+}
+
+ol.bjqs-markers li a {
+	padding: 5px 8px;
+	border-radius: 5px 0 5px 5px;
+	line-height:13px;
+	background: rgba(118, 189, 229, 0.5);
+	color: #FFF;
+	margin: 0 20px 0 0;
+	font-size: 1em;
+	text-decoration: none;
+}
+
+ol.bjqs-markers li.active-marker a,
+ol.bjqs-markers li a:hover{
+	background: rgba(118, 189, 229, 1);
+}
\ No newline at end of file
diff --git a/src/core/css/jquery.utip.css b/src/core/css/jquery.utip.css
new file mode 100644
index 0000000..b2c35e7
--- /dev/null
+++ b/src/core/css/jquery.utip.css
@@ -0,0 +1,118 @@
+#utip {
+	position: absolute;
+	z-index: 1000;
+	padding: 6px 8px;
+	border-radius: 4px 0 4px 4px;
+	background: #76BDE5;
+	color: white;
+	font-size: 14px;
+	max-width: 256px;
+	line-height: 16px;
+	font-size: 12px;
+	color: #FFF;
+	box-shadow: 0 0 0 2px rgba(118, 189, 229, 0.4);
+	background: #76BDE5;
+	-webkit-user-select: none;
+	user-select: none;
+	-moz-user-select: none;
+}
+#utip::before {
+  position: absolute;
+  content: ''; }
+#utip[data-gravity="ne"] {
+  margin-top: -10px;
+  margin-left: -15px; }
+  #utip[data-gravity="ne"]::before {
+	bottom: -10px;
+	left: 10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-top-color: #78A4BC; }
+#utip[data-gravity="n"] {
+  margin-top: -10px; }
+  #utip[data-gravity="n"]::before {
+	bottom: -10px;
+	left: 50%;
+	margin-left: -5px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-top-color: #78A4BC; }
+#utip[data-gravity="nw"] {
+  margin-top: -10px;
+  margin-left: 15px; }
+  #utip[data-gravity="nw"]::before {
+	bottom: -10px;
+	right: 10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-top-color: #78A4BC; }
+#utip[data-gravity="e"] {
+  margin-left: 10px; }
+  #utip[data-gravity="e"]::before {
+	top: 50%;
+	margin-top: -5px;
+	left: -10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-right-color: #78A4BC; }
+#utip[data-gravity="se"] {
+  margin-top: 10px;
+  margin-left: -15px; }
+  #utip[data-gravity="se"]::before {
+	top: -10px;
+	left: 10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-bottom-color: #78A4BC; }
+#utip[data-gravity="s"] {
+  margin-top: 10px; }
+  #utip[data-gravity="s"]::before {
+	top: -10px;
+	left: 50%;
+	margin-left: -5px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-bottom-color: #78A4BC; }
+#utip[data-gravity="sw"] {
+  margin-top: 10px;
+  margin-left: 15px; }
+  #utip[data-gravity="sw"]::before {
+	top: -10px;
+	right: 10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-bottom-color: #78A4BC; }
+#utip[data-gravity="w"] {
+  margin-left: -10px; }
+  #utip[data-gravity="w"]::before {
+	top: 50%;
+	margin-top: -5px;
+	right: -10px;
+	height: 0;
+	width: 0;
+	border-color: transparent;
+	border-style: solid;
+	border-width: 5px;
+	border-left-color: #78A4BC;
+}
\ No newline at end of file
diff --git a/src/core/css/style.css b/src/core/css/style.css
new file mode 100755
index 0000000..4a4d15e
--- /dev/null
+++ b/src/core/css/style.css
@@ -0,0 +1,1323 @@
+/*
+-------------------
+PRE-DEFINED TAGS
+-------------------
+*/
+
+ at font-face {
+	font-family: 'Monda';
+	font-style: normal;
+	font-weight: 400;
+	src: url('../fonts/monda.woff') format('woff');
+}
+
+ at font-face {
+	font-family: 'Logotype';
+	font-style: normal;
+	font-weight: 400;
+	src: url('../fonts/logotype.woff') format('woff');
+}
+
+body {
+	height: 100%;
+	background: #C7E5F5;
+	margin: 0;
+	padding: 0;
+	font-family: 'Helvetica Neue', Helvetica, Arial, Verdana;
+	line-height: 2em;
+}
+
+a {
+	color: #76BDE5;
+	text-decoration: none;
+}
+
+a:hover {
+	text-decoration: underline;
+}
+
+img {
+	image-rendering: optimizeSpeed;
+	image-rendering: -moz-crisp-edges;
+	image-rendering: -o-crisp-edges;
+	image-rendering: -webkit-optimize-contrast;
+	image-rendering: optimize-contrast;
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	user-select: none;
+	-moz-user-select: none;
+}
+
+webview {
+	border-radius: 8px 0 8px 8px;
+	width: 450px;
+	height: 350px;
+	overflow: hidden;
+	margin: 10px auto 0 auto;
+	display: block;
+}
+
+[type=text], [type=password], [type=button], [type=submit], textarea {
+	display: block;
+	background: #303040;
+	font-size: 1em;
+	padding: 3px;
+	color: #FFF;
+	border: none;
+	border: 1px solid #222;
+	width: 205px;
+	box-shadow: 2px 2px #97CEEC;
+	outline: none;
+}
+
+[type=button], [type=submit] {
+	color: #FFF;
+	background: #97CEEC;
+	border: none;
+	cursor: pointer;
+	border-radius: 4px 0 4px 4px;
+}
+
+[type=button]:active, [type=submit]:active {
+	box-shadow: inset 2px 2px #72B1DB;
+}
+
+::-webkit-input-placeholder {
+	color: rgba(255, 255, 255, 0.85);
+}
+
+::-moz-placeholder {
+	color: rgba(255, 255, 255, 0.85);
+}
+
+/*
+-------------------
+MAIN WINDOW (BUBBLE)
+-------------------
+*/
+
+div#bubbleWrapper {
+	margin: 0 auto;
+	width: 732px;
+	position: relative;
+}
+
+div#bubble {
+	position: relative;
+	z-index: 1;
+	width: 716px;
+	height: 540px;
+	background: #FFF;
+	border-radius: 15px 0 15px 15px;
+	box-shadow: 3px 3px #76BDE5;
+	border: 6px solid #303040;
+	padding: 0 0 50px 0;
+	display: none;
+	float: left;
+}
+
+div#av {
+	background: #303040;
+	width: 0;
+	height: 0;
+	padding: 36px 0;
+	/* display: inline-block; */
+	display: none;
+	float: left;
+	position: relative;
+	z-index: 2;
+	border-radius: 0 0 15px 0;
+	margin-left: -12px;
+	box-shadow: 5px 5px #76BDE5, inset 5px 0 rgba(150, 200, 255, 0.5);
+}
+
+div#bubble div#header {
+	width: 100%;
+	height: 28px;
+	padding: 8px 0 0 0;
+	direction: ltr;
+}
+
+div#bubble img.logo {
+	display: inline;
+	padding: 1px 0 0 0;
+	float: left;
+	height: 34px;
+	border-radius: 5px 0 5px 5px;
+	margin: -5px 5px 0 5px;
+	direction: ltr;
+}
+
+div#bubble #logoText {
+	display: inline-block;
+	position: relative;
+	z-index: 100;
+	color: #97CEEC;
+	font-family: 'Logotype';
+	-webkit-font-smoothing: none;
+	font-size: 30px;
+	text-shadow: 2px 2px #45A7DD;
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	user-select: none;
+	-moz-user-select: none;
+	cursor: default;
+}
+
+div#bubble #optionButtons {
+	display: none;
+	float: right;
+	position: absolute;
+	top: -1px;
+	right: 2px;
+}
+
+div#bubble .button {
+	margin: 7px 0 0 3px;
+	width: 16px;
+	height: 16px;
+}
+
+div#bubble #login {
+	margin: 70px auto 0 auto;
+}
+
+div#bubble #loginTabs {
+	margin: 0 0 0 132px;
+	font-weight: bold;
+	width: 300px;
+	z-index: 0;
+	position: relative;
+}
+
+div#bubble #loginTabs span {
+	background: #BBB;
+	color: #FFF;
+	padding: 5px 10px;
+	font-size: 1.2em;
+	border-radius: 5px 0 0 0;
+	cursor: default;
+}
+
+div#bubble #loginTabs [data-selected=false]:hover {
+	background: #97CEEC;
+}
+
+div#bubble #loginTabs [data-selected=true] {
+	background: #76BDE5;
+}
+
+div#bubble .loginForm {
+	width: 466px;
+	height: 172px;
+	margin: 0 auto;
+	display: block;
+	z-index: 1;
+	position: relative;
+}
+
+div#bubble #cryptocatLogin {
+}
+
+div#bubble #facebookLogin {
+	display: none;
+	font-size: 1.3em;
+	text-align: center;
+	height: 172px;
+	padding: 0 10px;
+	width: 446px;
+	background: #76BDE5;
+	color: #FFF;
+	border-radius: 5px 0 5px 5px;
+}
+
+div#bubble #facebookLogin #facebookConnect {
+	margin: 20px auto -5px auto;
+	font-size: 1.2em;
+	padding: 10px;
+	width: 300px;
+}
+
+div#bubble #facebookLogin #facebookWarning {
+	font-weight: bold;
+}
+
+div#bubble .loginForm [type=text] {
+	margin: 9px 0;
+	border: none;
+	display: block;
+	padding: 7px;
+	text-align: center;
+	resize: none;
+	background: #76BDE5;
+}
+
+div#bubble .loginForm #conversationName {
+	border-radius: 4px 0 0 0;
+	width: 450px;
+	font-size: 2.8em;
+	margin: 0 auto 10px auto;
+	display: block;
+}
+
+div#bubble .loginForm #nickname {
+	width: 300px;
+	font-size: 2em;
+	margin: 0;
+	display: inline;
+}
+
+div#bubble #loginSubmit {
+	width: 136px;
+	display: inline;
+	height: 41px;
+	margin-left: 10px;
+	position: relative;
+	bottom: 2px;
+	font-size: 1.6em;
+	border: 1px solid #97CEEC;
+	box-shadow: 2px 2px #97CEEC;
+	background: #76BDE5;
+}
+
+div#bubble #loginSubmit:hover {
+	background: #97CEEC;
+}
+
+div#bubble #loginSubmit:active {
+	background: #70B7DE;
+}
+
+div#loginInfo {
+	text-align: center;
+	font-size: 1.3em;
+	background: #76BDE5;
+	padding: 3px;
+	color: #FFF;
+	border-radius: 0 0 4px 4px;
+	width: 458px;
+	margin: 9px 0;
+}
+
+div#bubble div#info {
+	border: 1px solid #97ceec;
+	padding: 4px 10px;
+	box-shadow: 4px 4px #97CEEC;
+	border-radius: 4px 0 4px 4px;
+	margin: 30px auto;
+	width: 650px;
+	height: 210px;
+	font-size: 1.1em;
+	line-height: 1.5em;
+	font-family: Verdana, Arial, Helvetica;
+}
+
+div#bubble #info #introParagraph {
+	color: #333;
+}
+
+div#bubble #info li {
+	list-style-type: none;
+	background: url('../img/available.png') no-repeat;
+	background-size: 32px auto;
+	height: 40px;
+	padding: 0 40px;
+	margin: 20px 0 0 -30px;
+}
+
+div#bubble #info li.key {
+	background: url('../img/key.png') no-repeat top left;
+	background-size: 32px auto;
+}
+
+div#bubble h1 {
+	font-family: Helvetica, Arial;
+	font-size: 1.5em;
+}
+
+div#bubble #footer {
+	background: #303040;
+	width: 100%;
+	height: 14px;
+	padding: 1px 0 2px 2px;
+	position: absolute;
+	right: 0;
+	bottom: 0;
+	color: #FFF;
+	font-weight: bold;
+	font-size: 1.1em;
+	z-index: 20;
+}
+
+div#footer a {
+	color: #97CEEC;
+}
+
+div#footer #version {
+	font-size: 8px;
+	font-family: monospace;
+	color: #FFF;
+	float: left;
+	margin-left: 4px;
+}
+
+a#customServer {
+	margin: 0 10px;
+	float: right;
+}
+
+div#customServerDialog {
+	display: none;
+	margin: 30px auto;
+}
+
+div#customServerList {
+	float: left;
+	width: 50%;
+	text-align: right;
+}
+
+div#customServerFields {
+	float: left;
+	text-align: left;
+}
+
+div#customServerDialog input, select#customServerSelector {
+	margin: 8px;
+	background: rgba(255, 255, 255, 0.08);
+	box-shadow: none;
+	border-radius: 4px 0 4px 4px;
+	padding: 4px;
+}
+
+div#customServerDialog input#customName {
+	background: rgba(151, 206, 236, 0.8);
+}
+
+div#customServerDialog input.disabled {
+	cursor: auto;
+	background: rgba(255, 255, 255, 0.2);
+}
+
+div#customServerDialog input.confirm {
+	color: #97CEEC;
+}
+
+div#customServerDialog [type=button] {
+	display: inline;
+	width: 96px;
+	box-shadow: 2px 2px #97CEEC;
+}
+
+div#customServerDialog #customServerSubmit {
+	width: 215px;
+}
+
+select#customServerSelector {
+	width: 210px;
+	color: white;
+	height: 124px;
+	margin: 8px 8px 8px auto;
+	display: block;
+}
+
+#customServerSelector::-webkit-scrollbar {
+	width: 15px;
+}
+
+#customServerSelector::-webkit-scrollbar-track {
+	-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
+	border-radius: 10px;
+}
+
+#customServerSelector::-webkit-scrollbar-thumb {
+	border-radius: 10px;
+	-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
+}
+
+div#footer #languageSelect {
+	margin: 0 10px;
+	float: right;
+}
+
+div#footer #languages {
+	-webkit-column-count: 9;
+	-moz-column-count: 9;
+	column-count: 9;
+	display: none;
+	position: relative;
+	margin: 30px 0 0 0;
+	width: 640px;
+}
+
+div#footer #languages li {
+	cursor: pointer;
+	color: #FFF;
+	background: rgba(255, 255, 255, 0.08);
+	border-radius: 3px;
+	margin: 4px auto;
+	padding: 3px;
+	width: 80px;
+	list-style-type: none;
+}
+
+/*
+-------------------
+BUDDY LIST
+-------------------
+*/
+
+#buddyWrapper {
+	overflow: hidden;
+	width: 117px;
+	height: 480px;
+	display: none;
+	position: absolute;
+	right: 0;
+	z-index: 1000;
+	top: 38px;
+}
+
+#buddyList {
+	height: 479px;
+	width: 150px;
+	overflow-y: scroll;
+	overflow-x: hidden;
+	line-height: 13px;
+	z-index: 1000;
+}
+
+.buddy {
+	width: 110px;
+	height: 15px;
+	background: #303040;
+	border-radius: 2px 0 0 2px;
+	margin-top: 2px;
+	background-position: 87% 3px;
+	background-repeat: no-repeat;
+	background-size: 16px 16px;
+	color: #FFF;
+	padding: 5px 4px 3px 6px;
+	font-family: 'Monda', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+	font-size: 1em;
+	display: none;
+	position: relative;
+	cursor: default;
+	z-index: 15;
+	transition: background-color 0.25s;
+}
+
+.buddy span.loginTypeIcon {
+	width: 16px;
+	height: 16px;
+	background-repeat: no-repeat;
+	background-size: 16px 16px;
+	position: absolute;
+	left: 4px;
+	padding: 0;
+	margin: 0;
+	border-radius: 3px 0 3px 3px;
+	bottom: 4px;
+	background-position: top left;
+	background-image: url('../img/icon-48.png');
+}
+
+.buddy span.notUsingCryptocat {
+	background-image: url('../img/facebook.png');
+}
+
+.buddy span.shortNickname {
+	margin-left: 18px;
+}
+
+.buddy[status=away] span {
+	color: #aab;
+}
+
+.buddy[status=offline] span {
+	color: #BBB;
+	border-bottom: none;
+}
+
+.buddy.currentConversation {
+	background-color: #97CEEC;
+	color: #FFF !important;
+}
+
+.buddy.currentConversation span {
+	color: #FFF !important;
+}
+
+.buddy.ignored span {
+	text-decoration: line-through;
+}
+
+.buddyMenu {
+	position: relative;
+	z-index: 2;
+	float: right;
+	margin: 0;
+	background-image: url("../img/down.png");
+	background-size: 14px 14px;
+	width: 14px;
+	height: 14px;
+	cursor: pointer;
+	border-radius: 3px 0 3px 3px;
+	direction: rtl;
+}
+
+.buddyMenu:hover {
+	background-color: #97CEEC;
+}
+
+.buddyMenu:active {
+	background-color: #303040;
+}
+
+.buddyMenuContents {
+	display: none;
+	margin: 5px 0 0 -2px;
+	font-size: 0.9em;
+	width: 100px;
+}
+
+.buddyMenuContents li {
+	list-style-type: none;
+	background: rgba(255, 255, 255, 0.3);
+	padding: 3px;
+	font-size: 0.9em;
+	cursor: pointer;
+	margin: 5px 0;
+	border-radius: 3px 0 3px 4px;
+}
+
+.buddyMenuContents li:hover {
+	background: rgba(255, 255, 255, 0.2);
+}
+
+#buddy-groupChat {
+	display: block;
+	background-image: url("../img/groupChat.png");
+	background-position: 97% 3px;
+}
+
+/* New message notification stuff */
+ at -webkit-keyframes pulse {
+	0% {background-color: #000000;}
+	50% {background-color: #A7D8F7;}
+	100% {background-color: #000000;}
+}
+
+ at -moz-keyframes pulse {
+	0% {background-color: #000000;}
+	50% {background-color: #A7D8F7;}
+	100% {background-color: #000000;}
+}
+
+ at -o-keyframes pulse {
+	0% {background-color: #000000;}
+	50% {background-color: #A7D8F7;}
+	100% {background-color: #000000;}
+}
+
+ at keyframes pulse {
+	0% {background-color: #000000;}
+	50% {background-color: #A7D8F7;}
+	100% {background-color: #000000;}
+}
+
+.newMessage {
+	-webkit-animation: pulse 2s infinite;
+	-moz-animation: pulse 2s infinite;
+	-o-animation: pulse 2s infinite;
+	animation: pulse 2s infinite;
+	background-image: url("../img/newMessage.png");
+}
+
+.composing {
+	background-image: url("../img/composing.png");
+}
+
+/*
+-------------------
+DIALOG BOX
+-------------------
+*/
+
+#dialogBox {
+	position: absolute;
+	width: 470px;
+	height: 240px;
+	border: 1px solid #97CEEC;
+	border-radius: 8px 0 8px 8px;
+	margin: 80px auto 0 -240px;
+	z-index: 4;
+	left: 50%;
+	font-size: 1.1em;
+	border-right: none;
+	border-bottom: none;
+	background: #FCFCFC;
+	box-shadow: 5px 5px #97CEEC;
+	display: none;
+	color: #151520;
+	font-family: 'Monda';
+}
+
+.dialogBoxError {
+	border-color: #F00 !important;
+	box-shadow: 5px 5px #F00 !important;
+}
+
+.dialogBoxError #openAuth {
+	padding: 10px;
+	margin: 20px auto 0 auto;
+}
+
+#dialogBox [type=text], #dialogBox [type=password], #dialogBox [type=submit] {
+	color: #FFF;
+	float: left;
+	margin: 0 5px 10px 5px;
+	font-size: 1em;
+}
+
+#dialogBoxClose {
+	height: 18px;
+	width: 0;
+	font-size: 0;
+	text-align: center;
+	color: #FFF;
+	background: #97CEEC;
+	float: right;
+	margin-top: 4px;
+	padding-bottom: 4px;
+	cursor: pointer;
+	font-family: Helvetica, Arial, Verdana;
+	font-weight: bold;
+}
+
+.dialogBoxError #dialogBoxClose {
+	background: #F00 !important;
+}
+
+#dialogBoxContent {
+	padding: 25px 0 0 0;
+}
+
+#dialogBoxContent p {
+	padding: 10px;
+	padding: 10px;
+	max-width: 340px;
+	margin: 0 auto;
+}
+
+#displayInfo {
+	font-size: 1.1em;
+	padding: 10px 20px;
+	height: 80px;
+}
+
+#displayInfo h2 {
+	color: #000;
+	margin: 0;
+	font-size: 13px;
+	margin-right: 10px;
+}
+
+#authenticated {
+	font-size: 0.9em;
+	cursor: pointer;
+	padding: 1px 30px;
+	height: 20px;
+	border: 1px solid #3992C0;
+	background-color: transparent;
+	box-shadow: 2px 2px #3992C0;
+	color: #3992C0;
+	text-align: center;
+	border-radius: 5px 0 5px 5px;
+	display: inline-block;
+	margin: 3px 25px 0 0;
+	opacity: 0.4;
+}
+
+#authenticated:hover {
+	opacity: 0.8;
+}
+
+#authenticated[data-active=true] {
+	color: #FFF;
+	background-color: #3992C0;
+	opacity: 1;
+}
+
+#notAuthenticated {
+	font-size: 0.9em;
+	cursor: pointer;
+	padding: 1px 30px;
+	height: 20px;
+	border: 1px solid #EF6C74;
+	background-color: transparent;
+	box-shadow: 2px 2px #EF6C74;
+	color: #EF6C74;
+	text-align: center;
+	border-radius: 5px 0 5px 5px;
+	display: inline-block;
+	opacity: 0.4;
+}
+
+#notAuthenticated:hover {
+	opacity: 0.8;
+}
+
+#notAuthenticated[data-active=true] {
+	color: #FFF;
+	background-color: #EF6C74;
+	opacity: 1;
+}
+
+#authLearnMore {
+	width: 230px;
+	font-size: 0.9em;
+	border: 1px dashed #76BDE5;
+	padding: 0 4px;
+	margin: 13px 0 5px 0;
+	display: inline-block;
+	border-radius: 3px 0 3px 3px;
+}
+
+#authLearnMore:hover {
+	cursor: pointer;
+	background: rgba(151, 206, 236, 0.1);
+}
+
+#authLearnMore:active {
+	background: rgba(151, 206, 236, 0.3);
+}
+
+#authTutorial {
+	margin-top: 10px;
+	display: none;
+}
+
+#authTutorial p {
+	max-width: 410px;
+	width: 410px;
+	margin-bottom: 10px;
+	padding: 5px 10px;
+	background: #76BDE5;
+	border-radius: 0 0 5px 5px;
+	color: #FFF;
+}
+
+.authInfo {
+	background: rgba(151, 206, 236, 0.4);
+	height: 130px;
+	padding: 5px 8px;
+	margin: 5px auto 0 auto;
+	border-radius: 8px 0 8px 8px;
+}
+
+.authInfo h2 {
+	font-size: 16px;
+	padding: 3px;
+	margin: 0 0 5px 0;
+}
+
+.authInfo #authQuestion, .authInfo #authAnswer {
+	box-shadow: none;
+	background: rgba(151, 206, 236, 0.7);
+	padding: 4px;
+	width: 260px;
+	border: none;
+	font-size: 1.05em;
+	border-radius: 5px 0 5px 5px;
+}
+
+.authInfo #authSubmit {
+	float: right;
+	width: 100px;
+	height: 57px;
+	margin: -35px 20px 0px 0;
+	border-radius: 5px 0 5px 5px;
+	font-size: 1.2em;
+	background: #97CEEC;
+	border: none;
+	word-wrap: break-word;
+}
+
+#authVerified {
+	text-align: center;
+	font-size: 1.1em;
+	color: #FFF;
+	text-shadow: #FFF;
+	display: none;
+}
+
+#authReplyForm {
+	margin: -20px 0 5px 55px;
+}
+
+#authReply {
+	background: #97CEEC;
+	border: none;
+	border-radius: 5px 0 5px 5px;
+	padding: 4px;
+	box-shadow: none;
+	width: 220px;
+}
+
+#authReplySubmit {
+	width: 100px;
+	border-radius: 5px 0 5px 5px;
+	box-shadow: 2px 2px #76BDE5;
+}
+
+#dialogBox #fileInfoField {
+	padding: 20px;
+}
+
+#progressForm {
+	font-size: 0.9em;
+}
+
+#progressInfo span {
+	font-size: 1.2em;
+	font-weight: bold;
+}
+
+#progressForm img {
+	float: left;
+	margin: 8px 20px 0 0;
+	box-shadow: 2px 2px #97CEEC;
+}
+
+#progressInfo {
+	width: 320px;
+	font-size: 1em;
+	position: relative;
+	bottom: 70px;
+	float: right;
+}
+
+#progressInfo #interestingFact {
+	font-size: 1em;
+	height: 55px;
+	line-height: 1.6em;
+	margin-top: 10px;
+	color: #444;
+}
+
+#progressBar {
+	margin-top: 10px;
+	height: 18px;
+	width: 290px;
+	border: 1px solid #97CEEC;
+	padding: 1px;
+}
+
+#progressBar #fill {
+	height: 100%;
+	width: 0%;
+	background: #97CEEC;
+	opacity: 0.3;
+}
+
+.fileProgressBar {
+	height: 14px;
+	width: 72%;
+	display: inline-block;
+	vertical-align: -5px;
+	margin-left: 10px;
+	border: 1px solid #97CEEC;
+	padding: 1px;
+}
+
+.fileProgressBarFill {
+	height: 100%;
+	width: 0%;
+	background: #97CEEC;
+}
+
+#dialogBox .title {
+	padding: 5px 20px 5px 10px;
+	border-left: none;
+	background: #97CEEC;
+	max-width: 220px;
+	margin-bottom: 2px;
+	color: #FFF;
+	font-size: 1.5em;
+}
+
+#dialogBox .errorTitle {
+	background: #F00 !important;
+}
+
+#dialogBox #fileSelectButton {
+	margin: 30px auto 10px auto;
+	padding: 10px;
+	border-radius: 4px 0 4px 4px;
+	font-size: 1.2em;
+}
+
+#dialogBox #fileInfoField {
+	color: #444;
+	font-family: 'Monda'
+}
+
+#dialogBox #resetKeysOK {
+	background: #303040;
+	border: 1px solid #97CEEC;
+	color: #97CEEC;
+	margin: 0 auto;
+	width: 100px;
+	box-shadow: 2px 2px #97CEEC;
+	font-size: 1.2em;
+	cursor: pointer;
+}
+
+#dialogBox [type=checkbox] {
+	width: 10px;
+	height: 10px;
+}
+
+#otrFingerprint, #multiPartyFingerprint {
+	font-weight: 600;
+	font-size: 14px;
+	font-family: monospace;
+}
+
+#fileSelector {
+	display: none;
+}
+
+#fileErrorField {
+	color: #F00;
+	margin: 20px 0;
+	font-weight: bold;
+}
+
+/*
+-------------------
+CONVERSATION WINDOW
+-------------------
+*/
+
+#conversationInfo {
+	color: #FFF;
+	font-family: 'Monda';
+	font-size: 1em;
+	margin: 0 0 0 20px;
+	position: relative;
+	bottom: 4px;
+	display: none;
+}
+
+#conversationInfo .conversationName {
+	color: #FFF;
+	background: #76BDE5;
+	padding: 2px 7px;
+	border-radius: 3px 0 3px 3px;
+	font-size: 1.1em;
+	margin-left: -5px;
+}
+
+#conversationInfo #encryptionStatus {
+	margin: 0 0 0 20px;
+	line-height: 1em;
+	font-size: 1.2em;
+	max-width: 400px;
+}
+
+#conversationInfo #encryptionStatus .encrypted {
+	font-weight: bold;
+	color: #3992C0;
+}
+
+#conversationInfo #encryptionStatus .notEncrypted {
+	font-weight: bold;
+	color: #EF6C74;
+}
+
+#conversationWrapper {
+	width: 597px;
+	overflow: hidden;
+	height: 80%;
+	margin-top: -5px;
+	display: none;
+	padding: 10px 8px 0 8px;
+	direction: ltr;
+	position: absolute;
+	left: 0;
+}
+
+#conversationWindow {
+	z-index: 3;
+	height: 100%;
+	width: 622px;
+	font-family: 'Monda', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+	color: transparent;
+	position: relative;
+	font-size: 1.15em;
+	overflow-x: hidden;
+	overflow-y: scroll;
+}
+
+#conversationWindow .authStatus {
+	width: 16px;
+	height: 16px;
+	display: inline-block;
+	margin: 0 7px -4px -2px;
+	border-radius: 4px;
+	background-color: #EF6C74;
+	background-image: url("../img/authStatusNo.png");
+	background-size: 16px 16px;
+	background-position: -2px 2px;
+	cursor: pointer;
+}
+
+#conversationWindow .authStatus[data-auth=true] {
+	background-color: #3992C0;
+	background-image: none;
+}
+
+#conversationWindow .timeStamp {
+	color: #151520;
+	text-shadow: 1px 1px #CCC;
+	font-size: 0.8em;
+	font-family: monospace;
+	vertical-align: 1px;
+	display: none;
+}
+
+#conversationWindow .sender {
+	padding: 5px;
+	font-size: 0.95em;
+	margin: 0 10px 0 -4px;
+	min-width: 90px;
+	cursor: default;
+	border-radius: 0 0 3px 0;
+	display: inline-block;
+	padding: 2px 4px;
+	text-align: left;
+}
+
+#conversationWindow .nickHighlight {
+	color: #76BDE5;
+}
+
+.missingRecipients {
+	background: #3992C0;
+	border-radius: 4px 0 0 0;
+	margin-bottom: 1px;
+	color: #FFF;
+	font-size: 0.8em;
+	padding: 0px 10px;
+	opacity: 0;
+	display: inline-block;
+	max-width: 90%;
+}
+
+.notUsingCryptocatWarning {
+	background: #EF6C74;
+	border-radius: 5px 0 5px 5px;
+	margin-bottom: 10px;
+	color: #FFF;
+	font-size: 1em;
+	padding: 10px;
+	opacity: 0;
+	display: inline-block;
+	max-width: 91%
+}
+
+.line1, .line2, .line3, .line4 {
+	box-shadow: 2px 2px #76BDE5;
+	border: 2px solid #76BDE5;
+	border-radius: 3px 0 3px 3px;
+	padding: 0 4px 0 4px;
+	color: #080812;
+	word-wrap: break-word;
+	text-align: left;
+	position: relative;
+	top: 20px;
+	opacity: 0;
+	margin-bottom: 15px;
+	font-size: 1em;
+	max-width: 92%;
+	white-space: pre-wrap;
+}
+
+.line1 {
+	box-shadow: 2px 2px #97CEEC;
+	border: 2px solid #97CEEC;
+}
+
+.line3 {
+	background-color: #EFF9FE;
+}
+
+.line4 {
+	box-shadow: 2px 2px #F00;
+	border: 2px solid #F00;
+	background: #F00;
+	color: #FFF;
+}
+
+.line1 .sender {
+	background: #97CEEC;
+	color: #FFF;
+}
+
+.line2 .sender, .line3 .sender {
+	background: #76BDE5;
+	color: #FFF;
+}
+
+.line4 .sender {
+	background: #FFF;
+	color: #F00;
+}
+
+.line1:after, .line2:after, .line3:after, .line4:after {
+	content: '';
+	position: absolute;
+	top: 100%;
+	border: 10px solid;
+}
+
+.line1:after {
+	right: 30px;
+	border-color: #97CEEC transparent transparent transparent;
+}
+
+.line2:after {
+	left: 30px;
+	border-color: #76BDE5 transparent transparent transparent;
+}
+
+.line3:after {
+	left: 30px;
+	border-color: #76BDE5 transparent transparent;
+}
+
+.line4:after {
+	left: 30px;
+	border-color: #F00 transparent transparent;
+}
+
+.visibleLine {
+	opacity: 1;
+	top: 0;
+}
+
+#userInput {
+	display: none;
+}
+
+#userInputText {
+	background: #202030;
+	border-radius: 7px 0 0 7px;
+	padding: 8px 10px;
+	height: 32px;
+	width: 580px;
+	margin: 10px 0 7px 0;
+	display: inline;
+	box-shadow: none;
+	font-family: 'Monda', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+	font-size: 1.25em;
+	text-align: left;
+	resize: none;
+	overflow: hidden;
+}
+
+#userInputSend {
+	width: 105px;
+	height: 48px;
+	float: right;
+	margin: 10px 3px 0 0;
+	border-radius: 0px 0 7px 0px;
+	font-size: 1.8em;
+	text-shadow: 3px 2px #76BDE5;
+	box-shadow: 2px 2px #76BDE5;
+}
+
+#userInputSend:hover {
+	text-shadow: 3px 2px #76BDE5, -1px -1px #999;
+}
+
+#userInputSend:active {
+	text-shadow: 3px 2px #76BDE5, -1px -1px #151520;
+}
+
+.fileView {
+	background: #97CEEC;
+	color: #FFF;
+	padding: 0 5px 0 21px;
+	font-size: 0.9em;
+	border-radius: 3px 0 3px 3px;
+	background-image: url("../img/file.png");
+	background-size: 16px auto;
+	background-position: center left;
+	background-repeat: no-repeat;
+	display: inline-block;
+}
+
+.userJoin, .userLeave {
+	background: #97CEEC;
+	max-width: 165px;
+	display: block;
+	margin: 0 0 15px 0;
+	padding: 0 5px;
+	color: #FFF;
+	border-radius: 3px 0 3px 3px;
+	font-size: 0.9em;
+}
+
+.userLeave {
+	background: #DADADA;
+}
+
+.userJoin .timestamp, .userLeave .timestamp {
+	background: rgba(0, 0, 0, 0.2);
+	padding: 3px 4px 2px 4px;
+}
+
+.userJoin strong, .userLeave strong {
+	margin: 0 5px 0 7px;
+	font-size: 1.4em;
+	vertical-align: -1px;
+}
+
+.fileView:hover {
+	text-decoration: none;
+}
+
+/*
+-------------------
+EMOTICONS
+-------------------
+*/
+
+.emoticon {
+	background-repeat: no-repeat;
+	height: 13px;
+	width: 13px;
+	display: inline-block;
+	margin: 0 3px;
+	background-size: 100% 100%;
+	font-size: 2px;
+	vertical-align: -2px;
+	color: transparent;
+}
+
+.balloon {
+	position: fixed;
+	bottom: -100px;
+	z-index: 100;
+	height: 100px;
+	width: auto;
+}
+
+/*
+-------------------
+AUDIO/VIDEO CHAT
+-------------------
+*/
diff --git a/src/core/firstRun.html b/src/core/firstRun.html
new file mode 100755
index 0000000..4da1002
--- /dev/null
+++ b/src/core/firstRun.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<head>
+	<meta charset="UTF-8" />
+	<title>Getting started with Cryptocat</title>
+	<link rel="stylesheet" type="text/css" href="css/firstRun.css" />
+	<link rel="icon" type="image/gif" href="img/favicon.gif" />
+	<script type="application/javascript" src="js/lib/jquery/jquery.js"></script>
+	<script type="application/javascript" src="js/etc/firstRun.js"></script>
+</head>
+<body>
+	<div id="main">
+		<img id="logo" src="img/cryptocat.png" alt="" />
+		<h1>Getting started with Cryptocat</h1>
+		<p>
+			Thanks for installing Cryptocat. Now, it's easier than ever to chat with friends without risking your privacy. Here's how you can open Cryptocat in <span class="browser"></span>:
+		</p>
+		<div class="instructions" data-browser="Chrome">
+			<ol>
+				<li>
+					Open a new tab and click the <strong>Apps</strong> icon in your Chrome bookmarks bar:
+					<img src="img/firstRun/chrome.png" alt="" /><br />
+				</li>
+				<li>
+					Click the Cryptocat icon to launch Cryptocat.
+				</li>
+				<li>
+					Start chatting by typing in a <strong>name for your conversation</strong> and <strong>a nickname for yourself</strong>!
+				</li>
+			</ol>
+		</div>
+		<div class="instructions" data-browser="Firefox">
+			<ol>
+				<li>
+					Click the <strong>Cryptocat</strong> icon in your Firefox toolbar to launch Cryptocat:
+					<img src="img/firstRun/firefox.png" alt="" /><br />
+				</li>
+				<li>
+					Start chatting by typing in a <strong>name for your conversation</strong> and <strong>a nickname for yourself</strong>!
+				</li>
+			</ol>
+		</div>
+		<div class="instructions" data-browser="Safari">
+			<ol>
+				<li>
+					Click the <strong>chat bubble</strong> icon in your Safari toolbar to launch Cryptocat:
+					<img src="img/firstRun/safari.png" alt="" /><br />
+				</li>
+				<li>
+					Start chatting by typing in a <strong>name for your conversation</strong> and <strong>a nickname for yourself</strong>!
+				</li>
+			</ol>
+		</div>
+		<div class="instructions" data-browser="Opera">
+			<ol>
+				<li>
+					Click the <strong>Cryptocat</strong> icon in your Opera toolbar to launch Cryptocat:
+					<img src="img/firstRun/opera.png" alt="" /><br />
+				</li>
+				<li>
+					Start chatting by typing in a <strong>name for your conversation</strong> and <strong>a nickname for yourself</strong>!
+				</li>
+			</ol>
+		</div>
+		<p>Please remember that Cryptocat is not a magic bullet. While we are trying our best to make encrypted chat easy to use and accessible, remember to never trust any software with your life. Cryptocat is still under continuous research and improvement.</p>
+		<p>
+			<strong>We hope you enjoy safe, private chats with Cryptocat!</strong> <img src="img/emoticon/cat.png" alt="" class="emoticon" />
+		</p>
+	</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/core/fonts/logotype.woff b/src/core/fonts/logotype.woff
new file mode 100755
index 0000000..acde290
Binary files /dev/null and b/src/core/fonts/logotype.woff differ
diff --git a/src/core/fonts/monda.woff b/src/core/fonts/monda.woff
new file mode 100644
index 0000000..e40a886
Binary files /dev/null and b/src/core/fonts/monda.woff differ
diff --git a/src/core/img/authStatusNo.png b/src/core/img/authStatusNo.png
new file mode 100644
index 0000000..5ac91bc
Binary files /dev/null and b/src/core/img/authStatusNo.png differ
diff --git a/src/core/img/authTutorial/1.png b/src/core/img/authTutorial/1.png
new file mode 100644
index 0000000..c0ea6ec
Binary files /dev/null and b/src/core/img/authTutorial/1.png differ
diff --git a/src/core/img/authTutorial/2.png b/src/core/img/authTutorial/2.png
new file mode 100644
index 0000000..68f220e
Binary files /dev/null and b/src/core/img/authTutorial/2.png differ
diff --git a/src/core/img/authTutorial/3.png b/src/core/img/authTutorial/3.png
new file mode 100644
index 0000000..71b9df4
Binary files /dev/null and b/src/core/img/authTutorial/3.png differ
diff --git a/src/core/img/authTutorial/4.png b/src/core/img/authTutorial/4.png
new file mode 100644
index 0000000..dce4374
Binary files /dev/null and b/src/core/img/authTutorial/4.png differ
diff --git a/src/core/img/available.png b/src/core/img/available.png
new file mode 100644
index 0000000..d33b5bb
Binary files /dev/null and b/src/core/img/available.png differ
diff --git a/src/core/img/away.png b/src/core/img/away.png
new file mode 100644
index 0000000..582d838
Binary files /dev/null and b/src/core/img/away.png differ
diff --git a/src/core/img/balloon.gif b/src/core/img/balloon.gif
new file mode 100644
index 0000000..545ffde
Binary files /dev/null and b/src/core/img/balloon.gif differ
diff --git a/src/core/img/bg.png b/src/core/img/bg.png
new file mode 100644
index 0000000..ac431e7
Binary files /dev/null and b/src/core/img/bg.png differ
diff --git a/src/core/img/composing.png b/src/core/img/composing.png
new file mode 100644
index 0000000..8e396db
Binary files /dev/null and b/src/core/img/composing.png differ
diff --git a/src/core/img/cryptocat.png b/src/core/img/cryptocat.png
new file mode 100644
index 0000000..0297563
Binary files /dev/null and b/src/core/img/cryptocat.png differ
diff --git a/src/core/img/down.png b/src/core/img/down.png
new file mode 100755
index 0000000..b32db0a
Binary files /dev/null and b/src/core/img/down.png differ
diff --git a/src/core/img/emoticon/cat.png b/src/core/img/emoticon/cat.png
new file mode 100644
index 0000000..d368282
Binary files /dev/null and b/src/core/img/emoticon/cat.png differ
diff --git a/src/core/img/emoticon/cry.png b/src/core/img/emoticon/cry.png
new file mode 100644
index 0000000..3dbe596
Binary files /dev/null and b/src/core/img/emoticon/cry.png differ
diff --git a/src/core/img/emoticon/gasp.png b/src/core/img/emoticon/gasp.png
new file mode 100644
index 0000000..28504c8
Binary files /dev/null and b/src/core/img/emoticon/gasp.png differ
diff --git a/src/core/img/emoticon/grin.png b/src/core/img/emoticon/grin.png
new file mode 100644
index 0000000..019efe3
Binary files /dev/null and b/src/core/img/emoticon/grin.png differ
diff --git a/src/core/img/emoticon/happy.png b/src/core/img/emoticon/happy.png
new file mode 100644
index 0000000..e43bca2
Binary files /dev/null and b/src/core/img/emoticon/happy.png differ
diff --git a/src/core/img/emoticon/sad.png b/src/core/img/emoticon/sad.png
new file mode 100644
index 0000000..002e688
Binary files /dev/null and b/src/core/img/emoticon/sad.png differ
diff --git a/src/core/img/emoticon/shut.png b/src/core/img/emoticon/shut.png
new file mode 100644
index 0000000..c19f89c
Binary files /dev/null and b/src/core/img/emoticon/shut.png differ
diff --git a/src/core/img/emoticon/smile.png b/src/core/img/emoticon/smile.png
new file mode 100644
index 0000000..af02b3b
Binary files /dev/null and b/src/core/img/emoticon/smile.png differ
diff --git a/src/core/img/emoticon/squint.png b/src/core/img/emoticon/squint.png
new file mode 100644
index 0000000..20e0d7e
Binary files /dev/null and b/src/core/img/emoticon/squint.png differ
diff --git a/src/core/img/emoticon/tongue.png b/src/core/img/emoticon/tongue.png
new file mode 100644
index 0000000..d8e3148
Binary files /dev/null and b/src/core/img/emoticon/tongue.png differ
diff --git a/src/core/img/emoticon/unsure.png b/src/core/img/emoticon/unsure.png
new file mode 100644
index 0000000..12b3e84
Binary files /dev/null and b/src/core/img/emoticon/unsure.png differ
diff --git a/src/core/img/emoticon/wink.png b/src/core/img/emoticon/wink.png
new file mode 100644
index 0000000..cbaa4e5
Binary files /dev/null and b/src/core/img/emoticon/wink.png differ
diff --git a/src/core/img/emoticon/winkTongue.png b/src/core/img/emoticon/winkTongue.png
new file mode 100644
index 0000000..edc3446
Binary files /dev/null and b/src/core/img/emoticon/winkTongue.png differ
diff --git a/src/core/img/facebook.png b/src/core/img/facebook.png
new file mode 100644
index 0000000..af7654b
Binary files /dev/null and b/src/core/img/facebook.png differ
diff --git a/src/core/img/favicon.gif b/src/core/img/favicon.gif
new file mode 100755
index 0000000..5208733
Binary files /dev/null and b/src/core/img/favicon.gif differ
diff --git a/src/core/img/file.png b/src/core/img/file.png
new file mode 100755
index 0000000..6def308
Binary files /dev/null and b/src/core/img/file.png differ
diff --git a/src/core/img/firstRun/chrome.png b/src/core/img/firstRun/chrome.png
new file mode 100644
index 0000000..7239feb
Binary files /dev/null and b/src/core/img/firstRun/chrome.png differ
diff --git a/src/core/img/firstRun/firefox.png b/src/core/img/firstRun/firefox.png
new file mode 100644
index 0000000..f59a071
Binary files /dev/null and b/src/core/img/firstRun/firefox.png differ
diff --git a/src/core/img/firstRun/opera.png b/src/core/img/firstRun/opera.png
new file mode 100644
index 0000000..62ce9e2
Binary files /dev/null and b/src/core/img/firstRun/opera.png differ
diff --git a/src/core/img/firstRun/safari.png b/src/core/img/firstRun/safari.png
new file mode 100644
index 0000000..58bf2ef
Binary files /dev/null and b/src/core/img/firstRun/safari.png differ
diff --git a/src/core/img/groupChat.png b/src/core/img/groupChat.png
new file mode 100755
index 0000000..0985757
Binary files /dev/null and b/src/core/img/groupChat.png differ
diff --git a/src/core/img/icon-128.png b/src/core/img/icon-128.png
new file mode 100755
index 0000000..6a7a27b
Binary files /dev/null and b/src/core/img/icon-128.png differ
diff --git a/src/core/img/icon-16.png b/src/core/img/icon-16.png
new file mode 100755
index 0000000..79cd72b
Binary files /dev/null and b/src/core/img/icon-16.png differ
diff --git a/src/core/img/icon-48.png b/src/core/img/icon-48.png
new file mode 100755
index 0000000..050b040
Binary files /dev/null and b/src/core/img/icon-48.png differ
diff --git a/src/core/img/key.png b/src/core/img/key.png
new file mode 100644
index 0000000..fa9a223
Binary files /dev/null and b/src/core/img/key.png differ
diff --git a/src/core/img/keygen.gif b/src/core/img/keygen.gif
new file mode 100755
index 0000000..4358955
Binary files /dev/null and b/src/core/img/keygen.gif differ
diff --git a/src/core/img/logout.png b/src/core/img/logout.png
new file mode 100644
index 0000000..83aa00c
Binary files /dev/null and b/src/core/img/logout.png differ
diff --git a/src/core/img/newMessage.png b/src/core/img/newMessage.png
new file mode 100755
index 0000000..a94e895
Binary files /dev/null and b/src/core/img/newMessage.png differ
diff --git a/src/core/img/noNotifications.png b/src/core/img/noNotifications.png
new file mode 100644
index 0000000..56ef57d
Binary files /dev/null and b/src/core/img/noNotifications.png differ
diff --git a/src/core/img/noSound.png b/src/core/img/noSound.png
new file mode 100644
index 0000000..edd34ec
Binary files /dev/null and b/src/core/img/noSound.png differ
diff --git a/src/core/img/notifications.png b/src/core/img/notifications.png
new file mode 100644
index 0000000..d205b63
Binary files /dev/null and b/src/core/img/notifications.png differ
diff --git a/src/core/img/sending.gif b/src/core/img/sending.gif
new file mode 100755
index 0000000..b9cf398
Binary files /dev/null and b/src/core/img/sending.gif differ
diff --git a/src/core/img/sound.png b/src/core/img/sound.png
new file mode 100644
index 0000000..caf28e1
Binary files /dev/null and b/src/core/img/sound.png differ
diff --git a/src/core/img/up.png b/src/core/img/up.png
new file mode 100755
index 0000000..e4e2cc8
Binary files /dev/null and b/src/core/img/up.png differ
diff --git a/src/core/index.html b/src/core/index.html
new file mode 100755
index 0000000..4d122c3
--- /dev/null
+++ b/src/core/index.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<head>
+	<meta charset="UTF-8" />
+	<title>Cryptocat</title>
+	<link rel="stylesheet" media="screen" href="css/style.css" type="text/css" />
+	<link rel="stylesheet" href="css/jquery.basicslider.css" type="text/css" />
+	<link rel="stylesheet" href="css/jquery.utip.css" type="text/css" />
+	<link rel="icon" type="image/gif" href="img/favicon.gif" />
+	<script type="application/javascript" src="js/lib/jquery/jquery.js"></script>
+	<script type="application/javascript" src="js/lib/jquery/jquery.basicslider.js"></script>
+	<script type="application/javascript" src="js/lib/jquery/jquery.color.js"></script>
+	<script type="application/javascript" src="js/lib/jquery/jquery.utip.js"></script>
+	<script type="application/javascript" src="js/lib/jquery/jquery.filterbydata.js"></script>
+	<script type="application/javascript" src="js/lib/mustache.js"></script>
+	<script type="application/javascript" src="js/lib/mousetrap.js"></script>
+	<script type="application/javascript" src="js/lib/strophe/strophe.js"></script>
+	<script type="application/javascript" src="js/lib/strophe/strophe.ibb.js"></script>
+	<script type="application/javascript" src="js/lib/strophe/strophe.si-filetransfer.js"></script>
+	<script type="application/javascript" src="js/lib/strophe/strophe.muc.js"></script>
+	<!-- <script type="application/javascript" src="js/lib/strophe/strophe.ping.js"></script> -->
+	<script type="application/javascript" src="js/lib/crypto-js.js"></script>
+	<script type="application/javascript" src="js/lib/salsa20.js"></script>
+	<script type="application/javascript" src="js/lib/eventemitter.js"></script>
+	<script type="application/javascript" src="js/lib/tinycon.js"></script>
+	<script type="application/javascript" src="js/cryptocat.js"></script>
+	<script type="application/javascript" src="js/etc/xmpp.js"></script>
+	<script type="application/javascript" src="js/etc/otr.js"></script>
+	<script type="application/javascript" src="js/etc/facebook.js"></script>
+	<script type="application/javascript" src="js/etc/catFacts.js"></script>
+	<script type="application/javascript" src="js/etc/storage.js"></script>
+	<script type="application/javascript" src="js/etc/templates.js"></script>
+	<script type="application/javascript" src="js/etc/random.js"></script>
+	<script type="application/javascript" src="js/etc/multiParty.js"></script>
+	<script type="application/javascript" src="js/etc/fileTransfer.js"></script>
+	<script type="application/javascript" src="js/etc/customServers.js"></script>
+	<script type="application/javascript" src="js/lib/bigint.js"></script>
+	<script type="application/javascript" src="js/lib/otr.js"></script>
+	<script type="application/javascript" src="js/lib/elliptic.js"></script>
+	<script type="application/javascript" src="js/etc/locale.js"></script>
+</head>
+<body>
+	<div id="fb-root"></div>
+	<div id="dialogBox">
+		<div id="dialogBoxClose">x</div>
+		<div id="dialogBoxContent"></div>
+	</div>
+	<div id="bubbleWrapper">
+		<div id="bubble">
+			<div id="header">
+				<img src="img/cryptocat.png" alt="Cryptocat" class="logo" />
+				<span id="logoText" dir="ltr">cryptocat</span>
+				<span id="conversationInfo">
+					<span class="conversationName"></span>
+					<span id="encryptionStatus"></span>
+				</span>
+				<div id="optionButtons" dir="ltr">
+					<img class="button" id="status" src="img/available.png" alt="" data-utip-gravity="sw" />
+					<img class="button" id="myInfo" src="img/key.png" alt="" data-utip-gravity="sw" />
+					<img class="button" id="notifications" src="img/noNotifications.png" alt="" data-utip-gravity="sw" />
+					<img class="button" id="audio" src="img/noSound.png" alt="" data-utip-gravity="sw" />
+					<img class="button" id="logout" src="img/logout.png" alt="" data-utip-gravity="sw" />
+				</div>
+			</div>
+			<div id="login" dir="ltr">
+				<div id="loginTabs" dir="ltr">
+					<span data-login="cryptocat" dir="ltr"></span>
+					<span data-login="facebook" dir="ltr"></span>
+				</div>
+				<form id="cryptocatLogin" class="loginForm" dir="ltr">
+					<input type="text" id="conversationName" class="mousetrap" maxlength="20" autocomplete="off" dir="ltr" data-utip-gravity="ne" />
+					<input type="text" id="nickname" class="mousetrap" maxlength="16" autocomplete="off" dir="ltr" />
+					<input type="submit" id="loginSubmit" dir="ltr" />
+					<div id="loginInfo"></div>
+				</form>
+				<form id="facebookLogin" class="loginForm">
+					<br /><span class="facebookInfo"></span><br />
+					<input type="button" id="facebookConnect" value="Chat via Facebook" />
+					<br />
+				</form>
+			</div>
+			<div id="info">
+				<h1 id="introHeader"></h1>
+				<p id="introParagraph"></p>
+			</div>
+			<div id="footer">
+				<div id="version"></div>
+				<div id="loginOptions">
+					<a href="#" id="languageSelect"></a>
+					<a href="#" id="customServer"></a>
+				</div>
+				<ul id="languages">
+					<li data-locale="en">English</li>
+					<li data-locale="ca">Català</li>
+					<li data-locale="ar">عربي</li>
+					<li data-locale="zh-cn">简体</li>
+					<li data-locale="zh-hk">繁體</li>
+					<!--<li data-locale="ur">اُردُو</li>-->
+					<li data-locale="bo">བོད་སྐད།</li>
+					<li data-locale="bg">Български</li>
+					<li data-locale="et">Eesty</li>
+					<li data-locale="cs">Česky</li>
+					<li data-locale="da">Dansk</li>
+					<li data-locale="de">Deutsch</li>
+					<li data-locale="es">Español</li>
+					<li data-locale="eu">Euskara</li>
+					<li data-locale="el">Ελληνικά</li>
+					<li data-locale="fa">فارسی</li>
+					<li data-locale="fr">Français</li>
+					<li data-locale="ja">日本語</li>
+					<li data-locale="he">עִבְרִית</li>
+					<li data-locale="in">বাংলা</li>
+					<li data-locale="it">Italiano</li>
+					<li data-locale="km">ភាសាខ្មែរ</li>
+					<li data-locale="kn">ಕನ್ನಡ</li>
+					<li data-locale="ko">한국어</li>
+					<li data-locale="lv">Latviešu</li>
+					<li data-locale="lol">Lolcat</li>
+					<li data-locale="nl">Nederlands</li>
+					<li data-locale="no">Norsk</li>
+					<li data-locale="pl">Polski</li>
+					<li data-locale="pt">Português</li>
+					<li data-locale="ru">Русский</li>
+					<li data-locale="sk">Slovenčina</li>
+					<li data-locale="sv">Svenska</li>
+					<li data-locale="tr">Türkçe</li>
+					<li data-locale="uk">Україна</li>
+					<li data-locale="vi">Tiếng Việt</li>
+					<!--<li data-locale="ug">Uighur</li>-->
+				</ul>
+				<div id="customServerDialog">
+					<div id="customServerList">
+						<select id="customServerSelector" size="4"></select>
+						<input type="button" id="customServerSave" value="Save" />
+						<input type="button" id="customServerDelete" value="Delete" />
+					</div>
+					<div id="customServerFields">
+						<input type="text" class="customServer" id="customName" data-utip="Name" data-utip-gravity="e" />
+						<input type="text" class="customServer" id="customDomain" data-utip="Domain" data-utip-gravity="e" />
+						<input type="text" class="customServer" id="customConferenceServer" data-utip="XMPP Conference Server" data-utip-gravity="e" />
+						<input type="text" class="customServer" id="customRelay" data-utip="BOSH/WebSocket Relay" data-utip-gravity="e" />
+						<input type="button" id="customServerSubmit" value="Continue" />
+					</div>
+				</div>
+				<form id="userInput">
+					<textarea id="userInputText" class="mousetrap"></textarea>
+					<input type="submit" value="►" id="userInputSend" />
+				</form>
+			</div>
+			<div id="conversationWrapper">
+				<div id="conversationWindow">
+				</div>
+			</div>
+			<div id="buddyWrapper" dir="ltr">
+				<div id="buddyList">
+					<div class="buddy" id="buddy-groupChat" data-id="groupChat">
+						<span></span>
+					</div>
+					<span id="buddiesOnline"></span>
+					<span id="buddiesAway"></span>
+				</div>
+			</div>
+		</div>
+		<div id="av">
+			<div id="avCamera"></div>
+		</div>
+	</div>
+</body>
+</html>
diff --git a/src/core/js/cryptocat.js b/src/core/js/cryptocat.js
new file mode 100755
index 0000000..333193b
--- /dev/null
+++ b/src/core/js/cryptocat.js
@@ -0,0 +1,1515 @@
+if (typeof Cryptocat === 'undefined') { Cryptocat = function() {} }
+
+/*
+-------------------
+GLOBAL VARIABLES
+-------------------
+*/
+
+Cryptocat.version = '2.2.2' // Version number
+
+Cryptocat.me = {
+	login:         'cryptocat',
+	newMessages:   0,
+	windowFocus:   true,
+	composing:     false,
+	conversation:  null,
+	nickname:      null,
+	otrKey:        null,
+	fileKey:       null,
+	mpPrivateKey:  null,
+	mpPublicKey:   null,
+	mpFingerprint: null,
+	currentBuddy:  null
+}
+
+Cryptocat.buddies = {}
+
+Cryptocat.audioExt = '.mp3'
+if (navigator.userAgent.match(/(OPR)|(Firefox)/)) {
+	Cryptocat.audioExt = '.ogg'
+}
+Cryptocat.sounds = {
+	'keygenStart': (new Audio('snd/keygenStart' + Cryptocat.audioExt)),
+	'keygenLoop':  (new Audio('snd/keygenLoop'  + Cryptocat.audioExt)),
+	'keygenEnd':   (new Audio('snd/keygenEnd'   + Cryptocat.audioExt)),
+	'userLeave':   (new Audio('snd/userLeave'   + Cryptocat.audioExt)),
+	'userJoin':    (new Audio('snd/userJoin'    + Cryptocat.audioExt)),
+	'msgGet':      (new Audio('snd/msgGet'      + Cryptocat.audioExt)),
+	'balloon':     (new Audio('snd/balloon'     + Cryptocat.audioExt))
+}
+
+/*
+-------------------
+END GLOBAL SCOPE
+-------------------
+*/
+
+if (typeof(window) !== 'undefined') { $(window).ready(function() {
+'use strict';
+
+/*
+-------------------
+INTIALIZATION
+-------------------
+*/
+
+// Set version number in UI.
+$('#version').text(Cryptocat.version)
+
+// Seed RNG.
+Cryptocat.random.setSeed(Cryptocat.random.generateSeed())
+
+var conversationBuffers = {}
+
+// Load favicon notification settings.
+Tinycon.setOptions({
+	colour: '#FFFFFF',
+	background: '#76BDE5'
+})
+
+/*
+-------------------
+GLOBAL INTERFACE FUNCTIONS
+-------------------
+*/
+
+// Update a file transfer progress bar.
+Cryptocat.updateFileProgressBar = function(file, chunk, size, recipient) {
+	var conversationBuffer = $(conversationBuffers[Cryptocat.buddies[recipient].id])
+	var progress = (chunk * 100) / (Math.ceil(size / Cryptocat.otr.chunkSize))
+	if (progress > 100) { progress = 100 }
+	$('.fileProgressBarFill')
+		.filterByData('file', file)
+		.filterByData('id', Cryptocat.buddies[recipient].id)
+		.animate({'width': progress + '%'})
+	conversationBuffer.find('.fileProgressBarFill')
+		.filterByData('file', file)
+		.filterByData('id', Cryptocat.buddies[recipient].id)
+		.width(progress + '%')
+	conversationBuffers[Cryptocat.buddies[recipient].id] = $('<div>').append($(conversationBuffer).clone()).html()
+}
+
+// Convert Data blob/url to downloadable file, replacing the progress bar.
+Cryptocat.addFile = function(url, file, conversation, filename) {
+	var conversationBuffer = $(conversationBuffers[Cryptocat.buddies[conversation].id])
+	var fileLinkString = 'fileLink'
+	if (navigator.userAgent === 'Chrome (Mac app)') {
+		fileLinkString += 'Mac'
+	}
+	var fileLink = Mustache.render(Cryptocat.templates[fileLinkString], {
+		url: url,
+		filename: filename,
+		downloadFile: Cryptocat.locale['chatWindow']['downloadFile']
+	})
+	$('.fileProgressBar')
+		.filterByData('file', file)
+		.filterByData('id', Cryptocat.buddies[conversation].id)
+		.replaceWith(fileLink)
+	conversationBuffer.find('.fileProgressBar')
+		.filterByData('file', file)
+		.filterByData('id', Cryptocat.buddies[conversation].id)
+		.replaceWith(fileLink)
+	conversationBuffers[Cryptocat.buddies[conversation].id] = $('<div>').append($(conversationBuffer).clone()).html()
+}
+
+// Signal a file transfer error in the UI.
+Cryptocat.fileTransferError = function(sid, nickname) {
+	$('.fileProgressBar')
+		.filterByData('file', sid)
+		.filterByData('id', Cryptocat.buddies[nickname].id)
+		.animate({'borderColor': '#F00'})
+	$('.fileProgressBarFill')
+		.filterByData('file', sid)
+		.filterByData('id', Cryptocat.buddies[nickname].id)
+		.animate({'background-color': '#F00'})
+}
+
+// Add a `message` from `nickname` to the `conversation` display and log.
+// `type` can be 'file', 'message', 'warning' or 'missingRecipients'.
+// In case `type` === 'missingRecipients', `message` becomes array of missing recipients.
+Cryptocat.addToConversation = function(message, nickname, conversation, type) {
+	var lineDecoration = 2
+	if (nickname === Cryptocat.me.nickname) {
+		lineDecoration = 1
+	}
+	else if (Cryptocat.buddies[nickname].ignored) {
+		return false
+	}
+	initializeConversationBuffer(conversation)
+	if (type === 'file') {
+		if (!message.length) { return false }
+		var id = conversation
+		if (nickname !== Cryptocat.me.nickname) {
+			id = Cryptocat.buddies[nickname].id
+			if (Cryptocat.audioNotifications) { Cryptocat.sounds.msgGet.play() }
+			desktopNotification(
+				'img/keygen.gif', nickname + ' @ ' + Cryptocat.me.conversation, message, 0x1337
+			)
+		}
+		message = Mustache.render(
+			Cryptocat.templates.file, {
+				file: message,
+				id: id
+			}
+		)
+	}
+	if (type === 'message') {
+		if (!message.length) { return false }
+		if (nickname !== Cryptocat.me.nickname) {
+			if (Cryptocat.audioNotifications) { Cryptocat.sounds.msgGet.play() }
+			desktopNotification(
+				'img/keygen.gif', nickname + ' @ ' + Cryptocat.me.conversation, message, 0x1337
+			)
+		}
+		message = Strophe.xmlescape(message)
+		message = Cryptocat.addLinks(message)
+		message = addEmoticons(message)
+		if (Cryptocat.me.login === 'facebook') {
+			message = message.replace(/\&amp\;apos\;/g, ''')
+		}
+		if (message.match(Cryptocat.me.nickname)) { lineDecoration = 3 }
+	}
+	if (type === 'warning') {
+		lineDecoration = 4
+		if (!message.length) { return false }
+		if (nickname !== Cryptocat.me.nickname) {
+			if (Cryptocat.audioNotifications) { Cryptocat.sounds.msgGet.play() }
+			desktopNotification(
+				'img/keygen.gif', nickname + ' @ ' + Cryptocat.me.conversation, message, 0x1337
+			)
+		}
+		message = Strophe.xmlescape(message)
+	}
+	if (type === 'missingRecipients') {
+		if (!message.length) { return false }
+		message = message.join(', ')
+		message = Mustache.render(Cryptocat.templates.missingRecipients, {
+			text: Cryptocat.locale.warnings.missingRecipientWarning
+				.replace('(NICKNAME)', message),
+			dir: Cryptocat.locale.direction
+		})
+		conversationBuffers[conversation] += message
+		if (conversation === Cryptocat.me.currentBuddy) {
+			$('#conversationWindow').append(message)
+			$('.missingRecipients').last().animate({'top': '0', 'opacity': '1'}, 100)
+			scrollDownConversation(400, true)
+		}
+		return true
+	}
+	var authStatus = false
+	if (
+		(nickname === Cryptocat.me.nickname) ||
+		Cryptocat.buddies[nickname].authenticated
+	) {
+		authStatus = true
+	}
+	message = message.replace(/:/g, ':')
+	var renderedMessage = Mustache.render(Cryptocat.templates.message, {
+		lineDecoration: lineDecoration,
+		nickname: shortenString(nickname, 16),
+		currentTime: currentTime(true),
+		authStatus: authStatus,
+		message: message
+	})
+	conversationBuffers[conversation] += renderedMessage
+	if (conversation === Cryptocat.me.currentBuddy) {
+		$('#conversationWindow').append(renderedMessage)
+		$('.line' + lineDecoration).last().animate({'top': '0', 'opacity': '1'}, 100)
+		bindSenderElement($('.line' + lineDecoration).last().find('.sender'))
+		scrollDownConversation(400, true)
+	}
+	else {
+		$('#buddy-' + conversation).addClass('newMessage')
+	}
+}
+
+// Show a preview for a received message from a buddy.
+// Message previews will not overlap and are removed after 5 seconds.
+Cryptocat.messagePreview = function(message, nickname) {
+	var buddyElement = $('#buddy-' + Cryptocat.buddies[nickname].id)
+	if (!buddyElement.attr('data-utip')) {
+		if (message.length > 15) {
+			message = message.substring(0, 15) + '..'
+		}
+		buddyElement.attr({
+			'data-utip-gravity': 'sw',
+			'data-utip': Strophe.xmlescape(message)
+		}).mouseenter()
+		window.setTimeout(function() {
+			buddyElement.mouseleave()
+			buddyElement.removeAttr('data-utip')
+		}, 0x1337)
+	}
+}
+
+// Handles login failures.
+Cryptocat.loginFail = function(message) {
+	$('#loginInfo').text(message)
+	$('#bubble').animate({'left': '+=5px'}, 130)
+		.animate({'left': '-=10px'}, 130)
+		.animate({'left': '+=5px'}, 130)
+	$('#loginInfo').animate({'background-color': '#E93028'}, 200)
+}
+
+// Handle detected new keys.
+Cryptocat.removeAuthAndWarn = function(nickname) {
+	var buddy = Cryptocat.buddies[nickname]
+	var openAuth = false
+	buddy.updateAuth(false)
+	var errorAKE = Mustache.render(
+		Cryptocat.templates.errorAKE, {
+			nickname: nickname,
+			errorText: Cryptocat.locale.auth.AKEWarning,
+			openAuth: Cryptocat.locale.chatWindow.authenticate
+		}
+	)
+	Cryptocat.dialogBox(errorAKE, {
+		extraClasses: 'dialogBoxError',
+		closeable: true,
+		height: 250,
+		onAppear: function() {
+			$('#openAuth').unbind().bind('click', function() {
+				openAuth = true
+				$('#dialogBoxClose').click()
+			})
+		},
+		onClose: function() {
+			if (openAuth) {
+				Cryptocat.displayInfo(nickname)
+			}
+		}
+	})
+}
+
+// Buddy constructor
+var Buddy = function(nickname, id, status) {
+	this.id             = id
+	this.ignored        = false
+	this.fingerprint    = null
+	this.authenticated  = false
+	this.fileKey        = null
+	this.mpPublicKey    = null
+	this.mpFingerprint  = null
+	this.mpSecretKey    = null
+	this.nickname       = nickname
+	this.genFingerState = null
+	this.usingCryptocat = true
+	this.status         = status
+	this.otr            = Cryptocat.otr.add(nickname)
+}
+
+Buddy.prototype = {
+	constructor: Buddy,
+	updateMpKeys: function(publicKey) {
+		this.mpPublicKey = publicKey
+		this.mpFingerprint = Cryptocat.multiParty.genFingerprint(this.nickname)
+		this.mpSecretKey = Cryptocat.multiParty.genSharedSecret(this.nickname)
+	},
+	updateAuth: function(auth) {
+		var nickname = this.nickname
+		this.authenticated = auth
+		if (auth) {
+			$('#authenticated').attr('data-active', true)
+			$('#notAuthenticated').attr('data-active', false)
+		}
+		else {
+			$('#authenticated').attr('data-active', false)
+			$('#notAuthenticated').attr('data-active', true)
+		}
+		$.each($('span').filterByData('sender', nickname),
+			function(index, value) {
+				$(value).find('.authStatus').attr('data-auth', auth)
+			}
+		)
+		var authStatusBuffers = [
+			'groupChat',
+			Cryptocat.buddies[nickname].id
+		]
+		$.each(authStatusBuffers, function(i, thisBuffer) {
+			var buffer = $(conversationBuffers[thisBuffer])
+			$.each(buffer.find('span').filterByData('sender', nickname),
+				function(index, value) {
+					$(value).find('.authStatus').attr('data-auth', auth)
+				}
+			)
+			conversationBuffers[thisBuffer] = $('<div>').append(
+				buffer.clone()
+			).html()
+		})
+	}
+}
+
+// Build new buddy.
+Cryptocat.addBuddy = function(nickname, id, status) {
+	if (!id) { id = getUniqueBuddyID() }
+	var buddy = Cryptocat.buddies[nickname] = new Buddy(nickname, id, status)
+	$('#buddyList').queue(function() {
+		var buddyTemplate = Mustache.render(Cryptocat.templates.buddy, {
+			buddyID: buddy.id,
+			shortNickname: shortenString(nickname, 11),
+			status: status
+		})
+		var placement = determineBuddyPlacement(nickname, id, status)
+		$(buddyTemplate).insertAfter(placement).slideDown(100, function() {
+			$('#buddy-' + buddy.id)
+				.unbind('click')
+				.click(function() {
+					Cryptocat.onBuddyClick($(this))
+				}
+			)
+			$('#menu-' + buddy.id).attr('status', 'inactive')
+				.unbind('click')
+				.click(function(e) {
+					e.stopPropagation()
+					openBuddyMenu(nickname)
+				}
+			)
+			buddyNotification(nickname, true)
+		})
+	})
+	$('#buddyList').dequeue()
+}
+
+// Set a buddy's status to `online` or `away`.
+Cryptocat.buddyStatus = function(nickname, status) {
+	Cryptocat.buddies[nickname].status = status
+	var thisBuddy = $('#buddy-' + Cryptocat.buddies[nickname].id)
+	var placement = determineBuddyPlacement(
+		nickname, Cryptocat.buddies[nickname].id, status
+	)
+	if (thisBuddy.attr('status') !== status) {
+		thisBuddy.attr('status', status)
+		thisBuddy.insertAfter(placement).slideDown(200)
+	}
+}
+
+// Handle buddy going offline.
+Cryptocat.removeBuddy = function(nickname) {
+	var buddyID = Cryptocat.buddies[nickname].id
+	var buddyElement = $('.buddy').filterByData('id', buddyID)
+	delete Cryptocat.buddies[nickname]
+	if (!buddyElement.length) {
+		return
+	}
+	buddyElement.each(function() {
+		$(this).attr('status', 'offline')
+		buddyNotification(nickname, false)
+		if (Cryptocat.me.currentBuddy === buddyID) {
+			return
+		}
+		if (!$(this).hasClass('newMessage')) {
+			$(this).slideUp(500, function() {
+				$(this).remove()
+			})
+		}
+	})
+}
+
+// Determine where to place a buddy in the buddy list
+// so the buddy list is in alphabetical order.
+var determineBuddyPlacement = function(nickname, id, status) {
+	var buddies = [{
+		nickname: nickname,
+		id: id
+	}]
+	for (var i in Cryptocat.buddies) {
+		if (
+			Cryptocat.buddies.hasOwnProperty(i) &&
+			(Cryptocat.buddies[i].status === status)
+		) {
+			buddies.push({
+				nickname: i,
+				id: Cryptocat.buddies[i].id
+			})
+		}
+	}
+	buddies.sort(function(a, b) {
+		var nameA = a.nickname.toLowerCase()
+		var nameB = b.nickname.toLowerCase()
+		if (nameA < nameB) {
+			return -1
+		}
+		if (nameA > nameB) {
+			return 1
+		}
+		return 0
+	})
+	var rightBefore
+	for (var o = 0; o < buddies.length; o++) {
+		if (buddies[o].id === id) {
+			if (o === 0) {
+				if (status === 'online') {
+					rightBefore = '#buddiesOnline'
+				}
+				if (status === 'away') {
+					rightBefore = '#buddiesAway'
+				}
+			}
+			else {
+				rightBefore = '[data-id=' + buddies[o - 1].id + ']'
+			}
+			break
+		}
+	}
+	return rightBefore
+}
+
+// Get a buddy's nickname from their ID.
+Cryptocat.getBuddyNicknameByID = function(id) {
+	for (var i in Cryptocat.buddies) {
+		if (Cryptocat.buddies.hasOwnProperty(i)) {
+			if (Cryptocat.buddies[i].id === id) {
+				return i
+			}
+		}
+	}
+}
+
+// Bind buddy click actions.
+Cryptocat.onBuddyClick = function(buddyElement) {
+	var nickname = Cryptocat.getBuddyNicknameByID(buddyElement.attr('data-id'))
+	buddyElement.removeClass('newMessage')
+	if (buddyElement.prev().attr('id') === 'currentConversation') {
+		$('#userInputText').focus()
+		return true
+	}
+	var id = buddyElement.attr('data-id')
+	Cryptocat.me.currentBuddy = id
+	initializeConversationBuffer(id)
+	// Render conversation info bar.
+	var styling = 'notEncrypted'
+	var encryptionStatus = Cryptocat.locale.login.notEncrypted
+	if (Cryptocat.me.login === 'cryptocat') {
+		styling = 'encrypted'
+		encryptionStatus = Cryptocat.locale.login.encrypted
+	}
+	else if (Cryptocat.me.login === 'facebook') {
+		if (Cryptocat.buddies[nickname].usingCryptocat) {
+			styling = 'encrypted'
+			encryptionStatus = Cryptocat.locale.login.encrypted
+		}
+	}
+	$('#encryptionStatus').html(
+		Mustache.render(Cryptocat.templates.encryptionStatus, {
+			conversationStatus: Cryptocat.locale.login.conversationStatus,
+			styling: styling,
+			encryptionStatus: encryptionStatus
+		})
+	)
+	// Switch currently active conversation.
+	$('#conversationWindow').html(conversationBuffers[id])
+	bindSenderElement()
+	scrollDownConversation(0, false)
+	$('#userInputText').focus()
+	$('#buddy-' + id).addClass('currentConversation')
+	// Clean up finished conversations.
+	$('#buddyList div').each(function() {
+		if ($(this).attr('data-id') !== id) {
+			$(this).removeClass('currentConversation')
+			if (
+				!$(this).hasClass('newMessage') &&
+				($(this).attr('status') === 'offline')
+			) {
+				$(this).slideUp(500, function() { $(this).remove() })
+			}
+		}
+	})
+	$('#conversationWindow').children().addClass('visibleLine')
+}
+
+// Close generating fingerprints dialog.
+Cryptocat.closeGenerateFingerprints = function(nickname) {
+	var state = Cryptocat.buddies[nickname].genFingerState
+	Cryptocat.buddies[nickname].genFingerState = null
+	$('#fill').stop().animate(
+		{'width': '100%', 'opacity': '1'},
+		400, 'linear',
+		function() {
+			$('#dialogBoxContent').fadeOut(function() {
+				$(this).empty().show()
+				if (state.close) {
+					$('#dialogBoxClose').click()
+				}
+				state.cb()
+			})
+		}
+	)
+}
+
+// Displays a pretty dialog box with `data` as the content HTML.
+Cryptocat.dialogBox = function(data, options) {
+	if (options.closeable) {
+		$('#dialogBoxClose').css('width', 18)
+		$('#dialogBoxClose').css('font-size', 12)
+		$(document).keydown(function(e) {
+			if (e.keyCode === 27) {
+				e.stopPropagation()
+				$('#dialogBoxClose').click()
+				$(document).unbind('keydown')
+			}
+		})
+	}
+	if (options.extraClasses) {
+		$('#dialogBox').addClass(options.extraClasses)
+	}
+	$('#dialogBoxContent').html(data)
+	$('#dialogBox').css('height', options.height)
+	$('#dialogBox').fadeIn(200, function() {
+		if (options.onAppear) { options.onAppear() }
+	})
+	$('#dialogBoxClose').unbind('click').click(function(e) {
+		e.stopPropagation()
+		$(this).unbind('click')
+		if ($(this).css('width') === 0) {
+			return false
+		}
+		$('#dialogBox').fadeOut(100, function() {
+			if (options.extraClasses) {
+				$('#dialogBox').removeClass(options.extraClasses)
+			}
+			$('#dialogBoxContent').empty()
+			$('#dialogBoxClose').css('width', '0')
+			$('#dialogBoxClose').css('font-size', '0')
+			if (options.onClose) { options.onClose() }
+		})
+		$('#userInputText').focus()
+	})
+}
+
+// Display buddy information, including fingerprints and authentication.
+Cryptocat.displayInfo = function(nickname) {
+	var isMe = nickname === Cryptocat.me.nickname,
+		infoDialog = isMe ? 'myInfo' : 'buddyInfo',
+		chatWindow = Cryptocat.locale.chatWindow
+	infoDialog = Mustache.render(Cryptocat.templates[infoDialog], {
+		nickname: nickname,
+		authenticated: Cryptocat.locale.auth.authenticated + ':',
+		learnMoreAuth: Cryptocat.locale.auth.learnMoreAuth,
+		otrFingerprint: chatWindow.otrFingerprint,
+		groupFingerprint: chatWindow.groupFingerprint,
+		authenticate: chatWindow.authenticate,
+		verifyUserIdentity: chatWindow.verifyUserIdentity,
+		secretQuestion: chatWindow.secretQuestion,
+		secretAnswer: chatWindow.secretAnswer,
+		ask: chatWindow.ask,
+		identityVerified: chatWindow.identityVerified
+	})
+	ensureOTRdialog(nickname, false, function() {
+		if (isMe) {
+			Cryptocat.dialogBox(infoDialog, {
+				height: 250,
+				closeable: true
+			})
+		}
+		else {
+			var authTutorial = Mustache.render(Cryptocat.templates.authTutorial, {
+				nickname: nickname,
+				slide1: Cryptocat.locale.auth.authSlide1,
+				slide2: Cryptocat.locale.auth.authSlide2,
+				slide3: Cryptocat.locale.auth.authSlide3,
+				slide4: Cryptocat.locale.auth.authSlide5
+			})
+			Cryptocat.dialogBox(infoDialog, {
+				height: 430,
+				closeable: true,
+				onAppear: function() {
+					$('#authTutorial').html(authTutorial)
+				}
+			})
+			bindAuthDialog(nickname)
+		}
+		$('#otrFingerprint').text(getFingerprint(nickname, true))
+		$('#multiPartyFingerprint').text(getFingerprint(nickname, false))
+	})
+}
+
+// Executes on user logout.
+Cryptocat.logout = function() {
+	Cryptocat.loginError = false
+	if (Cryptocat.me.login === 'cryptocat') {
+		Cryptocat.xmpp.connection.muc.leave(
+			Cryptocat.me.conversation + '@'
+			+ Cryptocat.xmpp.conferenceServer
+		)
+		$('#loginInfo').text(Cryptocat.locale['loginMessage']['thankYouUsing'])
+		$('#loginInfo').animate({'background-color': '#97CEEC'}, 200)
+	}
+	if (Cryptocat.me.login === 'facebook') {
+		clearInterval(Cryptocat.FB.statusInterval)
+		Cryptocat.FB.accessToken = null
+		Cryptocat.FB.userID = null
+		Cryptocat.storage.removeItem('fbAccessToken')
+	}
+	Cryptocat.xmpp.connection.disconnect()
+	Cryptocat.xmpp.connection = null
+	document.title = 'Cryptocat'
+	$('#conversationInfo,#optionButtons').fadeOut()
+	$('#header').animate({'background-color': 'transparent'})
+	$('.logo').animate({'margin': '-5px 5px 0 5px'})
+	$('#buddyWrapper').slideUp()
+	$('.buddy').unbind('click')
+	$('.buddyMenu').unbind('click')
+	$('#buddy-groupChat').insertAfter('#buddiesOnline')
+	$('#userInput').fadeOut(function() {
+		$('#logoText').fadeIn()
+		$('#footer').animate({'height': 14})
+		$('#conversationWrapper').fadeOut(function() {
+			$('#info,#loginOptions,#version,#loginInfo').fadeIn()
+			$('#login').fadeIn(200, function() {
+				$('#login').css({opacity: 1})
+				$('#conversationName').select()
+				$('#conversationName,#nickname').removeAttr('readonly')
+				$('#loginSubmit,#facebookConnect').removeAttr('readonly')
+				$('#encryptionStatus').text('')
+			})
+			$('#dialogBoxClose').click()
+			$('#buddyList div').each(function() {
+				if ($(this).attr('id') !== 'buddy-groupChat') {
+					$(this).remove()
+				}
+			})
+			$('#conversationWindow').html('')
+			for (var b in Cryptocat.buddies) {
+				if (Cryptocat.buddies.hasOwnProperty(b)) {
+					delete Cryptocat.buddies[b]
+				}
+			}
+			conversationBuffers = {}
+		})
+	})
+}
+
+Cryptocat.prepareAnswer = function(answer, ask, buddyMpFingerprint) {
+	var first, second
+	answer = answer.toLowerCase().replace(/(\s|\.|\,|\'|\"|\;|\?|\!)/, '')
+	if (buddyMpFingerprint) {
+		first = ask ? Cryptocat.me.mpFingerprint : buddyMpFingerprint
+		second = ask ? buddyMpFingerprint : Cryptocat.me.mpFingerprint
+		answer += ';' + first + ';' + second
+	}
+	return answer
+}
+
+/*
+-------------------
+PRIVATE INTERFACE FUNCTIONS
+-------------------
+*/
+
+// Outputs the current hh:mm.
+// If `seconds = true`, outputs hh:mm:ss.
+var currentTime = function(seconds) {
+	var date = new Date()
+	var time = []
+	time.push(date.getHours().toString())
+	time.push(date.getMinutes().toString())
+	if (seconds) { time.push(date.getSeconds().toString()) }
+	for (var just in time) {
+		if (time[just].length === 1) {
+			time[just] = '0' + time[just]
+		}
+	}
+	return time.join(':')
+}
+
+// Initializes a conversation buffer. Internal use.
+var initializeConversationBuffer = function(id) {
+	if (!conversationBuffers.hasOwnProperty(id)) {
+		conversationBuffers[id] = ''
+	}
+	if (
+		id !== 'groupChat' &&
+		!Cryptocat.buddies[Cryptocat.getBuddyNicknameByID(id)].usingCryptocat
+		&& conversationBuffers[id] === ''
+	) {
+		conversationBuffers[id] += Mustache.render(Cryptocat.templates.notUsingCryptocat, {
+			text: Cryptocat.locale.login.facebookWarning,
+			dir:  Cryptocat.locale.direction
+		})
+	}
+}
+
+// Get a unique buddy identifier.
+var getUniqueBuddyID = function() {
+	var buddyID = Cryptocat.random.encodedBytes(16, CryptoJS.enc.Hex)
+	for (var b in Cryptocat.buddies) {
+		if (Cryptocat.buddies.hasOwnProperty(b)) {
+			if (Cryptocat.buddies[b].id === buddyID) {
+				return getUniqueBuddyID()
+			}
+		}
+	}
+	return buddyID
+}
+
+// Simply shortens a string `string` to length `length.
+// Adds '..' to delineate that string was shortened.
+var shortenString = function(string, length) {
+	if (string.length > length) {
+		return string.substring(0, (length - 2)) + '..'
+	}
+	return string
+}
+
+// Get a fingerprint, formatted for readability.
+var getFingerprint = function(nickname, OTR) {
+	var buddy = Cryptocat.buddies[nickname],
+		isMe = nickname === Cryptocat.me.nickname,
+		fingerprint
+
+	if (OTR) {
+		fingerprint = isMe
+			? Cryptocat.me.otrKey.fingerprint()
+			: fingerprint = buddy.fingerprint
+	} else {
+		fingerprint = isMe
+			? Cryptocat.me.mpFingerprint
+			: buddy.mpFingerprint
+	}
+
+	var formatted = ''
+	for (var i in fingerprint) {
+		if (fingerprint.hasOwnProperty(i)) {
+			if ((i !== 0) && (i % 8) === 0) {
+				formatted += ' '
+			}
+			formatted += fingerprint[i]
+		}
+	}
+	return formatted.toUpperCase()
+}
+
+// Convert message URLs to links. Used internally.
+Cryptocat.addLinks = function(message) {
+	var sanitize
+	var URLs = message.match(/((http(s?)\:\/\/){1}\S+)/gi)
+	if (!URLs) { return message }
+	for (var i = 0; i !== URLs.length; i++) {
+		sanitize = URLs[i].split('')
+		for (var l = 0; l !== sanitize.length; l++) {
+			if (!sanitize[l].match(
+				/\w|\d|\:|\/|\?|\=|\#|\+|\,|\.|\&|\;|\%/)
+			) {
+				sanitize[l] = encodeURIComponent(sanitize[l])
+			}
+		}
+		sanitize = sanitize.join('')
+		var url = sanitize.replace(':', ':')
+		if (navigator.userAgent === 'Chrome (Mac app)') {
+			message = message.replace(
+				sanitize, '<a href="' + url + '">' + url + '</a>'
+			)
+			continue
+		}
+		message = message.replace(
+			sanitize, '<a href="' + url + '" target="_blank">' + url + '</a>'
+		)
+	}
+	return message
+}
+
+// Convert text emoticons to graphical emoticons.
+var addEmoticons = function(message) {
+	var emoticons = {
+		cry:                   /(\s|^)(:|(=))-?\'\((?=(\s|$))/gi,
+		unsure:               /(\s|^)(:|(=))-?(\/|s)(?=(\s|$))/gi,
+		cat:                 /(\s|^)(:|(=))-?3(?=(\s|$))/gi,
+		gasp:               /(\s|^)(:|(=))-?o(?=(\s|$))/gi,
+		grin:              /(\s|^)(:|(=))-?D(?=(\s|$))/gi,
+		sad:              /(\s|^)(:|(=))-?\((?=(\s|$))/gi,
+		smile:           /(\s|^)(:|(=))-?\)(?=(\s|$))/gi,
+		tongue:         /(\s|^)(:|(=))-?p(?=(\s|$))/gi,
+		happy:         /(\s|^)\^(_|\.)?\^(?=(\s|$))/gi,
+		shut:         /(\s|^)(:|(=))-?x\b(?=(\s|$))/gi,
+		wink:        /(\s|^);-?\)(?=(\s|$))/gi,
+		winkTongue: /(\s|^);-?\p(?=(\s|$))/gi,
+		squint:    /(\s|^)-_-(?=(\s|$))/gi,
+	}
+	for (var e in emoticons) {
+		if (emoticons.hasOwnProperty(e)) {
+			message = message.replace(
+				emoticons[e],
+				Mustache.render(Cryptocat.templates.emoticon, {
+					emoticon: e
+				})
+			)
+		}
+	}
+	return message.replace(
+		/(\s|^)\&lt\;3\b(?=(\s|$))/g,
+		' <span class="monospace">♥</span> '
+	)
+}
+
+// Bind `nickname`'s authentication dialog buttons and options.
+var bindAuthDialog = function(nickname) {
+	var buddy = Cryptocat.buddies[nickname]
+	if (Cryptocat.buddies[nickname].authenticated) {
+		buddy.updateAuth(true)
+	}
+	else {
+		buddy.updateAuth(false)
+	}
+	$('#authenticated').unbind('click').bind('click', function() {
+		buddy.updateAuth(true)
+	})
+	$('#notAuthenticated').unbind('click').bind('click', function() {
+		buddy.updateAuth(false)
+	})
+	// If the current locale doesn't have the translation
+	// for the auth slides yet, then don't display the option
+	// for opening the auth tutorial.
+	// This is temporary until all translations are ready.
+	// — Nadim, March 29 2014
+	if (
+		Cryptocat.locale.language !== 'en' &&
+		Cryptocat.locale.auth.learnMoreAuth === 'Learn more about authentication') {
+		$('#authLearnMore').hide()
+	}
+	$('#authLearnMore').unbind('click').bind('click', function() {
+		if ($(this).attr('data-active') === 'true') {
+			$('#authTutorial').fadeOut(function() {
+				$('#authLearnMore').attr('data-active', 'false')
+					.text(Cryptocat.locale.auth.learnMoreAuth)
+				$('.authInfo').fadeIn()
+			})
+		}
+		else {
+			$('.authInfo').fadeOut(function() {
+				$('#authLearnMore').attr('data-active', 'true')
+					.text(Cryptocat.locale.chatWindow.cont)
+				$('#authTutorial').fadeIn(function() {
+					if ($('.bjqs-slide').length) {
+						return
+					}
+					$('#authTutorialSlides').bjqs({
+						width: 430,
+						height: 230,
+						animduration: 250,
+						animspeed: 7000,
+						responsive: true,
+						nexttext: '>',
+						prevtext: '<'
+					})
+				})
+			})
+		}
+	})
+	$('#authSubmit').unbind('click').bind('click', function(e) {
+		e.preventDefault()
+		var question = $('#authQuestion').val()
+		var answer = $('#authAnswer').val()
+		if (answer.length === 0) {
+			return
+		}
+		$('#authSubmit').val(Cryptocat.locale.chatWindow.asking)
+		$('#authSubmit').unbind('click').bind('click', function(e) {
+			e.preventDefault()
+		})
+		buddy.updateAuth(false)
+		answer = Cryptocat.prepareAnswer(answer, true, buddy.mpFingerprint)
+		buddy.otr.smpSecret(answer, question)
+	})
+}
+
+// Bind sender element to show authStatus information and timestamps.
+var bindSenderElement = function(senderElement) {
+	if (!senderElement) {
+		senderElement = $('.sender')
+	}
+	senderElement.children().unbind('mouseenter,mouseleave,click')
+	senderElement.find('.nickname').mouseenter(function() {
+		$(this).text($(this).parent().attr('data-timestamp'))
+	})
+	senderElement.find('.nickname').mouseleave(function() {
+		$(this).text($(this).parent().attr('data-sender'))
+	})
+	senderElement.find('.authStatus').mouseenter(function() {
+		if ($(this).attr('data-auth') === 'true') {
+			$(this).attr('data-utip', Cryptocat.locale.auth.authenticated)
+		}
+		else {
+			$(this).attr('data-utip',
+				Mustache.render(Cryptocat.templates.authStatusFalseUtip, {
+					text: Cryptocat.locale.auth.userNotAuthenticated,
+					learnMore: Cryptocat.locale.auth.clickToLearnMore
+				})
+			)
+		}
+		// This is pretty ugly, sorry! Feel free to clean up via pull request.
+		var bgc = $(this).css('background-color')
+		var boxShadow = bgc.replace('rgb', 'rgba')
+			.substring(0, bgc.length - 1) + ', 0.3)'
+		$(this).attr('data-utip-style', JSON.stringify({
+			'width': 'auto',
+			'max-width': '110px',
+			'font-size': '11px',
+			'background-color': bgc,
+			'box-shadow': '0 0 0 2px ' + boxShadow
+		}))
+		$(this).attr('data-utip-click', 'Cryptocat.displayInfo()')
+	})
+	senderElement.find('.authStatus').click(function() {
+		Cryptocat.displayInfo($(this).parent().attr('data-sender'))
+	})
+}
+
+var desktopNotification = function(image, title, body, timeout) {
+	Tinycon.setBubble(++Cryptocat.me.newMessages)
+	if (!Cryptocat.desktopNotifications || Cryptocat.me.windowFocus) { return false }
+	// Mac
+	if (navigator.userAgent === 'Chrome (Mac app)') {
+		var iframe = document.createElement('IFRAME')
+		iframe.setAttribute('src', 'js-call:' + title + ':' + body)
+		document.documentElement.appendChild(iframe)
+		iframe.parentNode.removeChild(iframe)
+		iframe = null
+	}
+	else {
+		var notice = new Notification(title, { tag: 'Cryptocat', body: body, icon: image })
+		if (timeout > 0) {
+			window.setTimeout(function() {
+				if (notice) { notice.cancel() }
+			}, timeout)
+		}
+	}
+}
+
+// Add a join/part notification to the conversation window.
+// If 'join === true', shows join notification, otherwise shows part.
+var buddyNotification = function(nickname, join) {
+	// Don't execute unless we're using Cryptocat group chat
+	if (Cryptocat.me.login !== 'cryptocat') {
+		return false
+	}
+	// Otherwise, go ahead
+	var status, audioNotification
+	if (join) {
+		status = Mustache.render(Cryptocat.templates.userJoin, {
+			nickname: nickname,
+			currentTime: currentTime(false)
+		})
+		audioNotification = 'userJoin'
+	}
+	else {
+		status = Mustache.render(Cryptocat.templates.userLeave, {
+			nickname: nickname,
+			currentTime: currentTime(false)
+		})
+		audioNotification = 'userLeave'
+	}
+	initializeConversationBuffer('groupChat')
+	conversationBuffers['groupChat'] += status
+	if (Cryptocat.me.currentBuddy === 'groupChat') {
+		$('#conversationWindow').append(status)
+	}
+	scrollDownConversation(400, true)
+	desktopNotification('img/keygen.gif',
+		nickname + ' has ' + (join ? 'joined ' : 'left ')
+		+ Cryptocat.me.conversation, '', 0x1337)
+	if (Cryptocat.audioNotifications) {
+		Cryptocat.sounds[audioNotification].play()
+	}
+}
+
+// Send encrypted file.
+var sendFile = function(nickname) {
+	var sendFileDialog = Mustache.render(Cryptocat.templates.sendFile, {
+		sendEncryptedFile: Cryptocat.locale['chatWindow']['sendEncryptedFile'],
+		fileTransferInfo: Cryptocat.locale['chatWindow']['fileTransferInfo']
+	})
+	ensureOTRdialog(nickname, false, function() {
+		Cryptocat.dialogBox(sendFileDialog, {
+			height: 250,
+			closeable: true
+		})
+		$('#fileSelector').change(function(e) {
+			e.stopPropagation()
+			if (this.files) {
+				var file = this.files[0]
+				var filename = Cryptocat.random.encodedBytes(16, CryptoJS.enc.Hex)
+				filename += file.name.match(/\.(\w)+$/)[0]
+				Cryptocat.buddies[nickname].otr.sendFile(filename)
+				var key = Cryptocat.buddies[nickname].fileKey[filename]
+				Cryptocat.otr.beginSendFile({
+					file: file,
+					filename: filename,
+					to: nickname,
+					key: key
+				})
+				;delete Cryptocat.buddies[nickname].fileKey[filename]
+			}
+		})
+		$('#fileSelectButton').click(function() {
+			$('#fileSelector').click()
+		})
+	})
+}
+
+// Scrolls down the chat window to the bottom in a smooth animation.
+// 'speed' is animation speed in milliseconds.
+// If `threshold` is true, we won't scroll down if the user
+// appears to be scrolling up to read messages.
+var scrollDownConversation = function(speed, threshold) {
+	var scrollPosition = $('#conversationWindow')[0].scrollHeight
+	scrollPosition -= $('#conversationWindow').scrollTop()
+	if ((scrollPosition < 700) || !threshold) {
+		$('#conversationWindow').stop().animate({
+			scrollTop: $('#conversationWindow')[0].scrollHeight + 20
+		}, speed)
+	}
+}
+
+// If OTR fingerprints have not been generated, show a progress bar and generate them.
+var ensureOTRdialog = function(nickname, close, cb) {
+	var buddy = Cryptocat.buddies[nickname]
+	if (nickname === Cryptocat.me.nickname || buddy.fingerprint) {
+		return cb()
+	}
+	var progressDialog = '<div id="progressBar"><div id="fill"></div></div>'
+	Cryptocat.dialogBox(progressDialog, {
+		height: 250,
+		closeable: true
+	})
+	$('#progressBar').css('margin', '70px auto 0 auto')
+	$('#fill').animate({'width': '100%', 'opacity': '1'}, 10000, 'linear')
+	// add some state for status callback
+	buddy.genFingerState = { close: close, cb: cb }
+	buddy.otr.sendQueryMsg()
+}
+
+// Open a buddy's contact list context menu.
+var openBuddyMenu = function(nickname) {
+	var buddy = Cryptocat.buddies[nickname],
+		chatWindow = Cryptocat.locale.chatWindow,
+		ignoreAction = chatWindow[buddy.ignored ? 'unignore' : 'ignore'],
+		$menu = $('#menu-' + buddy.id),
+		$buddy = $('#buddy-' + buddy.id)
+	if ($menu.attr('status') === 'active') {
+		$menu.attr('status', 'inactive')
+		$menu.css('background-image', 'url("img/down.png")')
+		$buddy.animate({'height': 15}, 190)
+		$('#' + buddy.id + '-contents').fadeOut(200, function() {
+			$(this).remove()
+		})
+		return
+	}
+	$menu.attr('status', 'active')
+	$menu.css('background-image', 'url("img/up.png")')
+	$buddy.delay(10).animate({'height': 130}, 180, function() {
+		$buddy.append(
+			Mustache.render(Cryptocat.templates.buddyMenu, {
+				buddyID: buddy.id,
+				sendEncryptedFile: chatWindow.sendEncryptedFile,
+				displayInfo: chatWindow.displayInfo,
+				ignore: ignoreAction
+			})
+		)
+		var $contents = $('#' + buddy.id + '-contents')
+		$contents.fadeIn(200)
+		$contents.find('.option1').click(function(e) {
+			e.stopPropagation()
+			Cryptocat.displayInfo(nickname)
+			$menu.click()
+		})
+		$contents.find('.option2').click(function(e) {
+			e.stopPropagation()
+			sendFile(nickname)
+			$menu.click()
+		})
+		$contents.find('.option3').click(function(e) {
+			e.stopPropagation()
+			if (buddy.ignored) {
+				$buddy.removeClass('ignored')
+			} else {
+				$buddy.addClass('ignored')
+			}
+			buddy.ignored = !buddy.ignored
+			$menu.click()
+		})
+		if (Cryptocat.me.login === 'facebook') {
+			$contents.find('.option2').remove()
+		}
+	})
+}
+
+// Check for nickname completion.
+// Called when pressing tab in user input.
+var nicknameCompletion = function(input) {
+	var nickname, match, suffix
+	for (nickname in Cryptocat.buddies) {
+		if (Cryptocat.buddies.hasOwnProperty(nickname)) {
+			try { match = nickname.match(input.match(/(\S)+$/)[0]) }
+			catch(err) {}
+			if (match) {
+				if (input.match(/\s/)) { suffix = ' ' }
+				else { suffix = ': ' }
+				return input.replace(/(\S)+$/, nickname + suffix)
+			}
+		}
+	}
+}
+
+/*
+-------------------
+USER INTERFACE BINDINGS
+-------------------
+*/
+
+// Buttons:
+// Status button.
+$('#status').click(function() {
+	var $this = $(this)
+	if ($this.attr('src') === 'img/available.png') {
+		$this.attr('src', 'img/away.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['statusAway'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['statusAway'])
+		$this.mouseenter()
+		Cryptocat.xmpp.currentStatus = 'away'
+		Cryptocat.xmpp.sendStatus()
+	}
+	else {
+		$this.attr('src', 'img/available.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['statusAvailable'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['statusAvailable'])
+		$this.mouseenter()
+		Cryptocat.xmpp.currentStatus = 'online'
+		Cryptocat.xmpp.sendStatus()
+	}
+})
+
+// My info button.
+$('#myInfo').click(function() {
+	Cryptocat.displayInfo(Cryptocat.me.nickname)
+})
+
+// Desktop notifications button.
+$('#notifications').click(function() {
+	var $this = $(this)
+	if ($this.attr('src') === 'img/noNotifications.png') {
+		$this.attr('src', 'img/notifications.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['desktopNotificationsOn'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['desktopNotificationsOn'])
+		$this.mouseenter()
+		Cryptocat.desktopNotifications = true
+		Cryptocat.storage.setItem('desktopNotifications', 'true')
+		if (window.webkitNotifications) {
+			if (window.webkitNotifications.checkPermission()) {
+				window.webkitNotifications.requestPermission(function() {})
+			}
+		}
+	}
+	else {
+		$this.attr('src', 'img/noNotifications.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['desktopNotificationsOff'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['desktopNotificationsOff'])
+		$this.mouseenter()
+		Cryptocat.desktopNotifications = false
+		Cryptocat.storage.setItem('desktopNotifications', 'false')
+	}
+})
+
+// Audio notifications button.
+$('#audio').click(function() {
+	var $this = $(this)
+	if ($this.attr('src') === 'img/noSound.png') {
+		$this.attr('src', 'img/sound.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['audioNotificationsOn'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['audioNotificationsOn'])
+		$this.mouseenter()
+		Cryptocat.audioNotifications = true
+		Cryptocat.storage.setItem('audioNotifications', 'true')
+	}
+	else {
+		$this.attr('src', 'img/noSound.png')
+		$this.attr('title', Cryptocat.locale['chatWindow']['audioNotificationsOff'])
+		$this.attr('data-utip', Cryptocat.locale['chatWindow']['audioNotificationsOff'])
+		$this.mouseenter()
+		Cryptocat.audioNotifications = false
+		Cryptocat.storage.setItem('audioNotifications', 'false')
+	}
+})
+
+// Logout button.
+$('#logout').click(function() {
+	Cryptocat.logout()
+})
+
+// Submit user input.
+$('#userInput').submit(function() {
+	var message = $.trim($('#userInputText').val())
+	$('#userInputText').val('')
+	if (!message.length) { return false }
+	if (Cryptocat.me.currentBuddy !== 'groupChat') {
+		Cryptocat.buddies[
+			Cryptocat.getBuddyNicknameByID(Cryptocat.me.currentBuddy)
+		].otr.sendMsg(message)
+	}
+	else if (Object.keys(Cryptocat.buddies).length) {
+		if (Cryptocat.me.login !== 'cryptocat') {
+			return false
+		}
+		var ciphertext = JSON.parse(Cryptocat.multiParty.sendMessage(message))
+		var missingRecipients = []
+		for (var i in Cryptocat.buddies) {
+			if (typeof(ciphertext['text'][i]) !== 'object') {
+				missingRecipients.push(i)
+			}
+		}
+		if (missingRecipients.length) {
+			Cryptocat.addToConversation(
+				missingRecipients, Cryptocat.me.nickname,
+				'groupChat', 'missingRecipients'
+			)
+		}
+		Cryptocat.xmpp.connection.muc.message(
+			Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+			null, JSON.stringify(ciphertext), null, 'groupchat', 'active'
+		)
+	}
+	Cryptocat.addToConversation(
+		message, Cryptocat.me.nickname,
+		Cryptocat.me.currentBuddy, 'message'
+	)
+	return false
+})
+
+// User input key event detection.
+// (Message submission, nick completion...)
+$('#userInputText').keydown(function(e) {
+	if (e.keyCode === 9) {
+		e.preventDefault()
+		var nickComplete = nicknameCompletion($(this).val())
+		if (nickComplete) {
+			$(this).val(nickComplete)
+		}
+	}
+	else if (e.keyCode === 13) {
+		e.preventDefault()
+		$('#userInput').submit()
+		Cryptocat.me.composing = false
+		return true
+	}
+	var destination, type
+	if (Cryptocat.me.currentBuddy === 'groupChat') {
+		destination = null
+		type = 'groupchat'
+	}
+	else {
+		destination = Cryptocat.getBuddyNicknameByID(Cryptocat.me.currentBuddy)
+		type = 'chat'
+	}
+	if (!Cryptocat.me.composing) {
+		Cryptocat.me.composing = true
+		Cryptocat.xmpp.connection.muc.message(
+			Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+			destination, '', null, type, 'composing'
+		)
+		window.setTimeout(function(d, t) {
+			Cryptocat.xmpp.connection.muc.message(
+				Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+				d, '', null, t, 'paused'
+			)
+			Cryptocat.me.composing = false
+		}, 7000, destination, type)
+	}
+})
+
+$('#userInputText').keyup(function(e) {
+	if (e.keyCode === 13) {
+		e.preventDefault()
+	}
+})
+
+$('#userInputSubmit').click(function() {
+	$('#userInput').submit()
+	$('#userInputText').select()
+})
+
+// Language selector.
+$('#languageSelect').click(function() {
+	$('#customServerDialog').hide()
+	$('#languages li').css({'color': '#FFF', 'font-weight': 'normal'})
+	$('[data-locale=' + Cryptocat.locale['language'] + ']').css({
+		'color': '#97CEEC',
+		'font-weight': 'bold'
+	})
+	$('#footer').animate({'height': 190}, function() {
+		$('#languages').fadeIn()
+	})
+	$('#languages li').click(function() {
+		var lang = $(this).attr('data-locale')
+		$('#languages').fadeOut(200, function() {
+			Cryptocat.locale.set(lang, true)
+			Cryptocat.storage.setItem('language', lang)
+			$('#footer').animate({'height': 14})
+		})
+	})
+})
+
+// Login form.
+$('#conversationName').click(function() {
+	$(this).select()
+})
+$('#nickname').click(function() {
+	$(this).select()
+})
+$('#cryptocatLogin').submit(function() {
+	// Don't submit if form is already being processed.
+	if (($('#loginSubmit').attr('readonly') === 'readonly')) {
+		return false
+	}
+	//Check validity of conversation name and nickname.
+	$('#conversationName').val($.trim($('#conversationName').val().toLowerCase()))
+	$('#nickname').val($.trim($('#nickname').val().toLowerCase()))
+	if ($('#conversationName').val() === '') {
+		Cryptocat.loginFail(Cryptocat.locale['loginMessage']['enterConversation'])
+		$('#conversationName').select()
+	}
+	else if (!$('#conversationName').val().match(/^\w{1,20}$/)) {
+		Cryptocat.loginFail(Cryptocat.locale['loginMessage']['conversationAlphanumeric'])
+		$('#conversationName').select()
+	}
+	else if ($('#nickname').val() === '') {
+		Cryptocat.loginFail(Cryptocat.locale['loginMessage']['enterNickname'])
+		$('#nickname').select()
+	}
+	else if (!$('#nickname').val().match(/^\w{1,16}$/)) {
+		Cryptocat.loginFail(Cryptocat.locale['loginMessage']['nicknameAlphanumeric'])
+		$('#nickname').select()
+	}
+	// Prepare keys and connect.
+	else {
+		$('#loginSubmit,#conversationName,#nickname').attr('readonly', 'readonly')
+		Cryptocat.xmpp.showKeyPreparationDialog(function() {
+			Cryptocat.xmpp.connect()
+		})
+	}
+	return false
+})
+
+/*
+-------------------
+KEYBOARD SHORTCUTS
+-------------------
+*/
+
+// Select previous buddy
+Mousetrap.bind('ctrl+1', function() {
+	var prev = $('.currentConversation').prevAll('.buddy')
+	prev.length ? prev[0].click() : $('.buddy').last().click()
+})
+
+// Select next buddy
+Mousetrap.bind('ctrl+2', function() {
+	var next = $('.currentConversation').nextAll('.buddy')
+	next.length ? next[0].click() : $('.buddy').first().click()
+})
+
+// ???
+Mousetrap.bind('up up down down left right left right b a enter', function() {
+	if (Cryptocat.sounds.balloon.loop) {
+		window.clearInterval(Cryptocat.balloon)
+		Cryptocat.sounds.balloon.pause()
+		Cryptocat.sounds.balloon.loop = false
+		return
+	}
+	window.setTimeout(function() {
+		Cryptocat.sounds.balloon.loop = true
+		Cryptocat.sounds.balloon.play()
+	}, 200)
+	Cryptocat.balloon = window.setInterval(function() {
+		$('<img/>').addClass('balloon')
+		.attr('src', 'img/balloon.gif')
+		.appendTo('body')
+		.css({
+			left: Math.round(
+				Math.random() * ($(window).width() - 200) + 100
+			)
+		})
+		.animate(
+			{bottom: '2000'},
+			25000 + Math.round(Math.random() * 8000),
+			'linear',
+			function() {
+				$(this).remove()
+			}
+		)
+	}, 999 + Math.round(Math.random() * 999))
+})
+
+/*
+-------------------
+WINDOW EVENT BINDINGS
+-------------------
+*/
+
+// When the window/tab is not selected, set `windowFocus` to false.
+// `windowFocus` is used to know when to show desktop notifications.
+$(window).blur(function() {
+	Cryptocat.me.windowFocus = false
+})
+
+// On window focus, select text input field automatically if we are chatting.
+// Also set `windowFocus` to true.
+$(window).focus(function() {
+	Cryptocat.me.windowFocus = true
+	Cryptocat.me.newMessages = 0
+	Tinycon.setBubble()
+	if (Cryptocat.me.currentBuddy) {
+		$('#userInputText').focus()
+	}
+})
+
+// Prevent accidental window close.
+$(window).bind('beforeunload', function() {
+	if (Object.keys(Cryptocat.buddies).length > 1) {
+		return Cryptocat.locale['loginMessage']['thankYouUsing']
+	}
+})
+
+// Logout on browser close.
+$(window).unload(function() {
+	if (Cryptocat.xmpp.connection !== null) {
+		Cryptocat.xmpp.connection.disconnect()
+	}
+})
+
+// Determine whether we are showing a top margin
+// Depending on window size
+$(window).resize(function() {
+	if ($(window).height() < 650) {
+		$('#bubble').css('margin-top', '0')
+	}
+	else {
+		$('#bubble').css('margin-top', '2%')
+	}
+})
+$(window).resize()
+
+// Show main window.
+$('#bubble').show()
+
+/*
+$('#bubbleWrapper').css(
+	{
+		height: '538px'
+	}
+).animate(
+	{
+		'width': '+=500px'
+	},
+	700,
+	function() {
+		$('#av').animate(
+			{
+				'width': '480px'
+			},
+			700
+		)
+	}
+)
+*/
+
+})}//:3
diff --git a/src/core/js/etc/catFacts.js b/src/core/js/etc/catFacts.js
new file mode 100755
index 0000000..790a8de
--- /dev/null
+++ b/src/core/js/etc/catFacts.js
@@ -0,0 +1,116 @@
+(function (root, factory) {
+	if (typeof exports === 'object' && exports) {
+		factory(exports) // CommonJS
+	}
+	else {
+		var CatFacts = function() {}
+		factory(CatFacts)
+		root.CatFacts = CatFacts // <script>
+	}
+} (this, function (CatFacts) {
+'use strict';
+
+var lastCatFact
+var interestingFacts = [
+'Cats and humans have identical regions in the brain responsible for emotion.',
+'A cat\'s brain is more similar to a human brain than that of a dog.',
+'Cats can rotate each ear independently from the other in 180 degrees.',
+'Cat hearing stops at 65kHz; human hearing stops at 20kHz.',
+'Cats see about 6 times better than humans at night.',
+'Cats can judge within 3 inches the precise location of a sound being made 1 yard away.',
+'Cats can be right-pawed or left-pawed.',
+'Cats cannot see directly under their nose.',
+'Cats express their present state of mind through their tail.',
+'Cats are the only animals that walk on their toes.',
+'Cats were worshipped as holy in Ancient Egypt and granted great respect in every household.',
+'Phoenician ships likely brought the first domesticated cats to Europe in about 900 BC.',
+'Ancient Egyptians shaved their eyebrows in mourning when the family cat died.',
+'In Siam, a cat rode in a chariot at the head of a parade celebrating the new king.',
+'Most cats adore sardines.',
+'Cats use their whiskers to measure distances and changes in air pressure.',
+'Cat whiskers are very sensitive to touch.',
+'Abraham Lincoln kept four cats at the White House during his presidency.',
+'Cats purr at the same frequency as an idling diesel engine.',
+'Nikola Tesla was inspired to study electricity after his cat static-shocked him in his youth.',
+'Isaac Newton invented the cat-flap.',
+'Cats use their tails for balance and have nearly 30 individual bones in them.',
+'Even though Napoleon was known as a ruthless conqueror, he was always very afraid of cats.',
+'A kitten\'s eyes are always blue at first.',
+'Cats can jump as high as seven times as they are tall.',
+'Kittens begin dreaming at just over one week old.',
+'Every cat\'s nose is unique, and no two nose-prints are the same.',
+'The Pilgrims were the first to introduce cats to North America.',
+'Cats purr to communicate.',
+'A group of kittens is called a kindle.',
+'The British Government owns thousands of cats, deployed in government buildings to get rid of mice.',
+'Cats are more active during the evening hours.',
+'Cats have a field of vision of about 200 degrees.',
+'Cats have the AB blood type, which is also found in humans.',
+'Cats have a homing ability that uses their biological clock, the sun\'s angle and the Earth\'s magnetic field.',
+'Cats taken far from their home can return to it thanks to remarkable homing instincts.',
+'Cats successfully catch mice in about one out of three attempts.',
+'Female cats tend to be right pawed, while male cats are more often left pawed.',
+'The first cat in space was a French cat named Felicette, sent in 1963. She survived the trip.',
+'Cats can travel at a top speed of approximately 31 mph (49 km/h) over a short distance.',
+'Ancient Egyptians worshipped the goddess Bast, who had a woman\'s body and a cat\'s head.',
+'Cats almost never meow at each other, and mostly do so only to communicate with humans.',
+'Approximately one third of cat owners think their pets are able to read their minds.',
+'In Japan, cats are thought to have the power to turn into powerful spirits when they die.',
+'According to Buddhist legend, the body of the cat is the temporary resting place of spiritual people.',
+'Cat hearts beat nearly twice as fast as human hearts.',
+'Cats sweat only through their paws.',
+'When cats are happy or pleased, they momentarily squeeze their eyes shut.',
+'Cryptocat\'s lead developer lives with a cat named Sprite. She is super cute and awesome!',
+'Cats can make over one hundred different vocalizations.',
+'Cats purr at a frequency that promotes bone density and helps with bone healing.',
+'On average, cats spend two-thirds of every day sleeping.',
+'Unlike dogs, cats do not have a sweet tooth. Scientists believe this is due to a mutation in a key taste receptor.',
+'When cats chase their prey, it keeps its head level. Dogs and humans bob their heads up and down.',
+'The technical term for a cat\'s hairball is a bezoar.',
+'A group of cats is called a clowder.',
+'Female cats tend to be right pawed, while male cats are more often left pawed.',
+'Cats cannot climb head first down a tree because their claws are curved the wrong way.',
+'Cats make about 100 different sounds. Dogs make only about 10.',
+'There are more than 500 million domestic cats in the world, with approximately 40 recognized breeds.',
+'The oldest known pet cat was recently found in a 9,500-year-old grave on the Mediterranean island of Cyprus.',
+'Over 30% of households in North America own a cat.',
+'According to Hebrew legend, cats are the result of a Lion\'s sneeze.',
+'Some cats have survived falls of over 65 feet (20 meters), due largely to their righting reflex.',
+'Cats rub against people to mark them as their territory.',
+'The biggest wildcat today is the Siberian Tiger.',
+'The smallest wildcat today is the Black-footed cat.',
+'Many Egyptians worshipped the goddess Bast, who had a woman\'s body and a cat\'s head.',
+'The most popular pedigreed cat is the Persian cat, followed by the Maine Coon cat and the Siamese cat.',
+'Some Siamese cats are cross-eyed to compensate for abnormal optic wiring.',
+'Researchers believe the word tabby comes from Attabiyah, a neighborhood in Baghdad, Iraq.',
+'Tabbies got their name because their coats resembled the wavy patterns in the silk produced in Attabiyah in Iraq.',
+'Cats don\'t like water because their fur does not insulate well when it\'s wet.',
+'The Egyptian Mau is the oldest breed of cat. The breed is so ancient that its name is the Egyptian word for cat.',
+'Cats usually have about 12 whiskers on each side of their face.',
+'Scientists believe grass appears red to cats.',
+'In the original Italian version of Cinderella, the benevolent fairy godmother figure was a cat.',
+'The ability of cats to find their way home is called psi-traveling.',
+'In Japan, cats are thought to have the power to turn into super spirits when they die.',
+'Most cats had short hair until about 100 years ago, until the intervention of cat breeding.',
+'During the nearly 18 hours a day that kittens sleep, an important growth hormone is released. ',
+'One reason that kittens sleep so much is because a growth hormone is released only during sleep.',
+'Cats have about 130,000 hairs per square inch.',
+'Cats typically can live up to 20 years, which is equivalent to about 96 human years.',
+'A commemorative tower was built in Scotland for a cat named Towser, who caught nearly 30,000 mice in her lifetime.',
+'In the 1750s, Europeans introduced cats into the Americas to control pests.',
+'The first cat show was organized in 1871 in London. Cat shows later became a worldwide craze.',
+'The first cartoon cat was Felix the Cat in 1919.',
+'Cat nose pads are ridged with unique patterns, just like the fingerprints of a human.',
+'A 2007 Gallup poll revealed that both men and women were equally likely to own a cat.'
+]
+
+CatFacts.getFact = function() {
+	var catFact
+	do {
+		catFact = Math.floor(Math.random() * interestingFacts.length)
+	} while (lastCatFact === catFact)
+	lastCatFact = catFact
+	return interestingFacts[catFact]
+}
+
+}))
diff --git a/src/core/js/etc/customServers.js b/src/core/js/etc/customServers.js
new file mode 100644
index 0000000..d2c1d9e
--- /dev/null
+++ b/src/core/js/etc/customServers.js
@@ -0,0 +1,146 @@
+$(window).ready(function() {
+'use strict';
+
+var updateCustomServers = function() {
+	var customServers = {}
+	$('#customServerSelector option').each(function() {
+		var name = $(this).val()
+		customServers[name] = {}
+		customServers[name].domain = $(this).attr('data-domain')
+		customServers[name].xmpp = $(this).attr('data-xmpp')
+		customServers[name].relay = $(this).attr('data-relay')
+	})
+	Cryptocat.storage.setItem('customServers', JSON.stringify(customServers))
+}
+
+$('#customServer').click(function() {
+	if (!document.getElementById('customServerSelector').firstChild) {
+		$('#customServerSelector').append(
+			Mustache.render(Cryptocat.templates['customServer'], {
+				name: 'Cryptocat',
+				domain: Cryptocat.xmpp.defaultDomain,
+				XMPP: Cryptocat.xmpp.defaultConferenceServer,
+				Relay: Cryptocat.xmpp.defaultRelay
+			})
+		)
+		$('#customServerSelector').append(
+			Mustache.render(Cryptocat.templates['customServer'], {
+				name: 'Cryptocat (Tor Hidden Service)',
+				domain: Cryptocat.xmpp.defaultDomain,
+				XMPP: Cryptocat.xmpp.defaultConferenceServer,
+				Relay: 'http://catmeow2zuqpkpyw.onion/http-bind'
+			})
+		)
+	}
+	$('#languages').hide()
+	$('#footer').animate({'height': 220}, function() {
+		$('#customServerDialog').fadeIn()
+		$('#customName').val(Cryptocat.serverName)
+		$('#customDomain').val(Cryptocat.xmpp.domain)
+		$('#customConferenceServer').val(Cryptocat.xmpp.conferenceServer)
+		$('#customRelay').val(Cryptocat.xmpp.relay)
+		$('#customServerReset').val(Cryptocat.locale['loginWindow']['reset']).click(function() {
+			$('#customName').val('Cryptocat')
+			$('#customDomain').val(Cryptocat.xmpp.defaultDomain)
+			$('#customConferenceServer').val(Cryptocat.xmpp.defaultConferenceServer)
+			$('#customRelay').val(Cryptocat.xmpp.defaultRelay)
+			Cryptocat.storage.removeItem('serverName')
+			Cryptocat.storage.removeItem('domain')
+			Cryptocat.storage.removeItem('conferenceServer')
+			Cryptocat.storage.removeItem('relay')
+		})
+		$('#customServerSubmit').val(Cryptocat.locale['chatWindow']['cont']).click(function() {
+			$('#customServerDialog').fadeOut(200, function() {
+				$('#footer').animate({'height': 14})
+			})
+			Cryptocat.serverName = $('#customName').val()
+			Cryptocat.xmpp.domain = $('#customDomain').val()
+			Cryptocat.xmpp.conferenceServer = $('#customConferenceServer').val()
+			Cryptocat.xmpp.relay = $('#customRelay').val()
+			Cryptocat.storage.setItem('serverName', Cryptocat.serverName)
+			Cryptocat.storage.setItem('domain', Cryptocat.xmpp.domain)
+			Cryptocat.storage.setItem('conferenceServer', Cryptocat.xmpp.conferenceServer)
+			Cryptocat.storage.setItem('relay', Cryptocat.xmpp.relay)
+		})
+		$('#customDomain').select()
+	})
+})
+
+$('#customServerSave').click(function() {
+	$('#customServerDelete').val('Delete')
+		.attr('data-deleteconfirm', '0')
+		.removeClass('confirm')
+	if ($('#customDomain').val() === Cryptocat.xmpp.defaultDomain) {
+		return // Cannot overwrite the default domain
+	}
+	var serverIsInList = false
+	$('#customServerSelector').children().each(function() {
+		if ($('#customName').val() === $(this).val()) {
+			serverIsInList = true
+			if ($('#customServerSave').attr('data-saveconfirm') !== '1') {
+				$('#customServerSave').val('Overwrite?').attr('data-saveconfirm', '1').addClass('confirm')
+				return
+			}
+			else {
+				$('#customServerSave').val('Save').attr('data-saveconfirm', '0').removeClass('confirm')
+			}
+		}
+	})
+	if (!serverIsInList) {
+		$('#customServerSelector').append(
+			Mustache.render(Cryptocat.templates['customServer'], {
+				name: $('#customName').val(),
+				domain: $('#customDomain').val(),
+				XMPP: $('#customConferenceServer').val(),
+				Relay: $('#customRelay').val()
+			})
+		)
+	}
+	else {
+		$.each($('#customServerSelector option'), function(index, value) {
+			if ($(value).val() === $('#customName').val()) {
+				$(value).attr('data-domain', $('#customDomain').val())
+				$(value).attr('data-relay', $('#customRelay').val())
+				$(value).attr('data-xmpp', $('#customConferenceServer').val())
+			}
+		})
+	}
+	updateCustomServers()
+})
+
+$('#customServerDelete').click(function() {
+	$('#customServerSave').val('Save').attr('data-saveconfirm', '0').removeClass('confirm')
+	if ($('#customServerDelete').attr('data-deleteconfirm') === '1') {
+		$.each($('#customServerSelector option'), function(index, value) {
+			if ($(value).val() === $('#customName').val()) {
+				$(value).remove()
+			}
+		})
+		updateCustomServers()
+		$('#customServerDelete').val('Delete').attr('data-deleteconfirm', '0').removeClass('confirm')
+	}
+	else {
+		$('#customServerDelete').val('Are you sure?').attr('data-deleteconfirm', '1').addClass('confirm')
+	}
+})
+
+$('#customServerSelector').change(function() {
+	$('#customServerDelete').val('Delete')
+		.attr('data-deleteconfirm', '0')
+		.removeClass('confirm')
+		.removeAttr('disabled')
+		.removeClass('disabled')
+	$('#customServerSave').val('Save')
+		.attr('data-saveconfirm', '0')
+		.removeClass('confirm')
+	var selectedOption = $(this).find(':selected')
+	if ($(selectedOption).attr('data-domain') === Cryptocat.xmpp.defaultDomain) {
+		$('#customServerDelete').attr('disabled', 'disabled').addClass('disabled')
+	}
+	$('#customName').val($(selectedOption).val())
+	$('#customDomain').val($(selectedOption).attr('data-domain'))
+	$('#customConferenceServer').val($(selectedOption).attr('data-xmpp'))
+	$('#customRelay').val($(selectedOption).attr('data-relay'))
+})
+
+})
diff --git a/src/core/js/etc/facebook.js b/src/core/js/etc/facebook.js
new file mode 100644
index 0000000..bb817c6
--- /dev/null
+++ b/src/core/js/etc/facebook.js
@@ -0,0 +1,578 @@
+$(window).load(function() {
+'use strict';
+
+Cryptocat.FB                = {}
+Cryptocat.FB.userID         = null
+Cryptocat.FB.accessToken    = null
+Cryptocat.FB.statusInterval = null
+Cryptocat.FB.authInterval   = null
+Cryptocat.FB.authID         = (function() {
+	var id = ''
+	while (id.length < 77) { // 2^256 ~= 10^77
+		id += Cryptocat.random.decimal()
+	}
+	return id
+})()
+Cryptocat.storage.getItem('fbAccessToken', function(accessToken) {
+	Cryptocat.FB.accessToken = accessToken
+})
+
+/*
+-------------------
+CRYPTOCAT INTEGRATION FUNCTIONS
+-------------------
+*/
+
+Cryptocat.FB.prepareLogin = function(accessToken) {
+	$.get(
+		'https://graph.facebook.com/me/',
+		{
+			'access_token': accessToken
+		},
+		function(id) {
+			Cryptocat.storage.setItem('fbAccessToken', accessToken)
+			Cryptocat.FB.accessToken = accessToken
+			Cryptocat.FB.userID      = id.id
+			Cryptocat.me.nickname    = id.name
+			$.get('https://outbound.crypto.cat/facebook/', {
+				'setuser': Cryptocat.FB.userID
+			})
+			document.title = '[' + id.name + '] Cryptocat'
+			$('.conversationName').text(id.name)
+			$('#facebookConnect').attr('readonly', 'readonly')
+			Cryptocat.xmpp.showKeyPreparationDialog(function() {
+				Cryptocat.FB.verifyLogin()
+			})
+		}
+	)
+}
+
+Cryptocat.FB.verifyLogin = function() {
+	if (
+		Cryptocat.FB.userID.match(/^\d+$/) &&
+		Cryptocat.FB.accessToken.match(/^\w+$/)
+	) {
+		Cryptocat.xmpp.connection = new Strophe.Connection('https://outbound.crypto.cat/http-bind/')
+		Cryptocat.xmpp.connection.facebookConnect(
+			Cryptocat.FB.userID + '@chat.facebook.com/cryptocat',
+			Cryptocat.FB.onConnect,
+			60,
+			1,
+			null,
+			Cryptocat.FB.accessToken
+		)
+	}
+}
+
+Cryptocat.FB.onConnect = function(status) {
+	/*
+	if (status === Strophe.Status.CONNECTING) {
+	}
+	if (status === Strophe.Status.CONNFAIL) {
+	}
+	if (status === Strophe.Status.DISCONNECTING) {
+	}
+	if (status === Strophe.Status.DISCONNECTED) {
+	}
+	*/
+	if (status === Strophe.Status.CONNECTED) {
+		Cryptocat.FB.onConnected()
+		Cryptocat.xmpp.connection.addHandler(
+			Cryptocat.FB.onMessage,
+			null,
+			'message',
+			null,
+			null,
+			null
+		)
+		Cryptocat.xmpp.connection.addHandler(
+			Cryptocat.FB.onPresence,
+			null,
+			'presence',
+			null,
+			null,
+			null
+		)
+	}
+}
+
+Cryptocat.FB.onConnected = function() {
+	// Do the regular onConnected UI shabang...
+	Cryptocat.xmpp.onConnected()
+	// Then do some special shwaza for Facebook.
+	Cryptocat.xmpp.connection.send($pres().tree())
+	Cryptocat.FB.getStatuses()
+	Cryptocat.FB.statusInterval = setInterval(function() {
+		Cryptocat.FB.getStatuses()
+	}, 12345)
+}
+
+Cryptocat.FB.onMessage = function(message) {
+	var from     = message.getAttribute('from').match(/\d+/)[0]
+	var type     = message.getAttribute('type')
+	var elements = message.getElementsByTagName('body')
+	if (
+		(type === 'chat') &&
+		(elements.length > 0)
+	) {
+		var body = elements[0]
+		var nickname = Cryptocat.getBuddyNicknameByID(from)
+		Cryptocat.buddies[nickname].otr.receiveMsg(Strophe.getText(body))
+	}
+	return true
+}
+
+Cryptocat.FB.onPresence = function() {
+	return true
+}
+
+Cryptocat.FB.getStatuses = function() {
+	var query = 'SELECT uid, name, online_presence, status '
+		+ 'FROM user WHERE uid IN ( SELECT uid2 FROM friend '
+		+ 'WHERE uid1 = me())'
+	$.get(
+		'https://graph.facebook.com/fql',
+		{
+			'q':            query,
+			'access_token': Cryptocat.FB.accessToken,
+			'method':       'GET'
+		},
+		function(data) {
+			var statuses = data.data
+			for (var i in statuses) {
+				if (statuses.hasOwnProperty(i)) {
+					Cryptocat.FB.handleStatus(statuses[i])
+				}
+			}
+		}
+	)
+}
+
+Cryptocat.FB.handleStatus = function(status) {
+	var presence = status['online_presence']
+	if (presence === 'offline') {
+		if (Cryptocat.buddies.hasOwnProperty(status.name)) {
+			Cryptocat.removeBuddy(status.name)
+		}
+	}
+	else if (/^(active|idle)$/.test(presence)) {
+		$.get(
+			'https://outbound.crypto.cat/facebook/',
+			{
+				user: status.uid
+			},
+			function(data) {
+				if (!data.match(/^(online)|(away)$/)) { return }
+				if (!Cryptocat.buddies.hasOwnProperty(status.name)) {
+					Cryptocat.addBuddy(status.name, status.uid, data)
+				}
+				else {
+					Cryptocat.buddyStatus(status.name, data)
+				}
+				if (data === 'online') {
+					$('#buddy-' + status.uid).find('.loginTypeIcon')
+						.removeClass('notUsingCryptocat')
+					$('#buddy-' + status.uid).find('.buddyMenu').show()
+					if (Cryptocat.me.currentBuddy === Cryptocat.buddies[status.name].id) {
+						$('#encryptionStatus').html(
+							Mustache.render(Cryptocat.templates.encryptionStatus, {
+								conversationStatus: Cryptocat.locale.login.conversationStatus,
+								styling: 'encrypted',
+								encryptionStatus: Cryptocat.locale.login.encrypted
+							})
+						)
+					}
+					Cryptocat.buddies[status.name].usingCryptocat         = true
+					Cryptocat.buddies[status.name].otr.REQUIRE_ENCRYPTION = true
+				}
+				else {
+					$('#buddy-' + status.uid).find('.loginTypeIcon')
+						.addClass('notUsingCryptocat')
+					$('#buddy-' + status.uid).find('.buddyMenu').hide()
+					if (Cryptocat.me.currentBuddy === Cryptocat.buddies[status.name].id) {
+						$('#encryptionStatus').html(
+							Mustache.render(Cryptocat.templates.encryptionStatus, {
+								conversationStatus: Cryptocat.locale.login.conversationStatus,
+								styling: 'notEncrypted',
+								encryptionStatus: Cryptocat.locale.login.notEncrypted
+							})
+						)
+					}
+					Cryptocat.buddies[status.name].usingCryptocat         = false
+					Cryptocat.buddies[status.name].otr.REQUIRE_ENCRYPTION = false
+				}
+			}
+		)
+	}
+}
+
+/*
+-------------------
+USER INTERFACE BINDINGS
+-------------------
+*/
+
+// Tabs for selecting login mode.
+$('#loginTabs span').click(function() {
+	Cryptocat.me.login = $(this).attr('data-login')
+	$('#loginTabs span').attr('data-selected', false)
+	$(this).attr('data-selected', 'true')
+	$('.loginForm').hide()
+	if (Cryptocat.me.login === 'cryptocat') {
+		Cryptocat.storage.setItem('login', 'cryptocat')
+		$('#cryptocatLogin').show()
+	}
+	if (Cryptocat.me.login === 'facebook') {
+		Cryptocat.storage.setItem('login', 'facebook')
+		$('#facebookLogin').show()
+	}
+})
+
+Cryptocat.storage.getItem('login', function(login) {
+	if (login === 'facebook') {
+		$('[data-login=facebook]').click()
+	}
+	else {
+		$('[data-login=cryptocat]').click()
+	}
+})
+
+// Launch Facebook authentication page
+$('#facebookConnect').click(function() {
+	clearInterval(Cryptocat.FB.authInterval)
+	if (!Cryptocat.FB.accessToken) {
+		var authURL = Mustache.render(
+			Cryptocat.templates.facebookAuthURL,
+			{
+				scope:  'xmpp_login,friends_online_presence',
+				appID:  '1430498997197900',
+				authID: Cryptocat.FB.authID
+			}
+		)
+		if (navigator.userAgent === 'Chrome (Mac app)') {
+			window.open(authURL)
+		}
+		else {
+			window.open(
+				authURL,
+				'',
+				'width=500px,height=300,top='
+				+ ((screen.height / 2.6) - (300 / 2))
+				+ ',left=' + ((screen.width / 2.05) - (500 / 2))
+			)
+		}
+		Cryptocat.FB.authInterval = setInterval(function() {
+			$.get(
+				'https://outbound.crypto.cat/facebook/',
+				{
+					'id': Cryptocat.FB.authID
+				},
+				function(data) {
+					if (data.match(/(\w|\-){32,512}/)) {
+						clearInterval(Cryptocat.FB.authInterval)
+						Cryptocat.FB.prepareLogin(data)
+					}
+				}
+			)
+		}, 1000)
+	}
+	else {
+		$.ajax({
+			type: 'GET',
+			url: 'https://graph.facebook.com/me/',
+			data: {
+				'access_token': Cryptocat.FB.accessToken
+			},
+			success: function() {
+				Cryptocat.FB.prepareLogin(Cryptocat.FB.accessToken)
+			},
+			error: function() {
+				Cryptocat.FB.accessToken = null
+				Cryptocat.storage.removeItem('fbAccessToken')
+				$('#facebookConnect').click()
+			}
+		})
+	}
+})
+
+/*
+-------------------
+XMPP-RELATED FUNCTIONS
+-------------------
+*/
+
+// Most of code is from (https://github.com/javierfigueroa/turedsocial).
+// MIT license.
+
+/* jshint -W106 */
+
+/**
+ * Split a string by string
+ * @param delimiter string The boundary string.
+ * @param string string The input string.
+ * @param limit int[optional] If limit is set and positive, the returned array will contain
+ * 		a maximum of limit elements with the last
+ * 		element containing the rest of string.
+ *
+ * 		If the limit parameter is negative, all components
+ * 		except the last -limit are returned.
+ *
+ * 		If the limit parameter is zero, then this is treated as 1.
+ *
+ * @returns array If delimiter is an empty string (''),
+ * 		explode will return false.
+ * 		If delimiter contains a value that is not
+ * 		contained in string and a negative
+ * 		limit is used, then an empty array will be
+ * 		returned. For any other limit, an array containing
+ * 		string will be returned.
+ */
+var explode = function(delimiter, string, limit) {
+	var emptyArray = { 0: '' }
+	// third argument is not required
+	if ( arguments.length < 2 ||
+		typeof arguments[0] === 'undefined' || typeof arguments[1] === 'undefined' ) {
+		return null
+	}
+	if ( delimiter === '' || delimiter === false ||
+		delimiter === null ) {
+		return false
+	}
+	if ( typeof delimiter === 'function' || typeof delimiter === 'object' ||
+		typeof string === 'function' || typeof string === 'object' ) {
+			return emptyArray
+	}
+	if ( delimiter === true ) {
+		delimiter = '1'
+	}
+	if (!limit) {
+		return string.toString().split(delimiter.toString())
+	}
+	else {
+		// support for limit argument
+		var splitted = string.toString().split(delimiter.toString())
+		var partA = splitted.splice(0, limit - 1)
+		var partB = splitted.join(delimiter.toString())
+		partA.push(partB)
+		return partA
+	}
+}
+
+/**
+ *  Handler for X-FACEBOOK-PLATFORM SASL authentication.
+ *
+ *  @param (XMLElement) elem - The challenge stanza.
+ *
+ *  @returns false to remove the handler.
+ */
+
+Strophe.Connection.prototype._sasl_challenge1_fb = function(elem) {
+	/* jshint -W106 */
+	var challenge = Base64.decode(Strophe.getText(elem))
+	var nonce     = ''
+	var method    = ''
+	var version   = ''
+	// remove unneeded handlers
+	this.deleteHandler(this._sasl_failure_handler)
+	var challenges = explode('&', challenge)
+	for(var i=0; i < challenges.length; i++) {
+		var map = explode('=', challenges[i])
+		switch (map[0]) {
+			case 'nonce':
+				nonce = map[1]
+				break
+			case 'method':
+				method = map[1]
+				break
+			case 'version':
+				version = map[1]
+				break
+	  }
+	}
+	var responseText = ''
+	responseText += 'api_key=' + this.apiKey
+	responseText += '&call_id=' + (Math.floor(new Date().getTime()/1000))
+	responseText += '&method=' + method
+	responseText += '&nonce=' + nonce
+	responseText += '&access_token=' + this.accessToken
+	responseText += '&v=' + '1.0'
+	this._sasl_challenge_handler = this._addSysHandler(
+		this._sasl_digest_challenge2_cb.bind(this), null,
+		'challenge', null, null)
+	this._sasl_success_handler = this._addSysHandler(
+		this._sasl_success_cb.bind(this), null,
+		'success', null, null)
+	this._sasl_failure_handler = this._addSysHandler(
+		this._sasl_failure_cb.bind(this), null,
+		'failure', null, null)
+	this.send($build('response', {
+		xmlns: Strophe.NS.SASL
+	}).t(Base64.encode(responseText)).tree())
+	return false
+}
+
+/**
+ *  Handler for initial connection request with Facebokk.
+ *
+ *  This handler is used to process the initial connection request
+ *  response from the BOSH server. It is used to set up authentication
+ *  handlers and start the authentication process.
+ *
+ *  SASL authentication will be attempted if available, otherwise
+ *  the code will fall back to legacy authentication.
+ *
+ *  @param (Strophe.Request) req - The current request.
+ */
+Strophe.Connection.prototype._connect_fb = function (req) {
+	/* jshint -W106 */
+	Strophe.info('_connect_fb was called')
+
+	this.connected = true
+	var bodyWrap = req.getResponse()
+	if (!bodyWrap) { return }
+
+	this.xmlInput(bodyWrap)
+	this.rawInput(Strophe.serialize(bodyWrap))
+
+	var typ = bodyWrap.getAttribute('type')
+	var cond, conflict
+	if (typ !== null && typ === 'terminate') {
+		// an error occurred
+		cond = bodyWrap.getAttribute('condition')
+		conflict = bodyWrap.getElementsByTagName('conflict')
+		if (cond !== null) {
+			if (cond === 'remote-stream-error' && conflict.length > 0) {
+				cond = 'conflict'
+			}
+			this._changeConnectStatus(Strophe.Status.CONNFAIL, cond)
+		} else {
+			this._changeConnectStatus(Strophe.Status.CONNFAIL, 'unknown')
+		}
+		return
+	}
+
+	// check to make sure we don't overwrite these if _connect_fb is
+	// called multiple times in the case of missing stream:features
+	if (!this.sid) {
+		this.sid = bodyWrap.getAttribute('sid')
+	}
+	if (!this.stream_id) {
+		this.stream_id = bodyWrap.getAttribute('authid')
+	}
+	var wind = bodyWrap.getAttribute('requests')
+	if (wind) { this.window = wind }
+	var hold = bodyWrap.getAttribute('hold')
+	if (hold) { this.hold = hold }
+	var wait = bodyWrap.getAttribute('wait')
+	if (wait) { this.wait = wait }
+
+	var mechanisms = bodyWrap.getElementsByTagName('mechanism')
+	var i, mech, xfacebook
+	if (mechanisms.length === 0) {
+		// we didn't get stream:features yet, so we need wait for it
+		// by sending a blank poll request
+		var body = this._buildBody()
+		this._requests.push(
+			new Strophe.Request(
+				body.tree(),
+				this._onRequestStateChange.bind(
+					this, this._connect_fb.bind(this)
+				),
+				body.tree().getAttribute('rid')
+			)
+		)
+		this._throttledRequestHandler()
+		return
+	}
+	else {
+		for (i = 0; i < mechanisms.length; i++) {
+			mech = Strophe.getText(mechanisms[i])
+			if (mech === 'X-FACEBOOK-PLATFORM') {
+				xfacebook = true
+				break
+			}
+		}
+	}
+	if (!xfacebook)	{
+		return
+	}
+	this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null)
+	this._sasl_challenge_handler = this._addSysHandler(
+		this._sasl_challenge1_fb.bind(this), null,
+		'challenge', null, null)
+	this._sasl_failure_handler = this._addSysHandler(
+		this._sasl_challenge1_fb.bind(this), null,
+		'failure', null, null)
+
+	this.send($build('auth', {
+		xmlns:     Strophe.NS.SASL,
+		mechanism: 'X-FACEBOOK-PLATFORM'
+	}).tree())
+}
+
+/**
+ *  Starts the connection process with facebok XMPP Chat Server.
+ *
+ *  As the connection process proceeds, the user supplied callback will
+ *  be triggered multiple times with status updates.  The callback
+ *  should take two arguments - the status code and the error condition.
+ *
+ *  The status code will be one of the values in the Strophe.Status
+ *  constants.  The error condition will be one of the conditions
+ *  defined in RFC 3920 or the condition 'strophe-parsererror'.
+ *
+ *  Please see XEP 124 for a more detailed explanation of the optional
+ *  parameters below.
+ *
+ *  @param (String) jid - The user's JID. It must be facebookid at chat.facebook.com,
+ *      where facebook id is the number id of the facebook profile
+ *  @param (Function) callback The connect callback function.
+ *  @param (Integer) wait - The optional HTTPBIND wait value.  This is the
+ *      time the server will wait before returning an empty result for
+ *      a request.  The default setting of 60 seconds is recommended.
+ *      Other settings will require tweaks to the Strophe.TIMEOUT value.
+ *  @param (Integer) hold - The optional HTTPBIND hold value.  This is the
+ *      number of connections the server will hold at one time.  This
+ *      should almost always be set to 1 (the default).
+ *  @param apiKey The API key of our Facebook Application
+ *  @param sessionKey The actual session key for the user who we are attempting to log in
+ */
+Strophe.Connection.prototype.facebookConnect = function(jid, callback, wait, hold, apiKey, accessToken) {
+	/* jshint -W106 */
+	this.jid = jid
+	this.connect_callback = callback
+	this.disconnecting = false
+	this.connected = false
+	this.authenticated = false
+	this.errors = 0
+	this.apiKey = apiKey
+	this.accessToken = accessToken
+	this.wait = wait || this.wait
+	this.hold = hold || this.hold
+	// parse jid for domain and resource
+	this.domain = Strophe.getDomainFromJid(this.jid)
+	// build the body tag
+	var body = this._buildBody().attrs({
+		to: this.domain,
+		'xml:lang': 'en',
+		wait: this.wait,
+		hold: this.hold,
+		content: 'text/xml; charset=utf-8',
+		ver: '1.6',
+		'xmpp:version': '1.0',
+		'xmlns:xmpp': Strophe.NS.BOSH
+	})
+	this._changeConnectStatus(Strophe.Status.CONNECTING, null)
+	this._requests.push(
+		new Strophe.Request(
+			body.tree(),
+			this._onRequestStateChange.bind(
+				this, this._connect_fb.bind(this)
+			),
+			body.tree().getAttribute('rid')
+		)
+	)
+	this._throttledRequestHandler()
+}
+
+})
\ No newline at end of file
diff --git a/src/core/js/etc/fileTransfer.js b/src/core/js/etc/fileTransfer.js
new file mode 100644
index 0000000..a7b3b88
--- /dev/null
+++ b/src/core/js/etc/fileTransfer.js
@@ -0,0 +1,276 @@
+$(window).ready(function() {
+'use strict';
+
+// Maximum encrypted file sharing size, in kilobytes.
+Cryptocat.otr.maximumFileSize = 5120
+
+// Size in which file chunks are split, in bytes.
+Cryptocat.otr.chunkSize = 64511
+
+// Safari compatibility
+window.URL = window.URL || window.webkitURL
+
+var files = {}
+var rcvFile = {}
+var fileMIME = new RegExp('^(image\/(png|jpeg|gif))|(application\/((x-compressed)|(x-zip-compressed)|(zip)'
+	+ '|(x-zip)|(octet-stream)|(x-compress)))|(multipart/x-zip)$'
+)
+
+var cn = function(to) {
+	if (Cryptocat.me.login === 'cryptocat') {
+		return Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer + '/' + to
+	}
+	if (Cryptocat.me.login === 'facebook') {
+		return '-' + Cryptocat.buddies[to].id + '@chat.facebook.com'
+	}
+}
+
+Cryptocat.otr.beginSendFile = function(data) {
+	if (!data.file.type.match(fileMIME)) {
+		$('#fileInfoField').text(Cryptocat.locale['chatWindow']['fileTypeError'])
+		return
+	}
+	else if (data.file.size > (Cryptocat.otr.maximumFileSize * 1024)) {
+		$('#fileInfoField').text(Cryptocat.locale['chatWindow']['fileSizeError'])
+		return
+	}
+	else {
+		window.setTimeout(function() {
+			$('#dialogBoxClose').click()
+		}, 500)
+	}
+	var sid = Cryptocat.xmpp.connection.getUniqueId()
+	files[sid] = {
+		to: data.to,
+		position: 0,
+		file: data.file,
+		key: data.key,
+		total: Math.ceil(data.file.size / Cryptocat.otr.chunkSize),
+		ctr: -1
+	}
+	/* jshint -W106 */
+	Cryptocat.xmpp.connection.si_filetransfer.send(
+	/* jshint +W106 */
+		cn(data.to),
+		sid,
+		data.filename,
+		data.file.size,
+		data.file.type,
+		function(err) {
+			if (err) {
+				return console.log(err)
+			}
+			Cryptocat.xmpp.connection.ibb.open(cn(data.to), sid, Cryptocat.otr.chunkSize, function(err) {
+				if (err) {
+					return console.log(err)
+				}
+				Cryptocat.addToConversation(sid, Cryptocat.me.nickname, Cryptocat.buddies[data.to].id, 'file')
+				Cryptocat.otr.sendFileData({
+					start: true,
+					to: data.to,
+					sid: sid
+				})
+			})
+		}
+	)
+}
+
+Cryptocat.otr.sendFileData = function(data) {
+	var sid = data.sid
+	var seq = data.start ? 0 : parseInt(data.seq, 10) + 1
+	if (seq > 65535) {
+		seq = 0
+	}
+	if (files[sid].position > files[sid].file.size) {
+		Cryptocat.xmpp.connection.ibb.close(cn(data.to), sid, function(err) {
+			if (err) {
+				return console.log(err)
+			}
+		})
+		Cryptocat.updateFileProgressBar(sid, files[sid].ctr + 1, files[sid].file.size, data.to)
+		return
+	}
+	// Split into chunk
+	var end = files[sid].position + Cryptocat.otr.chunkSize
+	// Check for slice function on file
+	var sliceStr = files[sid].file.slice ? 'slice' : 'webkitSlice'
+	var chunk = files[sid].file[sliceStr](files[sid].position, end)
+	files[sid].position = end
+	files[sid].ctr += 1
+	var reader = new FileReader()
+	reader.onload = function(event) {
+		var msg = event.target.result
+		// Remove dataURL header
+		msg = msg.split(',')[1]
+		// Encrypt
+		// don't use seq as a counter
+		// it repeats after 65535 above
+		var opts = {
+			mode: CryptoJS.mode.CTR,
+			iv: CryptoJS.enc.Latin1.parse(OTR.HLP.packCtr(files[sid].ctr)),
+			padding: CryptoJS.pad.NoPadding
+		}
+		var aesctr = CryptoJS.AES.encrypt (
+			CryptoJS.enc.Base64.parse(msg),
+			CryptoJS.enc.Latin1.parse(files[sid].key.encryptKey),
+			opts
+		)
+		msg = aesctr.toString()
+		// Then mac
+		var prefix = OTR.HLP.packBytes(files[sid].ctr, 8)
+		prefix += OTR.HLP.packBytes(files[sid].total, 8)
+		var mac = CryptoJS.HmacSHA512(
+			CryptoJS.enc.Base64.parse(prefix + msg),
+			CryptoJS.enc.Latin1.parse(files[sid].key.macKey)
+		)
+		// Combine ciphertext and mac, then transfer chunk
+		msg += mac.toString(CryptoJS.enc.Base64)
+		Cryptocat.xmpp.connection.ibb.data(cn(data.to), sid, seq, msg, function(err) {
+			if (err) {
+				return console.log(err)
+			}
+			Cryptocat.otr.sendFileData({
+				seq: seq,
+				to: data.to,
+				sid: sid
+			})
+		})
+		Cryptocat.updateFileProgressBar(sid, files[sid].ctr + 1, files[sid].file.size, data.to)
+	}
+	reader.readAsDataURL(chunk)
+}
+
+Cryptocat.otr.ibbHandler = function(type, from, sid, data, seq) {
+	var nick = from.split('/')[1]
+	switch (type) {
+		case 'open':
+			var file = rcvFile[from][sid].filename
+			rcvFile[from][sid].key = Cryptocat.buddies[nick].fileKey[file]
+			if (sid.match(/^\w{1,64}$/) && rcvFile[from][sid].mime.match(fileMIME)) {
+				Cryptocat.addToConversation(sid, nick, Cryptocat.buddies[nick].id, 'file')
+			}
+			delete Cryptocat.buddies[nick].fileKey[file]
+			break
+		case 'data':
+			if (rcvFile[from][sid].abort) {
+				return
+			}
+			if (rcvFile[from][sid].ctr > rcvFile[from][sid].total - 1) {
+				rcvFile[from][sid].abort = true
+				Cryptocat.fileTransferError(sid, nick)
+				return
+			}
+			rcvFile[from][sid].seq = seq
+			var key = rcvFile[from][sid].key
+			var ss = data.length - 88
+			var msg = data.substring(0, ss)
+			var mac = data.substring(ss)
+			var prefix = OTR.HLP.packBytes(rcvFile[from][sid].ctr, 8)
+			prefix += OTR.HLP.packBytes(rcvFile[from][sid].total, 8)
+			var cmac = CryptoJS.HmacSHA512(
+				CryptoJS.enc.Base64.parse(prefix + msg),
+				CryptoJS.enc.Latin1.parse(key.macKey)
+			)
+			if (
+				!OTR.HLP.compare(mac, cmac.toString(CryptoJS.enc.Base64))
+			) {
+				rcvFile[from][sid].abort = true
+				Cryptocat.fileTransferError(sid, nick)
+				console.log('OTR file transfer: MACs do not match.')
+				return
+			}
+			var opts = {
+				mode: CryptoJS.mode.CTR,
+				iv: CryptoJS.enc.Latin1.parse(OTR.HLP.packCtr(rcvFile[from][sid].ctr)),
+				padding: CryptoJS.pad.NoPadding
+			}
+			msg = CryptoJS.AES.decrypt(msg, CryptoJS.enc.Latin1.parse(key.encryptKey), opts)
+			rcvFile[from][sid].data += (msg.toString(CryptoJS.enc.Latin1))
+			rcvFile[from][sid].ctr += 1
+			Cryptocat.updateFileProgressBar(sid, rcvFile[from][sid].ctr, rcvFile[from][sid].size, nick)
+			break
+		case 'close':
+			if (!rcvFile[from][sid].abort && rcvFile[from][sid].total === rcvFile[from][sid].ctr) {
+				var url
+				if (navigator.userAgent === 'Chrome (Mac app)' ||
+				(navigator.userAgent.match('Safari') && !navigator.userAgent.match('Chrome'))) {
+					// Safari older than 6.0.5 can only support 128kb
+					if (navigator.userAgent !== 'Chrome (Mac app)' &&
+					!matchSafariVersion([6, 0, 5]) &&
+					rcvFile[from][sid].size >= 131072) {
+						Cryptocat.fileTransferError(sid, nick)
+						console.log('File size is too large for this version of Safari')
+						;delete rcvFile[from][sid]
+						return
+					}
+					url = 'data:application/octet-stream;base64,' +
+						CryptoJS.enc.Latin1
+							.parse(rcvFile[from][sid].data)
+							.toString(CryptoJS.enc.Base64)
+				}
+				else {
+					// Convert data to blob
+					var ia = new Uint8Array(rcvFile[from][sid].data.length)
+					for (var i = 0; i < rcvFile[from][sid].data.length; i++) {
+						ia[i] = rcvFile[from][sid].data.charCodeAt(i)
+					}
+					var blob = new Blob([ia], { type: rcvFile[from][sid].mime })
+					url = window.URL.createObjectURL(blob)
+				}
+				if (rcvFile[from][sid].filename.match(/^[\w.\-]+$/)
+				&& rcvFile[from][sid].mime.match(fileMIME)) {
+					Cryptocat.addFile(url, sid, nick, rcvFile[from][sid].filename)
+				}
+				else {
+					Cryptocat.fileTransferError(sid, nick)
+					console.log('Received file of unallowed file type ' +
+						rcvFile[from][sid].mime + ' from ' + nick)
+				}
+			}
+			delete rcvFile[from][sid]
+			break
+	}
+}
+
+Cryptocat.otr.fileHandler = function(from, sid, filename, size, mime) {
+	if (!rcvFile[from]) {
+		rcvFile[from] = {}
+	}
+	rcvFile[from][sid] = {
+		filename: filename,
+		size: size,
+		mime: mime,
+		seq: 0,
+		ctr: 0,
+		total: Math.ceil(size / Cryptocat.otr.chunkSize),
+		abort: false,
+		data: ''
+	}
+}
+
+// make sure current Safari is at least <version>
+function matchSafariVersion(version) {
+	var match = navigator.userAgent.match(/\bversion\/(\d+)\.(\d+)\.(\d+)/i)
+	if (match == null) {
+		return false
+	}
+	match = match.slice(1).map(function (i) {
+		return parseInt(i, 10)
+	})
+	function ver(arr, pos) {
+		if (arr[pos] > version[pos]) {
+			return true
+		}
+		if (arr[pos] === version[pos]) {
+			pos += 1
+			if (pos === version.length) {
+				return true
+			}
+			return ver(arr, pos)
+		}
+		return false
+	}
+	return ver(match, 0)
+}
+
+})
diff --git a/src/core/js/etc/firstRun.js b/src/core/js/etc/firstRun.js
new file mode 100644
index 0000000..9f7d715
--- /dev/null
+++ b/src/core/js/etc/firstRun.js
@@ -0,0 +1,27 @@
+$(window).ready(function() {
+'use strict';
+
+var detectBrowser = function() {
+	if (navigator.userAgent.match('OPR')) {
+		return 'Opera'
+	}
+	if (navigator.userAgent.match('Chrome')) {
+		return 'Chrome'
+	}
+	if (navigator.userAgent.match('Firefox')) {
+		return 'Firefox'
+	}
+	if (navigator.userAgent.match('MSIE')) {
+		return 'Internet Explorer'
+	}
+	return 'Safari'
+}
+
+var showInstructions = function(browser) {
+	$('.browser').text(browser)
+	$('.instructions[data-browser=' + browser + ']').show()
+}
+
+showInstructions(detectBrowser())
+
+})
diff --git a/src/core/js/etc/locale.js b/src/core/js/etc/locale.js
new file mode 100755
index 0000000..ca33fda
--- /dev/null
+++ b/src/core/js/etc/locale.js
@@ -0,0 +1,239 @@
+(function() {
+'use strict';
+
+Cryptocat.locale = {}
+var languageObject
+
+// Get locale file, call other functions
+Cryptocat.locale.set = function(locale, refresh) {
+	locale = Cryptocat.locale.handleAliases(locale.toLowerCase())
+	$.ajax({
+		url : 'locale/' + locale + '.txt',
+		dataType: 'text',
+		accepts: 'text/html',
+		contentType: 'text/html',
+		complete: function(data) {
+			try {
+				var language = data.responseText.split('\n')
+				if (language.length < 5) { // data too small, dismiss
+					Cryptocat.locale.set('en', true)
+					return false
+				}
+				for (var i in language) {
+					if (language.hasOwnProperty(i)) {
+						language[i] = $.trim(language[i])
+					}
+				}
+				languageObject = Cryptocat.locale.buildObject(locale, language)
+				if (refresh) {
+					Cryptocat.locale.refresh(languageObject)
+				}
+			}
+			catch(err) {
+				Cryptocat.locale.set('en', true)
+			}
+		},
+		error: function() {
+			Cryptocat.locale.set('en', true)
+		}
+	})
+}
+
+// Build and deliver language object
+Cryptocat.locale.buildObject = function(locale, language) {
+	var i = 0
+	languageObject = {
+		language:                     locale,
+		direction:                    language[i++],
+		fonts:                        language[i++],
+		loginWindow: {
+			introHeader:              language[i++],
+			introParagraph:           language[i++],
+			blog:                     language[i++],
+			customServer:             language[i++],
+			reset:                    language[i++],
+			conversationName:         language[i++],
+			nickname:                 language[i++],
+			connect:                  language[i++],
+			conversationNameTooltip:  language[i++],
+			enterConversation:        language[i++]
+		},
+		loginMessage: {
+			enterConversation:        language[i++],
+			conversationAlphanumeric: language[i++],
+			enterNickname:            language[i++],
+			nicknameAlphanumeric:     language[i++],
+			nicknameInUse:            language[i++],
+			authenticationFailure:    language[i++],
+			connectionFailed:         language[i++],
+			thankYouUsing:            language[i++],
+			registering:              language[i++],
+			connecting:               language[i++],
+			connected:                language[i++],
+			typeRandomly:             language[i++],
+			generatingKeys:           language[i++]
+		},
+		chatWindow: {
+			groupConversation:        language[i++],
+			otrFingerprint:           language[i++],
+			groupFingerprint:         language[i++],
+			resetKeys:                language[i++],
+			resetKeysWarn:            language[i++],
+			cont:                     language[i++],
+			statusAvailable:          language[i++],
+			statusAway:               language[i++],
+			myInfo:                   language[i++],
+			desktopNotificationsOn:   language[i++],
+			desktopNotificationsOff:  language[i++],
+			audioNotificationsOn:     language[i++],
+			audioNotificationsOff:    language[i++],
+			rememberNickname:         language[i++],
+			doNotRememberNickname:    language[i++],
+			logout:                   language[i++],
+			displayInfo:              language[i++],
+			sendEncryptedFile:        language[i++],
+			viewImage:                language[i++],
+			downloadFile:             language[i++],
+			conversation:             language[i++],
+			fileTransferInfo:         language[i++],
+			fileTypeError:            language[i++],
+			fileSizeError:            language[i++],
+			startVideoChat:           language[i++],
+			endVideoChat:             language[i++],
+			videoChatQuery:           language[i++],
+			cancel:                   language[i++],
+			ignore:                   language[i++],
+			unignore:                 language[i++],
+			authenticate:             language[i++],
+			verifyUserIdentity:       language[i++],
+			secretQuestion:           language[i++],
+			secretAnswer:             language[i++],
+			ask:                      language[i++],
+			asking:                   language[i++],
+			failed:                   language[i++],
+			identityVerified:         language[i++],
+			authRequest:              language[i++],
+			answerMustMatch:          language[i++],
+			answer:                   language[i++]
+		},
+		warnings: {
+			messageWarning:           language[i++]
+			                            || languageObject.warnings.messageWarning,
+			updateWarning:            language[i++]
+			                            || languageObject.warnings.updateWarning,
+			missingRecipientWarning:  language[i++]
+			                            || languageObject.warnings.missingRecipientWarning
+		},
+		auth: {
+			authenticated:            language[i++]
+			                            || languageObject.auth.authenticated,
+			userNotAuthenticated:     language[i++]
+			                            || languageObject.auth.userNotAuthenticated,
+			clickToLearnMore:         language[i++]
+			                            || languageObject.auth.clickToLearnMore,
+			learnMoreAuth:            language[i++]
+                                        || languageObject.auth.learnMoreAuth,
+			authSlide1:               language[i++]
+			                            || languageObject.auth.authSlide1,
+			authSlide2:               language[i++]
+			                            || languageObject.auth.authSlide2,
+			authSlide3:               language[i++]
+			                            || languageObject.auth.authSlide3,
+			authSlide4:               language[i++]
+			                            || languageObject.auth.authSlide4,
+			authSlide5:               language[i++]
+			                            || languageObject.auth.authSlide5,
+			AKEWarning:               language[i++]
+			                            || languageObject.auth.AKEWarning
+		},
+		login: {
+			groupChat:               language[i++]
+			|| languageObject.login.groupChat,
+			facebook:               language[i++]
+			|| languageObject.login.facebook,
+			facebookInfo:           language[i++]
+			|| languageObject.login.facebookInfo,
+			chatViaFacebook:        language[i++]
+			|| languageObject.login.chatViaFacebook,
+			conversationStatus:     language[i++]
+			|| languageObject.login.conversationStatus,
+			encrypted:              language[i++]
+			|| languageObject.login.encrypted,
+			notEncrypted:           language[i++]
+			|| languageObject.login.notEncrypted,
+			facebookWarning:        language[i++]
+			|| languageObject.login.facebookWarning,
+		}
+	}
+	var decodeFileSize = function (str) { return str.replace('(SIZE)', (Cryptocat.otr.maximumFileSize / 1024)) }
+	languageObject.chatWindow.fileTransferInfo = decodeFileSize(languageObject.chatWindow.fileTransferInfo)
+	languageObject.chatWindow.fileSizeError = decodeFileSize(languageObject.chatWindow.fileSizeError)
+	for (var o in languageObject) {
+		if (languageObject.hasOwnProperty(o)) {
+			Cryptocat.locale[o] = languageObject[o]
+		}
+	}
+	return languageObject
+}
+
+// Re-render login page with new strings
+Cryptocat.locale.refresh = function(languageObject) {
+	var smallType = ['bo', 'ar', 'in']
+	if (smallType.indexOf(languageObject['language']) >= 0) {
+		$('body').css({'font-size': '13px'})
+	}
+	else {
+		$('body').css({'font-size': '11px'})
+	}
+	$('body').css('font-family', languageObject['fonts'])
+	$('#introHeader').text(languageObject['loginWindow']['introHeader'])
+	$('#introParagraph').html(languageObject['loginWindow']['introParagraph'])
+	$('#customServer').text(languageObject['loginWindow']['customServer'])
+	$('#conversationName').attr('placeholder', languageObject['loginWindow']['conversationName'])
+	$('#conversationName').attr('data-utip', languageObject['loginWindow']['conversationNameTooltip'])
+	$('#nickname').attr('placeholder', languageObject['loginWindow']['nickname'])
+	$('#loginSubmit').val(languageObject['loginWindow']['connect'])
+	$('#loginInfo').text(languageObject['loginWindow']['enterConversation'])
+	$('#logout').attr('data-utip', languageObject['chatWindow']['logout'])
+	$('#audio').attr('data-utip', languageObject['chatWindow']['audioNotificationsOff'])
+	$('#notifications').attr('data-utip', languageObject['chatWindow']['desktopNotificationsOff'])
+	$('#myInfo').attr('data-utip', languageObject['chatWindow']['myInfo'])
+	$('#status').attr('data-utip', languageObject['chatWindow']['statusAvailable'])
+	$('#buddy-groupChat').find('span').text(languageObject['chatWindow']['conversation'])
+	$('#languageSelect').text($('[data-locale=' + languageObject['language'] + ']').text())
+	$('[data-login=cryptocat]').text(languageObject.login.groupChat)
+	$('[data-login=facebook]').text(languageObject.login.facebook)
+	$('.facebookInfo').text(languageObject.login.facebookInfo)
+	$('#facebookConnect').val(languageObject.login.chatViaFacebook)
+	$('[data-utip]').utip()
+	$('html').attr('dir', languageObject['direction'])
+	$('#encryptionStatus').attr('dir', languageObject['direction'])
+	if (languageObject['direction'] === 'ltr') {
+		$('div#bubble #info li').css('background-position', 'top left')
+	}
+	else {
+		$('div#bubble #info li').css('background-position', 'top right')
+	}
+	$('#conversationName').select()
+}
+
+// Handle aliases
+Cryptocat.locale.handleAliases = function(locale) {
+	if (locale === ('zh-hk' || 'zh-tw')) {
+		return 'zh-hk'
+	}
+	else if (locale === ('zh-cn' || 'zh-sg')) {
+		return 'zh-cn'
+	}
+	else if (locale.match('-')) {
+		return locale.match(/[a-z]+/)[0]
+	}
+	return locale
+}
+
+// Populate language
+if (typeof(window) !== 'undefined') {
+	Cryptocat.locale.set('en', true)
+}
+
+})()
diff --git a/src/core/js/etc/multiParty.js b/src/core/js/etc/multiParty.js
new file mode 100755
index 0000000..668e12e
--- /dev/null
+++ b/src/core/js/etc/multiParty.js
@@ -0,0 +1,336 @@
+Cryptocat.multiParty = function() {};
+
+(function(){
+'use strict';
+
+var usedIVs = []
+
+var correctIvLength = function(iv){
+	var ivAsWordArray = CryptoJS.enc.Base64.parse(iv)
+	var ivAsArray = ivAsWordArray.words
+	ivAsArray.push(0)  // adds 0 as the 4th element, causing the equivalent
+					   // bytestring to have a length of 16 bytes, with
+					   // \x00\x00\x00\x00 at the end.
+					   // without this, crypto-js will take in a counter of
+					   // 12 bytes, and the first 2 counter iterations will
+					   // use 0, instead of 0 and then 1.
+					   // see https://github.com/cryptocat/cryptocat/issues/258
+	return CryptoJS.lib.WordArray.create(ivAsArray)
+}
+
+// AES-CTR-256 encryption
+// No padding, starting IV of 0
+// Input: WordArray, Output: Base64
+// Key input: WordArray
+var encryptAES = function(msg, c, iv) {
+	var opts = {
+		mode: CryptoJS.mode.CTR,
+		iv: correctIvLength(iv),
+		padding: CryptoJS.pad.NoPadding
+	}
+	var aesctr = CryptoJS.AES.encrypt(
+		msg,
+		c,
+		opts
+	)
+	return aesctr.toString()
+}
+
+// AES-CTR-256 decryption
+// No padding, starting IV of 0
+// Input: Base64, Output: WordArray
+// Key input: WordArray
+var decryptAES = function(msg, c, iv) {
+	var opts = {
+		mode: CryptoJS.mode.CTR,
+		iv: correctIvLength(iv),
+		padding: CryptoJS.pad.NoPadding
+	}
+	var aesctr = CryptoJS.AES.decrypt(
+		msg,
+		c,
+		opts
+	)
+	return aesctr
+}
+
+// HMAC-SHA512
+// Output: Base64
+// Key input: WordArray
+var HMAC = function(msg, key) {
+	return CryptoJS.HmacSHA512(
+		msg, key
+	).toString(CryptoJS.enc.Base64)
+}
+
+// Generate private key (32 random bytes)
+// Represented as BigInt
+Cryptocat.multiParty.genPrivateKey = function() {
+	return BigInt.randBigInt(256)
+}
+
+// Generate public key (Curve 25519 Diffie-Hellman with basePoint 9)
+// Represented as BigInt
+Cryptocat.multiParty.genPublicKey = function(privateKey) {
+	return Curve25519.ecDH(privateKey)
+}
+
+// Generate shared secrets
+// First 256 bytes are for encryption, last 256 bytes are for HMAC.
+// Represented as WordArrays
+Cryptocat.multiParty.genSharedSecret = function(nickname) {
+	//I need to convert the BigInt to WordArray here. I do it using the Base64 representation.
+	var sharedSecret = CryptoJS.SHA512(
+		CryptoJS.enc.Base64.parse(
+			BigInt.bigInt2base64(
+				Curve25519.ecDH(
+					Cryptocat.me.mpPrivateKey,
+					Cryptocat.buddies[nickname].mpPublicKey
+				),
+				32
+			)
+		)
+	)
+	return {
+		'message': CryptoJS.lib.WordArray.create(sharedSecret.words.slice(0, 8)),
+		'hmac': CryptoJS.lib.WordArray.create(sharedSecret.words.slice(8, 16))
+	}
+}
+
+// Get fingerprint
+// If nickname is null, returns own fingerprint
+Cryptocat.multiParty.genFingerprint = function(nickname) {
+	var key = Cryptocat.me.mpPublicKey
+	if (nickname) {
+		key = Cryptocat.buddies[nickname].mpPublicKey
+	}
+	return CryptoJS.SHA512(
+		CryptoJS.enc.Base64.parse(
+			BigInt.bigInt2base64(key, 32)
+		)
+	).toString().substring(0, 40).toUpperCase()
+}
+
+// Send my public key in response to a public key request.
+Cryptocat.multiParty.sendPublicKey = function(nickname) {
+	var answer = {}
+	answer['type'] = 'publicKey'
+	answer['text'] = {}
+	answer['text'][nickname] = {}
+	answer['text'][nickname]['message'] = BigInt.bigInt2base64(
+		Cryptocat.me.mpPublicKey, 32
+	)
+	return JSON.stringify(answer)
+}
+
+// Issue a warning for decryption failure to the main conversation window
+Cryptocat.multiParty.messageWarning = function(sender) {
+	var messageWarning = Cryptocat.locale['warnings']['messageWarning']
+		.replace('(NICKNAME)', sender)
+	Cryptocat.addToConversation(messageWarning, sender, 'groupChat', 'warning')
+}
+
+// Generate message tag. 8 rounds of SHA512
+// Input: WordArray
+// Output: Base64
+Cryptocat.multiParty.messageTag = function(message) {
+	for (var i = 0; i !== 8; i++) {
+		message = CryptoJS.SHA512(message)
+	}
+	return message.toString(CryptoJS.enc.Base64)
+}
+
+// Send message.
+Cryptocat.multiParty.sendMessage = function(message) {
+	//Convert from UTF8
+	message = CryptoJS.enc.Utf8.parse(message)
+	// Add 64 bytes of padding
+	message.concat(Cryptocat.random.rawBytes(64))
+	var encrypted = {
+		text: {},
+		type: 'message'
+	}
+	//Sort recipients
+	var sortedRecipients = []
+	for (var b in Cryptocat.buddies) {
+		if (Cryptocat.buddies[b].mpSecretKey) {
+			sortedRecipients.push(b)
+		}
+	}
+	sortedRecipients.sort()
+	var hmac = CryptoJS.lib.WordArray.create()
+	//For each recipient
+	var i, iv
+	for (i = 0; i !== sortedRecipients.length; i++) {
+		//Generate a random IV
+		iv = Cryptocat.random.encodedBytes(12, CryptoJS.enc.Base64)
+		// Do not reuse IVs
+		while (usedIVs.indexOf(iv) >= 0) {
+			iv = Cryptocat.random.encodedBytes(12, CryptoJS.enc.Base64)
+		}
+		usedIVs.push(iv)
+		//Encrypt the message
+		encrypted['text'][sortedRecipients[i]] = {}
+		encrypted['text'][sortedRecipients[i]]['message'] = encryptAES(
+			message,
+			Cryptocat.buddies[sortedRecipients[i]].mpSecretKey['message'],
+			iv
+		)
+		encrypted['text'][sortedRecipients[i]]['iv'] = iv
+		//Append to HMAC
+		hmac.concat(CryptoJS.enc.Base64.parse(encrypted['text'][sortedRecipients[i]]['message']))
+		hmac.concat(CryptoJS.enc.Base64.parse(encrypted['text'][sortedRecipients[i]]['iv']))
+	}
+	encrypted['tag'] = message.clone()
+	//For each recipient again
+	for (i = 0; i !== sortedRecipients.length; i++) {
+		//Compute the HMAC
+		encrypted['text'][sortedRecipients[i]]['hmac'] = HMAC(
+			hmac,
+			Cryptocat.buddies[sortedRecipients[i]].mpSecretKey['hmac']
+		)
+		//Append to tag
+		encrypted['tag'].concat(CryptoJS.enc.Base64.parse(encrypted['text'][sortedRecipients[i]]['hmac']))
+	}
+	//Compute tag
+	encrypted['tag'] = Cryptocat.multiParty.messageTag(encrypted['tag'])
+	return JSON.stringify(encrypted)
+}
+
+// Receive message. Detects requests/reception of public keys.
+Cryptocat.multiParty.receiveMessage = function(sender, myName, message) {
+	var buddy = Cryptocat.buddies[sender]
+	try {
+		message = JSON.parse(message)
+	}
+	catch(err) {
+		console.log('multiParty: failed to parse message object')
+		return false
+	}
+	if (typeof(message['text'][myName]) === 'object') {
+		// Detect public key reception, store public key and generate shared secret
+		if (message.type === 'publicKey') {
+			var msg = message.text[myName].message
+			if (typeof(msg) !== 'string') {
+				console.log('multiParty: publicKey without message field')
+				return false
+			}
+			var publicKey = BigInt.base642bigInt(msg)
+			// if we already have a public key for this buddy, make sure it's
+			// the one we have
+			if (buddy.mpPublicKey &&
+				!BigInt.equals(buddy.mpPublicKey, publicKey)
+			) {
+				buddy.updateMpKeys(publicKey)
+				Cryptocat.removeAuthAndWarn(sender)
+			}
+			// if we're missing their key, make sure we aren't already
+			// authenticated (prevents a possible active attack)
+			else if (!buddy.mpPublicKey && buddy.authenticated) {
+				buddy.updateMpKeys(publicKey)
+				Cryptocat.removeAuthAndWarn(sender)
+			} else {
+				buddy.updateMpKeys(publicKey)
+			}
+			return false
+		}
+		// Detect public key request and send public key
+		else if (message['type'] === 'publicKeyRequest') {
+			Cryptocat.xmpp.sendPublicKey(sender)
+		}
+		else if (message['type'] === 'message') {
+			// Make sure message is being sent to all chat room participants
+			var recipients = Object.keys(Cryptocat.buddies)
+			var missingRecipients = []
+			recipients.splice(recipients.indexOf(sender), 1)
+			for (var r = 0; r !== recipients.length; r++) {
+				try {
+					if (typeof(message['text'][recipients[r]]) === 'object') {
+						var noMessage = typeof(message['text'][recipients[r]]['message']) !== 'string'
+						var noIV = typeof(message['text'][recipients[r]]['iv']) !== 'string'
+						var noHMAC = typeof(message['text'][recipients[r]]['hmac']) !== 'string'
+						if (noMessage || noIV || noHMAC) {
+							missingRecipients.push(recipients[r])
+						}
+					}
+					else {
+						missingRecipients.push(recipients[r])
+					}
+				}
+				catch(err) {
+					missingRecipients.push(recipients[r])
+				}
+			}
+			if (missingRecipients.length) {
+				Cryptocat.addToConversation(missingRecipients, sender, 'groupChat', 'missingRecipients')
+			}
+			// Decrypt message
+			if (!Cryptocat.buddies[sender].mpSecretKey) {
+				return false
+			}
+			// Sort recipients
+			var sortedRecipients = Object.keys(message['text']).sort()
+			// Check HMAC
+			var hmac = CryptoJS.lib.WordArray.create()
+			var i
+			for (i = 0; i !== sortedRecipients.length; i++) {
+				hmac.concat(CryptoJS.enc.Base64.parse(message['text'][sortedRecipients[i]]['message']))
+				hmac.concat(CryptoJS.enc.Base64.parse(message['text'][sortedRecipients[i]]['iv']))
+			}
+			if (
+				!OTR.HLP.compare(
+					message['text'][myName]['hmac'],
+					HMAC(hmac, Cryptocat.buddies[sender].mpSecretKey['hmac'])
+				)
+			) {
+				console.log('multiParty: HMAC failure')
+				Cryptocat.multiParty.messageWarning(sender)
+				return false
+			}
+			// Check IV reuse
+			if (usedIVs.indexOf(message['text'][myName]['iv']) >= 0) {
+				console.log('multiParty: IV reuse detected, possible replay attack')
+				Cryptocat.multiParty.messageWarning(sender)
+				return false
+			}
+			usedIVs.push(message['text'][myName]['iv'])
+			// Decrypt
+			var plaintext = decryptAES(
+				message['text'][myName]['message'],
+				Cryptocat.buddies[sender].mpSecretKey['message'],
+				message['text'][myName]['iv']
+			)
+			// Check tag
+			var messageTag = plaintext.clone()
+			for (i = 0; i !== sortedRecipients.length; i++) {
+				messageTag.concat(CryptoJS.enc.Base64.parse(message['text'][sortedRecipients[i]]['hmac']))
+			}
+			if (Cryptocat.multiParty.messageTag(messageTag) !== message['tag']) {
+				console.log('multiParty: message tag failure')
+				Cryptocat.multiParty.messageWarning(sender)
+				return false
+			}
+			// Remove padding
+			if (plaintext.sigBytes < 64) {
+				console.log('multiParty: invalid plaintext size')
+				Cryptocat.multiParty.messageWarning(sender)
+				return false
+			}
+			plaintext = CryptoJS.lib.WordArray.create(plaintext.words, plaintext.sigBytes-64)
+			// Convert to UTF8
+			return plaintext.toString(CryptoJS.enc.Utf8)
+		}
+		else {
+			console.log('multiParty: Unknown message type: ' + message['type'])
+			Cryptocat.multiParty.messageWarning(sender)
+		}
+	}
+	return false
+}
+
+// Reset everything except my own key pair
+Cryptocat.multiParty.reset = function() {
+	usedIVs = []
+}
+
+})()//:3
diff --git a/src/core/js/etc/otr.js b/src/core/js/etc/otr.js
new file mode 100644
index 0000000..c265fc1
--- /dev/null
+++ b/src/core/js/etc/otr.js
@@ -0,0 +1,206 @@
+;(function() {
+'use strict';
+
+// Cryptocat OTR functions and callbacks.
+Cryptocat.otr = {}
+
+// Construct a new OTR conversation
+Cryptocat.otr.add = function(nickname) {
+	var otr = new OTR({
+		priv: Cryptocat.me.otrKey,
+		smw: {
+			path: 'js/workers/smp.js',
+			seed: Cryptocat.random.generateSeed
+		}
+	})
+	otr.REQUIRE_ENCRYPTION = true
+	otr.on('ui',     onIncoming.bind(null, nickname))
+	otr.on('io',     onOutgoing.bind(null, nickname))
+	otr.on('smp',    onSMPAnswer.bind(null, nickname))
+	otr.on('status', onStatusChange.bind(null, nickname))
+	otr.on('file',   onFile.bind(null, nickname))
+	return otr
+}
+
+
+// Handle incoming messages.
+var onIncoming = function(nickname, msg, encrypted) {
+	if (Cryptocat.me.login === 'cryptocat') {
+		// Drop unencrypted messages.
+		if (!encrypted) {
+			return
+		}
+		Cryptocat.addToConversation(
+			msg, nickname, Cryptocat.buddies[nickname].id, 'message'
+		)
+		if (Cryptocat.me.currentBuddy !== Cryptocat.buddies[nickname].id) {
+			Cryptocat.messagePreview(msg, nickname)
+		}
+	}
+	if (Cryptocat.me.login === 'facebook') {
+		// Drop unencrypted messages if buddy claims to use Cryptocat.
+		if (Cryptocat.buddies[nickname].usingCryptocat &&
+			!encrypted
+		) {
+			return
+		}
+		if (!Cryptocat.buddies[nickname].usingCryptocat &&
+			encrypted
+		) {
+			$.get('https://outbound.crypto.cat/facebook/', {
+				'setuser': Cryptocat.buddies[nickname].id
+			})
+			Cryptocat.buddyStatus(nickname, 'online')
+			$('#buddy-' + Cryptocat.buddies[nickname].id)
+				.find('.loginTypeIcon')
+				.removeClass('notUsingCryptocat')
+			$('#buddy-' + Cryptocat.buddies[nickname].id)
+				.find('.buddyMenu').show()
+			if (Cryptocat.me.currentBuddy === Cryptocat.buddies[nickname].id) {
+				$('#encryptionStatus').html(
+					Mustache.render(Cryptocat.templates.encryptionStatus, {
+						conversationStatus: Cryptocat.locale.login.conversationStatus,
+						styling: 'encrypted',
+						encryptionStatus: Cryptocat.locale.login.encrypted
+					})
+				)
+			}
+			Cryptocat.buddies[nickname].usingCryptocat         = true
+			Cryptocat.buddies[nickname].otr.REQUIRE_ENCRYPTION = true
+		}
+		Cryptocat.addToConversation(
+			msg, nickname, Cryptocat.buddies[nickname].id, 'message'
+		)
+		if (Cryptocat.me.currentBuddy !== Cryptocat.buddies[nickname].id) {
+			Cryptocat.messagePreview(msg, nickname)
+		}
+	}
+}
+
+// Handle outgoing messages depending on connection type.
+var onOutgoing = function(nickname, message) {
+	if (Cryptocat.me.login === 'cryptocat') {
+		Cryptocat.xmpp.connection.muc.message(
+			Cryptocat.me.conversation
+				+ '@'
+				+ Cryptocat.xmpp.conferenceServer,
+			nickname, message, null, 'chat', 'active'
+		)
+	}
+	if (Cryptocat.me.login === 'facebook') {
+		var to = '-' + Cryptocat.buddies[nickname].id + '@chat.facebook.com'
+		var reply = $msg({
+			to: to,
+			type: 'chat',
+			cryptocat: 'true',
+		}).cnode(Strophe.xmlElement('body', message))
+		Cryptocat.xmpp.connection.send(reply.tree())
+	}
+}
+
+// Handle otr state changes.
+var onStatusChange = function(nickname, state) {
+	/*jshint camelcase:false */
+	var buddy = Cryptocat.buddies[nickname]
+	if (state === OTR.CONST.STATUS_AKE_SUCCESS) {
+		var fingerprint = buddy.otr.their_priv_pk.fingerprint()
+		if (buddy.fingerprint === null) {
+			buddy.fingerprint = fingerprint
+			Cryptocat.closeGenerateFingerprints(nickname)
+		}
+		else if (buddy.fingerprint !== fingerprint) {
+			// re-aked with a different key
+			buddy.fingerprint = fingerprint
+			Cryptocat.removeAuthAndWarn(nickname)
+		}
+	}
+}
+
+// Store received filename.
+var onFile = function(nickname, type, key, filename) {
+	var buddy = Cryptocat.buddies[nickname]
+	// filename is being relied on as diversifier
+	// and should continue to be generated uniquely
+	// as in sendFile()
+	var derivedKey = CryptoJS.PBKDF2(key, filename, { keySize: 16 })
+	derivedKey = derivedKey.toString(CryptoJS.enc.Latin1)
+	if (!buddy.fileKey) {
+		buddy.fileKey = {}
+	}
+	buddy.fileKey[filename] = {
+		encryptKey: derivedKey.substring(0, 32),
+		macKey:     derivedKey.substring(32)
+	}
+}
+
+// Receive an SMP question
+var onSMPQuestion = function(nickname, question) {
+	var chatWindow = Cryptocat.locale.chatWindow,
+		buddy = Cryptocat.buddies[nickname],
+		answer = false
+	var info = Mustache.render(Cryptocat.templates.authRequest, {
+		authenticate: chatWindow.authenticate,
+		authRequest: chatWindow.authRequest.replace('(NICKNAME)', nickname),
+		answerMustMatch: chatWindow.answerMustMatch
+			.replace('(NICKNAME)', nickname),
+		question: question,
+		answer: chatWindow.answer
+	})
+	$('#dialogBoxClose').click()
+	window.setTimeout(function() {
+		Cryptocat.dialogBox(info, {
+			height: 240,
+			closeable: true,
+			onAppear: function() {
+				$('#authReplySubmit').unbind('click').bind('click', function(e) {
+					e.preventDefault()
+					answer = $('#authReply').val()
+					answer = Cryptocat.prepareAnswer(answer, false, buddy.mpFingerprint)
+					buddy.otr.smpSecret(answer)
+					$('#dialogBoxClose').click()
+				})
+			},
+			onClose: function() {
+				if (answer) { return }
+				buddy.otr.smpSecret(
+					Cryptocat.random.encodedBytes(16, CryptoJS.enc.Hex)
+				)
+			}
+		})
+	}, 500)
+}
+
+// Handle SMP callback
+var onSMPAnswer = function(nickname, type, data, act) {
+	var chatWindow = Cryptocat.locale.chatWindow,
+		buddy = Cryptocat.buddies[nickname]
+	switch(type) {
+	case 'question':
+		onSMPQuestion(nickname, data)
+		break
+	case 'trust':
+		if (act === 'asked') {
+			// Set authentication result
+			buddy.updateAuth(data)
+			if ($('.authSMP').length) {
+				if (buddy.authenticated) {
+					$('#authSubmit').val(chatWindow.identityVerified)
+					$('#authenticated').click()
+				}
+				else {
+					$('#authSubmit').val(chatWindow.failed)
+						.animate({'background-color': '#F00'})
+				}
+			}
+		}
+		break
+	case 'abort':
+		if ($('.authSMP').length) {
+			$('#authSubmit').val(chatWindow.failed)
+				.animate({'background-color': '#F00'})
+		}
+		break
+	}
+}
+
+}())
\ No newline at end of file
diff --git a/src/core/js/etc/random.js b/src/core/js/etc/random.js
new file mode 100644
index 0000000..6a71654
--- /dev/null
+++ b/src/core/js/etc/random.js
@@ -0,0 +1,99 @@
+if (typeof Cryptocat === 'undefined') {
+	Cryptocat = function() {}
+}
+
+;(function (root, factory) {
+	if (typeof module !== 'undefined' && module.exports) {
+		module.exports = factory({}, require('../lib/salsa20.js'), true)
+	} else {
+		if (typeof root.Cryptocat === 'undefined') {
+			root.Cryptocat = function () {}
+		}
+		factory(root.Cryptocat, root.Salsa20, false)
+	}
+}(this, function (Cryptocat, Salsa20, node) {
+'use strict';
+
+Cryptocat.random = {}
+
+var state
+
+Cryptocat.random.generateSeed = function() {
+	var buffer, crypto
+	// Node.js ... for tests
+	if (typeof window === 'undefined' && typeof require !== 'undefined') {
+		crypto = require('crypto')
+		try {
+			buffer = crypto.randomBytes(40)
+		} catch (e) { throw e }
+	}
+	else {
+		buffer = new Uint8Array(40)
+		window.crypto.getRandomValues(buffer)
+	}
+	return buffer
+}
+
+Cryptocat.random.setSeed = function(s) {
+	if (!s) { return false }
+	state = new Salsa20(
+		[
+			s[ 0],s[ 1],s[ 2],s[ 3],s[ 4],s[ 5],s[ 6],s[ 7],
+			s[ 8],s[ 9],s[10],s[11],s[12],s[13],s[14],s[15],
+			s[16],s[17],s[18],s[19],s[20],s[21],s[22],s[23],
+			s[24],s[25],s[26],s[27],s[28],s[29],s[30],s[31]
+		],
+		[
+			s[32],s[33],s[34],s[35],s[36],s[37],s[38],s[39]
+		]
+	)
+}
+
+Cryptocat.random.getBytes = function(i) {
+	if (i.constructor !== Number || i < 1) {
+		throw new Error('Expecting a number greater than 0.')
+	}
+	return state.getBytes(i)
+}
+
+Cryptocat.random.bitInt = function(k) {
+	if (k > 31) {
+		throw new Error('That\'s more than JS can handle.')
+	}
+	var i = 0, r = 0
+	var b = Math.floor(k / 8)
+	var mask = (1 << (k % 8)) - 1
+	if (mask) {
+		r = Cryptocat.random.getBytes(1)[0] & mask
+	}
+	for (; i < b; i++) {
+		r = (256 * r) + Cryptocat.random.getBytes(1)[0]
+	}
+	return r
+}
+
+Cryptocat.random.decimal = function() {
+	var r = 250;
+	while ( r > 249 ) {
+		r = Cryptocat.random.getBytes(1)[0]
+	}
+	return r % 10;
+}
+
+Cryptocat.random.rawBytes = function(bytes) {
+	var sa = String.fromCharCode.apply(null, Cryptocat.random.getBytes(bytes))
+	return CryptoJS.enc.Latin1.parse(sa)
+}
+
+Cryptocat.random.encodedBytes = function(bytes, encoding) {
+	return Cryptocat.random.rawBytes(bytes).toString(encoding)
+}
+
+if (node) {
+	// Seed RNG in tests.
+	Cryptocat.random.setSeed(Cryptocat.random.generateSeed())
+}
+
+return Cryptocat
+
+}))//:3
diff --git a/src/core/js/etc/storage.js b/src/core/js/etc/storage.js
new file mode 100644
index 0000000..5345114
--- /dev/null
+++ b/src/core/js/etc/storage.js
@@ -0,0 +1,158 @@
+Cryptocat.storage = {}
+
+$(window).ready(function() {
+'use strict';
+
+// Cryptocat Storage API
+// This API uses different local storage solutions,
+// depending on the browser engine, to offer a uniform
+// storage interface for Cryptocat user preferences and settings.
+
+// How to use:
+// Cryptocat.storage.setItem(itemName, itemValue)
+// Sets itemName's value to itemValue.
+
+// Cryptocat.storage.getItem(itemName, callbackFunction(result))
+// Gets itemName's value from local storage, and passes it to
+// the callback function as result.
+
+// Cryptocat.storage.removeItem(itemName)
+// Removes itemName and its value from local storage.
+
+// Define the wrapper, depending on our browser or environment.
+Cryptocat.storage = (function() {
+	// Chrome
+	if (typeof(chrome) === 'object' && chrome.storage) {
+		return {
+			setItem: function(key, val) {
+				var keyValuePair = {}
+				keyValuePair[key] = val
+				chrome.storage.local.set(keyValuePair)
+			},
+			getItem: function(key, callback) {
+				chrome.storage.local.get(key, function(r) {
+					callback(r[key])
+				})
+			},
+			removeItem: function(key) {
+				chrome.storage.local.remove(key)
+			}
+		}
+	}
+	// Firefox
+	else if (navigator.userAgent.match('Firefox')) {
+		return {
+			setItem: function(key, val) {
+				var element = document.createElement('cryptocatFirefoxElement')
+				document.documentElement.appendChild(element)
+				var evt = document.createEvent('HTMLEvents')
+				element.setAttribute('type', 'set')
+				element.setAttribute('key', key)
+				element.setAttribute('val', val)
+				evt.initEvent('cryptocatFirefoxStorage', true, false)
+				element.dispatchEvent(evt)
+			},
+			getItem: function(key, callback) {
+				var element = document.createElement('cryptocatFirefoxElement')
+				document.documentElement.appendChild(element)
+				var evt = document.createEvent('HTMLEvents')
+				element.setAttribute('type', 'get')
+				element.setAttribute('key', key)
+				evt.initEvent('cryptocatFirefoxStorage', true, false)
+				element.dispatchEvent(evt)
+				callback(element.getAttribute('firefoxStorageGet'))
+			},
+			removeItem: function(key) {
+				var element = document.createElement('cryptocatFirefoxElement')
+				document.documentElement.appendChild(element)
+				var evt = document.createEvent('HTMLEvents')
+				element.setAttribute('type', 'remove')
+				element.setAttribute('key', key)
+				evt.initEvent('cryptocatFirefoxStorage', true, false)
+				element.dispatchEvent(evt)
+			}
+		}
+	}
+	// Everything else
+	else {
+		return {
+			setItem: function(key, value) {
+				localStorage.setItem(key, value)
+			},
+			getItem: function(key, callback) {
+				callback(localStorage.getItem(key))
+			},
+			removeItem: function(key) {
+				localStorage.removeItem(key)
+			}
+		}
+	}
+})()
+
+// Initialize language settings.
+Cryptocat.storage.getItem('language', function(key) {
+	if (key) {
+		Cryptocat.locale.set(key, true)
+	}
+	else {
+		Cryptocat.locale.set(window.navigator.language.toLowerCase())
+	}
+})
+
+// Load custom server settings
+Cryptocat.storage.getItem('serverName', function(key) {
+	if (key) { Cryptocat.serverName = key }
+})
+Cryptocat.storage.getItem('domain', function(key) {
+	if (key) { Cryptocat.xmpp.domain = key }
+})
+Cryptocat.storage.getItem('conferenceServer', function(key) {
+	if (key) { Cryptocat.xmpp.conferenceServer = key }
+})
+Cryptocat.storage.getItem('relay', function(key) {
+	if (key) { Cryptocat.xmpp.relay = key }
+})
+Cryptocat.storage.getItem('customServers', function(key) {
+	if (key) {
+		$('#customServerSelector').empty()
+		var servers = $.parseJSON(key)
+		$.each(servers, function(name) {
+			$('#customServerSelector').append(
+				Mustache.render(Cryptocat.templates['customServer'], {
+					name: name,
+					domain: servers[name]['domain'],
+					XMPP: servers[name]['xmpp'],
+					Relay: servers[name]['relay']
+				})
+			)
+		})
+	}
+})
+
+// Load nickname settings.
+Cryptocat.storage.getItem('myNickname', function(key) {
+	if (key) {
+		$('#nickname').animate({'color': 'transparent'}, function() {
+			$(this).val(key)
+			$(this).animate({'color': '#FFF'})
+		})
+	}
+})
+
+// Load notification settings.
+window.setTimeout(function() {
+	Cryptocat.storage.getItem('desktopNotifications', function(key) {
+		if (key === 'true') {
+			$('#notifications').click()
+			$('#utip').hide()
+		}
+	})
+	Cryptocat.storage.getItem('audioNotifications', function(key) {
+		if ((key === 'true') || !key) {
+			$('#audio').click()
+			$('#utip').hide()
+		}
+	})
+}, 800)
+
+})
diff --git a/src/core/js/etc/templates.js b/src/core/js/etc/templates.js
new file mode 100644
index 0000000..0783ce4
--- /dev/null
+++ b/src/core/js/etc/templates.js
@@ -0,0 +1,105 @@
+// Cryptocat templates for use with mustache.js.
+'use strict';
+
+Cryptocat.templates = {
+
+	customServer: '<option data-domain="{{domain}}" data-xmpp="{{XMPP}}" data-relay="{{Relay}}">'
+		+ '{{name}}</option>',
+
+	generatingKeys: '<br /><p id="progressForm"><img src="img/keygen.gif" '
+		+ 'alt="" /><p id="progressInfo"><span>{{text}}</span></p>',
+
+	catFact: '<br />Here is an interesting fact while you wait:'
+		+ '<br /><div id="interestingFact">{{catFact}}</div>',
+
+	buddy: '<div class="buddy" id="buddy-{{buddyID}}" status="{{status}}" data-id="{{buddyID}}" dir="ltr">'
+		+ '<span class="loginTypeIcon"></span><span class="shortNickname">{{shortNickname}}</span>'
+		+ '<div class="buddyMenu" id="menu-{{buddyID}}"></div></div>',
+
+	buddyMenu: '<div class="buddyMenuContents" id="{{buddyID}}-contents">'
+		+ '<li class="option1">{{displayInfo}}</li>'
+		+ '<li class="option2">{{sendEncryptedFile}}</li>'
+		+ '<li class="option3">{{ignore}}</li>'
+		+ '</div>',
+
+	myInfo: '<div class="title">{{nickname}}</div>'
+		+ '<div id="displayInfo">'
+		+ '{{groupFingerprint}}<br /><span id="multiPartyFingerprint"></span><br />'
+		+ '{{otrFingerprint}}<br /><span id="otrFingerprint"></span></div>',
+
+	errorAKE: '<div class="title errorTitle">{{nickname}}</div>'
+		+ '<div id="displayInfo">'
+		+ '{{errorText}}<br />'
+		+ '<input type="button" id="openAuth" value="{{openAuth}}" />'
+		+ '</div>',
+
+	buddyInfo: '<div class="title">{{nickname}}</div>'
+		+ '<div id="displayInfo">'
+		+ '<h2>{{authenticated}}</h2>'
+		+ '<span id="authenticated">&#x2713</span><span id="notAuthenticated">X</span><br />'
+		+ '<span id="authLearnMore">{{learnMoreAuth}}</span><br />'
+		+ '<div class="authInfo" style="height:95px">{{groupFingerprint}}<br />'
+		+ '<span id="multiPartyFingerprint"></span><br />'
+		+ '{{otrFingerprint}}<br /><span id="otrFingerprint"></span></div>'
+		+ '<div class="authInfo authSMP"><span>{{verifyUserIdentity}}</span><br /><br />'
+		+ '<form><input type="text" id="authQuestion" placeholder="{{secretQuestion}}" maxlength="64" />'
+		+ '<input type="password" id="authAnswer" placeholder="{{secretAnswer}}" maxlength="64" />'
+		+ '<input id="authSubmit" type="submit" value="{{ask}}" /></form>'
+		+ '<p id="authVerified">{{identityVerified}}</p></div>'
+		+ '<div id="authTutorial"></div></div>',
+
+	authTutorial: '<div id="authTutorialSlides"><ul class="bjqs">'
+		+ '<li><img src="img/authTutorial/1.png" title="{{slide1}}"></li>'
+		+ '<li><img src="img/authTutorial/2.png" title="{{slide2}}"></li>'
+		+ '<li><img src="img/authTutorial/3.png" title="{{slide3}}"></li>'
+		+ '<li><img src="img/authTutorial/4.png" title="{{slide4}}"></li>'
+		+ '</ul></div>',
+
+	authRequest: '<div class="title">{{authenticate}}</div>'
+		+ '<p>{{authRequest}}<br />'
+		+ '<strong>{{question}}</strong><br /><br />'
+		+ '<form id="authReplyForm"><input id="authReply" type="password" placeholder="{{secretAnswer}}" maxlength="64" />'
+		+ '<input id="authReplySubmit" type="submit" value="{{answer}}" /></form></p>'
+		+ '<p>{{answerMustMatch}}</p>',
+
+	sendFile: '<div class="title">{{sendEncryptedFile}}</div>'
+		+ '<input type="file" id="fileSelector" name="file[]" />'
+		+ '<input type="button" id="fileSelectButton" value="{{sendEncryptedFile}}" />'
+		+ '<div id="fileInfoField">{{fileTransferInfo}}</div>',
+
+	file: '<div class="fileProgressBar" data-file="{{file}}" data-id="{{id}}">'
+		+ '<div class="fileProgressBarFill" data-file="{{file}}" data-id="{{id}}"></div></div>',
+
+	fileLink: '<a href="{{url}}" class="fileView" target="_blank" download="{{filename}}">{{downloadFile}}</a>',
+
+	fileLinkMac: '<a href="{{url}}" class="fileView" download="{{filename}}">{{downloadFile}}</a>',
+
+	missingRecipients: '<div class="missingRecipients" dir="{{dir}}">{{text}}</div>',
+
+	message: '<div class="line{{lineDecoration}}"><span class="sender" data-sender="{{nickname}}"'
+		+ ' data-timestamp="{{currentTime}}"><span class="authStatus" data-auth="{{authStatus}}" '
+		+ 'data-utip-gravity="se"></span>'
+		+ '<span class="nickname">{{nickname}}</span></span>{{&message}}</div>',
+
+	emoticon: ' <img class="emoticon" src="img/emoticon/{{emoticon}}.png" alt="" /> ',
+
+	authStatusFalseUtip: '<div id="authStatusUtip">{{text}}<br /><strong>{{learnMore}}</strong></div>',
+
+	userJoin: '<div class="userJoin"><span class="timestamp">{{currentTime}}</span>'
+		+ '<strong>+</strong>{{nickname}}</div>',
+
+	userLeave: '<div class="userLeave"><span class="timestamp">{{currentTime}}</span>'
+		+ '<strong>-</strong>{{nickname}}</div>',
+
+	facebookAuthURL: 'https://www.facebook.com/dialog/oauth'
+		+ '?scope={{scope}}'
+		+ '&app_id={{appID}}'
+		+ '&client_id={{appID}}'
+		+ '&redirect_uri=https://outbound.crypto.cat/facebook/'
+		+ '?id={{authID}}&close=true&display=popup',
+
+	encryptionStatus: '{{conversationStatus}}: <strong class="{{styling}}">{{encryptionStatus}}</strong>',
+
+	notUsingCryptocat: '<div class="notUsingCryptocatWarning" dir="{{dir}}">{{text}}</div>',
+
+}
diff --git a/src/core/js/etc/xmpp.js b/src/core/js/etc/xmpp.js
new file mode 100644
index 0000000..f0d83bd
--- /dev/null
+++ b/src/core/js/etc/xmpp.js
@@ -0,0 +1,330 @@
+// Cryptocat XMPP functions and callbacks.
+
+Cryptocat.xmpp = {}
+Cryptocat.xmpp.currentStatus = 'online'
+Cryptocat.xmpp.connection = null
+
+// Default connection settings.
+Cryptocat.xmpp.defaultDomain = 'crypto.cat'
+Cryptocat.xmpp.defaultConferenceServer = 'conference.crypto.cat'
+Cryptocat.xmpp.defaultRelay = 'https://crypto.cat/http-bind'
+
+Cryptocat.xmpp.domain = Cryptocat.xmpp.defaultDomain
+Cryptocat.xmpp.conferenceServer = Cryptocat.xmpp.defaultConferenceServer
+Cryptocat.xmpp.relay = Cryptocat.xmpp.defaultRelay
+
+$(window).ready(function() {
+'use strict';
+
+// Prepares necessary encryption key operations before XMPP connection.
+// Shows a progress bar (and cute cat facts!) while doing so.
+Cryptocat.xmpp.showKeyPreparationDialog = function(callback) {
+	// Key storage currently disabled as we are not yet sure if this is safe to do.
+	// Cryptocat.storage.setItem('multiPartyKey', Cryptocat.multiParty.genPrivateKey())
+	//else {
+	Cryptocat.me.mpPrivateKey = Cryptocat.multiParty.genPrivateKey()
+	//}
+	Cryptocat.me.mpPublicKey = Cryptocat.multiParty.genPublicKey(
+		Cryptocat.me.mpPrivateKey
+	)
+	Cryptocat.me.mpFingerprint = Cryptocat.multiParty.genFingerprint()
+	// If we already have keys, just skip to the callback.
+	if (Cryptocat.me.otrKey) {
+		callback()
+		return
+	}
+	var progressForm = Mustache.render(Cryptocat.templates.generatingKeys, {
+		text: Cryptocat.locale['loginMessage']['generatingKeys']
+	})
+	if (Cryptocat.audioNotifications) { Cryptocat.sounds.keygenStart.play() }
+	Cryptocat.dialogBox(progressForm, {
+		height: 250,
+		closeable: false,
+		onAppear: Cryptocat.xmpp.prepareKeys(callback)
+	})
+	if (Cryptocat.locale['language'] === 'en') {
+		$('#progressInfo').append(
+			Mustache.render(Cryptocat.templates.catFact, {
+				catFact: CatFacts.getFact()
+			})
+		)
+	}
+	$('#progressInfo').append(
+		'<div id="progressBar"><div id="fill"></div></div>'
+	)
+	CatFacts.interval = window.setInterval(function() {
+		$('#interestingFact').fadeOut(function() {
+			$(this).text(CatFacts.getFact()).fadeIn()
+		})
+	}, 9000)
+	$('#fill').animate({'width': '100%', 'opacity': '1'}, 14000, 'linear')
+}
+
+// See above.
+Cryptocat.xmpp.prepareKeys = function(callback) {
+	if (Cryptocat.audioNotifications) {
+		window.setTimeout(function() {
+			Cryptocat.sounds.keygenLoop.loop = true
+			Cryptocat.sounds.keygenLoop.play()
+		}, 800)
+	}
+	// Create DSA key for OTR.
+	DSA.createInWebWorker({
+		path: 'js/workers/dsa.js',
+		seed: Cryptocat.random.generateSeed
+	}, function(key) {
+		Cryptocat.me.otrKey = key
+		// Key storage currently disabled as we are not yet sure if this is safe to do.
+		//	Cryptocat.storage.setItem('myKey', JSON.stringify(Cryptocat.me.otrKey))
+		if (callback) { callback() }
+	})
+}
+
+// Connect anonymously and join conversation.
+Cryptocat.xmpp.connect = function() {
+	Cryptocat.me.conversation = Strophe.xmlescape($('#conversationName').val())
+	Cryptocat.me.nickname = Strophe.xmlescape($('#nickname').val())
+	Cryptocat.xmpp.connection = new Strophe.Connection(Cryptocat.xmpp.relay)
+	Cryptocat.xmpp.connection.connect(Cryptocat.xmpp.domain, null, function(status) {
+		if (
+			(status === Strophe.Status.CONNECTING) &&
+			(Cryptocat.me.login === 'cryptocat')
+		) {
+			$('#loginInfo').animate({'background-color': '#97CEEC'}, 200)
+			$('#loginInfo').text(Cryptocat.locale['loginMessage']['connecting'])
+		}
+		else if (status === Strophe.Status.CONNECTED) {
+			Cryptocat.xmpp.connection.muc.join(
+				Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+				Cryptocat.me.nickname,
+				function(message) {
+					if (Cryptocat.xmpp.onMessage(message)) { return true }
+				},
+				function(presence) {
+					if (Cryptocat.xmpp.onPresence(presence)) { return true }
+				}
+			)
+			Cryptocat.xmpp.onConnected()
+			document.title = Cryptocat.me.nickname + '@' + Cryptocat.me.conversation
+			$('.conversationName').text(document.title)
+			Cryptocat.storage.setItem('myNickname', Cryptocat.me.nickname)
+		}
+		else if ((status === Strophe.Status.CONNFAIL) || (status === Strophe.Status.DISCONNECTED)) {
+			if (Cryptocat.loginError) {
+				Cryptocat.xmpp.reconnect()
+			}
+		}
+	})
+}
+
+// Executes on successfully completed XMPP connection.
+Cryptocat.xmpp.onConnected = function() {
+	afterConnect()
+	clearInterval(CatFacts.interval)
+	if (Cryptocat.me.login === 'cryptocat') {
+		$('#loginInfo').text('✓')
+		$('#buddy-groupChat,#status').show()
+		$('#buddy-groupChat').insertBefore('#buddiesOnline')
+	}
+	else {
+		$('#buddy-groupChat,#status').hide()
+	}
+	$('#fill').stop().animate({
+		'width': '100%', 'opacity': '1'
+	}, 250, 'linear')
+	window.setTimeout(function() {
+		$('#dialogBoxClose').click()
+	}, 400)
+	window.setTimeout(function() {
+		$('#loginOptions,#languages,#customServerDialog').fadeOut(200)
+		$('#version,#logoText,#loginInfo,#info').fadeOut(200)
+		$('#header').animate({'background-color': '#303040'})
+		$('.logo').animate({'margin': '-11px 5px 0 0'})
+		$('#login').fadeOut(200, function() {
+			$('#conversationInfo').fadeIn()
+			if (Cryptocat.me.login === 'cryptocat') {
+				$('#buddy-groupChat').click(function() {
+					Cryptocat.onBuddyClick($(this))
+				})
+				$('#buddy-groupChat').click()
+			}
+			$('#conversationWrapper').fadeIn()
+			$('#optionButtons').fadeIn()
+			$('#footer').delay(200).animate({'height': 60}, function() {
+				$('#userInput').fadeIn(200, function() {
+					$('#userInputText').focus()
+				})
+			})
+			$('#buddyWrapper').slideDown()
+		})
+	}, 800)
+	Cryptocat.loginError = true
+}
+
+// Reconnect to the same chatroom, on accidental connection loss.
+Cryptocat.xmpp.reconnect = function() {
+	if (Cryptocat.xmpp.connection) {
+	    Cryptocat.xmpp.connection.reset()
+	}
+	Cryptocat.xmpp.connection = new Strophe.Connection(Cryptocat.xmpp.relay)
+	Cryptocat.xmpp.connection.connect(Cryptocat.xmpp.domain, null, function(status) {
+		if (status === Strophe.Status.CONNECTING) {
+			$('.conversationName').animate({'background-color': '#F00'})
+		}
+		else if (status === Strophe.Status.CONNECTED) {
+			afterConnect()
+			if (Cryptocat.me.login === 'cryptocat') {
+				Cryptocat.xmpp.connection.muc.join(
+					Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+					Cryptocat.me.nickname
+				)
+			}
+		}
+		else if ((status === Strophe.Status.CONNFAIL) || (status === Strophe.Status.DISCONNECTED)) {
+			if (Cryptocat.loginError) {
+				window.setTimeout(function() {
+					Cryptocat.xmpp.reconnect()
+				}, 5000)
+			}
+		}
+	})
+}
+
+// Handle incoming messages from the XMPP server.
+Cryptocat.xmpp.onMessage = function(message) {
+	var nickname = cleanNickname($(message).attr('from'))
+	var body = $(message).find('body').text()
+	var type = $(message).attr('type')
+	// If archived message, ignore.
+	if ($(message).find('delay').length !== 0) {
+		return true
+	}
+	//If message is from me, ignore.
+	if (nickname === Cryptocat.me.nickname) {
+		return true
+	}
+	// If message is from someone not on buddy list, ignore.
+	if (!Cryptocat.buddies.hasOwnProperty(nickname)) {
+		return true
+	}
+	// Check if message has a 'composing' notification.
+	if ($(message).find('composing').length && !body.length) {
+		$('#buddy-' + Cryptocat.buddies[nickname].id).addClass('composing')
+		return true
+	}
+	// Check if message has a 'paused' (stopped writing) notification.
+	if ($(message).find('paused').length) {
+		$('#buddy-' + Cryptocat.buddies[nickname].id).removeClass('composing')
+	}
+	// Check if message is a group chat message.
+	else if (type === 'groupchat' && body.length) {
+		$('#buddy-' + Cryptocat.buddies[nickname].id).removeClass('composing')
+		body = Cryptocat.multiParty.receiveMessage(nickname, Cryptocat.me.nickname, body)
+		if (typeof(body) === 'string') {
+			Cryptocat.addToConversation(body, nickname, 'groupChat', 'message')
+		}
+	}
+	// Check if this is a private OTR message.
+	else if (type === 'chat') {
+		$('#buddy-' + Cryptocat.buddies[nickname].id).removeClass('composing')
+		Cryptocat.buddies[nickname].otr.receiveMsg(body)
+	}
+	return true
+}
+
+// Handle incoming presence updates from the XMPP server.
+Cryptocat.xmpp.onPresence = function(presence) {
+	var status
+	var nickname = cleanNickname($(presence).attr('from'))
+	// If invalid nickname, do not process
+	if ($(presence).attr('type') === 'error') {
+		if ($(presence).find('error').attr('code') === '409') {
+			// Delay logout in order to avoid race condition with window animation
+			window.setTimeout(function() {
+				Cryptocat.logout()
+				Cryptocat.loginFail(Cryptocat.locale['loginMessage']['nicknameInUse'])
+			}, 3000)
+			return false
+		}
+		return true
+	}
+	// Ignore if presence status is coming from myself
+	if (nickname === Cryptocat.me.nickname) {
+		return true
+	}
+	// Detect nickname change (which may be done by non-Cryptocat XMPP clients)
+	if ($(presence).find('status').attr('code') === '303') {
+		Cryptocat.removeBuddy(nickname)
+		return true
+	}
+	// Detect buddy going offline.
+	if ($(presence).attr('type') === 'unavailable') {
+		Cryptocat.removeBuddy(nickname)
+		return true
+	}
+	// Create buddy element if buddy is new.
+	else if (!Cryptocat.buddies.hasOwnProperty(nickname)) {
+		Cryptocat.addBuddy(nickname, null, 'online')
+		for (var u = 0; u < 4000; u += 2000) {
+			window.setTimeout(Cryptocat.xmpp.sendPublicKey, u, nickname)
+		}
+	}
+	// Handle buddy status change to 'available'.
+	else if (
+		$(presence).find('show').text() === '' ||
+		$(presence).find('show').text() === 'chat'
+	) {
+		status = 'online'
+	}
+	// Handle buddy status change to 'away'.
+	else {
+		status = 'away'
+	}
+	// Perform status change.
+	Cryptocat.buddyStatus(nickname, status)
+	return true
+}
+
+// Send your own multiparty public key to `nickname`, via XMPP-MUC.
+Cryptocat.xmpp.sendPublicKey = function(nickname) {
+	Cryptocat.xmpp.connection.muc.message(
+		Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+		null, Cryptocat.multiParty.sendPublicKey(nickname), null, 'groupchat', 'active'
+	)
+}
+
+// Send your current status to the XMPP server.
+Cryptocat.xmpp.sendStatus = function() {
+        var status = ''
+	if (Cryptocat.xmpp.currentStatus === 'away') {
+                status = 'away'
+	}
+	Cryptocat.xmpp.connection.muc.setStatus(
+		Cryptocat.me.conversation + '@' + Cryptocat.xmpp.conferenceServer,
+		Cryptocat.me.nickname, status, status
+	)
+}
+
+// Executed (manually) after connection.
+var afterConnect = function() {
+	$('.conversationName').animate({'background-color': '#97CEEC'})
+	Cryptocat.xmpp.connection.ibb.addIBBHandler(Cryptocat.otr.ibbHandler)
+	/* jshint -W106 */
+	Cryptocat.xmpp.connection.si_filetransfer.addFileHandler(Cryptocat.otr.fileHandler)
+	/* jshint +W106 */
+	if (Cryptocat.audioNotifications) {
+		Cryptocat.sounds.keygenLoop.pause()
+		Cryptocat.sounds.keygenEnd.play()
+	}
+}
+
+// Clean nickname so that it's safe to use.
+var cleanNickname = function(nickname) {
+	var clean = nickname.match(/\/([\s\S]+)/)
+	if (clean) {
+		return clean[1]
+	}
+	return false
+}
+
+})
diff --git a/src/core/js/lib/bigint.js b/src/core/js/lib/bigint.js
new file mode 100755
index 0000000..e8fd764
--- /dev/null
+++ b/src/core/js/lib/bigint.js
@@ -0,0 +1,1705 @@
+;(function (root, factory) {
+
+  if (typeof define === 'function' && define.amd) {
+    define(['../etc/random'], factory)
+  } else if (typeof module !== 'undefined' && module.exports) {
+    module.exports = factory(require('../etc/random.js'))
+  } else {
+    root.BigInt = factory(root.Cryptocat)
+  }
+
+}(this, function (Cryptocat) {
+
+  ////////////////////////////////////////////////////////////////////////////////////////
+  // Big Integer Library v. 5.5
+  // Created 2000, last modified 2013
+  // Leemon Baird
+  // www.leemon.com
+  //
+  // Version history:
+  // v 5.5  17 Mar 2013
+  //   - two lines of a form like "if (x<0) x+=n" had the "if" changed to "while" to
+  //     handle the case when x<-n. (Thanks to James Ansell for finding that bug)
+  // v 5.4  3 Oct 2009
+  //   - added "var i" to greaterShift() so i is not global. (Thanks to Péter Szabó for finding that bug)
+  //
+  // v 5.3  21 Sep 2009
+  //   - added randProbPrime(k) for probable primes
+  //   - unrolled loop in mont_ (slightly faster)
+  //   - millerRabin now takes a bigInt parameter rather than an int
+  //
+  // v 5.2  15 Sep 2009
+  //   - fixed capitalization in call to int2bigInt in randBigInt
+  //     (thanks to Emili Evripidou, Reinhold Behringer, and Samuel Macaleese for finding that bug)
+  //
+  // v 5.1  8 Oct 2007
+  //   - renamed inverseModInt_ to inverseModInt since it doesn't change its parameters
+  //   - added functions GCD and randBigInt, which call GCD_ and randBigInt_
+  //   - fixed a bug found by Rob Visser (see comment with his name below)
+  //   - improved comments
+  //
+  // This file is public domain.   You can use it for any purpose without restriction.
+  // I do not guarantee that it is correct, so use it at your own risk.  If you use
+  // it for something interesting, I'd appreciate hearing about it.  If you find
+  // any bugs or make any improvements, I'd appreciate hearing about those too.
+  // It would also be nice if my name and URL were left in the comments.  But none
+  // of that is required.
+  //
+  // This code defines a bigInt library for arbitrary-precision integers.
+  // A bigInt is an array of integers storing the value in chunks of bpe bits,
+  // little endian (buff[0] is the least significant word).
+  // Negative bigInts are stored two's complement.  Almost all the functions treat
+  // bigInts as nonnegative.  The few that view them as two's complement say so
+  // in their comments.  Some functions assume their parameters have at least one
+  // leading zero element. Functions with an underscore at the end of the name put
+  // their answer into one of the arrays passed in, and have unpredictable behavior
+  // in case of overflow, so the caller must make sure the arrays are big enough to
+  // hold the answer.  But the average user should never have to call any of the
+  // underscored functions.  Each important underscored function has a wrapper function
+  // of the same name without the underscore that takes care of the details for you.
+  // For each underscored function where a parameter is modified, that same variable
+  // must not be used as another argument too.  So, you cannot square x by doing
+  // multMod_(x,x,n).  You must use squareMod_(x,n) instead, or do y=dup(x); multMod_(x,y,n).
+  // Or simply use the multMod(x,x,n) function without the underscore, where
+  // such issues never arise, because non-underscored functions never change
+  // their parameters; they always allocate new memory for the answer that is returned.
+  //
+  // These functions are designed to avoid frequent dynamic memory allocation in the inner loop.
+  // For most functions, if it needs a BigInt as a local variable it will actually use
+  // a global, and will only allocate to it only when it's not the right size.  This ensures
+  // that when a function is called repeatedly with same-sized parameters, it only allocates
+  // memory on the first call.
+  //
+  // Note that for cryptographic purposes, the calls to Math.random() must
+  // be replaced with calls to a better pseudorandom number generator.
+  //
+  // In the following, "bigInt" means a bigInt with at least one leading zero element,
+  // and "integer" means a nonnegative integer less than radix.  In some cases, integer
+  // can be negative.  Negative bigInts are 2s complement.
+  //
+  // The following functions do not modify their inputs.
+  // Those returning a bigInt, string, or Array will dynamically allocate memory for that value.
+  // Those returning a boolean will return the integer 0 (false) or 1 (true).
+  // Those returning boolean or int will not allocate memory except possibly on the first
+  // time they're called with a given parameter size.
+  //
+  // bigInt  add(x,y)               //return (x+y) for bigInts x and y.
+  // bigInt  addInt(x,n)            //return (x+n) where x is a bigInt and n is an integer.
+  // string  bigInt2str(x,base)     //return a string form of bigInt x in a given base, with 2 <= base <= 95
+  // int     bitSize(x)             //return how many bits long the bigInt x is, not counting leading zeros
+  // bigInt  dup(x)                 //return a copy of bigInt x
+  // boolean equals(x,y)            //is the bigInt x equal to the bigint y?
+  // boolean equalsInt(x,y)         //is bigint x equal to integer y?
+  // bigInt  expand(x,n)            //return a copy of x with at least n elements, adding leading zeros if needed
+  // Array   findPrimes(n)          //return array of all primes less than integer n
+  // bigInt  GCD(x,y)               //return greatest common divisor of bigInts x and y (each with same number of elements).
+  // boolean greater(x,y)           //is x>y?  (x and y are nonnegative bigInts)
+  // boolean greaterShift(x,y,shift)//is (x <<(shift*bpe)) > y?
+  // bigInt  int2bigInt(t,n,m)      //return a bigInt equal to integer t, with at least n bits and m array elements
+  // bigInt  inverseMod(x,n)        //return (x**(-1) mod n) for bigInts x and n.  If no inverse exists, it returns null
+  // int     inverseModInt(x,n)     //return x**(-1) mod n, for integers x and n.  Return 0 if there is no inverse
+  // boolean isZero(x)              //is the bigInt x equal to zero?
+  // boolean millerRabin(x,b)       //does one round of Miller-Rabin base integer b say that bigInt x is possibly prime? (b is bigInt, 1<b<x)
+  // boolean millerRabinInt(x,b)    //does one round of Miller-Rabin base integer b say that bigInt x is possibly prime? (b is int,    1<b<x)
+  // bigInt  mod(x,n)               //return a new bigInt equal to (x mod n) for bigInts x and n.
+  // int     modInt(x,n)            //return x mod n for bigInt x and integer n.
+  // bigInt  mult(x,y)              //return x*y for bigInts x and y. This is faster when y<x.
+  // bigInt  multMod(x,y,n)         //return (x*y mod n) for bigInts x,y,n.  For greater speed, let y<x.
+  // boolean negative(x)            //is bigInt x negative?
+  // bigInt  powMod(x,y,n)          //return (x**y mod n) where x,y,n are bigInts and ** is exponentiation.  0**0=1. Faster for odd n.
+  // bigInt  randBigInt(n,s)        //return an n-bit random BigInt (n>=1).  If s=1, then the most significant of those n bits is set to 1.
+  // bigInt  randTruePrime(k)       //return a new, random, k-bit, true prime bigInt using Maurer's algorithm.
+  // bigInt  randProbPrime(k)       //return a new, random, k-bit, probable prime bigInt (probability it's composite less than 2^-80).
+  // bigInt  str2bigInt(s,b,n,m)    //return a bigInt for number represented in string s in base b with at least n bits and m array elements
+  // bigInt  sub(x,y)               //return (x-y) for bigInts x and y.  Negative answers will be 2s complement
+  // bigInt  trim(x,k)              //return a copy of x with exactly k leading zero elements
+  //
+  //
+  // The following functions each have a non-underscored version, which most users should call instead.
+  // These functions each write to a single parameter, and the caller is responsible for ensuring the array
+  // passed in is large enough to hold the result.
+  //
+  // void    addInt_(x,n)          //do x=x+n where x is a bigInt and n is an integer
+  // void    add_(x,y)             //do x=x+y for bigInts x and y
+  // void    copy_(x,y)            //do x=y on bigInts x and y
+  // void    copyInt_(x,n)         //do x=n on bigInt x and integer n
+  // void    GCD_(x,y)             //set x to the greatest common divisor of bigInts x and y, (y is destroyed).  (This never overflows its array).
+  // boolean inverseMod_(x,n)      //do x=x**(-1) mod n, for bigInts x and n. Returns 1 (0) if inverse does (doesn't) exist
+  // void    mod_(x,n)             //do x=x mod n for bigInts x and n. (This never overflows its array).
+  // void    mult_(x,y)            //do x=x*y for bigInts x and y.
+  // void    multMod_(x,y,n)       //do x=x*y  mod n for bigInts x,y,n.
+  // void    powMod_(x,y,n)        //do x=x**y mod n, where x,y,n are bigInts (n is odd) and ** is exponentiation.  0**0=1.
+  // void    randBigInt_(b,n,s)    //do b = an n-bit random BigInt. if s=1, then nth bit (most significant bit) is set to 1. n>=1.
+  // void    randTruePrime_(ans,k) //do ans = a random k-bit true random prime (not just probable prime) with 1 in the msb.
+  // void    sub_(x,y)             //do x=x-y for bigInts x and y. Negative answers will be 2s complement.
+  //
+  // The following functions do NOT have a non-underscored version.
+  // They each write a bigInt result to one or more parameters.  The caller is responsible for
+  // ensuring the arrays passed in are large enough to hold the results.
+  //
+  // void addShift_(x,y,ys)       //do x=x+(y<<(ys*bpe))
+  // void carry_(x)               //do carries and borrows so each element of the bigInt x fits in bpe bits.
+  // void divide_(x,y,q,r)        //divide x by y giving quotient q and remainder r
+  // int  divInt_(x,n)            //do x=floor(x/n) for bigInt x and integer n, and return the remainder. (This never overflows its array).
+  // int  eGCD_(x,y,d,a,b)        //sets a,b,d to positive bigInts such that d = GCD_(x,y) = a*x-b*y
+  // void halve_(x)               //do x=floor(|x|/2)*sgn(x) for bigInt x in 2's complement.  (This never overflows its array).
+  // void leftShift_(x,n)         //left shift bigInt x by n bits.  n<bpe.
+  // void linComb_(x,y,a,b)       //do x=a*x+b*y for bigInts x and y and integers a and b
+  // void linCombShift_(x,y,b,ys) //do x=x+b*(y<<(ys*bpe)) for bigInts x and y, and integers b and ys
+  // void mont_(x,y,n,np)         //Montgomery multiplication (see comments where the function is defined)
+  // void multInt_(x,n)           //do x=x*n where x is a bigInt and n is an integer.
+  // void rightShift_(x,n)        //right shift bigInt x by n bits. (This never overflows its array).
+  // void squareMod_(x,n)         //do x=x*x  mod n for bigInts x,n
+  // void subShift_(x,y,ys)       //do x=x-(y<<(ys*bpe)). Negative answers will be 2s complement.
+  //
+  // The following functions are based on algorithms from the _Handbook of Applied Cryptography_
+  //    powMod_()           = algorithm 14.94, Montgomery exponentiation
+  //    eGCD_,inverseMod_() = algorithm 14.61, Binary extended GCD_
+  //    GCD_()              = algorothm 14.57, Lehmer's algorithm
+  //    mont_()             = algorithm 14.36, Montgomery multiplication
+  //    divide_()           = algorithm 14.20  Multiple-precision division
+  //    squareMod_()        = algorithm 14.16  Multiple-precision squaring
+  //    randTruePrime_()    = algorithm  4.62, Maurer's algorithm
+  //    millerRabin()       = algorithm  4.24, Miller-Rabin algorithm
+  //
+  // Profiling shows:
+  //     randTruePrime_() spends:
+  //         10% of its time in calls to powMod_()
+  //         85% of its time in calls to millerRabin()
+  //     millerRabin() spends:
+  //         99% of its time in calls to powMod_()   (always with a base of 2)
+  //     powMod_() spends:
+  //         94% of its time in calls to mont_()  (almost always with x==y)
+  //
+  // This suggests there are several ways to speed up this library slightly:
+  //     - convert powMod_ to use a Montgomery form of k-ary window (or maybe a Montgomery form of sliding window)
+  //         -- this should especially focus on being fast when raising 2 to a power mod n
+  //     - convert randTruePrime_() to use a minimum r of 1/3 instead of 1/2 with the appropriate change to the test
+  //     - tune the parameters in randTruePrime_(), including c, m, and recLimit
+  //     - speed up the single loop in mont_() that takes 95% of the runtime, perhaps by reducing checking
+  //       within the loop when all the parameters are the same length.
+  //
+  // There are several ideas that look like they wouldn't help much at all:
+  //     - replacing trial division in randTruePrime_() with a sieve (that speeds up something taking almost no time anyway)
+  //     - increase bpe from 15 to 30 (that would help if we had a 32*32->64 multiplier, but not with JavaScript's 32*32->32)
+  //     - speeding up mont_(x,y,n,np) when x==y by doing a non-modular, non-Montgomery square
+  //       followed by a Montgomery reduction.  The intermediate answer will be twice as long as x, so that
+  //       method would be slower.  This is unfortunate because the code currently spends almost all of its time
+  //       doing mont_(x,x,...), both for randTruePrime_() and powMod_().  A faster method for Montgomery squaring
+  //       would have a large impact on the speed of randTruePrime_() and powMod_().  HAC has a couple of poorly-worded
+  //       sentences that seem to imply it's faster to do a non-modular square followed by a single
+  //       Montgomery reduction, but that's obviously wrong.
+  ////////////////////////////////////////////////////////////////////////////////////////
+
+  //globals
+
+  // The number of significant bits in the fraction of a JavaScript
+  // floating-point number is 52, independent of platform.
+  // See: https://github.com/arlolra/otr/issues/41
+
+  var bpe = 15;          // bits stored per array element
+  var radix = 1 << bpe;  // equals 2^bpe
+  var mask = radix - 1;  // AND this with an array element to chop it down to bpe bits
+
+  //the digits for converting to different bases
+  var digitsStr='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_=!@#$%^&*()[]{}|;:,.<>/?`~ \\\'\"+-';
+
+  var one=int2bigInt(1,1,1);     //constant used in powMod_()
+
+  //the following global variables are scratchpad memory to
+  //reduce dynamic memory allocation in the inner loop
+  var t=new Array(0);
+  var ss=t;       //used in mult_()
+  var s0=t;       //used in multMod_(), squareMod_()
+  var s1=t;       //used in powMod_(), multMod_(), squareMod_()
+  var s2=t;       //used in powMod_(), multMod_()
+  var s3=t;       //used in powMod_()
+  var s4=t, s5=t; //used in mod_()
+  var s6=t;       //used in bigInt2str()
+  var s7=t;       //used in powMod_()
+  var T=t;        //used in GCD_()
+  var sa=t;       //used in mont_()
+  var mr_x1=t, mr_r=t, mr_a=t;                                      //used in millerRabin()
+  var eg_v=t, eg_u=t, eg_A=t, eg_B=t, eg_C=t, eg_D=t;               //used in eGCD_(), inverseMod_()
+  var md_q1=t, md_q2=t, md_q3=t, md_r=t, md_r1=t, md_r2=t, md_tt=t; //used in mod_()
+
+  var primes=t, pows=t, s_i=t, s_i2=t, s_R=t, s_rm=t, s_q=t, s_n1=t;
+  var s_a=t, s_r2=t, s_n=t, s_b=t, s_d=t, s_x1=t, s_x2=t, s_aa=t; //used in randTruePrime_()
+
+  var rpprb=t; //used in randProbPrimeRounds() (which also uses "primes")
+
+  ////////////////////////////////////////////////////////////////////////////////////////
+
+
+  //return array of all primes less than integer n
+  function findPrimes(n) {
+    var i,s,p,ans;
+    s=new Array(n);
+    for (i=0;i<n;i++)
+      s[i]=0;
+    s[0]=2;
+    p=0;    //first p elements of s are primes, the rest are a sieve
+    for(;s[p]<n;) {                  //s[p] is the pth prime
+      for(i=s[p]*s[p]; i<n; i+=s[p]) //mark multiples of s[p]
+        s[i]=1;
+      p++;
+      s[p]=s[p-1]+1;
+      for(; s[p]<n && s[s[p]]; s[p]++); //find next prime (where s[p]==0)
+    }
+    ans=new Array(p);
+    for(i=0;i<p;i++)
+      ans[i]=s[i];
+    return ans;
+  }
+
+
+  //does a single round of Miller-Rabin base b consider x to be a possible prime?
+  //x is a bigInt, and b is an integer, with b<x
+  function millerRabinInt(x,b) {
+    if (mr_x1.length!=x.length) {
+      mr_x1=dup(x);
+      mr_r=dup(x);
+      mr_a=dup(x);
+    }
+
+    copyInt_(mr_a,b);
+    return millerRabin(x,mr_a);
+  }
+
+  //does a single round of Miller-Rabin base b consider x to be a possible prime?
+  //x and b are bigInts with b<x
+  function millerRabin(x,b) {
+    var i,j,k,s;
+
+    if (mr_x1.length!=x.length) {
+      mr_x1=dup(x);
+      mr_r=dup(x);
+      mr_a=dup(x);
+    }
+
+    copy_(mr_a,b);
+    copy_(mr_r,x);
+    copy_(mr_x1,x);
+
+    addInt_(mr_r,-1);
+    addInt_(mr_x1,-1);
+
+    //s=the highest power of two that divides mr_r
+
+    /*
+    k=0;
+    for (i=0;i<mr_r.length;i++)
+      for (j=1;j<mask;j<<=1)
+        if (x[i] & j) {
+          s=(k<mr_r.length+bpe ? k : 0);
+           i=mr_r.length;
+           j=mask;
+        } else
+          k++;
+    */
+
+    /* http://www.javascripter.net/math/primes/millerrabinbug-bigint54.htm */
+    if (isZero(mr_r)) return 0;
+    for (k=0; mr_r[k]==0; k++);
+    for (i=1,j=2; mr_r[k]%j==0; j*=2,i++ );
+    s = k*bpe + i - 1;
+    /* end */
+
+    if (s)
+      rightShift_(mr_r,s);
+
+    powMod_(mr_a,mr_r,x);
+
+    if (!equalsInt(mr_a,1) && !equals(mr_a,mr_x1)) {
+      j=1;
+      while (j<=s-1 && !equals(mr_a,mr_x1)) {
+        squareMod_(mr_a,x);
+        if (equalsInt(mr_a,1)) {
+          return 0;
+        }
+        j++;
+      }
+      if (!equals(mr_a,mr_x1)) {
+        return 0;
+      }
+    }
+    return 1;
+  }
+
+  //returns how many bits long the bigInt is, not counting leading zeros.
+  function bitSize(x) {
+    var j,z,w;
+    for (j=x.length-1; (x[j]==0) && (j>0); j--);
+    for (z=0,w=x[j]; w; (w>>=1),z++);
+    z+=bpe*j;
+    return z;
+  }
+
+  //return a copy of x with at least n elements, adding leading zeros if needed
+  function expand(x,n) {
+    var ans=int2bigInt(0,(x.length>n ? x.length : n)*bpe,0);
+    copy_(ans,x);
+    return ans;
+  }
+
+  //return a k-bit true random prime using Maurer's algorithm.
+  function randTruePrime(k) {
+    var ans=int2bigInt(0,k,0);
+    randTruePrime_(ans,k);
+    return trim(ans,1);
+  }
+
+  //return a k-bit random probable prime with probability of error < 2^-80
+  function randProbPrime(k) {
+    if (k>=600) return randProbPrimeRounds(k,2); //numbers from HAC table 4.3
+    if (k>=550) return randProbPrimeRounds(k,4);
+    if (k>=500) return randProbPrimeRounds(k,5);
+    if (k>=400) return randProbPrimeRounds(k,6);
+    if (k>=350) return randProbPrimeRounds(k,7);
+    if (k>=300) return randProbPrimeRounds(k,9);
+    if (k>=250) return randProbPrimeRounds(k,12); //numbers from HAC table 4.4
+    if (k>=200) return randProbPrimeRounds(k,15);
+    if (k>=150) return randProbPrimeRounds(k,18);
+    if (k>=100) return randProbPrimeRounds(k,27);
+                return randProbPrimeRounds(k,40); //number from HAC remark 4.26 (only an estimate)
+  }
+
+  //return a k-bit probable random prime using n rounds of Miller Rabin (after trial division with small primes)
+  function randProbPrimeRounds(k,n) {
+    var ans, i, divisible, B;
+    B=30000;  //B is largest prime to use in trial division
+    ans=int2bigInt(0,k,0);
+
+    //optimization: try larger and smaller B to find the best limit.
+
+    if (primes.length==0)
+      primes=findPrimes(30000);  //check for divisibility by primes <=30000
+
+    if (rpprb.length!=ans.length)
+      rpprb=dup(ans);
+
+    for (;;) { //keep trying random values for ans until one appears to be prime
+      //optimization: pick a random number times L=2*3*5*...*p, plus a
+      //   random element of the list of all numbers in [0,L) not divisible by any prime up to p.
+      //   This can reduce the amount of random number generation.
+
+      randBigInt_(ans,k,0); //ans = a random odd number to check
+      ans[0] |= 1;
+      divisible=0;
+
+      //check ans for divisibility by small primes up to B
+      for (i=0; (i<primes.length) && (primes[i]<=B); i++)
+        if (modInt(ans,primes[i])==0 && !equalsInt(ans,primes[i])) {
+          divisible=1;
+          break;
+        }
+
+      //optimization: change millerRabin so the base can be bigger than the number being checked, then eliminate the while here.
+
+      //do n rounds of Miller Rabin, with random bases less than ans
+      for (i=0; i<n && !divisible; i++) {
+        randBigInt_(rpprb,k,0);
+        while(!greater(ans,rpprb)) //pick a random rpprb that's < ans
+          randBigInt_(rpprb,k,0);
+        if (!millerRabin(ans,rpprb))
+          divisible=1;
+      }
+
+      if(!divisible)
+        return ans;
+    }
+  }
+
+  //return a new bigInt equal to (x mod n) for bigInts x and n.
+  function mod(x,n) {
+    var ans=dup(x);
+    mod_(ans,n);
+    return trim(ans,1);
+  }
+
+  //return (x+n) where x is a bigInt and n is an integer.
+  function addInt(x,n) {
+    var ans=expand(x,x.length+1);
+    addInt_(ans,n);
+    return trim(ans,1);
+  }
+
+  //return x*y for bigInts x and y. This is faster when y<x.
+  function mult(x,y) {
+    var ans=expand(x,x.length+y.length);
+    mult_(ans,y);
+    return trim(ans,1);
+  }
+
+  //return (x**y mod n) where x,y,n are bigInts and ** is exponentiation.  0**0=1. Faster for odd n.
+  function powMod(x,y,n) {
+    var ans=expand(x,n.length);
+    powMod_(ans,trim(y,2),trim(n,2),0);  //this should work without the trim, but doesn't
+    return trim(ans,1);
+  }
+
+  //return (x-y) for bigInts x and y.  Negative answers will be 2s complement
+  function sub(x,y) {
+    var ans=expand(x,(x.length>y.length ? x.length+1 : y.length+1));
+    sub_(ans,y);
+    return trim(ans,1);
+  }
+
+  //return (x+y) for bigInts x and y.
+  function add(x,y) {
+    var ans=expand(x,(x.length>y.length ? x.length+1 : y.length+1));
+    add_(ans,y);
+    return trim(ans,1);
+  }
+
+  //return (x**(-1) mod n) for bigInts x and n.  If no inverse exists, it returns null
+  function inverseMod(x,n) {
+    var ans=expand(x,n.length);
+    var s;
+    s=inverseMod_(ans,n);
+    return s ? trim(ans,1) : null;
+  }
+
+  //return (x*y mod n) for bigInts x,y,n.  For greater speed, let y<x.
+  function multMod(x,y,n) {
+    var ans=expand(x,n.length);
+    multMod_(ans,y,n);
+    return trim(ans,1);
+  }
+
+  //generate a k-bit true random prime using Maurer's algorithm,
+  //and put it into ans.  The bigInt ans must be large enough to hold it.
+  function randTruePrime_(ans,k) {
+    var c,w,m,pm,dd,j,r,B,divisible,z,zz,recSize,recLimit;
+
+    if (primes.length==0)
+      primes=findPrimes(30000);  //check for divisibility by primes <=30000
+
+    if (pows.length==0) {
+      pows=new Array(512);
+      for (j=0;j<512;j++) {
+        pows[j]=Math.pow(2,j/511.0-1.0);
+      }
+    }
+
+    //c and m should be tuned for a particular machine and value of k, to maximize speed
+    c=0.1;  //c=0.1 in HAC
+    m=20;   //generate this k-bit number by first recursively generating a number that has between k/2 and k-m bits
+    recLimit=20; //stop recursion when k <=recLimit.  Must have recLimit >= 2
+
+    if (s_i2.length!=ans.length) {
+      s_i2=dup(ans);
+      s_R =dup(ans);
+      s_n1=dup(ans);
+      s_r2=dup(ans);
+      s_d =dup(ans);
+      s_x1=dup(ans);
+      s_x2=dup(ans);
+      s_b =dup(ans);
+      s_n =dup(ans);
+      s_i =dup(ans);
+      s_rm=dup(ans);
+      s_q =dup(ans);
+      s_a =dup(ans);
+      s_aa=dup(ans);
+    }
+
+    if (k <= recLimit) {  //generate small random primes by trial division up to its square root
+      pm=(1<<((k+2)>>1))-1; //pm is binary number with all ones, just over sqrt(2^k)
+      copyInt_(ans,0);
+      for (dd=1;dd;) {
+        dd=0;
+        ans[0]= 1 | (1<<(k-1)) | Cryptocat.random.bitInt(k);  //random, k-bit, odd integer, with msb 1
+        for (j=1;(j<primes.length) && ((primes[j]&pm)==primes[j]);j++) { //trial division by all primes 3...sqrt(2^k)
+          if (0==(ans[0]%primes[j])) {
+            dd=1;
+            break;
+          }
+        }
+      }
+      carry_(ans);
+      return;
+    }
+
+    B=c*k*k;    //try small primes up to B (or all the primes[] array if the largest is less than B).
+    if (k>2*m)  //generate this k-bit number by first recursively generating a number that has between k/2 and k-m bits
+      for (r=1; k-k*r<=m; )
+        r=pows[Cryptocat.random.bitInt(9)];
+    else
+      r=0.5;
+
+    //simulation suggests the more complex algorithm using r=.333 is only slightly faster.
+
+    recSize=Math.floor(r*k)+1;
+
+    randTruePrime_(s_q,recSize);
+    copyInt_(s_i2,0);
+    s_i2[Math.floor((k-2)/bpe)] |= (1<<((k-2)%bpe));   //s_i2=2^(k-2)
+    divide_(s_i2,s_q,s_i,s_rm);                        //s_i=floor((2^(k-1))/(2q))
+
+    z=bitSize(s_i);
+
+    for (;;) {
+      for (;;) {  //generate z-bit numbers until one falls in the range [0,s_i-1]
+        randBigInt_(s_R,z,0);
+        if (greater(s_i,s_R))
+          break;
+      }                //now s_R is in the range [0,s_i-1]
+      addInt_(s_R,1);  //now s_R is in the range [1,s_i]
+      add_(s_R,s_i);   //now s_R is in the range [s_i+1,2*s_i]
+
+      copy_(s_n,s_q);
+      mult_(s_n,s_R);
+      multInt_(s_n,2);
+      addInt_(s_n,1);    //s_n=2*s_R*s_q+1
+
+      copy_(s_r2,s_R);
+      multInt_(s_r2,2);  //s_r2=2*s_R
+
+      //check s_n for divisibility by small primes up to B
+      for (divisible=0,j=0; (j<primes.length) && (primes[j]<B); j++)
+        if (modInt(s_n,primes[j])==0 && !equalsInt(s_n,primes[j])) {
+          divisible=1;
+          break;
+        }
+
+      if (!divisible)    //if it passes small primes check, then try a single Miller-Rabin base 2
+        if (!millerRabinInt(s_n,2)) //this line represents 75% of the total runtime for randTruePrime_
+          divisible=1;
+
+      if (!divisible) {  //if it passes that test, continue checking s_n
+        addInt_(s_n,-3);
+        for (j=s_n.length-1;(s_n[j]==0) && (j>0); j--);  //strip leading zeros
+        for (zz=0,w=s_n[j]; w; (w>>=1),zz++);
+        zz+=bpe*j;                             //zz=number of bits in s_n, ignoring leading zeros
+        for (;;) {  //generate z-bit numbers until one falls in the range [0,s_n-1]
+          randBigInt_(s_a,zz,0);
+          if (greater(s_n,s_a))
+            break;
+        }                //now s_a is in the range [0,s_n-1]
+        addInt_(s_n,3);  //now s_a is in the range [0,s_n-4]
+        addInt_(s_a,2);  //now s_a is in the range [2,s_n-2]
+        copy_(s_b,s_a);
+        copy_(s_n1,s_n);
+        addInt_(s_n1,-1);
+        powMod_(s_b,s_n1,s_n);   //s_b=s_a^(s_n-1) modulo s_n
+        addInt_(s_b,-1);
+        if (isZero(s_b)) {
+          copy_(s_b,s_a);
+          powMod_(s_b,s_r2,s_n);
+          addInt_(s_b,-1);
+          copy_(s_aa,s_n);
+          copy_(s_d,s_b);
+          GCD_(s_d,s_n);  //if s_b and s_n are relatively prime, then s_n is a prime
+          if (equalsInt(s_d,1)) {
+            copy_(ans,s_aa);
+            return;     //if we've made it this far, then s_n is absolutely guaranteed to be prime
+          }
+        }
+      }
+    }
+  }
+
+  //Return an n-bit random BigInt (n>=1).  If s=1, then the most significant of those n bits is set to 1.
+  function randBigInt(n,s) {
+    var a,b;
+    a=Math.floor((n-1)/bpe)+2; //# array elements to hold the BigInt with a leading 0 element
+    b=int2bigInt(0,0,a);
+    randBigInt_(b,n,s);
+    return b;
+  }
+
+  //Set b to an n-bit random BigInt.  If s=1, then the most significant of those n bits is set to 1.
+  //Array b must be big enough to hold the result. Must have n>=1
+  function randBigInt_(b,n,s) {
+    var i,a;
+    for (i=0;i<b.length;i++)
+      b[i]=0;
+    a=Math.floor((n-1)/bpe)+1; //# array elements to hold the BigInt
+    for (i=0;i<a;i++) {
+      b[i]=Cryptocat.random.bitInt(bpe);
+    }
+    b[a-1] &= (2<<((n-1)%bpe))-1;
+    if (s==1)
+      b[a-1] |= (1<<((n-1)%bpe));
+  }
+
+  //Return the greatest common divisor of bigInts x and y (each with same number of elements).
+  function GCD(x,y) {
+    var xc,yc;
+    xc=dup(x);
+    yc=dup(y);
+    GCD_(xc,yc);
+    return xc;
+  }
+
+  //set x to the greatest common divisor of bigInts x and y (each with same number of elements).
+  //y is destroyed.
+  function GCD_(x,y) {
+    var i,xp,yp,A,B,C,D,q,sing,qp;
+    if (T.length!=x.length)
+      T=dup(x);
+
+    sing=1;
+    while (sing) { //while y has nonzero elements other than y[0]
+      sing=0;
+      for (i=1;i<y.length;i++) //check if y has nonzero elements other than 0
+        if (y[i]) {
+          sing=1;
+          break;
+        }
+      if (!sing) break; //quit when y all zero elements except possibly y[0]
+
+      for (i=x.length;!x[i] && i>=0;i--);  //find most significant element of x
+      xp=x[i];
+      yp=y[i];
+      A=1; B=0; C=0; D=1;
+      while ((yp+C) && (yp+D)) {
+        q =Math.floor((xp+A)/(yp+C));
+        qp=Math.floor((xp+B)/(yp+D));
+        if (q!=qp)
+          break;
+        t= A-q*C;   A=C;   C=t;    //  do (A,B,xp, C,D,yp) = (C,D,yp, A,B,xp) - q*(0,0,0, C,D,yp)
+        t= B-q*D;   B=D;   D=t;
+        t=xp-q*yp; xp=yp; yp=t;
+      }
+      if (B) {
+        copy_(T,x);
+        linComb_(x,y,A,B); //x=A*x+B*y
+        linComb_(y,T,D,C); //y=D*y+C*T
+      } else {
+        mod_(x,y);
+        copy_(T,x);
+        copy_(x,y);
+        copy_(y,T);
+      }
+    }
+    if (y[0]==0)
+      return;
+    t=modInt(x,y[0]);
+    copyInt_(x,y[0]);
+    y[0]=t;
+    while (y[0]) {
+      x[0]%=y[0];
+      t=x[0]; x[0]=y[0]; y[0]=t;
+    }
+  }
+
+  //do x=x**(-1) mod n, for bigInts x and n.
+  //If no inverse exists, it sets x to zero and returns 0, else it returns 1.
+  //The x array must be at least as large as the n array.
+  function inverseMod_(x,n) {
+    var k=1+2*Math.max(x.length,n.length);
+
+    if(!(x[0]&1)  && !(n[0]&1)) {  //if both inputs are even, then inverse doesn't exist
+      copyInt_(x,0);
+      return 0;
+    }
+
+    if (eg_u.length!=k) {
+      eg_u=new Array(k);
+      eg_v=new Array(k);
+      eg_A=new Array(k);
+      eg_B=new Array(k);
+      eg_C=new Array(k);
+      eg_D=new Array(k);
+    }
+
+    copy_(eg_u,x);
+    copy_(eg_v,n);
+    copyInt_(eg_A,1);
+    copyInt_(eg_B,0);
+    copyInt_(eg_C,0);
+    copyInt_(eg_D,1);
+    for (;;) {
+      while(!(eg_u[0]&1)) {  //while eg_u is even
+        halve_(eg_u);
+        if (!(eg_A[0]&1) && !(eg_B[0]&1)) { //if eg_A==eg_B==0 mod 2
+          halve_(eg_A);
+          halve_(eg_B);
+        } else {
+          add_(eg_A,n);  halve_(eg_A);
+          sub_(eg_B,x);  halve_(eg_B);
+        }
+      }
+
+      while (!(eg_v[0]&1)) {  //while eg_v is even
+        halve_(eg_v);
+        if (!(eg_C[0]&1) && !(eg_D[0]&1)) { //if eg_C==eg_D==0 mod 2
+          halve_(eg_C);
+          halve_(eg_D);
+        } else {
+          add_(eg_C,n);  halve_(eg_C);
+          sub_(eg_D,x);  halve_(eg_D);
+        }
+      }
+
+      if (!greater(eg_v,eg_u)) { //eg_v <= eg_u
+        sub_(eg_u,eg_v);
+        sub_(eg_A,eg_C);
+        sub_(eg_B,eg_D);
+      } else {                   //eg_v > eg_u
+        sub_(eg_v,eg_u);
+        sub_(eg_C,eg_A);
+        sub_(eg_D,eg_B);
+      }
+
+      if (equalsInt(eg_u,0)) {
+        while (negative(eg_C)) //make sure answer is nonnegative
+          add_(eg_C,n);
+        copy_(x,eg_C);
+
+        if (!equalsInt(eg_v,1)) { //if GCD_(x,n)!=1, then there is no inverse
+          copyInt_(x,0);
+          return 0;
+        }
+        return 1;
+      }
+    }
+  }
+
+  //return x**(-1) mod n, for integers x and n.  Return 0 if there is no inverse
+  function inverseModInt(x,n) {
+    var a=1,b=0,t;
+    for (;;) {
+      if (x==1) return a;
+      if (x==0) return 0;
+      b-=a*Math.floor(n/x);
+      n%=x;
+
+      if (n==1) return b; //to avoid negatives, change this b to n-b, and each -= to +=
+      if (n==0) return 0;
+      a-=b*Math.floor(x/n);
+      x%=n;
+    }
+  }
+
+  //this deprecated function is for backward compatibility only.
+  function inverseModInt_(x,n) {
+     return inverseModInt(x,n);
+  }
+
+
+  //Given positive bigInts x and y, change the bigints v, a, and b to positive bigInts such that:
+  //     v = GCD_(x,y) = a*x-b*y
+  //The bigInts v, a, b, must have exactly as many elements as the larger of x and y.
+  function eGCD_(x,y,v,a,b) {
+    var g=0;
+    var k=Math.max(x.length,y.length);
+    if (eg_u.length!=k) {
+      eg_u=new Array(k);
+      eg_A=new Array(k);
+      eg_B=new Array(k);
+      eg_C=new Array(k);
+      eg_D=new Array(k);
+    }
+    while(!(x[0]&1)  && !(y[0]&1)) {  //while x and y both even
+      halve_(x);
+      halve_(y);
+      g++;
+    }
+    copy_(eg_u,x);
+    copy_(v,y);
+    copyInt_(eg_A,1);
+    copyInt_(eg_B,0);
+    copyInt_(eg_C,0);
+    copyInt_(eg_D,1);
+    for (;;) {
+      while(!(eg_u[0]&1)) {  //while u is even
+        halve_(eg_u);
+        if (!(eg_A[0]&1) && !(eg_B[0]&1)) { //if A==B==0 mod 2
+          halve_(eg_A);
+          halve_(eg_B);
+        } else {
+          add_(eg_A,y);  halve_(eg_A);
+          sub_(eg_B,x);  halve_(eg_B);
+        }
+      }
+
+      while (!(v[0]&1)) {  //while v is even
+        halve_(v);
+        if (!(eg_C[0]&1) && !(eg_D[0]&1)) { //if C==D==0 mod 2
+          halve_(eg_C);
+          halve_(eg_D);
+        } else {
+          add_(eg_C,y);  halve_(eg_C);
+          sub_(eg_D,x);  halve_(eg_D);
+        }
+      }
+
+      if (!greater(v,eg_u)) { //v<=u
+        sub_(eg_u,v);
+        sub_(eg_A,eg_C);
+        sub_(eg_B,eg_D);
+      } else {                //v>u
+        sub_(v,eg_u);
+        sub_(eg_C,eg_A);
+        sub_(eg_D,eg_B);
+      }
+      if (equalsInt(eg_u,0)) {
+        while (negative(eg_C)) {   //make sure a (C) is nonnegative
+          add_(eg_C,y);
+          sub_(eg_D,x);
+        }
+        multInt_(eg_D,-1);  ///make sure b (D) is nonnegative
+        copy_(a,eg_C);
+        copy_(b,eg_D);
+        leftShift_(v,g);
+        return;
+      }
+    }
+  }
+
+
+  //is bigInt x negative?
+  function negative(x) {
+    return ((x[x.length-1]>>(bpe-1))&1);
+  }
+
+
+  //is (x << (shift*bpe)) > y?
+  //x and y are nonnegative bigInts
+  //shift is a nonnegative integer
+  function greaterShift(x,y,shift) {
+    var i, kx=x.length, ky=y.length;
+    var k=((kx+shift)<ky) ? (kx+shift) : ky;
+    for (i=ky-1-shift; i<kx && i>=0; i++)
+      if (x[i]>0)
+        return 1; //if there are nonzeros in x to the left of the first column of y, then x is bigger
+    for (i=kx-1+shift; i<ky; i++)
+      if (y[i]>0)
+        return 0; //if there are nonzeros in y to the left of the first column of x, then x is not bigger
+    for (i=k-1; i>=shift; i--)
+      if      (x[i-shift]>y[i]) return 1;
+      else if (x[i-shift]<y[i]) return 0;
+    return 0;
+  }
+
+  //is x > y? (x and y both nonnegative)
+  function greater(x,y) {
+    var i;
+    var k=(x.length<y.length) ? x.length : y.length;
+
+    for (i=x.length;i<y.length;i++)
+      if (y[i])
+        return 0;  //y has more digits
+
+    for (i=y.length;i<x.length;i++)
+      if (x[i])
+        return 1;  //x has more digits
+
+    for (i=k-1;i>=0;i--)
+      if (x[i]>y[i])
+        return 1;
+      else if (x[i]<y[i])
+        return 0;
+    return 0;
+  }
+
+  //divide x by y giving quotient q and remainder r.  (q=floor(x/y),  r=x mod y).  All 4 are bigints.
+  //x must have at least one leading zero element.
+  //y must be nonzero.
+  //q and r must be arrays that are exactly the same length as x. (Or q can have more).
+  //Must have x.length >= y.length >= 2.
+  function divide_(x,y,q,r) {
+    var kx, ky;
+    var i,j,y1,y2,c,a,b;
+    copy_(r,x);
+    for (ky=y.length;y[ky-1]==0;ky--); //ky is number of elements in y, not including leading zeros
+
+    //normalize: ensure the most significant element of y has its highest bit set
+    b=y[ky-1];
+    for (a=0; b; a++)
+      b>>=1;
+    a=bpe-a;  //a is how many bits to shift so that the high order bit of y is leftmost in its array element
+    leftShift_(y,a);  //multiply both by 1<<a now, then divide both by that at the end
+    leftShift_(r,a);
+
+    //Rob Visser discovered a bug: the following line was originally just before the normalization.
+    for (kx=r.length;r[kx-1]==0 && kx>ky;kx--); //kx is number of elements in normalized x, not including leading zeros
+
+    copyInt_(q,0);                      // q=0
+    while (!greaterShift(y,r,kx-ky)) {  // while (leftShift_(y,kx-ky) <= r) {
+      subShift_(r,y,kx-ky);             //   r=r-leftShift_(y,kx-ky)
+      q[kx-ky]++;                       //   q[kx-ky]++;
+    }                                   // }
+
+    for (i=kx-1; i>=ky; i--) {
+      if (r[i]==y[ky-1])
+        q[i-ky]=mask;
+      else
+        q[i-ky]=Math.floor((r[i]*radix+r[i-1])/y[ky-1]);
+
+      //The following for(;;) loop is equivalent to the commented while loop,
+      //except that the uncommented version avoids overflow.
+      //The commented loop comes from HAC, which assumes r[-1]==y[-1]==0
+      //  while (q[i-ky]*(y[ky-1]*radix+y[ky-2]) > r[i]*radix*radix+r[i-1]*radix+r[i-2])
+      //    q[i-ky]--;
+      for (;;) {
+        y2=(ky>1 ? y[ky-2] : 0)*q[i-ky];
+        c=y2>>bpe;
+        y2=y2 & mask;
+        y1=c+q[i-ky]*y[ky-1];
+        c=y1>>bpe;
+        y1=y1 & mask;
+
+        if (c==r[i] ? y1==r[i-1] ? y2>(i>1 ? r[i-2] : 0) : y1>r[i-1] : c>r[i])
+          q[i-ky]--;
+        else
+          break;
+      }
+
+      linCombShift_(r,y,-q[i-ky],i-ky);    //r=r-q[i-ky]*leftShift_(y,i-ky)
+      if (negative(r)) {
+        addShift_(r,y,i-ky);         //r=r+leftShift_(y,i-ky)
+        q[i-ky]--;
+      }
+    }
+
+    rightShift_(y,a);  //undo the normalization step
+    rightShift_(r,a);  //undo the normalization step
+  }
+
+  //do carries and borrows so each element of the bigInt x fits in bpe bits.
+  function carry_(x) {
+    var i,k,c,b;
+    k=x.length;
+    c=0;
+    for (i=0;i<k;i++) {
+      c+=x[i];
+      b=0;
+      if (c<0) {
+        b=-(c>>bpe);
+        c+=b*radix;
+      }
+      x[i]=c & mask;
+      c=(c>>bpe)-b;
+    }
+  }
+
+  //return x mod n for bigInt x and integer n.
+  function modInt(x,n) {
+    var i,c=0;
+    for (i=x.length-1; i>=0; i--)
+      c=(c*radix+x[i])%n;
+    return c;
+  }
+
+  //convert the integer t into a bigInt with at least the given number of bits.
+  //the returned array stores the bigInt in bpe-bit chunks, little endian (buff[0] is least significant word)
+  //Pad the array with leading zeros so that it has at least minSize elements.
+  //There will always be at least one leading 0 element.
+  function int2bigInt(t,bits,minSize) {
+    var i,k, buff;
+    k=Math.ceil(bits/bpe)+1;
+    k=minSize>k ? minSize : k;
+    buff=new Array(k);
+    copyInt_(buff,t);
+    return buff;
+  }
+
+  //return the bigInt given a string representation in a given base.
+  //Pad the array with leading zeros so that it has at least minSize elements.
+  //If base=-1, then it reads in a space-separated list of array elements in decimal.
+  //The array will always have at least one leading zero, unless base=-1.
+  function str2bigInt(s,base,minSize) {
+    var d, i, j, x, y, kk;
+    var k=s.length;
+    if (base==-1) { //comma-separated list of array elements in decimal
+      x=new Array(0);
+      for (;;) {
+        y=new Array(x.length+1);
+        for (i=0;i<x.length;i++)
+          y[i+1]=x[i];
+        y[0]=parseInt(s,10);
+        x=y;
+        d=s.indexOf(',',0);
+        if (d<1)
+          break;
+        s=s.substring(d+1);
+        if (s.length==0)
+          break;
+      }
+      if (x.length<minSize) {
+        y=new Array(minSize);
+        copy_(y,x);
+        return y;
+      }
+      return x;
+    }
+
+    // log2(base)*k
+    var bb = base, p = 0;
+    var b = base == 1 ? k : 0;
+    while (bb > 1) {
+      if (bb & 1) p = 1;
+      b += k;
+      bb >>= 1;
+    }
+    b += p*k;
+
+    x=int2bigInt(0,b,0);
+    for (i=0;i<k;i++) {
+      d=digitsStr.indexOf(s.substring(i,i+1),0);
+      if (base<=36 && d>=36)  //convert lowercase to uppercase if base<=36
+        d-=26;
+      if (d>=base || d<0) {   //stop at first illegal character
+        break;
+      }
+      multInt_(x,base);
+      addInt_(x,d);
+    }
+
+    for (k=x.length;k>0 && !x[k-1];k--); //strip off leading zeros
+    k=minSize>k+1 ? minSize : k+1;
+    y=new Array(k);
+    kk=k<x.length ? k : x.length;
+    for (i=0;i<kk;i++)
+      y[i]=x[i];
+    for (;i<k;i++)
+      y[i]=0;
+    return y;
+  }
+
+  //is bigint x equal to integer y?
+  //y must have less than bpe bits
+  function equalsInt(x,y) {
+    var i;
+    if (x[0]!=y)
+      return 0;
+    for (i=1;i<x.length;i++)
+      if (x[i])
+        return 0;
+    return 1;
+  }
+
+  //are bigints x and y equal?
+  //this works even if x and y are different lengths and have arbitrarily many leading zeros
+  function equals(x,y) {
+    var i;
+    var k=x.length<y.length ? x.length : y.length;
+    for (i=0;i<k;i++)
+      if (x[i]!=y[i])
+        return 0;
+    if (x.length>y.length) {
+      for (;i<x.length;i++)
+        if (x[i])
+          return 0;
+    } else {
+      for (;i<y.length;i++)
+        if (y[i])
+          return 0;
+    }
+    return 1;
+  }
+
+  //is the bigInt x equal to zero?
+  function isZero(x) {
+    var i;
+    for (i=0;i<x.length;i++)
+      if (x[i])
+        return 0;
+    return 1;
+  }
+
+  //convert a bigInt into a string in a given base, from base 2 up to base 95.
+  //Base -1 prints the contents of the array representing the number.
+  function bigInt2str(x,base) {
+    var i,t,s="";
+
+    if (s6.length!=x.length)
+      s6=dup(x);
+    else
+      copy_(s6,x);
+
+    if (base==-1) { //return the list of array contents
+      for (i=x.length-1;i>0;i--)
+        s+=x[i]+',';
+      s+=x[0];
+    }
+    else { //return it in the given base
+      while (!isZero(s6)) {
+        t=divInt_(s6,base);  //t=s6 % base; s6=floor(s6/base);
+        s=digitsStr.substring(t,t+1)+s;
+      }
+    }
+    if (s.length==0)
+      s="0";
+    return s;
+  }
+
+  //returns a duplicate of bigInt x
+  function dup(x) {
+    var i, buff;
+    buff=new Array(x.length);
+    copy_(buff,x);
+    return buff;
+  }
+
+  //do x=y on bigInts x and y.  x must be an array at least as big as y (not counting the leading zeros in y).
+  function copy_(x,y) {
+    var i;
+    var k=x.length<y.length ? x.length : y.length;
+    for (i=0;i<k;i++)
+      x[i]=y[i];
+    for (i=k;i<x.length;i++)
+      x[i]=0;
+  }
+
+  //do x=y on bigInt x and integer y.
+  function copyInt_(x,n) {
+    var i,c;
+    for (c=n,i=0;i<x.length;i++) {
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x+n where x is a bigInt and n is an integer.
+  //x must be large enough to hold the result.
+  function addInt_(x,n) {
+    var i,k,c,b;
+    x[0]+=n;
+    k=x.length;
+    c=0;
+    for (i=0;i<k;i++) {
+      c+=x[i];
+      b=0;
+      if (c<0) {
+        b=-(c>>bpe);
+        c+=b*radix;
+      }
+      x[i]=c & mask;
+      c=(c>>bpe)-b;
+      if (!c) return; //stop carrying as soon as the carry is zero
+    }
+  }
+
+  //right shift bigInt x by n bits.
+  function rightShift_(x,n) {
+    var i;
+    var k=Math.floor(n/bpe);
+    if (k) {
+      for (i=0;i<x.length-k;i++) //right shift x by k elements
+        x[i]=x[i+k];
+      for (;i<x.length;i++)
+        x[i]=0;
+      n%=bpe;
+    }
+    for (i=0;i<x.length-1;i++) {
+      x[i]=mask & ((x[i+1]<<(bpe-n)) | (x[i]>>n));
+    }
+    x[i]>>=n;
+  }
+
+  //do x=floor(|x|/2)*sgn(x) for bigInt x in 2's complement
+  function halve_(x) {
+    var i;
+    for (i=0;i<x.length-1;i++) {
+      x[i]=mask & ((x[i+1]<<(bpe-1)) | (x[i]>>1));
+    }
+    x[i]=(x[i]>>1) | (x[i] & (radix>>1));  //most significant bit stays the same
+  }
+
+  //left shift bigInt x by n bits.
+  function leftShift_(x,n) {
+    var i;
+    var k=Math.floor(n/bpe);
+    if (k) {
+      for (i=x.length; i>=k; i--) //left shift x by k elements
+        x[i]=x[i-k];
+      for (;i>=0;i--)
+        x[i]=0;
+      n%=bpe;
+    }
+    if (!n)
+      return;
+    for (i=x.length-1;i>0;i--) {
+      x[i]=mask & ((x[i]<<n) | (x[i-1]>>(bpe-n)));
+    }
+    x[i]=mask & (x[i]<<n);
+  }
+
+  //do x=x*n where x is a bigInt and n is an integer.
+  //x must be large enough to hold the result.
+  function multInt_(x,n) {
+    var i,k,c,b;
+    if (!n)
+      return;
+    k=x.length;
+    c=0;
+    for (i=0;i<k;i++) {
+      c+=x[i]*n;
+      b=0;
+      if (c<0) {
+        b=-(c>>bpe);
+        c+=b*radix;
+      }
+      x[i]=c & mask;
+      c=(c>>bpe)-b;
+    }
+  }
+
+  //do x=floor(x/n) for bigInt x and integer n, and return the remainder
+  function divInt_(x,n) {
+    var i,r=0,s;
+    for (i=x.length-1;i>=0;i--) {
+      s=r*radix+x[i];
+      x[i]=Math.floor(s/n);
+      r=s%n;
+    }
+    return r;
+  }
+
+  //do the linear combination x=a*x+b*y for bigInts x and y, and integers a and b.
+  //x must be large enough to hold the answer.
+  function linComb_(x,y,a,b) {
+    var i,c,k,kk;
+    k=x.length<y.length ? x.length : y.length;
+    kk=x.length;
+    for (c=0,i=0;i<k;i++) {
+      c+=a*x[i]+b*y[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;i<kk;i++) {
+      c+=a*x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do the linear combination x=a*x+b*(y<<(ys*bpe)) for bigInts x and y, and integers a, b and ys.
+  //x must be large enough to hold the answer.
+  function linCombShift_(x,y,b,ys) {
+    var i,c,k,kk;
+    k=x.length<ys+y.length ? x.length : ys+y.length;
+    kk=x.length;
+    for (c=0,i=ys;i<k;i++) {
+      c+=x[i]+b*y[i-ys];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;c && i<kk;i++) {
+      c+=x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x+(y<<(ys*bpe)) for bigInts x and y, and integers a,b and ys.
+  //x must be large enough to hold the answer.
+  function addShift_(x,y,ys) {
+    var i,c,k,kk;
+    k=x.length<ys+y.length ? x.length : ys+y.length;
+    kk=x.length;
+    for (c=0,i=ys;i<k;i++) {
+      c+=x[i]+y[i-ys];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;c && i<kk;i++) {
+      c+=x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x-(y<<(ys*bpe)) for bigInts x and y, and integers a,b and ys.
+  //x must be large enough to hold the answer.
+  function subShift_(x,y,ys) {
+    var i,c,k,kk;
+    k=x.length<ys+y.length ? x.length : ys+y.length;
+    kk=x.length;
+    for (c=0,i=ys;i<k;i++) {
+      c+=x[i]-y[i-ys];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;c && i<kk;i++) {
+      c+=x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x-y for bigInts x and y.
+  //x must be large enough to hold the answer.
+  //negative answers will be 2s complement
+  function sub_(x,y) {
+    var i,c,k,kk;
+    k=x.length<y.length ? x.length : y.length;
+    for (c=0,i=0;i<k;i++) {
+      c+=x[i]-y[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;c && i<x.length;i++) {
+      c+=x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x+y for bigInts x and y.
+  //x must be large enough to hold the answer.
+  function add_(x,y) {
+    var i,c,k,kk;
+    k=x.length<y.length ? x.length : y.length;
+    for (c=0,i=0;i<k;i++) {
+      c+=x[i]+y[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+    for (i=k;c && i<x.length;i++) {
+      c+=x[i];
+      x[i]=c & mask;
+      c>>=bpe;
+    }
+  }
+
+  //do x=x*y for bigInts x and y.  This is faster when y<x.
+  function mult_(x,y) {
+    var i;
+    if (ss.length!=2*x.length)
+      ss=new Array(2*x.length);
+    copyInt_(ss,0);
+    for (i=0;i<y.length;i++)
+      if (y[i])
+        linCombShift_(ss,x,y[i],i);   //ss=1*ss+y[i]*(x<<(i*bpe))
+    copy_(x,ss);
+  }
+
+  //do x=x mod n for bigInts x and n.
+  function mod_(x,n) {
+    if (s4.length!=x.length)
+      s4=dup(x);
+    else
+      copy_(s4,x);
+    if (s5.length!=x.length)
+      s5=dup(x);
+    divide_(s4,n,s5,x);  //x = remainder of s4 / n
+  }
+
+  //do x=x*y mod n for bigInts x,y,n.
+  //for greater speed, let y<x.
+  function multMod_(x,y,n) {
+    var i;
+    if (s0.length!=2*x.length)
+      s0=new Array(2*x.length);
+    copyInt_(s0,0);
+    for (i=0;i<y.length;i++)
+      if (y[i])
+        linCombShift_(s0,x,y[i],i);   //s0=1*s0+y[i]*(x<<(i*bpe))
+    mod_(s0,n);
+    copy_(x,s0);
+  }
+
+  //do x=x*x mod n for bigInts x,n.
+  function squareMod_(x,n) {
+    var i,j,d,c,kx,kn,k;
+    for (kx=x.length; kx>0 && !x[kx-1]; kx--);  //ignore leading zeros in x
+    k=kx>n.length ? 2*kx : 2*n.length; //k=# elements in the product, which is twice the elements in the larger of x and n
+    if (s0.length!=k)
+      s0=new Array(k);
+    copyInt_(s0,0);
+    for (i=0;i<kx;i++) {
+      c=s0[2*i]+x[i]*x[i];
+      s0[2*i]=c & mask;
+      c>>=bpe;
+      for (j=i+1;j<kx;j++) {
+        c=s0[i+j]+2*x[i]*x[j]+c;
+        s0[i+j]=(c & mask);
+        c>>=bpe;
+      }
+      s0[i+kx]=c;
+    }
+    mod_(s0,n);
+    copy_(x,s0);
+  }
+
+  //return x with exactly k leading zero elements
+  function trim(x,k) {
+    var i,y;
+    for (i=x.length; i>0 && !x[i-1]; i--);
+    y=new Array(i+k);
+    copy_(y,x);
+    return y;
+  }
+
+  //do x=x**y mod n, where x,y,n are bigInts and ** is exponentiation.  0**0=1.
+  //this is faster when n is odd.  x usually needs to have as many elements as n.
+  function powMod_(x,y,n) {
+    var k1,k2,kn,np;
+    if(s7.length!=n.length)
+      s7=dup(n);
+
+    //for even modulus, use a simple square-and-multiply algorithm,
+    //rather than using the more complex Montgomery algorithm.
+    if ((n[0]&1)==0) {
+      copy_(s7,x);
+      copyInt_(x,1);
+      while(!equalsInt(y,0)) {
+        if (y[0]&1)
+          multMod_(x,s7,n);
+        divInt_(y,2);
+        squareMod_(s7,n);
+      }
+      return;
+    }
+
+    //calculate np from n for the Montgomery multiplications
+    copyInt_(s7,0);
+    for (kn=n.length;kn>0 && !n[kn-1];kn--);
+    np=radix-inverseModInt(modInt(n,radix),radix);
+    s7[kn]=1;
+    multMod_(x ,s7,n);   // x = x * 2**(kn*bp) mod n
+
+    if (s3.length!=x.length)
+      s3=dup(x);
+    else
+      copy_(s3,x);
+
+    for (k1=y.length-1;k1>0 & !y[k1]; k1--);  //k1=first nonzero element of y
+    if (y[k1]==0) {  //anything to the 0th power is 1
+      copyInt_(x,1);
+      return;
+    }
+    for (k2=1<<(bpe-1);k2 && !(y[k1] & k2); k2>>=1);  //k2=position of first 1 bit in y[k1]
+    for (;;) {
+      if (!(k2>>=1)) {  //look at next bit of y
+        k1--;
+        if (k1<0) {
+          mont_(x,one,n,np);
+          return;
+        }
+        k2=1<<(bpe-1);
+      }
+      mont_(x,x,n,np);
+
+      if (k2 & y[k1]) //if next bit is a 1
+        mont_(x,s3,n,np);
+    }
+  }
+
+
+  //do x=x*y*Ri mod n for bigInts x,y,n,
+  //  where Ri = 2**(-kn*bpe) mod n, and kn is the
+  //  number of elements in the n array, not
+  //  counting leading zeros.
+  //x array must have at least as many elemnts as the n array
+  //It's OK if x and y are the same variable.
+  //must have:
+  //  x,y < n
+  //  n is odd
+  //  np = -(n^(-1)) mod radix
+  function mont_(x,y,n,np) {
+    var i,j,c,ui,t,ks;
+    var kn=n.length;
+    var ky=y.length;
+
+    if (sa.length!=kn)
+      sa=new Array(kn);
+
+    copyInt_(sa,0);
+
+    for (;kn>0 && n[kn-1]==0;kn--); //ignore leading zeros of n
+    for (;ky>0 && y[ky-1]==0;ky--); //ignore leading zeros of y
+    ks=sa.length-1; //sa will never have more than this many nonzero elements.
+
+    //the following loop consumes 95% of the runtime for randTruePrime_() and powMod_() for large numbers
+    for (i=0; i<kn; i++) {
+      t=sa[0]+x[i]*y[0];
+      ui=((t & mask) * np) & mask;  //the inner "& mask" was needed on Safari (but not MSIE) at one time
+      c=(t+ui*n[0]) >> bpe;
+      t=x[i];
+
+      //do sa=(sa+x[i]*y+ui*n)/b   where b=2**bpe.  Loop is unrolled 5-fold for speed
+      j=1;
+      for (;j<ky-4;) {
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+      }
+      for (;j<ky;)   {
+        c+=sa[j]+ui*n[j]+t*y[j]; sa[j-1]=c & mask; c>>=bpe; j++;
+      }
+      for (;j<kn-4;) {
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+      }
+      for (;j<kn;)   {
+        c+=sa[j]+ui*n[j];        sa[j-1]=c & mask; c>>=bpe; j++;
+      }
+      for (;j<ks;)   {
+        c+=sa[j];                sa[j-1]=c & mask; c>>=bpe; j++;
+      }
+      sa[j-1]=c & mask;
+    }
+
+    if (!greater(n,sa))
+      sub_(sa,n);
+    copy_(x,sa);
+  }
+
+
+  // ADDITIONS to the BigInt library.
+
+  //converts a bigInt into a little endian byte array containing the specified amount of bytes which is then encoded into Base64.
+  function bigInt2base64 (x, bytes) {
+    if (s6.length!=x.length)
+      s6=dup(x);
+    else
+      copy_(s6,x);
+
+	var result = "";
+	while (bytes--)
+		result += String.fromCharCode(divInt_(s6,256));  //t=s6 % base; s6=floor(s6/base);
+
+	return btoa(result);
+  }
+
+  //return the bigInt given a string representation in a given base.
+  //Pad the array with leading zeros so that it has at least minSize elements.
+  //If base=-1, then it reads in a space-separated list of array elements in decimal.
+  //The array will always have at least one leading zero, unless base=-1.
+  function base642bigInt (s,minSize) {
+    var d, i, j, x, y, kk;
+
+    s = atob(s);
+    var k=s.length;
+    var base=256;
+
+    x=int2bigInt(0,8*k,0);
+    for (i=0;i<k;i++) {
+      d=s.charCodeAt(k-1-i);
+      multInt_(x,base);
+      addInt_(x,d);
+    }
+
+    for (k=x.length;k>0 && !x[k-1];k--); //strip off leading zeros
+    k=minSize>k+1 ? minSize : k+1;
+    y=new Array(k);
+    kk=k<x.length ? k : x.length;
+    for (i=0;i<kk;i++)
+      y[i]=x[i];
+    for (;i<k;i++)
+      y[i]=0;
+    return y;
+  }
+
+  // computes num / den mod n
+  function divMod(num, den, n) {
+    return multMod(num, inverseMod(den, n), n)
+  }
+
+  // computes one - two mod n
+  function subMod(one, two, n) {
+    one = mod(one, n)
+    two = mod(two, n)
+    if (greater(two, one)) one = add(one, n)
+    return sub(one, two)
+  }
+
+  // computes 2^m as a bigInt
+  function twoToThe(m) {
+    var b = Math.floor(m / bpe) + 2
+    var t = new Array(b)
+    for (var i = 0; i < b; i++) t[i] = 0
+    t[b - 2] = 1 << (m % bpe)
+    return t
+  }
+
+  // cache these results for faster lookup
+  var _num2bin = (function () {
+    var i = 0, _num2bin= {}
+    for (; i < 0x100; ++i) {
+      _num2bin[i] = String.fromCharCode(i)  // 0 -> "\00"
+    }
+    return _num2bin
+  }())
+
+  // serialize a bigInt to an ascii string
+  // padded up to pad length
+  function bigInt2bits(bi, pad) {
+    pad || (pad = 0)
+    bi = dup(bi)
+    var ba = ''
+    while (!isZero(bi)) {
+      ba = _num2bin[bi[0] & 0xff] + ba
+      rightShift_(bi, 8)
+    }
+    while (ba.length < pad) {
+      ba = '\x00' + ba
+    }
+    return ba
+  }
+
+  // converts a byte array to a bigInt
+  function ba2bigInt(data) {
+    var mpi = str2bigInt('0', 10, data.length)
+    data.forEach(function (d, i) {
+      if (i) leftShift_(mpi, 8)
+      mpi[0] |= d
+    })
+    return mpi
+  }
+  
+	// Gets a bit from a BigInt
+	function getBit(x, n) {
+		var i = Math.floor(n / bpe)
+		if (i >= x.length) {
+			return 0
+		}
+		return (x[i] >> (n % bpe)) & 1
+	}
+
+  return {
+      base642bigInt : base642bigInt
+    , bigInt2base64 : bigInt2base64
+    , str2bigInt    : str2bigInt
+    , bigInt2str    : bigInt2str
+    , int2bigInt    : int2bigInt
+    , multMod       : multMod
+    , powMod        : powMod
+    , inverseMod    : inverseMod
+    , randBigInt    : randBigInt
+    , randBigInt_   : randBigInt_
+    , equals        : equals
+    , equalsInt     : equalsInt
+    , sub           : sub
+    , mod           : mod
+    , mod_          : mod_
+    , modInt        : modInt
+    , mult          : mult
+    , divInt_       : divInt_
+    , rightShift_   : rightShift_
+    , leftShift_    : leftShift_
+    , dup           : dup
+    , greater       : greater
+    , add           : add
+    , addInt        : addInt
+    , addInt_       : addInt_
+    , isZero        : isZero
+    , bitSize       : bitSize
+    , randTruePrime : randTruePrime
+    , millerRabin   : millerRabin
+    , divide_       : divide_
+    , trim          : trim
+    , expand        : expand
+    , primes        : primes
+    , findPrimes    : findPrimes
+    , divMod        : divMod
+    , subMod        : subMod
+    , twoToThe      : twoToThe
+    , bigInt2bits   : bigInt2bits
+    , ba2bigInt     : ba2bigInt
+    , getBit        : getBit
+  }
+
+}))
diff --git a/src/core/js/lib/crypto-js.js b/src/core/js/lib/crypto-js.js
new file mode 100644
index 0000000..a3be5cc
--- /dev/null
+++ b/src/core/js/lib/crypto-js.js
@@ -0,0 +1,3163 @@
+;(function (root, factory) {
+	'use strict';
+
+	if (typeof define === "function" && define.amd) {
+		define(factory)
+	} else if (typeof module !== 'undefined' && module.exports) {
+		module.exports = factory()
+	} else {
+		root.CryptoJS = factory()
+	}
+
+}(this, function () {
+	'use strict';
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * CryptoJS core components.
+ */
+var CryptoJS = CryptoJS || (function (Math, undefined) {
+    /**
+     * CryptoJS namespace.
+     */
+    var C = {};
+
+    /**
+     * Library namespace.
+     */
+    var C_lib = C.lib = {};
+
+    /**
+     * Base object for prototypal inheritance.
+     */
+    var Base = C_lib.Base = (function () {
+        function F() {}
+
+        return {
+            /**
+             * Creates a new object that inherits from this object.
+             *
+             * @param {Object} overrides Properties to copy into the new object.
+             *
+             * @return {Object} The new object.
+             *
+             * @static
+             *
+             * @example
+             *
+             *     var MyType = CryptoJS.lib.Base.extend({
+             *         field: 'value',
+             *
+             *         method: function () {
+             *         }
+             *     });
+             */
+            extend: function (overrides) {
+                // Spawn
+                F.prototype = this;
+                var subtype = new F();
+
+                // Augment
+                if (overrides) {
+                    subtype.mixIn(overrides);
+                }
+
+                // Create default initializer
+                if (!subtype.hasOwnProperty('init')) {
+                    subtype.init = function () {
+                        subtype.$super.init.apply(this, arguments);
+                    };
+                }
+
+                // Initializer's prototype is the subtype object
+                subtype.init.prototype = subtype;
+
+                // Reference supertype
+                subtype.$super = this;
+
+                return subtype;
+            },
+
+            /**
+             * Extends this object and runs the init method.
+             * Arguments to create() will be passed to init().
+             *
+             * @return {Object} The new object.
+             *
+             * @static
+             *
+             * @example
+             *
+             *     var instance = MyType.create();
+             */
+            create: function () {
+                var instance = this.extend();
+                instance.init.apply(instance, arguments);
+
+                return instance;
+            },
+
+            /**
+             * Initializes a newly created object.
+             * Override this method to add some logic when your objects are created.
+             *
+             * @example
+             *
+             *     var MyType = CryptoJS.lib.Base.extend({
+             *         init: function () {
+             *             // ...
+             *         }
+             *     });
+             */
+            init: function () {
+            },
+
+            /**
+             * Copies properties into this object.
+             *
+             * @param {Object} properties The properties to mix in.
+             *
+             * @example
+             *
+             *     MyType.mixIn({
+             *         field: 'value'
+             *     });
+             */
+            mixIn: function (properties) {
+                for (var propertyName in properties) {
+                    if (properties.hasOwnProperty(propertyName)) {
+                        this[propertyName] = properties[propertyName];
+                    }
+                }
+
+                // IE won't copy toString using the loop above
+                if (properties.hasOwnProperty('toString')) {
+                    this.toString = properties.toString;
+                }
+            },
+
+            /**
+             * Creates a copy of this object.
+             *
+             * @return {Object} The clone.
+             *
+             * @example
+             *
+             *     var clone = instance.clone();
+             */
+            clone: function () {
+                return this.init.prototype.extend(this);
+            }
+        };
+    }());
+
+    /**
+     * An array of 32-bit words.
+     *
+     * @property {Array} words The array of 32-bit words.
+     * @property {number} sigBytes The number of significant bytes in this word array.
+     */
+    var WordArray = C_lib.WordArray = Base.extend({
+        /**
+         * Initializes a newly created word array.
+         *
+         * @param {Array} words (Optional) An array of 32-bit words.
+         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.lib.WordArray.create();
+         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
+         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
+         */
+        init: function (words, sigBytes) {
+            words = this.words = words || [];
+
+            if (sigBytes != undefined) {
+                this.sigBytes = sigBytes;
+            } else {
+                this.sigBytes = words.length * 4;
+            }
+        },
+
+        /**
+         * Converts this word array to a string.
+         *
+         * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
+         *
+         * @return {string} The stringified word array.
+         *
+         * @example
+         *
+         *     var string = wordArray + '';
+         *     var string = wordArray.toString();
+         *     var string = wordArray.toString(CryptoJS.enc.Utf8);
+         */
+        toString: function (encoder) {
+            return (encoder || Hex).stringify(this);
+        },
+
+        /**
+         * Concatenates a word array to this word array.
+         *
+         * @param {WordArray} wordArray The word array to append.
+         *
+         * @return {WordArray} This word array.
+         *
+         * @example
+         *
+         *     wordArray1.concat(wordArray2);
+         */
+        concat: function (wordArray) {
+            // Shortcuts
+            var thisWords = this.words;
+            var thatWords = wordArray.words;
+            var thisSigBytes = this.sigBytes;
+            var thatSigBytes = wordArray.sigBytes;
+
+            // Clamp excess bits
+            this.clamp();
+
+            // Concat
+            if (thisSigBytes % 4) {
+                // Copy one byte at a time
+                for (var i = 0; i < thatSigBytes; i++) {
+                    var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                    thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
+                }
+            } else if (thatWords.length > 0xffff) {
+                // Copy one word at a time
+                for (var i = 0; i < thatSigBytes; i += 4) {
+                    thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
+                }
+            } else {
+                // Copy all words at once
+                thisWords.push.apply(thisWords, thatWords);
+            }
+            this.sigBytes += thatSigBytes;
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Removes insignificant bits.
+         *
+         * @example
+         *
+         *     wordArray.clamp();
+         */
+        clamp: function () {
+            // Shortcuts
+            var words = this.words;
+            var sigBytes = this.sigBytes;
+
+            // Clamp
+            words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
+            words.length = Math.ceil(sigBytes / 4);
+        },
+
+        /**
+         * Creates a copy of this word array.
+         *
+         * @return {WordArray} The clone.
+         *
+         * @example
+         *
+         *     var clone = wordArray.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+            clone.words = this.words.slice(0);
+
+            return clone;
+        },
+
+        /**
+         * Creates a word array filled with random bytes.
+         *
+         * @param {number} nBytes The number of random bytes to generate.
+         *
+         * @return {WordArray} The random word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.lib.WordArray.random(16);
+         */
+        random: function (nBytes) {
+            var words = [];
+            for (var i = 0; i < nBytes; i += 4) {
+                words.push((Math.random() * 0x100000000) | 0);
+            }
+
+            return new WordArray.init(words, nBytes);
+        }
+    });
+
+    /**
+     * Encoder namespace.
+     */
+    var C_enc = C.enc = {};
+
+    /**
+     * Hex encoding strategy.
+     */
+    var Hex = C_enc.Hex = {
+        /**
+         * Converts a word array to a hex string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The hex string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var hexString = CryptoJS.enc.Hex.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+
+            // Convert
+            var hexChars = [];
+            for (var i = 0; i < sigBytes; i++) {
+                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                hexChars.push((bite >>> 4).toString(16));
+                hexChars.push((bite & 0x0f).toString(16));
+            }
+
+            return hexChars.join('');
+        },
+
+        /**
+         * Converts a hex string to a word array.
+         *
+         * @param {string} hexStr The hex string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Hex.parse(hexString);
+         */
+        parse: function (hexStr) {
+            // Shortcut
+            var hexStrLength = hexStr.length;
+
+            // Convert
+            var words = [];
+            for (var i = 0; i < hexStrLength; i += 2) {
+                words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
+            }
+
+            return new WordArray.init(words, hexStrLength / 2);
+        }
+    };
+
+    /**
+     * Latin1 encoding strategy.
+     */
+    var Latin1 = C_enc.Latin1 = {
+        /**
+         * Converts a word array to a Latin1 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The Latin1 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+
+            // Convert
+            var latin1Chars = [];
+            for (var i = 0; i < sigBytes; i++) {
+                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                latin1Chars.push(String.fromCharCode(bite));
+            }
+
+            return latin1Chars.join('');
+        },
+
+        /**
+         * Converts a Latin1 string to a word array.
+         *
+         * @param {string} latin1Str The Latin1 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
+         */
+        parse: function (latin1Str) {
+            // Shortcut
+            var latin1StrLength = latin1Str.length;
+
+            // Convert
+            var words = [];
+            for (var i = 0; i < latin1StrLength; i++) {
+                words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
+            }
+
+            return new WordArray.init(words, latin1StrLength);
+        }
+    };
+
+    /**
+     * UTF-8 encoding strategy.
+     */
+    var Utf8 = C_enc.Utf8 = {
+        /**
+         * Converts a word array to a UTF-8 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The UTF-8 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            try {
+                return decodeURIComponent(escape(Latin1.stringify(wordArray)));
+            } catch (e) {
+                throw new Error('Malformed UTF-8 data');
+            }
+        },
+
+        /**
+         * Converts a UTF-8 string to a word array.
+         *
+         * @param {string} utf8Str The UTF-8 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
+         */
+        parse: function (utf8Str) {
+            return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
+        }
+    };
+
+    /**
+     * Abstract buffered block algorithm template.
+     *
+     * The property blockSize must be implemented in a concrete subtype.
+     *
+     * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
+     */
+    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
+        /**
+         * Resets this block algorithm's data buffer to its initial state.
+         *
+         * @example
+         *
+         *     bufferedBlockAlgorithm.reset();
+         */
+        reset: function () {
+            // Initial values
+            this._data = new WordArray.init();
+            this._nDataBytes = 0;
+        },
+
+        /**
+         * Adds new data to this block algorithm's buffer.
+         *
+         * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
+         *
+         * @example
+         *
+         *     bufferedBlockAlgorithm._append('data');
+         *     bufferedBlockAlgorithm._append(wordArray);
+         */
+        _append: function (data) {
+            // Convert string to WordArray, else assume WordArray already
+            if (typeof data == 'string') {
+                data = Utf8.parse(data);
+            }
+
+            // Append
+            this._data.concat(data);
+            this._nDataBytes += data.sigBytes;
+        },
+
+        /**
+         * Processes available data blocks.
+         *
+         * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
+         *
+         * @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
+         *
+         * @return {WordArray} The processed data.
+         *
+         * @example
+         *
+         *     var processedData = bufferedBlockAlgorithm._process();
+         *     var processedData = bufferedBlockAlgorithm._process(!!'flush');
+         */
+        _process: function (doFlush) {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+            var dataSigBytes = data.sigBytes;
+            var blockSize = this.blockSize;
+            var blockSizeBytes = blockSize * 4;
+
+            // Count blocks ready
+            var nBlocksReady = dataSigBytes / blockSizeBytes;
+            if (doFlush) {
+                // Round up to include partial blocks
+                nBlocksReady = Math.ceil(nBlocksReady);
+            } else {
+                // Round down to include only full blocks,
+                // less the number of blocks that must remain in the buffer
+                nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
+            }
+
+            // Count words ready
+            var nWordsReady = nBlocksReady * blockSize;
+
+            // Count bytes ready
+            var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
+
+            // Process blocks
+            if (nWordsReady) {
+                for (var offset = 0; offset < nWordsReady; offset += blockSize) {
+                    // Perform concrete-algorithm logic
+                    this._doProcessBlock(dataWords, offset);
+                }
+
+                // Remove processed words
+                var processedWords = dataWords.splice(0, nWordsReady);
+                data.sigBytes -= nBytesReady;
+            }
+
+            // Return processed words
+            return new WordArray.init(processedWords, nBytesReady);
+        },
+
+        /**
+         * Creates a copy of this object.
+         *
+         * @return {Object} The clone.
+         *
+         * @example
+         *
+         *     var clone = bufferedBlockAlgorithm.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+            clone._data = this._data.clone();
+
+            return clone;
+        },
+
+        _minBufferSize: 0
+    });
+
+    /**
+     * Abstract hasher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
+     */
+    var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
+        /**
+         * Configuration options.
+         */
+        cfg: Base.extend(),
+
+        /**
+         * Initializes a newly created hasher.
+         *
+         * @param {Object} cfg (Optional) The configuration options to use for this hash computation.
+         *
+         * @example
+         *
+         *     var hasher = CryptoJS.algo.SHA256.create();
+         */
+        init: function (cfg) {
+            // Apply config defaults
+            this.cfg = this.cfg.extend(cfg);
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this hasher to its initial state.
+         *
+         * @example
+         *
+         *     hasher.reset();
+         */
+        reset: function () {
+            // Reset data buffer
+            BufferedBlockAlgorithm.reset.call(this);
+
+            // Perform concrete-hasher logic
+            this._doReset();
+        },
+
+        /**
+         * Updates this hasher with a message.
+         *
+         * @param {WordArray|string} messageUpdate The message to append.
+         *
+         * @return {Hasher} This hasher.
+         *
+         * @example
+         *
+         *     hasher.update('message');
+         *     hasher.update(wordArray);
+         */
+        update: function (messageUpdate) {
+            // Append
+            this._append(messageUpdate);
+
+            // Update the hash
+            this._process();
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Finalizes the hash computation.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} messageUpdate (Optional) A final message update.
+         *
+         * @return {WordArray} The hash.
+         *
+         * @example
+         *
+         *     var hash = hasher.finalize();
+         *     var hash = hasher.finalize('message');
+         *     var hash = hasher.finalize(wordArray);
+         */
+        finalize: function (messageUpdate) {
+            // Final message update
+            if (messageUpdate) {
+                this._append(messageUpdate);
+            }
+
+            // Perform concrete-hasher logic
+            var hash = this._doFinalize();
+
+            return hash;
+        },
+
+        blockSize: 512/32,
+
+        /**
+         * Creates a shortcut function to a hasher's object interface.
+         *
+         * @param {Hasher} hasher The hasher to create a helper for.
+         *
+         * @return {Function} The shortcut function.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
+         */
+        _createHelper: function (hasher) {
+            return function (message, cfg) {
+                return new hasher.init(cfg).finalize(message);
+            };
+        },
+
+        /**
+         * Creates a shortcut function to the HMAC's object interface.
+         *
+         * @param {Hasher} hasher The hasher to use in this HMAC helper.
+         *
+         * @return {Function} The shortcut function.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
+         */
+        _createHmacHelper: function (hasher) {
+            return function (message, key) {
+                return new C_algo.HMAC.init(hasher, key).finalize(message);
+            };
+        }
+    });
+
+    /**
+     * Algorithm namespace.
+     */
+    var C_algo = C.algo = {};
+
+    return C;
+}(Math));
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var C_enc = C.enc;
+
+    /**
+     * Base64 encoding strategy.
+     */
+    var Base64 = C_enc.Base64 = {
+        /**
+         * Converts a word array to a Base64 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The Base64 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var base64String = CryptoJS.enc.Base64.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+            var map = this._map;
+
+            // Clamp excess bits
+            wordArray.clamp();
+
+            // Convert
+            var base64Chars = [];
+            for (var i = 0; i < sigBytes; i += 3) {
+                var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
+                var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
+                var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;
+
+                var triplet = (byte1 << 16) | (byte2 << 8) | byte3;
+
+                for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
+                    base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
+                }
+            }
+
+            // Add padding
+            var paddingChar = map.charAt(64);
+            if (paddingChar) {
+                while (base64Chars.length % 4) {
+                    base64Chars.push(paddingChar);
+                }
+            }
+
+            return base64Chars.join('');
+        },
+
+        /**
+         * Converts a Base64 string to a word array.
+         *
+         * @param {string} base64Str The Base64 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Base64.parse(base64String);
+         */
+        parse: function (base64Str) {
+            // Shortcuts
+            var base64StrLength = base64Str.length;
+            var map = this._map;
+
+            // Ignore padding
+            var paddingChar = map.charAt(64);
+            if (paddingChar) {
+                var paddingIndex = base64Str.indexOf(paddingChar);
+                if (paddingIndex != -1) {
+                    base64StrLength = paddingIndex;
+                }
+            }
+
+            // Convert
+            var words = [];
+            var nBytes = 0;
+            for (var i = 0; i < base64StrLength; i++) {
+                if (i % 4) {
+                    var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2);
+                    var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2);
+                    words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8);
+                    nBytes++;
+                }
+            }
+
+            return WordArray.create(words, nBytes);
+        },
+
+        _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+    };
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * Cipher core components.
+ */
+CryptoJS.lib.Cipher || (function (undefined) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var WordArray = C_lib.WordArray;
+    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm;
+    var C_enc = C.enc;
+    var Utf8 = C_enc.Utf8;
+    var Base64 = C_enc.Base64;
+    var C_algo = C.algo;
+    var EvpKDF = C_algo.EvpKDF;
+
+    /**
+     * Abstract base cipher template.
+     *
+     * @property {number} keySize This cipher's key size. Default: 4 (128 bits)
+     * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits)
+     * @property {number} _ENC_XFORM_MODE A constant representing encryption mode.
+     * @property {number} _DEC_XFORM_MODE A constant representing decryption mode.
+     */
+    var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {WordArray} iv The IV to use for this operation.
+         */
+        cfg: Base.extend(),
+
+        /**
+         * Creates this cipher in encryption mode.
+         *
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {Cipher} A cipher instance.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray });
+         */
+        createEncryptor: function (key, cfg) {
+            return this.create(this._ENC_XFORM_MODE, key, cfg);
+        },
+
+        /**
+         * Creates this cipher in decryption mode.
+         *
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {Cipher} A cipher instance.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray });
+         */
+        createDecryptor: function (key, cfg) {
+            return this.create(this._DEC_XFORM_MODE, key, cfg);
+        },
+
+        /**
+         * Initializes a newly created cipher.
+         *
+         * @param {number} xformMode Either the encryption or decryption transormation mode constant.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray });
+         */
+        init: function (xformMode, key, cfg) {
+            // Apply config defaults
+            this.cfg = this.cfg.extend(cfg);
+
+            // Store transform mode and key
+            this._xformMode = xformMode;
+            this._key = key;
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this cipher to its initial state.
+         *
+         * @example
+         *
+         *     cipher.reset();
+         */
+        reset: function () {
+            // Reset data buffer
+            BufferedBlockAlgorithm.reset.call(this);
+
+            // Perform concrete-cipher logic
+            this._doReset();
+        },
+
+        /**
+         * Adds data to be encrypted or decrypted.
+         *
+         * @param {WordArray|string} dataUpdate The data to encrypt or decrypt.
+         *
+         * @return {WordArray} The data after processing.
+         *
+         * @example
+         *
+         *     var encrypted = cipher.process('data');
+         *     var encrypted = cipher.process(wordArray);
+         */
+        process: function (dataUpdate) {
+            // Append
+            this._append(dataUpdate);
+
+            // Process available blocks
+            return this._process();
+        },
+
+        /**
+         * Finalizes the encryption or decryption process.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt.
+         *
+         * @return {WordArray} The data after final processing.
+         *
+         * @example
+         *
+         *     var encrypted = cipher.finalize();
+         *     var encrypted = cipher.finalize('data');
+         *     var encrypted = cipher.finalize(wordArray);
+         */
+        finalize: function (dataUpdate) {
+            // Final data update
+            if (dataUpdate) {
+                this._append(dataUpdate);
+            }
+
+            // Perform concrete-cipher logic
+            var finalProcessedData = this._doFinalize();
+
+            return finalProcessedData;
+        },
+
+        keySize: 128/32,
+
+        ivSize: 128/32,
+
+        _ENC_XFORM_MODE: 1,
+
+        _DEC_XFORM_MODE: 2,
+
+        /**
+         * Creates shortcut functions to a cipher's object interface.
+         *
+         * @param {Cipher} cipher The cipher to create a helper for.
+         *
+         * @return {Object} An object with encrypt and decrypt shortcut functions.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES);
+         */
+        _createHelper: (function () {
+            function selectCipherStrategy(key) {
+                if (typeof key == 'string') {
+                    return PasswordBasedCipher;
+                } else {
+                    return SerializableCipher;
+                }
+            }
+
+            return function (cipher) {
+                return {
+                    encrypt: function (message, key, cfg) {
+                        return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
+                    },
+
+                    decrypt: function (ciphertext, key, cfg) {
+                        return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
+                    }
+                };
+            };
+        }())
+    });
+
+    /**
+     * Abstract base stream cipher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits)
+     */
+    var StreamCipher = C_lib.StreamCipher = Cipher.extend({
+        _doFinalize: function () {
+            // Process partial blocks
+            var finalProcessedBlocks = this._process(!!'flush');
+
+            return finalProcessedBlocks;
+        },
+
+        blockSize: 1
+    });
+
+    /**
+     * Mode namespace.
+     */
+    var C_mode = C.mode = {};
+
+    /**
+     * Abstract base block cipher mode template.
+     */
+    var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({
+        /**
+         * Creates this mode for encryption.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words);
+         */
+        createEncryptor: function (cipher, iv) {
+            return this.Encryptor.create(cipher, iv);
+        },
+
+        /**
+         * Creates this mode for decryption.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words);
+         */
+        createDecryptor: function (cipher, iv) {
+            return this.Decryptor.create(cipher, iv);
+        },
+
+        /**
+         * Initializes a newly created mode.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words);
+         */
+        init: function (cipher, iv) {
+            this._cipher = cipher;
+            this._iv = iv;
+        }
+    });
+
+    /**
+     * Cipher Block Chaining mode.
+     */
+    var CBC = C_mode.CBC = (function () {
+        /**
+         * Abstract base CBC mode.
+         */
+        var CBC = BlockCipherMode.extend();
+
+        /**
+         * CBC encryptor.
+         */
+        CBC.Encryptor = CBC.extend({
+            /**
+             * Processes the data block at offset.
+             *
+             * @param {Array} words The data words to operate on.
+             * @param {number} offset The offset where the block starts.
+             *
+             * @example
+             *
+             *     mode.processBlock(data.words, offset);
+             */
+            processBlock: function (words, offset) {
+                // Shortcuts
+                var cipher = this._cipher;
+                var blockSize = cipher.blockSize;
+
+                // XOR and encrypt
+                xorBlock.call(this, words, offset, blockSize);
+                cipher.encryptBlock(words, offset);
+
+                // Remember this block to use with next block
+                this._prevBlock = words.slice(offset, offset + blockSize);
+            }
+        });
+
+        /**
+         * CBC decryptor.
+         */
+        CBC.Decryptor = CBC.extend({
+            /**
+             * Processes the data block at offset.
+             *
+             * @param {Array} words The data words to operate on.
+             * @param {number} offset The offset where the block starts.
+             *
+             * @example
+             *
+             *     mode.processBlock(data.words, offset);
+             */
+            processBlock: function (words, offset) {
+                // Shortcuts
+                var cipher = this._cipher;
+                var blockSize = cipher.blockSize;
+
+                // Remember this block to use with next block
+                var thisBlock = words.slice(offset, offset + blockSize);
+
+                // Decrypt and XOR
+                cipher.decryptBlock(words, offset);
+                xorBlock.call(this, words, offset, blockSize);
+
+                // This block becomes the previous block
+                this._prevBlock = thisBlock;
+            }
+        });
+
+        function xorBlock(words, offset, blockSize) {
+            // Shortcut
+            var iv = this._iv;
+
+            // Choose mixing block
+            if (iv) {
+                var block = iv;
+
+                // Remove IV for subsequent blocks
+                this._iv = undefined;
+            } else {
+                var block = this._prevBlock;
+            }
+
+            // XOR blocks
+            for (var i = 0; i < blockSize; i++) {
+                words[offset + i] ^= block[i];
+            }
+        }
+
+        return CBC;
+    }());
+
+    /**
+     * Padding namespace.
+     */
+    var C_pad = C.pad = {};
+
+    /**
+     * PKCS #5/7 padding strategy.
+     */
+    var Pkcs7 = C_pad.Pkcs7 = {
+        /**
+         * Pads data using the algorithm defined in PKCS #5/7.
+         *
+         * @param {WordArray} data The data to pad.
+         * @param {number} blockSize The multiple that the data should be padded to.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     CryptoJS.pad.Pkcs7.pad(wordArray, 4);
+         */
+        pad: function (data, blockSize) {
+            // Shortcut
+            var blockSizeBytes = blockSize * 4;
+
+            // Count padding bytes
+            var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;
+
+            // Create padding word
+            var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes;
+
+            // Create padding
+            var paddingWords = [];
+            for (var i = 0; i < nPaddingBytes; i += 4) {
+                paddingWords.push(paddingWord);
+            }
+            var padding = WordArray.create(paddingWords, nPaddingBytes);
+
+            // Add padding
+            data.concat(padding);
+        },
+
+        /**
+         * Unpads data that had been padded using the algorithm defined in PKCS #5/7.
+         *
+         * @param {WordArray} data The data to unpad.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     CryptoJS.pad.Pkcs7.unpad(wordArray);
+         */
+        unpad: function (data) {
+            // Get number of padding bytes from last byte
+            var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
+
+            // Remove padding
+            data.sigBytes -= nPaddingBytes;
+        }
+    };
+
+    /**
+     * Abstract base block cipher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits)
+     */
+    var BlockCipher = C_lib.BlockCipher = Cipher.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {Mode} mode The block mode to use. Default: CBC
+         * @property {Padding} padding The padding strategy to use. Default: Pkcs7
+         */
+        cfg: Cipher.cfg.extend({
+            mode: CBC,
+            padding: Pkcs7
+        }),
+
+        reset: function () {
+            // Reset cipher
+            Cipher.reset.call(this);
+
+            // Shortcuts
+            var cfg = this.cfg;
+            var iv = cfg.iv;
+            var mode = cfg.mode;
+
+            // Reset block mode
+            if (this._xformMode == this._ENC_XFORM_MODE) {
+                var modeCreator = mode.createEncryptor;
+            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+                var modeCreator = mode.createDecryptor;
+
+                // Keep at least one block in the buffer for unpadding
+                this._minBufferSize = 1;
+            }
+            this._mode = modeCreator.call(mode, this, iv && iv.words);
+        },
+
+        _doProcessBlock: function (words, offset) {
+            this._mode.processBlock(words, offset);
+        },
+
+        _doFinalize: function () {
+            // Shortcut
+            var padding = this.cfg.padding;
+
+            // Finalize
+            if (this._xformMode == this._ENC_XFORM_MODE) {
+                // Pad data
+                padding.pad(this._data, this.blockSize);
+
+                // Process final blocks
+                var finalProcessedBlocks = this._process(!!'flush');
+            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+                // Process final blocks
+                var finalProcessedBlocks = this._process(!!'flush');
+
+                // Unpad data
+                padding.unpad(finalProcessedBlocks);
+            }
+
+            return finalProcessedBlocks;
+        },
+
+        blockSize: 128/32
+    });
+
+    /**
+     * A collection of cipher parameters.
+     *
+     * @property {WordArray} ciphertext The raw ciphertext.
+     * @property {WordArray} key The key to this ciphertext.
+     * @property {WordArray} iv The IV used in the ciphering operation.
+     * @property {WordArray} salt The salt used with a key derivation function.
+     * @property {Cipher} algorithm The cipher algorithm.
+     * @property {Mode} mode The block mode used in the ciphering operation.
+     * @property {Padding} padding The padding scheme used in the ciphering operation.
+     * @property {number} blockSize The block size of the cipher.
+     * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string.
+     */
+    var CipherParams = C_lib.CipherParams = Base.extend({
+        /**
+         * Initializes a newly created cipher params object.
+         *
+         * @param {Object} cipherParams An object with any of the possible cipher parameters.
+         *
+         * @example
+         *
+         *     var cipherParams = CryptoJS.lib.CipherParams.create({
+         *         ciphertext: ciphertextWordArray,
+         *         key: keyWordArray,
+         *         iv: ivWordArray,
+         *         salt: saltWordArray,
+         *         algorithm: CryptoJS.algo.AES,
+         *         mode: CryptoJS.mode.CBC,
+         *         padding: CryptoJS.pad.PKCS7,
+         *         blockSize: 4,
+         *         formatter: CryptoJS.format.OpenSSL
+         *     });
+         */
+        init: function (cipherParams) {
+            this.mixIn(cipherParams);
+        },
+
+        /**
+         * Converts this cipher params object to a string.
+         *
+         * @param {Format} formatter (Optional) The formatting strategy to use.
+         *
+         * @return {string} The stringified cipher params.
+         *
+         * @throws Error If neither the formatter nor the default formatter is set.
+         *
+         * @example
+         *
+         *     var string = cipherParams + '';
+         *     var string = cipherParams.toString();
+         *     var string = cipherParams.toString(CryptoJS.format.OpenSSL);
+         */
+        toString: function (formatter) {
+            return (formatter || this.formatter).stringify(this);
+        }
+    });
+
+    /**
+     * Format namespace.
+     */
+    var C_format = C.format = {};
+
+    /**
+     * OpenSSL formatting strategy.
+     */
+    var OpenSSLFormatter = C_format.OpenSSL = {
+        /**
+         * Converts a cipher params object to an OpenSSL-compatible string.
+         *
+         * @param {CipherParams} cipherParams The cipher params object.
+         *
+         * @return {string} The OpenSSL-compatible string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams);
+         */
+        stringify: function (cipherParams) {
+            // Shortcuts
+            var ciphertext = cipherParams.ciphertext;
+            var salt = cipherParams.salt;
+
+            // Format
+            if (salt) {
+                var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
+            } else {
+                var wordArray = ciphertext;
+            }
+
+            return wordArray.toString(Base64);
+        },
+
+        /**
+         * Converts an OpenSSL-compatible string to a cipher params object.
+         *
+         * @param {string} openSSLStr The OpenSSL-compatible string.
+         *
+         * @return {CipherParams} The cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString);
+         */
+        parse: function (openSSLStr) {
+            // Parse base64
+            var ciphertext = Base64.parse(openSSLStr);
+
+            // Shortcut
+            var ciphertextWords = ciphertext.words;
+
+            // Test for salt
+            if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) {
+                // Extract salt
+                var salt = WordArray.create(ciphertextWords.slice(2, 4));
+
+                // Remove salt from ciphertext
+                ciphertextWords.splice(0, 4);
+                ciphertext.sigBytes -= 16;
+            }
+
+            return CipherParams.create({ ciphertext: ciphertext, salt: salt });
+        }
+    };
+
+    /**
+     * A cipher wrapper that returns ciphertext as a serializable cipher params object.
+     */
+    var SerializableCipher = C_lib.SerializableCipher = Base.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL
+         */
+        cfg: Base.extend({
+            format: OpenSSLFormatter
+        }),
+
+        /**
+         * Encrypts a message.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {WordArray|string} message The message to encrypt.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {CipherParams} A cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key);
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv });
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         */
+        encrypt: function (cipher, message, key, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Encrypt
+            var encryptor = cipher.createEncryptor(key, cfg);
+            var ciphertext = encryptor.finalize(message);
+
+            // Shortcut
+            var cipherCfg = encryptor.cfg;
+
+            // Create and return serializable cipher params
+            return CipherParams.create({
+                ciphertext: ciphertext,
+                key: key,
+                iv: cipherCfg.iv,
+                algorithm: cipher,
+                mode: cipherCfg.mode,
+                padding: cipherCfg.padding,
+                blockSize: cipher.blockSize,
+                formatter: cfg.format
+            });
+        },
+
+        /**
+         * Decrypts serialized ciphertext.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {WordArray} The plaintext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         */
+        decrypt: function (cipher, ciphertext, key, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Convert string to CipherParams
+            ciphertext = this._parse(ciphertext, cfg.format);
+
+            // Decrypt
+            var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext);
+
+            return plaintext;
+        },
+
+        /**
+         * Converts serialized ciphertext to CipherParams,
+         * else assumed CipherParams already and returns ciphertext unchanged.
+         *
+         * @param {CipherParams|string} ciphertext The ciphertext.
+         * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext.
+         *
+         * @return {CipherParams} The unserialized ciphertext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format);
+         */
+        _parse: function (ciphertext, format) {
+            if (typeof ciphertext == 'string') {
+                return format.parse(ciphertext, this);
+            } else {
+                return ciphertext;
+            }
+        }
+    });
+
+    /**
+     * Key derivation function namespace.
+     */
+    var C_kdf = C.kdf = {};
+
+    /**
+     * OpenSSL key derivation function.
+     */
+    var OpenSSLKdf = C_kdf.OpenSSL = {
+        /**
+         * Derives a key and IV from a password.
+         *
+         * @param {string} password The password to derive from.
+         * @param {number} keySize The size in words of the key to generate.
+         * @param {number} ivSize The size in words of the IV to generate.
+         * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly.
+         *
+         * @return {CipherParams} A cipher params object with the key, IV, and salt.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
+         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
+         */
+        execute: function (password, keySize, ivSize, salt) {
+            // Generate random salt
+            if (!salt) {
+                salt = WordArray.random(64/8);
+            }
+
+            // Derive key and IV
+            var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
+
+            // Separate key and IV
+            var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
+            key.sigBytes = keySize * 4;
+
+            // Return params
+            return CipherParams.create({ key: key, iv: iv, salt: salt });
+        }
+    };
+
+    /**
+     * A serializable cipher wrapper that derives the key from a password,
+     * and returns ciphertext as a serializable cipher params object.
+     */
+    var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL
+         */
+        cfg: SerializableCipher.cfg.extend({
+            kdf: OpenSSLKdf
+        }),
+
+        /**
+         * Encrypts a message using a password.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {WordArray|string} message The message to encrypt.
+         * @param {string} password The password.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {CipherParams} A cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password');
+         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL });
+         */
+        encrypt: function (cipher, message, password, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Derive key and other params
+            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize);
+
+            // Add IV to config
+            cfg.iv = derivedParams.iv;
+
+            // Encrypt
+            var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);
+
+            // Mix in derived params
+            ciphertext.mixIn(derivedParams);
+
+            return ciphertext;
+        },
+
+        /**
+         * Decrypts serialized ciphertext using a password.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+         * @param {string} password The password.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {WordArray} The plaintext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL });
+         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL });
+         */
+        decrypt: function (cipher, ciphertext, password, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Convert string to CipherParams
+            ciphertext = this._parse(ciphertext, cfg.format);
+
+            // Derive key and other params
+            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt);
+
+            // Add IV to config
+            cfg.iv = derivedParams.iv;
+
+            // Decrypt
+            var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg);
+
+            return plaintext;
+        }
+    });
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function (undefined) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var X32WordArray = C_lib.WordArray;
+
+    /**
+     * x64 namespace.
+     */
+    var C_x64 = C.x64 = {};
+
+    /**
+     * A 64-bit word.
+     */
+    var X64Word = C_x64.Word = Base.extend({
+        /**
+         * Initializes a newly created 64-bit word.
+         *
+         * @param {number} high The high 32 bits.
+         * @param {number} low The low 32 bits.
+         *
+         * @example
+         *
+         *     var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607);
+         */
+        init: function (high, low) {
+            this.high = high;
+            this.low = low;
+        }
+
+        /**
+         * Bitwise NOTs this word.
+         *
+         * @return {X64Word} A new x64-Word object after negating.
+         *
+         * @example
+         *
+         *     var negated = x64Word.not();
+         */
+        // not: function () {
+            // var high = ~this.high;
+            // var low = ~this.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise ANDs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to AND with this word.
+         *
+         * @return {X64Word} A new x64-Word object after ANDing.
+         *
+         * @example
+         *
+         *     var anded = x64Word.and(anotherX64Word);
+         */
+        // and: function (word) {
+            // var high = this.high & word.high;
+            // var low = this.low & word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise ORs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to OR with this word.
+         *
+         * @return {X64Word} A new x64-Word object after ORing.
+         *
+         * @example
+         *
+         *     var ored = x64Word.or(anotherX64Word);
+         */
+        // or: function (word) {
+            // var high = this.high | word.high;
+            // var low = this.low | word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise XORs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to XOR with this word.
+         *
+         * @return {X64Word} A new x64-Word object after XORing.
+         *
+         * @example
+         *
+         *     var xored = x64Word.xor(anotherX64Word);
+         */
+        // xor: function (word) {
+            // var high = this.high ^ word.high;
+            // var low = this.low ^ word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Shifts this word n bits to the left.
+         *
+         * @param {number} n The number of bits to shift.
+         *
+         * @return {X64Word} A new x64-Word object after shifting.
+         *
+         * @example
+         *
+         *     var shifted = x64Word.shiftL(25);
+         */
+        // shiftL: function (n) {
+            // if (n < 32) {
+                // var high = (this.high << n) | (this.low >>> (32 - n));
+                // var low = this.low << n;
+            // } else {
+                // var high = this.low << (n - 32);
+                // var low = 0;
+            // }
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Shifts this word n bits to the right.
+         *
+         * @param {number} n The number of bits to shift.
+         *
+         * @return {X64Word} A new x64-Word object after shifting.
+         *
+         * @example
+         *
+         *     var shifted = x64Word.shiftR(7);
+         */
+        // shiftR: function (n) {
+            // if (n < 32) {
+                // var low = (this.low >>> n) | (this.high << (32 - n));
+                // var high = this.high >>> n;
+            // } else {
+                // var low = this.high >>> (n - 32);
+                // var high = 0;
+            // }
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Rotates this word n bits to the left.
+         *
+         * @param {number} n The number of bits to rotate.
+         *
+         * @return {X64Word} A new x64-Word object after rotating.
+         *
+         * @example
+         *
+         *     var rotated = x64Word.rotL(25);
+         */
+        // rotL: function (n) {
+            // return this.shiftL(n).or(this.shiftR(64 - n));
+        // },
+
+        /**
+         * Rotates this word n bits to the right.
+         *
+         * @param {number} n The number of bits to rotate.
+         *
+         * @return {X64Word} A new x64-Word object after rotating.
+         *
+         * @example
+         *
+         *     var rotated = x64Word.rotR(7);
+         */
+        // rotR: function (n) {
+            // return this.shiftR(n).or(this.shiftL(64 - n));
+        // },
+
+        /**
+         * Adds this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to add with this word.
+         *
+         * @return {X64Word} A new x64-Word object after adding.
+         *
+         * @example
+         *
+         *     var added = x64Word.add(anotherX64Word);
+         */
+        // add: function (word) {
+            // var low = (this.low + word.low) | 0;
+            // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0;
+            // var high = (this.high + word.high + carry) | 0;
+
+            // return X64Word.create(high, low);
+        // }
+    });
+
+    /**
+     * An array of 64-bit words.
+     *
+     * @property {Array} words The array of CryptoJS.x64.Word objects.
+     * @property {number} sigBytes The number of significant bytes in this word array.
+     */
+    var X64WordArray = C_x64.WordArray = Base.extend({
+        /**
+         * Initializes a newly created word array.
+         *
+         * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects.
+         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create();
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create([
+         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+         *     ]);
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create([
+         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+         *     ], 10);
+         */
+        init: function (words, sigBytes) {
+            words = this.words = words || [];
+
+            if (sigBytes != undefined) {
+                this.sigBytes = sigBytes;
+            } else {
+                this.sigBytes = words.length * 8;
+            }
+        },
+
+        /**
+         * Converts this 64-bit word array to a 32-bit word array.
+         *
+         * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array.
+         *
+         * @example
+         *
+         *     var x32WordArray = x64WordArray.toX32();
+         */
+        toX32: function () {
+            // Shortcuts
+            var x64Words = this.words;
+            var x64WordsLength = x64Words.length;
+
+            // Convert
+            var x32Words = [];
+            for (var i = 0; i < x64WordsLength; i++) {
+                var x64Word = x64Words[i];
+                x32Words.push(x64Word.high);
+                x32Words.push(x64Word.low);
+            }
+
+            return X32WordArray.create(x32Words, this.sigBytes);
+        },
+
+        /**
+         * Creates a copy of this word array.
+         *
+         * @return {X64WordArray} The clone.
+         *
+         * @example
+         *
+         *     var clone = x64WordArray.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+
+            // Clone "words" array
+            var words = clone.words = this.words.slice(0);
+
+            // Clone each X64Word object
+            var wordsLength = words.length;
+            for (var i = 0; i < wordsLength; i++) {
+                words[i] = words[i].clone();
+            }
+
+            return clone;
+        }
+    });
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var BlockCipher = C_lib.BlockCipher;
+    var C_algo = C.algo;
+
+    // Lookup tables
+    var SBOX = [];
+    var INV_SBOX = [];
+    var SUB_MIX_0 = [];
+    var SUB_MIX_1 = [];
+    var SUB_MIX_2 = [];
+    var SUB_MIX_3 = [];
+    var INV_SUB_MIX_0 = [];
+    var INV_SUB_MIX_1 = [];
+    var INV_SUB_MIX_2 = [];
+    var INV_SUB_MIX_3 = [];
+
+    // Compute lookup tables
+    (function () {
+        // Compute double table
+        var d = [];
+        for (var i = 0; i < 256; i++) {
+            if (i < 128) {
+                d[i] = i << 1;
+            } else {
+                d[i] = (i << 1) ^ 0x11b;
+            }
+        }
+
+        // Walk GF(2^8)
+        var x = 0;
+        var xi = 0;
+        for (var i = 0; i < 256; i++) {
+            // Compute sbox
+            var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
+            sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
+            SBOX[x] = sx;
+            INV_SBOX[sx] = x;
+
+            // Compute multiplication
+            var x2 = d[x];
+            var x4 = d[x2];
+            var x8 = d[x4];
+
+            // Compute sub bytes, mix columns tables
+            var t = (d[sx] * 0x101) ^ (sx * 0x1010100);
+            SUB_MIX_0[x] = (t << 24) | (t >>> 8);
+            SUB_MIX_1[x] = (t << 16) | (t >>> 16);
+            SUB_MIX_2[x] = (t << 8)  | (t >>> 24);
+            SUB_MIX_3[x] = t;
+
+            // Compute inv sub bytes, inv mix columns tables
+            var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
+            INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8);
+            INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16);
+            INV_SUB_MIX_2[sx] = (t << 8)  | (t >>> 24);
+            INV_SUB_MIX_3[sx] = t;
+
+            // Compute next counter
+            if (!x) {
+                x = xi = 1;
+            } else {
+                x = x2 ^ d[d[d[x8 ^ x2]]];
+                xi ^= d[d[xi]];
+            }
+        }
+    }());
+
+    // Precomputed Rcon lookup
+    var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
+
+    /**
+     * AES block cipher algorithm.
+     */
+    var AES = C_algo.AES = BlockCipher.extend({
+        _doReset: function () {
+            // Shortcuts
+            var key = this._key;
+            var keyWords = key.words;
+            var keySize = key.sigBytes / 4;
+
+            // Compute number of rounds
+            var nRounds = this._nRounds = keySize + 6
+
+            // Compute number of key schedule rows
+            var ksRows = (nRounds + 1) * 4;
+
+            // Compute key schedule
+            var keySchedule = this._keySchedule = [];
+            for (var ksRow = 0; ksRow < ksRows; ksRow++) {
+                if (ksRow < keySize) {
+                    keySchedule[ksRow] = keyWords[ksRow];
+                } else {
+                    var t = keySchedule[ksRow - 1];
+
+                    if (!(ksRow % keySize)) {
+                        // Rot word
+                        t = (t << 8) | (t >>> 24);
+
+                        // Sub word
+                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+
+                        // Mix Rcon
+                        t ^= RCON[(ksRow / keySize) | 0] << 24;
+                    } else if (keySize > 6 && ksRow % keySize == 4) {
+                        // Sub word
+                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+                    }
+
+                    keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t;
+                }
+            }
+
+            // Compute inv key schedule
+            var invKeySchedule = this._invKeySchedule = [];
+            for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) {
+                var ksRow = ksRows - invKsRow;
+
+                if (invKsRow % 4) {
+                    var t = keySchedule[ksRow];
+                } else {
+                    var t = keySchedule[ksRow - 4];
+                }
+
+                if (invKsRow < 4 || ksRow <= 4) {
+                    invKeySchedule[invKsRow] = t;
+                } else {
+                    invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^
+                                               INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]];
+                }
+            }
+        },
+
+        encryptBlock: function (M, offset) {
+            this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX);
+        },
+
+        decryptBlock: function (M, offset) {
+            // Swap 2nd and 4th rows
+            var t = M[offset + 1];
+            M[offset + 1] = M[offset + 3];
+            M[offset + 3] = t;
+
+            this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX);
+
+            // Inv swap 2nd and 4th rows
+            var t = M[offset + 1];
+            M[offset + 1] = M[offset + 3];
+            M[offset + 3] = t;
+        },
+
+        _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) {
+            // Shortcut
+            var nRounds = this._nRounds;
+
+            // Get input, add round key
+            var s0 = M[offset]     ^ keySchedule[0];
+            var s1 = M[offset + 1] ^ keySchedule[1];
+            var s2 = M[offset + 2] ^ keySchedule[2];
+            var s3 = M[offset + 3] ^ keySchedule[3];
+
+            // Key schedule row counter
+            var ksRow = 4;
+
+            // Rounds
+            for (var round = 1; round < nRounds; round++) {
+                // Shift rows, sub bytes, mix columns, add round key
+                var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++];
+                var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++];
+                var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++];
+                var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++];
+
+                // Update state
+                s0 = t0;
+                s1 = t1;
+                s2 = t2;
+                s3 = t3;
+            }
+
+            // Shift rows, sub bytes, add round key
+            var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++];
+            var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++];
+            var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++];
+            var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++];
+
+            // Set output
+            M[offset]     = t0;
+            M[offset + 1] = t1;
+            M[offset + 2] = t2;
+            M[offset + 3] = t3;
+        },
+
+        keySize: 256/32
+    });
+
+    /**
+     * Shortcut functions to the cipher's object interface.
+     *
+     * @example
+     *
+     *     var ciphertext = CryptoJS.AES.encrypt(message, key, cfg);
+     *     var plaintext  = CryptoJS.AES.decrypt(ciphertext, key, cfg);
+     */
+    C.AES = BlockCipher._createHelper(AES);
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var Hasher = C_lib.Hasher;
+    var C_algo = C.algo;
+
+    // Reusable object
+    var W = [];
+
+    /**
+     * SHA-1 hash algorithm.
+     */
+    var SHA1 = C_algo.SHA1 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new WordArray.init([
+                0x67452301, 0xefcdab89,
+                0x98badcfe, 0x10325476,
+                0xc3d2e1f0
+            ]);
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcut
+            var H = this._hash.words;
+
+            // Working variables
+            var a = H[0];
+            var b = H[1];
+            var c = H[2];
+            var d = H[3];
+            var e = H[4];
+
+            // Computation
+            for (var i = 0; i < 80; i++) {
+                if (i < 16) {
+                    W[i] = M[offset + i] | 0;
+                } else {
+                    var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
+                    W[i] = (n << 1) | (n >>> 31);
+                }
+
+                var t = ((a << 5) | (a >>> 27)) + e + W[i];
+                if (i < 20) {
+                    t += ((b & c) | (~b & d)) + 0x5a827999;
+                } else if (i < 40) {
+                    t += (b ^ c ^ d) + 0x6ed9eba1;
+                } else if (i < 60) {
+                    t += ((b & c) | (b & d) | (c & d)) - 0x70e44324;
+                } else /* if (i < 80) */ {
+                    t += (b ^ c ^ d) - 0x359d3e2a;
+                }
+
+                e = d;
+                d = c;
+                c = (b << 30) | (b >>> 2);
+                b = a;
+                a = t;
+            }
+
+            // Intermediate hash value
+            H[0] = (H[0] + a) | 0;
+            H[1] = (H[1] + b) | 0;
+            H[2] = (H[2] + c) | 0;
+            H[3] = (H[3] + d) | 0;
+            H[4] = (H[4] + e) | 0;
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Return final computed hash
+            return this._hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        }
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA1('message');
+     *     var hash = CryptoJS.SHA1(wordArray);
+     */
+    C.SHA1 = Hasher._createHelper(SHA1);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA1(message, key);
+     */
+    C.HmacSHA1 = Hasher._createHmacHelper(SHA1);
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function (Math) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var Hasher = C_lib.Hasher;
+    var C_algo = C.algo;
+
+    // Initialization and round constants tables
+    var H = [];
+    var K = [];
+
+    // Compute constants
+    (function () {
+        function isPrime(n) {
+            var sqrtN = Math.sqrt(n);
+            for (var factor = 2; factor <= sqrtN; factor++) {
+                if (!(n % factor)) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        function getFractionalBits(n) {
+            return ((n - (n | 0)) * 0x100000000) | 0;
+        }
+
+        var n = 2;
+        var nPrime = 0;
+        while (nPrime < 64) {
+            if (isPrime(n)) {
+                if (nPrime < 8) {
+                    H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
+                }
+                K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
+
+                nPrime++;
+            }
+
+            n++;
+        }
+    }());
+
+    // Reusable object
+    var W = [];
+
+    /**
+     * SHA-256 hash algorithm.
+     */
+    var SHA256 = C_algo.SHA256 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new WordArray.init(H.slice(0));
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcut
+            var H = this._hash.words;
+
+            // Working variables
+            var a = H[0];
+            var b = H[1];
+            var c = H[2];
+            var d = H[3];
+            var e = H[4];
+            var f = H[5];
+            var g = H[6];
+            var h = H[7];
+
+            // Computation
+            for (var i = 0; i < 64; i++) {
+                if (i < 16) {
+                    W[i] = M[offset + i] | 0;
+                } else {
+                    var gamma0x = W[i - 15];
+                    var gamma0  = ((gamma0x << 25) | (gamma0x >>> 7))  ^
+                                  ((gamma0x << 14) | (gamma0x >>> 18)) ^
+                                   (gamma0x >>> 3);
+
+                    var gamma1x = W[i - 2];
+                    var gamma1  = ((gamma1x << 15) | (gamma1x >>> 17)) ^
+                                  ((gamma1x << 13) | (gamma1x >>> 19)) ^
+                                   (gamma1x >>> 10);
+
+                    W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
+                }
+
+                var ch  = (e & f) ^ (~e & g);
+                var maj = (a & b) ^ (a & c) ^ (b & c);
+
+                var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
+                var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7)  | (e >>> 25));
+
+                var t1 = h + sigma1 + ch + K[i] + W[i];
+                var t2 = sigma0 + maj;
+
+                h = g;
+                g = f;
+                f = e;
+                e = (d + t1) | 0;
+                d = c;
+                c = b;
+                b = a;
+                a = (t1 + t2) | 0;
+            }
+
+            // Intermediate hash value
+            H[0] = (H[0] + a) | 0;
+            H[1] = (H[1] + b) | 0;
+            H[2] = (H[2] + c) | 0;
+            H[3] = (H[3] + d) | 0;
+            H[4] = (H[4] + e) | 0;
+            H[5] = (H[5] + f) | 0;
+            H[6] = (H[6] + g) | 0;
+            H[7] = (H[7] + h) | 0;
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Return final computed hash
+            return this._hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        }
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA256('message');
+     *     var hash = CryptoJS.SHA256(wordArray);
+     */
+    C.SHA256 = Hasher._createHelper(SHA256);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA256(message, key);
+     */
+    C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
+}(Math));
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Hasher = C_lib.Hasher;
+    var C_x64 = C.x64;
+    var X64Word = C_x64.Word;
+    var X64WordArray = C_x64.WordArray;
+    var C_algo = C.algo;
+
+    function X64Word_create() {
+        return X64Word.create.apply(X64Word, arguments);
+    }
+
+    // Constants
+    var K = [
+        X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd),
+        X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc),
+        X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019),
+        X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118),
+        X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe),
+        X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2),
+        X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1),
+        X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694),
+        X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3),
+        X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65),
+        X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483),
+        X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5),
+        X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210),
+        X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4),
+        X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725),
+        X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70),
+        X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926),
+        X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df),
+        X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8),
+        X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b),
+        X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001),
+        X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30),
+        X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910),
+        X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8),
+        X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53),
+        X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8),
+        X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb),
+        X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3),
+        X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60),
+        X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec),
+        X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9),
+        X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b),
+        X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207),
+        X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178),
+        X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6),
+        X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b),
+        X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493),
+        X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c),
+        X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a),
+        X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817)
+    ];
+
+    // Reusable objects
+    var W = [];
+    (function () {
+        for (var i = 0; i < 80; i++) {
+            W[i] = X64Word_create();
+        }
+    }());
+
+    /**
+     * SHA-512 hash algorithm.
+     */
+    var SHA512 = C_algo.SHA512 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new X64WordArray.init([
+                new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b),
+                new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1),
+                new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f),
+                new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179)
+            ]);
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcuts
+            var H = this._hash.words;
+
+            var H0 = H[0];
+            var H1 = H[1];
+            var H2 = H[2];
+            var H3 = H[3];
+            var H4 = H[4];
+            var H5 = H[5];
+            var H6 = H[6];
+            var H7 = H[7];
+
+            var H0h = H0.high;
+            var H0l = H0.low;
+            var H1h = H1.high;
+            var H1l = H1.low;
+            var H2h = H2.high;
+            var H2l = H2.low;
+            var H3h = H3.high;
+            var H3l = H3.low;
+            var H4h = H4.high;
+            var H4l = H4.low;
+            var H5h = H5.high;
+            var H5l = H5.low;
+            var H6h = H6.high;
+            var H6l = H6.low;
+            var H7h = H7.high;
+            var H7l = H7.low;
+
+            // Working variables
+            var ah = H0h;
+            var al = H0l;
+            var bh = H1h;
+            var bl = H1l;
+            var ch = H2h;
+            var cl = H2l;
+            var dh = H3h;
+            var dl = H3l;
+            var eh = H4h;
+            var el = H4l;
+            var fh = H5h;
+            var fl = H5l;
+            var gh = H6h;
+            var gl = H6l;
+            var hh = H7h;
+            var hl = H7l;
+
+            // Rounds
+            for (var i = 0; i < 80; i++) {
+                // Shortcut
+                var Wi = W[i];
+
+                // Extend message
+                if (i < 16) {
+                    var Wih = Wi.high = M[offset + i * 2]     | 0;
+                    var Wil = Wi.low  = M[offset + i * 2 + 1] | 0;
+                } else {
+                    // Gamma0
+                    var gamma0x  = W[i - 15];
+                    var gamma0xh = gamma0x.high;
+                    var gamma0xl = gamma0x.low;
+                    var gamma0h  = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7);
+                    var gamma0l  = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25));
+
+                    // Gamma1
+                    var gamma1x  = W[i - 2];
+                    var gamma1xh = gamma1x.high;
+                    var gamma1xl = gamma1x.low;
+                    var gamma1h  = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6);
+                    var gamma1l  = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26));
+
+                    // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]
+                    var Wi7  = W[i - 7];
+                    var Wi7h = Wi7.high;
+                    var Wi7l = Wi7.low;
+
+                    var Wi16  = W[i - 16];
+                    var Wi16h = Wi16.high;
+                    var Wi16l = Wi16.low;
+
+                    var Wil = gamma0l + Wi7l;
+                    var Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0);
+                    var Wil = Wil + gamma1l;
+                    var Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0);
+                    var Wil = Wil + Wi16l;
+                    var Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0);
+
+                    Wi.high = Wih;
+                    Wi.low  = Wil;
+                }
+
+                var chh  = (eh & fh) ^ (~eh & gh);
+                var chl  = (el & fl) ^ (~el & gl);
+                var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch);
+                var majl = (al & bl) ^ (al & cl) ^ (bl & cl);
+
+                var sigma0h = ((ah >>> 28) | (al << 4))  ^ ((ah << 30)  | (al >>> 2)) ^ ((ah << 25) | (al >>> 7));
+                var sigma0l = ((al >>> 28) | (ah << 4))  ^ ((al << 30)  | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7));
+                var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9));
+                var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9));
+
+                // t1 = h + sigma1 + ch + K[i] + W[i]
+                var Ki  = K[i];
+                var Kih = Ki.high;
+                var Kil = Ki.low;
+
+                var t1l = hl + sigma1l;
+                var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0);
+                var t1l = t1l + chl;
+                var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0);
+                var t1l = t1l + Kil;
+                var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0);
+                var t1l = t1l + Wil;
+                var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0);
+
+                // t2 = sigma0 + maj
+                var t2l = sigma0l + majl;
+                var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0);
+
+                // Update working variables
+                hh = gh;
+                hl = gl;
+                gh = fh;
+                gl = fl;
+                fh = eh;
+                fl = el;
+                el = (dl + t1l) | 0;
+                eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0;
+                dh = ch;
+                dl = cl;
+                ch = bh;
+                cl = bl;
+                bh = ah;
+                bl = al;
+                al = (t1l + t2l) | 0;
+                ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0;
+            }
+
+            // Intermediate hash value
+            H0l = H0.low  = (H0l + al);
+            H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0));
+            H1l = H1.low  = (H1l + bl);
+            H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0));
+            H2l = H2.low  = (H2l + cl);
+            H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0));
+            H3l = H3.low  = (H3l + dl);
+            H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0));
+            H4l = H4.low  = (H4l + el);
+            H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0));
+            H5l = H5.low  = (H5l + fl);
+            H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0));
+            H6l = H6.low  = (H6l + gl);
+            H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0));
+            H7l = H7.low  = (H7l + hl);
+            H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0));
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Convert hash to 32-bit word array before returning
+            var hash = this._hash.toX32();
+
+            // Return final computed hash
+            return hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        },
+
+        blockSize: 1024/32
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA512('message');
+     *     var hash = CryptoJS.SHA512(wordArray);
+     */
+    C.SHA512 = Hasher._createHelper(SHA512);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA512(message, key);
+     */
+    C.HmacSHA512 = Hasher._createHmacHelper(SHA512);
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var C_enc = C.enc;
+    var Utf8 = C_enc.Utf8;
+    var C_algo = C.algo;
+
+    /**
+     * HMAC algorithm.
+     */
+    var HMAC = C_algo.HMAC = Base.extend({
+        /**
+         * Initializes a newly created HMAC.
+         *
+         * @param {Hasher} hasher The hash algorithm to use.
+         * @param {WordArray|string} key The secret key.
+         *
+         * @example
+         *
+         *     var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
+         */
+        init: function (hasher, key) {
+            // Init hasher
+            hasher = this._hasher = new hasher.init();
+
+            // Convert string to WordArray, else assume WordArray already
+            if (typeof key == 'string') {
+                key = Utf8.parse(key);
+            }
+
+            // Shortcuts
+            var hasherBlockSize = hasher.blockSize;
+            var hasherBlockSizeBytes = hasherBlockSize * 4;
+
+            // Allow arbitrary length keys
+            if (key.sigBytes > hasherBlockSizeBytes) {
+                key = hasher.finalize(key);
+            }
+
+            // Clamp excess bits
+            key.clamp();
+
+            // Clone key for inner and outer pads
+            var oKey = this._oKey = key.clone();
+            var iKey = this._iKey = key.clone();
+
+            // Shortcuts
+            var oKeyWords = oKey.words;
+            var iKeyWords = iKey.words;
+
+            // XOR keys with pad constants
+            for (var i = 0; i < hasherBlockSize; i++) {
+                oKeyWords[i] ^= 0x5c5c5c5c;
+                iKeyWords[i] ^= 0x36363636;
+            }
+            oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this HMAC to its initial state.
+         *
+         * @example
+         *
+         *     hmacHasher.reset();
+         */
+        reset: function () {
+            // Shortcut
+            var hasher = this._hasher;
+
+            // Reset
+            hasher.reset();
+            hasher.update(this._iKey);
+        },
+
+        /**
+         * Updates this HMAC with a message.
+         *
+         * @param {WordArray|string} messageUpdate The message to append.
+         *
+         * @return {HMAC} This HMAC instance.
+         *
+         * @example
+         *
+         *     hmacHasher.update('message');
+         *     hmacHasher.update(wordArray);
+         */
+        update: function (messageUpdate) {
+            this._hasher.update(messageUpdate);
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Finalizes the HMAC computation.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} messageUpdate (Optional) A final message update.
+         *
+         * @return {WordArray} The HMAC.
+         *
+         * @example
+         *
+         *     var hmac = hmacHasher.finalize();
+         *     var hmac = hmacHasher.finalize('message');
+         *     var hmac = hmacHasher.finalize(wordArray);
+         */
+        finalize: function (messageUpdate) {
+            // Shortcut
+            var hasher = this._hasher;
+
+            // Compute HMAC
+            var innerHash = hasher.finalize(messageUpdate);
+            hasher.reset();
+            var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));
+
+            return hmac;
+        }
+    });
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var WordArray = C_lib.WordArray;
+    var C_algo = C.algo;
+    var SHA1 = C_algo.SHA1;
+    var HMAC = C_algo.HMAC;
+
+    /**
+     * Password-Based Key Derivation Function 2 algorithm.
+     */
+    var PBKDF2 = C_algo.PBKDF2 = Base.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
+         * @property {Hasher} hasher The hasher to use. Default: SHA1
+         * @property {number} iterations The number of iterations to perform. Default: 1
+         */
+        cfg: Base.extend({
+            keySize: 128/32,
+            hasher: SHA1,
+            iterations: 1
+        }),
+
+        /**
+         * Initializes a newly created key derivation function.
+         *
+         * @param {Object} cfg (Optional) The configuration options to use for the derivation.
+         *
+         * @example
+         *
+         *     var kdf = CryptoJS.algo.PBKDF2.create();
+         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 });
+         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 });
+         */
+        init: function (cfg) {
+            this.cfg = this.cfg.extend(cfg);
+        },
+
+        /**
+         * Computes the Password-Based Key Derivation Function 2.
+         *
+         * @param {WordArray|string} password The password.
+         * @param {WordArray|string} salt A salt.
+         *
+         * @return {WordArray} The derived key.
+         *
+         * @example
+         *
+         *     var key = kdf.compute(password, salt);
+         */
+        compute: function (password, salt) {
+            // Shortcut
+            var cfg = this.cfg;
+
+            // Init HMAC
+            var hmac = HMAC.create(cfg.hasher, password);
+
+            // Initial values
+            var derivedKey = WordArray.create();
+            var blockIndex = WordArray.create([0x00000001]);
+
+            // Shortcuts
+            var derivedKeyWords = derivedKey.words;
+            var blockIndexWords = blockIndex.words;
+            var keySize = cfg.keySize;
+            var iterations = cfg.iterations;
+
+            // Generate key
+            while (derivedKeyWords.length < keySize) {
+                var block = hmac.update(salt).finalize(blockIndex);
+                hmac.reset();
+
+                // Shortcuts
+                var blockWords = block.words;
+                var blockWordsLength = blockWords.length;
+
+                // Iterations
+                var intermediate = block;
+                for (var i = 1; i < iterations; i++) {
+                    intermediate = hmac.finalize(intermediate);
+                    hmac.reset();
+
+                    // Shortcut
+                    var intermediateWords = intermediate.words;
+
+                    // XOR intermediate with block
+                    for (var j = 0; j < blockWordsLength; j++) {
+                        blockWords[j] ^= intermediateWords[j];
+                    }
+                }
+
+                derivedKey.concat(block);
+                blockIndexWords[0]++;
+            }
+            derivedKey.sigBytes = keySize * 4;
+
+            return derivedKey;
+        }
+    });
+
+    /**
+     * Computes the Password-Based Key Derivation Function 2.
+     *
+     * @param {WordArray|string} password The password.
+     * @param {WordArray|string} salt A salt.
+     * @param {Object} cfg (Optional) The configuration options to use for this computation.
+     *
+     * @return {WordArray} The derived key.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var key = CryptoJS.PBKDF2(password, salt);
+     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 });
+     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 });
+     */
+    C.PBKDF2 = function (password, salt, cfg) {
+        return PBKDF2.create(cfg).compute(password, salt);
+    };
+}());
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * A noop padding strategy.
+ */
+CryptoJS.pad.NoPadding = {
+    pad: function () {
+    },
+
+    unpad: function () {
+    }
+};
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * Counter block mode.
+ */
+CryptoJS.mode.CTR = (function () {
+    var CTR = CryptoJS.lib.BlockCipherMode.extend();
+
+    var Encryptor = CTR.Encryptor = CTR.extend({
+        processBlock: function (words, offset) {
+            // Shortcuts
+            var cipher = this._cipher
+            var blockSize = cipher.blockSize;
+            var iv = this._iv;
+            var counter = this._counter;
+
+            // Generate keystream
+            if (iv) {
+                counter = this._counter = iv.slice(0);
+
+                // Remove IV for subsequent blocks
+                this._iv = undefined;
+            }
+            var keystream = counter.slice(0);
+            cipher.encryptBlock(keystream, 0);
+
+            // Increment counter
+            counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0
+
+            // Encrypt
+            for (var i = 0; i < blockSize; i++) {
+                words[offset + i] ^= keystream[i];
+            }
+        }
+    });
+
+    CTR.Decryptor = Encryptor;
+
+    return CTR;
+}());
+
+	// This provides backwards compatibility with CryptoJS 3.0.2
+	// CryptoJS.enc.Base64.parse used to do this by default.
+	var _base64Parse = CryptoJS.enc.Base64.parse
+	CryptoJS.enc.Base64.parse = function (base64Str) {
+		return _base64Parse.call(CryptoJS.enc.Base64, base64Str.replace(/\s/g, ''))
+	}
+
+	return CryptoJS
+
+}))
\ No newline at end of file
diff --git a/src/core/js/lib/crypto-js/aes.js b/src/core/js/lib/crypto-js/aes.js
new file mode 100755
index 0000000..7ba1054
--- /dev/null
+++ b/src/core/js/lib/crypto-js/aes.js
@@ -0,0 +1,213 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var BlockCipher = C_lib.BlockCipher;
+    var C_algo = C.algo;
+
+    // Lookup tables
+    var SBOX = [];
+    var INV_SBOX = [];
+    var SUB_MIX_0 = [];
+    var SUB_MIX_1 = [];
+    var SUB_MIX_2 = [];
+    var SUB_MIX_3 = [];
+    var INV_SUB_MIX_0 = [];
+    var INV_SUB_MIX_1 = [];
+    var INV_SUB_MIX_2 = [];
+    var INV_SUB_MIX_3 = [];
+
+    // Compute lookup tables
+    (function () {
+        // Compute double table
+        var d = [];
+        for (var i = 0; i < 256; i++) {
+            if (i < 128) {
+                d[i] = i << 1;
+            } else {
+                d[i] = (i << 1) ^ 0x11b;
+            }
+        }
+
+        // Walk GF(2^8)
+        var x = 0;
+        var xi = 0;
+        for (var i = 0; i < 256; i++) {
+            // Compute sbox
+            var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
+            sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
+            SBOX[x] = sx;
+            INV_SBOX[sx] = x;
+
+            // Compute multiplication
+            var x2 = d[x];
+            var x4 = d[x2];
+            var x8 = d[x4];
+
+            // Compute sub bytes, mix columns tables
+            var t = (d[sx] * 0x101) ^ (sx * 0x1010100);
+            SUB_MIX_0[x] = (t << 24) | (t >>> 8);
+            SUB_MIX_1[x] = (t << 16) | (t >>> 16);
+            SUB_MIX_2[x] = (t << 8)  | (t >>> 24);
+            SUB_MIX_3[x] = t;
+
+            // Compute inv sub bytes, inv mix columns tables
+            var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
+            INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8);
+            INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16);
+            INV_SUB_MIX_2[sx] = (t << 8)  | (t >>> 24);
+            INV_SUB_MIX_3[sx] = t;
+
+            // Compute next counter
+            if (!x) {
+                x = xi = 1;
+            } else {
+                x = x2 ^ d[d[d[x8 ^ x2]]];
+                xi ^= d[d[xi]];
+            }
+        }
+    }());
+
+    // Precomputed Rcon lookup
+    var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
+
+    /**
+     * AES block cipher algorithm.
+     */
+    var AES = C_algo.AES = BlockCipher.extend({
+        _doReset: function () {
+            // Shortcuts
+            var key = this._key;
+            var keyWords = key.words;
+            var keySize = key.sigBytes / 4;
+
+            // Compute number of rounds
+            var nRounds = this._nRounds = keySize + 6
+
+            // Compute number of key schedule rows
+            var ksRows = (nRounds + 1) * 4;
+
+            // Compute key schedule
+            var keySchedule = this._keySchedule = [];
+            for (var ksRow = 0; ksRow < ksRows; ksRow++) {
+                if (ksRow < keySize) {
+                    keySchedule[ksRow] = keyWords[ksRow];
+                } else {
+                    var t = keySchedule[ksRow - 1];
+
+                    if (!(ksRow % keySize)) {
+                        // Rot word
+                        t = (t << 8) | (t >>> 24);
+
+                        // Sub word
+                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+
+                        // Mix Rcon
+                        t ^= RCON[(ksRow / keySize) | 0] << 24;
+                    } else if (keySize > 6 && ksRow % keySize == 4) {
+                        // Sub word
+                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+                    }
+
+                    keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t;
+                }
+            }
+
+            // Compute inv key schedule
+            var invKeySchedule = this._invKeySchedule = [];
+            for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) {
+                var ksRow = ksRows - invKsRow;
+
+                if (invKsRow % 4) {
+                    var t = keySchedule[ksRow];
+                } else {
+                    var t = keySchedule[ksRow - 4];
+                }
+
+                if (invKsRow < 4 || ksRow <= 4) {
+                    invKeySchedule[invKsRow] = t;
+                } else {
+                    invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^
+                                               INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]];
+                }
+            }
+        },
+
+        encryptBlock: function (M, offset) {
+            this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX);
+        },
+
+        decryptBlock: function (M, offset) {
+            // Swap 2nd and 4th rows
+            var t = M[offset + 1];
+            M[offset + 1] = M[offset + 3];
+            M[offset + 3] = t;
+
+            this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX);
+
+            // Inv swap 2nd and 4th rows
+            var t = M[offset + 1];
+            M[offset + 1] = M[offset + 3];
+            M[offset + 3] = t;
+        },
+
+        _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) {
+            // Shortcut
+            var nRounds = this._nRounds;
+
+            // Get input, add round key
+            var s0 = M[offset]     ^ keySchedule[0];
+            var s1 = M[offset + 1] ^ keySchedule[1];
+            var s2 = M[offset + 2] ^ keySchedule[2];
+            var s3 = M[offset + 3] ^ keySchedule[3];
+
+            // Key schedule row counter
+            var ksRow = 4;
+
+            // Rounds
+            for (var round = 1; round < nRounds; round++) {
+                // Shift rows, sub bytes, mix columns, add round key
+                var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++];
+                var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++];
+                var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++];
+                var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++];
+
+                // Update state
+                s0 = t0;
+                s1 = t1;
+                s2 = t2;
+                s3 = t3;
+            }
+
+            // Shift rows, sub bytes, add round key
+            var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++];
+            var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++];
+            var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++];
+            var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++];
+
+            // Set output
+            M[offset]     = t0;
+            M[offset + 1] = t1;
+            M[offset + 2] = t2;
+            M[offset + 3] = t3;
+        },
+
+        keySize: 256/32
+    });
+
+    /**
+     * Shortcut functions to the cipher's object interface.
+     *
+     * @example
+     *
+     *     var ciphertext = CryptoJS.AES.encrypt(message, key, cfg);
+     *     var plaintext  = CryptoJS.AES.decrypt(ciphertext, key, cfg);
+     */
+    C.AES = BlockCipher._createHelper(AES);
+}());
diff --git a/src/core/js/lib/crypto-js/cipher-core.js b/src/core/js/lib/crypto-js/cipher-core.js
new file mode 100755
index 0000000..2ec8eb5
--- /dev/null
+++ b/src/core/js/lib/crypto-js/cipher-core.js
@@ -0,0 +1,863 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * Cipher core components.
+ */
+CryptoJS.lib.Cipher || (function (undefined) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var WordArray = C_lib.WordArray;
+    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm;
+    var C_enc = C.enc;
+    var Utf8 = C_enc.Utf8;
+    var Base64 = C_enc.Base64;
+    var C_algo = C.algo;
+    var EvpKDF = C_algo.EvpKDF;
+
+    /**
+     * Abstract base cipher template.
+     *
+     * @property {number} keySize This cipher's key size. Default: 4 (128 bits)
+     * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits)
+     * @property {number} _ENC_XFORM_MODE A constant representing encryption mode.
+     * @property {number} _DEC_XFORM_MODE A constant representing decryption mode.
+     */
+    var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {WordArray} iv The IV to use for this operation.
+         */
+        cfg: Base.extend(),
+
+        /**
+         * Creates this cipher in encryption mode.
+         *
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {Cipher} A cipher instance.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray });
+         */
+        createEncryptor: function (key, cfg) {
+            return this.create(this._ENC_XFORM_MODE, key, cfg);
+        },
+
+        /**
+         * Creates this cipher in decryption mode.
+         *
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {Cipher} A cipher instance.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray });
+         */
+        createDecryptor: function (key, cfg) {
+            return this.create(this._DEC_XFORM_MODE, key, cfg);
+        },
+
+        /**
+         * Initializes a newly created cipher.
+         *
+         * @param {number} xformMode Either the encryption or decryption transormation mode constant.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @example
+         *
+         *     var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray });
+         */
+        init: function (xformMode, key, cfg) {
+            // Apply config defaults
+            this.cfg = this.cfg.extend(cfg);
+
+            // Store transform mode and key
+            this._xformMode = xformMode;
+            this._key = key;
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this cipher to its initial state.
+         *
+         * @example
+         *
+         *     cipher.reset();
+         */
+        reset: function () {
+            // Reset data buffer
+            BufferedBlockAlgorithm.reset.call(this);
+
+            // Perform concrete-cipher logic
+            this._doReset();
+        },
+
+        /**
+         * Adds data to be encrypted or decrypted.
+         *
+         * @param {WordArray|string} dataUpdate The data to encrypt or decrypt.
+         *
+         * @return {WordArray} The data after processing.
+         *
+         * @example
+         *
+         *     var encrypted = cipher.process('data');
+         *     var encrypted = cipher.process(wordArray);
+         */
+        process: function (dataUpdate) {
+            // Append
+            this._append(dataUpdate);
+
+            // Process available blocks
+            return this._process();
+        },
+
+        /**
+         * Finalizes the encryption or decryption process.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt.
+         *
+         * @return {WordArray} The data after final processing.
+         *
+         * @example
+         *
+         *     var encrypted = cipher.finalize();
+         *     var encrypted = cipher.finalize('data');
+         *     var encrypted = cipher.finalize(wordArray);
+         */
+        finalize: function (dataUpdate) {
+            // Final data update
+            if (dataUpdate) {
+                this._append(dataUpdate);
+            }
+
+            // Perform concrete-cipher logic
+            var finalProcessedData = this._doFinalize();
+
+            return finalProcessedData;
+        },
+
+        keySize: 128/32,
+
+        ivSize: 128/32,
+
+        _ENC_XFORM_MODE: 1,
+
+        _DEC_XFORM_MODE: 2,
+
+        /**
+         * Creates shortcut functions to a cipher's object interface.
+         *
+         * @param {Cipher} cipher The cipher to create a helper for.
+         *
+         * @return {Object} An object with encrypt and decrypt shortcut functions.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES);
+         */
+        _createHelper: (function () {
+            function selectCipherStrategy(key) {
+                if (typeof key == 'string') {
+                    return PasswordBasedCipher;
+                } else {
+                    return SerializableCipher;
+                }
+            }
+
+            return function (cipher) {
+                return {
+                    encrypt: function (message, key, cfg) {
+                        return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
+                    },
+
+                    decrypt: function (ciphertext, key, cfg) {
+                        return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
+                    }
+                };
+            };
+        }())
+    });
+
+    /**
+     * Abstract base stream cipher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits)
+     */
+    var StreamCipher = C_lib.StreamCipher = Cipher.extend({
+        _doFinalize: function () {
+            // Process partial blocks
+            var finalProcessedBlocks = this._process(!!'flush');
+
+            return finalProcessedBlocks;
+        },
+
+        blockSize: 1
+    });
+
+    /**
+     * Mode namespace.
+     */
+    var C_mode = C.mode = {};
+
+    /**
+     * Abstract base block cipher mode template.
+     */
+    var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({
+        /**
+         * Creates this mode for encryption.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words);
+         */
+        createEncryptor: function (cipher, iv) {
+            return this.Encryptor.create(cipher, iv);
+        },
+
+        /**
+         * Creates this mode for decryption.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words);
+         */
+        createDecryptor: function (cipher, iv) {
+            return this.Decryptor.create(cipher, iv);
+        },
+
+        /**
+         * Initializes a newly created mode.
+         *
+         * @param {Cipher} cipher A block cipher instance.
+         * @param {Array} iv The IV words.
+         *
+         * @example
+         *
+         *     var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words);
+         */
+        init: function (cipher, iv) {
+            this._cipher = cipher;
+            this._iv = iv;
+        }
+    });
+
+    /**
+     * Cipher Block Chaining mode.
+     */
+    var CBC = C_mode.CBC = (function () {
+        /**
+         * Abstract base CBC mode.
+         */
+        var CBC = BlockCipherMode.extend();
+
+        /**
+         * CBC encryptor.
+         */
+        CBC.Encryptor = CBC.extend({
+            /**
+             * Processes the data block at offset.
+             *
+             * @param {Array} words The data words to operate on.
+             * @param {number} offset The offset where the block starts.
+             *
+             * @example
+             *
+             *     mode.processBlock(data.words, offset);
+             */
+            processBlock: function (words, offset) {
+                // Shortcuts
+                var cipher = this._cipher;
+                var blockSize = cipher.blockSize;
+
+                // XOR and encrypt
+                xorBlock.call(this, words, offset, blockSize);
+                cipher.encryptBlock(words, offset);
+
+                // Remember this block to use with next block
+                this._prevBlock = words.slice(offset, offset + blockSize);
+            }
+        });
+
+        /**
+         * CBC decryptor.
+         */
+        CBC.Decryptor = CBC.extend({
+            /**
+             * Processes the data block at offset.
+             *
+             * @param {Array} words The data words to operate on.
+             * @param {number} offset The offset where the block starts.
+             *
+             * @example
+             *
+             *     mode.processBlock(data.words, offset);
+             */
+            processBlock: function (words, offset) {
+                // Shortcuts
+                var cipher = this._cipher;
+                var blockSize = cipher.blockSize;
+
+                // Remember this block to use with next block
+                var thisBlock = words.slice(offset, offset + blockSize);
+
+                // Decrypt and XOR
+                cipher.decryptBlock(words, offset);
+                xorBlock.call(this, words, offset, blockSize);
+
+                // This block becomes the previous block
+                this._prevBlock = thisBlock;
+            }
+        });
+
+        function xorBlock(words, offset, blockSize) {
+            // Shortcut
+            var iv = this._iv;
+
+            // Choose mixing block
+            if (iv) {
+                var block = iv;
+
+                // Remove IV for subsequent blocks
+                this._iv = undefined;
+            } else {
+                var block = this._prevBlock;
+            }
+
+            // XOR blocks
+            for (var i = 0; i < blockSize; i++) {
+                words[offset + i] ^= block[i];
+            }
+        }
+
+        return CBC;
+    }());
+
+    /**
+     * Padding namespace.
+     */
+    var C_pad = C.pad = {};
+
+    /**
+     * PKCS #5/7 padding strategy.
+     */
+    var Pkcs7 = C_pad.Pkcs7 = {
+        /**
+         * Pads data using the algorithm defined in PKCS #5/7.
+         *
+         * @param {WordArray} data The data to pad.
+         * @param {number} blockSize The multiple that the data should be padded to.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     CryptoJS.pad.Pkcs7.pad(wordArray, 4);
+         */
+        pad: function (data, blockSize) {
+            // Shortcut
+            var blockSizeBytes = blockSize * 4;
+
+            // Count padding bytes
+            var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;
+
+            // Create padding word
+            var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes;
+
+            // Create padding
+            var paddingWords = [];
+            for (var i = 0; i < nPaddingBytes; i += 4) {
+                paddingWords.push(paddingWord);
+            }
+            var padding = WordArray.create(paddingWords, nPaddingBytes);
+
+            // Add padding
+            data.concat(padding);
+        },
+
+        /**
+         * Unpads data that had been padded using the algorithm defined in PKCS #5/7.
+         *
+         * @param {WordArray} data The data to unpad.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     CryptoJS.pad.Pkcs7.unpad(wordArray);
+         */
+        unpad: function (data) {
+            // Get number of padding bytes from last byte
+            var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
+
+            // Remove padding
+            data.sigBytes -= nPaddingBytes;
+        }
+    };
+
+    /**
+     * Abstract base block cipher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits)
+     */
+    var BlockCipher = C_lib.BlockCipher = Cipher.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {Mode} mode The block mode to use. Default: CBC
+         * @property {Padding} padding The padding strategy to use. Default: Pkcs7
+         */
+        cfg: Cipher.cfg.extend({
+            mode: CBC,
+            padding: Pkcs7
+        }),
+
+        reset: function () {
+            // Reset cipher
+            Cipher.reset.call(this);
+
+            // Shortcuts
+            var cfg = this.cfg;
+            var iv = cfg.iv;
+            var mode = cfg.mode;
+
+            // Reset block mode
+            if (this._xformMode == this._ENC_XFORM_MODE) {
+                var modeCreator = mode.createEncryptor;
+            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+                var modeCreator = mode.createDecryptor;
+
+                // Keep at least one block in the buffer for unpadding
+                this._minBufferSize = 1;
+            }
+            this._mode = modeCreator.call(mode, this, iv && iv.words);
+        },
+
+        _doProcessBlock: function (words, offset) {
+            this._mode.processBlock(words, offset);
+        },
+
+        _doFinalize: function () {
+            // Shortcut
+            var padding = this.cfg.padding;
+
+            // Finalize
+            if (this._xformMode == this._ENC_XFORM_MODE) {
+                // Pad data
+                padding.pad(this._data, this.blockSize);
+
+                // Process final blocks
+                var finalProcessedBlocks = this._process(!!'flush');
+            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+                // Process final blocks
+                var finalProcessedBlocks = this._process(!!'flush');
+
+                // Unpad data
+                padding.unpad(finalProcessedBlocks);
+            }
+
+            return finalProcessedBlocks;
+        },
+
+        blockSize: 128/32
+    });
+
+    /**
+     * A collection of cipher parameters.
+     *
+     * @property {WordArray} ciphertext The raw ciphertext.
+     * @property {WordArray} key The key to this ciphertext.
+     * @property {WordArray} iv The IV used in the ciphering operation.
+     * @property {WordArray} salt The salt used with a key derivation function.
+     * @property {Cipher} algorithm The cipher algorithm.
+     * @property {Mode} mode The block mode used in the ciphering operation.
+     * @property {Padding} padding The padding scheme used in the ciphering operation.
+     * @property {number} blockSize The block size of the cipher.
+     * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string.
+     */
+    var CipherParams = C_lib.CipherParams = Base.extend({
+        /**
+         * Initializes a newly created cipher params object.
+         *
+         * @param {Object} cipherParams An object with any of the possible cipher parameters.
+         *
+         * @example
+         *
+         *     var cipherParams = CryptoJS.lib.CipherParams.create({
+         *         ciphertext: ciphertextWordArray,
+         *         key: keyWordArray,
+         *         iv: ivWordArray,
+         *         salt: saltWordArray,
+         *         algorithm: CryptoJS.algo.AES,
+         *         mode: CryptoJS.mode.CBC,
+         *         padding: CryptoJS.pad.PKCS7,
+         *         blockSize: 4,
+         *         formatter: CryptoJS.format.OpenSSL
+         *     });
+         */
+        init: function (cipherParams) {
+            this.mixIn(cipherParams);
+        },
+
+        /**
+         * Converts this cipher params object to a string.
+         *
+         * @param {Format} formatter (Optional) The formatting strategy to use.
+         *
+         * @return {string} The stringified cipher params.
+         *
+         * @throws Error If neither the formatter nor the default formatter is set.
+         *
+         * @example
+         *
+         *     var string = cipherParams + '';
+         *     var string = cipherParams.toString();
+         *     var string = cipherParams.toString(CryptoJS.format.OpenSSL);
+         */
+        toString: function (formatter) {
+            return (formatter || this.formatter).stringify(this);
+        }
+    });
+
+    /**
+     * Format namespace.
+     */
+    var C_format = C.format = {};
+
+    /**
+     * OpenSSL formatting strategy.
+     */
+    var OpenSSLFormatter = C_format.OpenSSL = {
+        /**
+         * Converts a cipher params object to an OpenSSL-compatible string.
+         *
+         * @param {CipherParams} cipherParams The cipher params object.
+         *
+         * @return {string} The OpenSSL-compatible string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams);
+         */
+        stringify: function (cipherParams) {
+            // Shortcuts
+            var ciphertext = cipherParams.ciphertext;
+            var salt = cipherParams.salt;
+
+            // Format
+            if (salt) {
+                var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
+            } else {
+                var wordArray = ciphertext;
+            }
+
+            return wordArray.toString(Base64);
+        },
+
+        /**
+         * Converts an OpenSSL-compatible string to a cipher params object.
+         *
+         * @param {string} openSSLStr The OpenSSL-compatible string.
+         *
+         * @return {CipherParams} The cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString);
+         */
+        parse: function (openSSLStr) {
+            // Parse base64
+            var ciphertext = Base64.parse(openSSLStr);
+
+            // Shortcut
+            var ciphertextWords = ciphertext.words;
+
+            // Test for salt
+            if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) {
+                // Extract salt
+                var salt = WordArray.create(ciphertextWords.slice(2, 4));
+
+                // Remove salt from ciphertext
+                ciphertextWords.splice(0, 4);
+                ciphertext.sigBytes -= 16;
+            }
+
+            return CipherParams.create({ ciphertext: ciphertext, salt: salt });
+        }
+    };
+
+    /**
+     * A cipher wrapper that returns ciphertext as a serializable cipher params object.
+     */
+    var SerializableCipher = C_lib.SerializableCipher = Base.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL
+         */
+        cfg: Base.extend({
+            format: OpenSSLFormatter
+        }),
+
+        /**
+         * Encrypts a message.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {WordArray|string} message The message to encrypt.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {CipherParams} A cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key);
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv });
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         */
+        encrypt: function (cipher, message, key, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Encrypt
+            var encryptor = cipher.createEncryptor(key, cfg);
+            var ciphertext = encryptor.finalize(message);
+
+            // Shortcut
+            var cipherCfg = encryptor.cfg;
+
+            // Create and return serializable cipher params
+            return CipherParams.create({
+                ciphertext: ciphertext,
+                key: key,
+                iv: cipherCfg.iv,
+                algorithm: cipher,
+                mode: cipherCfg.mode,
+                padding: cipherCfg.padding,
+                blockSize: cipher.blockSize,
+                formatter: cfg.format
+            });
+        },
+
+        /**
+         * Decrypts serialized ciphertext.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+         * @param {WordArray} key The key.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {WordArray} The plaintext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+         */
+        decrypt: function (cipher, ciphertext, key, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Convert string to CipherParams
+            ciphertext = this._parse(ciphertext, cfg.format);
+
+            // Decrypt
+            var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext);
+
+            return plaintext;
+        },
+
+        /**
+         * Converts serialized ciphertext to CipherParams,
+         * else assumed CipherParams already and returns ciphertext unchanged.
+         *
+         * @param {CipherParams|string} ciphertext The ciphertext.
+         * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext.
+         *
+         * @return {CipherParams} The unserialized ciphertext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format);
+         */
+        _parse: function (ciphertext, format) {
+            if (typeof ciphertext == 'string') {
+                return format.parse(ciphertext, this);
+            } else {
+                return ciphertext;
+            }
+        }
+    });
+
+    /**
+     * Key derivation function namespace.
+     */
+    var C_kdf = C.kdf = {};
+
+    /**
+     * OpenSSL key derivation function.
+     */
+    var OpenSSLKdf = C_kdf.OpenSSL = {
+        /**
+         * Derives a key and IV from a password.
+         *
+         * @param {string} password The password to derive from.
+         * @param {number} keySize The size in words of the key to generate.
+         * @param {number} ivSize The size in words of the IV to generate.
+         * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly.
+         *
+         * @return {CipherParams} A cipher params object with the key, IV, and salt.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
+         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
+         */
+        execute: function (password, keySize, ivSize, salt) {
+            // Generate random salt
+            if (!salt) {
+                salt = WordArray.random(64/8);
+            }
+
+            // Derive key and IV
+            var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
+
+            // Separate key and IV
+            var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
+            key.sigBytes = keySize * 4;
+
+            // Return params
+            return CipherParams.create({ key: key, iv: iv, salt: salt });
+        }
+    };
+
+    /**
+     * A serializable cipher wrapper that derives the key from a password,
+     * and returns ciphertext as a serializable cipher params object.
+     */
+    var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL
+         */
+        cfg: SerializableCipher.cfg.extend({
+            kdf: OpenSSLKdf
+        }),
+
+        /**
+         * Encrypts a message using a password.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {WordArray|string} message The message to encrypt.
+         * @param {string} password The password.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {CipherParams} A cipher params object.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password');
+         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL });
+         */
+        encrypt: function (cipher, message, password, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Derive key and other params
+            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize);
+
+            // Add IV to config
+            cfg.iv = derivedParams.iv;
+
+            // Encrypt
+            var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);
+
+            // Mix in derived params
+            ciphertext.mixIn(derivedParams);
+
+            return ciphertext;
+        },
+
+        /**
+         * Decrypts serialized ciphertext using a password.
+         *
+         * @param {Cipher} cipher The cipher algorithm to use.
+         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+         * @param {string} password The password.
+         * @param {Object} cfg (Optional) The configuration options to use for this operation.
+         *
+         * @return {WordArray} The plaintext.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL });
+         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL });
+         */
+        decrypt: function (cipher, ciphertext, password, cfg) {
+            // Apply config defaults
+            cfg = this.cfg.extend(cfg);
+
+            // Convert string to CipherParams
+            ciphertext = this._parse(ciphertext, cfg.format);
+
+            // Derive key and other params
+            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt);
+
+            // Add IV to config
+            cfg.iv = derivedParams.iv;
+
+            // Decrypt
+            var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg);
+
+            return plaintext;
+        }
+    });
+}());
diff --git a/src/core/js/lib/crypto-js/core.js b/src/core/js/lib/crypto-js/core.js
new file mode 100755
index 0000000..b5b2a67
--- /dev/null
+++ b/src/core/js/lib/crypto-js/core.js
@@ -0,0 +1,712 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * CryptoJS core components.
+ */
+var CryptoJS = CryptoJS || (function (Math, undefined) {
+    /**
+     * CryptoJS namespace.
+     */
+    var C = {};
+
+    /**
+     * Library namespace.
+     */
+    var C_lib = C.lib = {};
+
+    /**
+     * Base object for prototypal inheritance.
+     */
+    var Base = C_lib.Base = (function () {
+        function F() {}
+
+        return {
+            /**
+             * Creates a new object that inherits from this object.
+             *
+             * @param {Object} overrides Properties to copy into the new object.
+             *
+             * @return {Object} The new object.
+             *
+             * @static
+             *
+             * @example
+             *
+             *     var MyType = CryptoJS.lib.Base.extend({
+             *         field: 'value',
+             *
+             *         method: function () {
+             *         }
+             *     });
+             */
+            extend: function (overrides) {
+                // Spawn
+                F.prototype = this;
+                var subtype = new F();
+
+                // Augment
+                if (overrides) {
+                    subtype.mixIn(overrides);
+                }
+
+                // Create default initializer
+                if (!subtype.hasOwnProperty('init')) {
+                    subtype.init = function () {
+                        subtype.$super.init.apply(this, arguments);
+                    };
+                }
+
+                // Initializer's prototype is the subtype object
+                subtype.init.prototype = subtype;
+
+                // Reference supertype
+                subtype.$super = this;
+
+                return subtype;
+            },
+
+            /**
+             * Extends this object and runs the init method.
+             * Arguments to create() will be passed to init().
+             *
+             * @return {Object} The new object.
+             *
+             * @static
+             *
+             * @example
+             *
+             *     var instance = MyType.create();
+             */
+            create: function () {
+                var instance = this.extend();
+                instance.init.apply(instance, arguments);
+
+                return instance;
+            },
+
+            /**
+             * Initializes a newly created object.
+             * Override this method to add some logic when your objects are created.
+             *
+             * @example
+             *
+             *     var MyType = CryptoJS.lib.Base.extend({
+             *         init: function () {
+             *             // ...
+             *         }
+             *     });
+             */
+            init: function () {
+            },
+
+            /**
+             * Copies properties into this object.
+             *
+             * @param {Object} properties The properties to mix in.
+             *
+             * @example
+             *
+             *     MyType.mixIn({
+             *         field: 'value'
+             *     });
+             */
+            mixIn: function (properties) {
+                for (var propertyName in properties) {
+                    if (properties.hasOwnProperty(propertyName)) {
+                        this[propertyName] = properties[propertyName];
+                    }
+                }
+
+                // IE won't copy toString using the loop above
+                if (properties.hasOwnProperty('toString')) {
+                    this.toString = properties.toString;
+                }
+            },
+
+            /**
+             * Creates a copy of this object.
+             *
+             * @return {Object} The clone.
+             *
+             * @example
+             *
+             *     var clone = instance.clone();
+             */
+            clone: function () {
+                return this.init.prototype.extend(this);
+            }
+        };
+    }());
+
+    /**
+     * An array of 32-bit words.
+     *
+     * @property {Array} words The array of 32-bit words.
+     * @property {number} sigBytes The number of significant bytes in this word array.
+     */
+    var WordArray = C_lib.WordArray = Base.extend({
+        /**
+         * Initializes a newly created word array.
+         *
+         * @param {Array} words (Optional) An array of 32-bit words.
+         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.lib.WordArray.create();
+         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
+         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
+         */
+        init: function (words, sigBytes) {
+            words = this.words = words || [];
+
+            if (sigBytes != undefined) {
+                this.sigBytes = sigBytes;
+            } else {
+                this.sigBytes = words.length * 4;
+            }
+        },
+
+        /**
+         * Converts this word array to a string.
+         *
+         * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
+         *
+         * @return {string} The stringified word array.
+         *
+         * @example
+         *
+         *     var string = wordArray + '';
+         *     var string = wordArray.toString();
+         *     var string = wordArray.toString(CryptoJS.enc.Utf8);
+         */
+        toString: function (encoder) {
+            return (encoder || Hex).stringify(this);
+        },
+
+        /**
+         * Concatenates a word array to this word array.
+         *
+         * @param {WordArray} wordArray The word array to append.
+         *
+         * @return {WordArray} This word array.
+         *
+         * @example
+         *
+         *     wordArray1.concat(wordArray2);
+         */
+        concat: function (wordArray) {
+            // Shortcuts
+            var thisWords = this.words;
+            var thatWords = wordArray.words;
+            var thisSigBytes = this.sigBytes;
+            var thatSigBytes = wordArray.sigBytes;
+
+            // Clamp excess bits
+            this.clamp();
+
+            // Concat
+            if (thisSigBytes % 4) {
+                // Copy one byte at a time
+                for (var i = 0; i < thatSigBytes; i++) {
+                    var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                    thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
+                }
+            } else if (thatWords.length > 0xffff) {
+                // Copy one word at a time
+                for (var i = 0; i < thatSigBytes; i += 4) {
+                    thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
+                }
+            } else {
+                // Copy all words at once
+                thisWords.push.apply(thisWords, thatWords);
+            }
+            this.sigBytes += thatSigBytes;
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Removes insignificant bits.
+         *
+         * @example
+         *
+         *     wordArray.clamp();
+         */
+        clamp: function () {
+            // Shortcuts
+            var words = this.words;
+            var sigBytes = this.sigBytes;
+
+            // Clamp
+            words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
+            words.length = Math.ceil(sigBytes / 4);
+        },
+
+        /**
+         * Creates a copy of this word array.
+         *
+         * @return {WordArray} The clone.
+         *
+         * @example
+         *
+         *     var clone = wordArray.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+            clone.words = this.words.slice(0);
+
+            return clone;
+        },
+
+        /**
+         * Creates a word array filled with random bytes.
+         *
+         * @param {number} nBytes The number of random bytes to generate.
+         *
+         * @return {WordArray} The random word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.lib.WordArray.random(16);
+         */
+        random: function (nBytes) {
+            var words = [];
+            for (var i = 0; i < nBytes; i += 4) {
+                words.push((Math.random() * 0x100000000) | 0);
+            }
+
+            return new WordArray.init(words, nBytes);
+        }
+    });
+
+    /**
+     * Encoder namespace.
+     */
+    var C_enc = C.enc = {};
+
+    /**
+     * Hex encoding strategy.
+     */
+    var Hex = C_enc.Hex = {
+        /**
+         * Converts a word array to a hex string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The hex string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var hexString = CryptoJS.enc.Hex.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+
+            // Convert
+            var hexChars = [];
+            for (var i = 0; i < sigBytes; i++) {
+                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                hexChars.push((bite >>> 4).toString(16));
+                hexChars.push((bite & 0x0f).toString(16));
+            }
+
+            return hexChars.join('');
+        },
+
+        /**
+         * Converts a hex string to a word array.
+         *
+         * @param {string} hexStr The hex string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Hex.parse(hexString);
+         */
+        parse: function (hexStr) {
+            // Shortcut
+            var hexStrLength = hexStr.length;
+
+            // Convert
+            var words = [];
+            for (var i = 0; i < hexStrLength; i += 2) {
+                words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
+            }
+
+            return new WordArray.init(words, hexStrLength / 2);
+        }
+    };
+
+    /**
+     * Latin1 encoding strategy.
+     */
+    var Latin1 = C_enc.Latin1 = {
+        /**
+         * Converts a word array to a Latin1 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The Latin1 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+
+            // Convert
+            var latin1Chars = [];
+            for (var i = 0; i < sigBytes; i++) {
+                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+                latin1Chars.push(String.fromCharCode(bite));
+            }
+
+            return latin1Chars.join('');
+        },
+
+        /**
+         * Converts a Latin1 string to a word array.
+         *
+         * @param {string} latin1Str The Latin1 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
+         */
+        parse: function (latin1Str) {
+            // Shortcut
+            var latin1StrLength = latin1Str.length;
+
+            // Convert
+            var words = [];
+            for (var i = 0; i < latin1StrLength; i++) {
+                words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
+            }
+
+            return new WordArray.init(words, latin1StrLength);
+        }
+    };
+
+    /**
+     * UTF-8 encoding strategy.
+     */
+    var Utf8 = C_enc.Utf8 = {
+        /**
+         * Converts a word array to a UTF-8 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The UTF-8 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            try {
+                return decodeURIComponent(escape(Latin1.stringify(wordArray)));
+            } catch (e) {
+                throw new Error('Malformed UTF-8 data');
+            }
+        },
+
+        /**
+         * Converts a UTF-8 string to a word array.
+         *
+         * @param {string} utf8Str The UTF-8 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
+         */
+        parse: function (utf8Str) {
+            return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
+        }
+    };
+
+    /**
+     * Abstract buffered block algorithm template.
+     *
+     * The property blockSize must be implemented in a concrete subtype.
+     *
+     * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
+     */
+    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
+        /**
+         * Resets this block algorithm's data buffer to its initial state.
+         *
+         * @example
+         *
+         *     bufferedBlockAlgorithm.reset();
+         */
+        reset: function () {
+            // Initial values
+            this._data = new WordArray.init();
+            this._nDataBytes = 0;
+        },
+
+        /**
+         * Adds new data to this block algorithm's buffer.
+         *
+         * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
+         *
+         * @example
+         *
+         *     bufferedBlockAlgorithm._append('data');
+         *     bufferedBlockAlgorithm._append(wordArray);
+         */
+        _append: function (data) {
+            // Convert string to WordArray, else assume WordArray already
+            if (typeof data == 'string') {
+                data = Utf8.parse(data);
+            }
+
+            // Append
+            this._data.concat(data);
+            this._nDataBytes += data.sigBytes;
+        },
+
+        /**
+         * Processes available data blocks.
+         *
+         * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
+         *
+         * @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
+         *
+         * @return {WordArray} The processed data.
+         *
+         * @example
+         *
+         *     var processedData = bufferedBlockAlgorithm._process();
+         *     var processedData = bufferedBlockAlgorithm._process(!!'flush');
+         */
+        _process: function (doFlush) {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+            var dataSigBytes = data.sigBytes;
+            var blockSize = this.blockSize;
+            var blockSizeBytes = blockSize * 4;
+
+            // Count blocks ready
+            var nBlocksReady = dataSigBytes / blockSizeBytes;
+            if (doFlush) {
+                // Round up to include partial blocks
+                nBlocksReady = Math.ceil(nBlocksReady);
+            } else {
+                // Round down to include only full blocks,
+                // less the number of blocks that must remain in the buffer
+                nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
+            }
+
+            // Count words ready
+            var nWordsReady = nBlocksReady * blockSize;
+
+            // Count bytes ready
+            var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
+
+            // Process blocks
+            if (nWordsReady) {
+                for (var offset = 0; offset < nWordsReady; offset += blockSize) {
+                    // Perform concrete-algorithm logic
+                    this._doProcessBlock(dataWords, offset);
+                }
+
+                // Remove processed words
+                var processedWords = dataWords.splice(0, nWordsReady);
+                data.sigBytes -= nBytesReady;
+            }
+
+            // Return processed words
+            return new WordArray.init(processedWords, nBytesReady);
+        },
+
+        /**
+         * Creates a copy of this object.
+         *
+         * @return {Object} The clone.
+         *
+         * @example
+         *
+         *     var clone = bufferedBlockAlgorithm.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+            clone._data = this._data.clone();
+
+            return clone;
+        },
+
+        _minBufferSize: 0
+    });
+
+    /**
+     * Abstract hasher template.
+     *
+     * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
+     */
+    var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
+        /**
+         * Configuration options.
+         */
+        cfg: Base.extend(),
+
+        /**
+         * Initializes a newly created hasher.
+         *
+         * @param {Object} cfg (Optional) The configuration options to use for this hash computation.
+         *
+         * @example
+         *
+         *     var hasher = CryptoJS.algo.SHA256.create();
+         */
+        init: function (cfg) {
+            // Apply config defaults
+            this.cfg = this.cfg.extend(cfg);
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this hasher to its initial state.
+         *
+         * @example
+         *
+         *     hasher.reset();
+         */
+        reset: function () {
+            // Reset data buffer
+            BufferedBlockAlgorithm.reset.call(this);
+
+            // Perform concrete-hasher logic
+            this._doReset();
+        },
+
+        /**
+         * Updates this hasher with a message.
+         *
+         * @param {WordArray|string} messageUpdate The message to append.
+         *
+         * @return {Hasher} This hasher.
+         *
+         * @example
+         *
+         *     hasher.update('message');
+         *     hasher.update(wordArray);
+         */
+        update: function (messageUpdate) {
+            // Append
+            this._append(messageUpdate);
+
+            // Update the hash
+            this._process();
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Finalizes the hash computation.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} messageUpdate (Optional) A final message update.
+         *
+         * @return {WordArray} The hash.
+         *
+         * @example
+         *
+         *     var hash = hasher.finalize();
+         *     var hash = hasher.finalize('message');
+         *     var hash = hasher.finalize(wordArray);
+         */
+        finalize: function (messageUpdate) {
+            // Final message update
+            if (messageUpdate) {
+                this._append(messageUpdate);
+            }
+
+            // Perform concrete-hasher logic
+            var hash = this._doFinalize();
+
+            return hash;
+        },
+
+        blockSize: 512/32,
+
+        /**
+         * Creates a shortcut function to a hasher's object interface.
+         *
+         * @param {Hasher} hasher The hasher to create a helper for.
+         *
+         * @return {Function} The shortcut function.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
+         */
+        _createHelper: function (hasher) {
+            return function (message, cfg) {
+                return new hasher.init(cfg).finalize(message);
+            };
+        },
+
+        /**
+         * Creates a shortcut function to the HMAC's object interface.
+         *
+         * @param {Hasher} hasher The hasher to use in this HMAC helper.
+         *
+         * @return {Function} The shortcut function.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
+         */
+        _createHmacHelper: function (hasher) {
+            return function (message, key) {
+                return new C_algo.HMAC.init(hasher, key).finalize(message);
+            };
+        }
+    });
+
+    /**
+     * Algorithm namespace.
+     */
+    var C_algo = C.algo = {};
+
+    return C;
+}(Math));
diff --git a/src/core/js/lib/crypto-js/enc-base64.js b/src/core/js/lib/crypto-js/enc-base64.js
new file mode 100755
index 0000000..739f4a8
--- /dev/null
+++ b/src/core/js/lib/crypto-js/enc-base64.js
@@ -0,0 +1,109 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var C_enc = C.enc;
+
+    /**
+     * Base64 encoding strategy.
+     */
+    var Base64 = C_enc.Base64 = {
+        /**
+         * Converts a word array to a Base64 string.
+         *
+         * @param {WordArray} wordArray The word array.
+         *
+         * @return {string} The Base64 string.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var base64String = CryptoJS.enc.Base64.stringify(wordArray);
+         */
+        stringify: function (wordArray) {
+            // Shortcuts
+            var words = wordArray.words;
+            var sigBytes = wordArray.sigBytes;
+            var map = this._map;
+
+            // Clamp excess bits
+            wordArray.clamp();
+
+            // Convert
+            var base64Chars = [];
+            for (var i = 0; i < sigBytes; i += 3) {
+                var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
+                var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
+                var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;
+
+                var triplet = (byte1 << 16) | (byte2 << 8) | byte3;
+
+                for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
+                    base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
+                }
+            }
+
+            // Add padding
+            var paddingChar = map.charAt(64);
+            if (paddingChar) {
+                while (base64Chars.length % 4) {
+                    base64Chars.push(paddingChar);
+                }
+            }
+
+            return base64Chars.join('');
+        },
+
+        /**
+         * Converts a Base64 string to a word array.
+         *
+         * @param {string} base64Str The Base64 string.
+         *
+         * @return {WordArray} The word array.
+         *
+         * @static
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.enc.Base64.parse(base64String);
+         */
+        parse: function (base64Str) {
+            // Shortcuts
+            var base64StrLength = base64Str.length;
+            var map = this._map;
+
+            // Ignore padding
+            var paddingChar = map.charAt(64);
+            if (paddingChar) {
+                var paddingIndex = base64Str.indexOf(paddingChar);
+                if (paddingIndex != -1) {
+                    base64StrLength = paddingIndex;
+                }
+            }
+
+            // Convert
+            var words = [];
+            var nBytes = 0;
+            for (var i = 0; i < base64StrLength; i++) {
+                if (i % 4) {
+                    var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2);
+                    var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2);
+                    words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8);
+                    nBytes++;
+                }
+            }
+
+            return WordArray.create(words, nBytes);
+        },
+
+        _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+    };
+}());
diff --git a/src/core/js/lib/crypto-js/footer.js b/src/core/js/lib/crypto-js/footer.js
new file mode 100644
index 0000000..f8be9dc
--- /dev/null
+++ b/src/core/js/lib/crypto-js/footer.js
@@ -0,0 +1,11 @@
+
+	// This provides backwards compatibility with CryptoJS 3.0.2
+	// CryptoJS.enc.Base64.parse used to do this by default.
+	var _base64Parse = CryptoJS.enc.Base64.parse
+	CryptoJS.enc.Base64.parse = function (base64Str) {
+		return _base64Parse.call(CryptoJS.enc.Base64, base64Str.replace(/\s/g, ''))
+	}
+
+	return CryptoJS
+
+}))
\ No newline at end of file
diff --git a/src/core/js/lib/crypto-js/header.js b/src/core/js/lib/crypto-js/header.js
new file mode 100644
index 0000000..0497e00
--- /dev/null
+++ b/src/core/js/lib/crypto-js/header.js
@@ -0,0 +1,13 @@
+;(function (root, factory) {
+	'use strict';
+
+	if (typeof define === "function" && define.amd) {
+		define(factory)
+	} else if (typeof module !== 'undefined' && module.exports) {
+		module.exports = factory()
+	} else {
+		root.CryptoJS = factory()
+	}
+
+}(this, function () {
+	'use strict';
diff --git a/src/core/js/lib/crypto-js/hmac.js b/src/core/js/lib/crypto-js/hmac.js
new file mode 100755
index 0000000..b2b8805
--- /dev/null
+++ b/src/core/js/lib/crypto-js/hmac.js
@@ -0,0 +1,131 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var C_enc = C.enc;
+    var Utf8 = C_enc.Utf8;
+    var C_algo = C.algo;
+
+    /**
+     * HMAC algorithm.
+     */
+    var HMAC = C_algo.HMAC = Base.extend({
+        /**
+         * Initializes a newly created HMAC.
+         *
+         * @param {Hasher} hasher The hash algorithm to use.
+         * @param {WordArray|string} key The secret key.
+         *
+         * @example
+         *
+         *     var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
+         */
+        init: function (hasher, key) {
+            // Init hasher
+            hasher = this._hasher = new hasher.init();
+
+            // Convert string to WordArray, else assume WordArray already
+            if (typeof key == 'string') {
+                key = Utf8.parse(key);
+            }
+
+            // Shortcuts
+            var hasherBlockSize = hasher.blockSize;
+            var hasherBlockSizeBytes = hasherBlockSize * 4;
+
+            // Allow arbitrary length keys
+            if (key.sigBytes > hasherBlockSizeBytes) {
+                key = hasher.finalize(key);
+            }
+
+            // Clamp excess bits
+            key.clamp();
+
+            // Clone key for inner and outer pads
+            var oKey = this._oKey = key.clone();
+            var iKey = this._iKey = key.clone();
+
+            // Shortcuts
+            var oKeyWords = oKey.words;
+            var iKeyWords = iKey.words;
+
+            // XOR keys with pad constants
+            for (var i = 0; i < hasherBlockSize; i++) {
+                oKeyWords[i] ^= 0x5c5c5c5c;
+                iKeyWords[i] ^= 0x36363636;
+            }
+            oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;
+
+            // Set initial values
+            this.reset();
+        },
+
+        /**
+         * Resets this HMAC to its initial state.
+         *
+         * @example
+         *
+         *     hmacHasher.reset();
+         */
+        reset: function () {
+            // Shortcut
+            var hasher = this._hasher;
+
+            // Reset
+            hasher.reset();
+            hasher.update(this._iKey);
+        },
+
+        /**
+         * Updates this HMAC with a message.
+         *
+         * @param {WordArray|string} messageUpdate The message to append.
+         *
+         * @return {HMAC} This HMAC instance.
+         *
+         * @example
+         *
+         *     hmacHasher.update('message');
+         *     hmacHasher.update(wordArray);
+         */
+        update: function (messageUpdate) {
+            this._hasher.update(messageUpdate);
+
+            // Chainable
+            return this;
+        },
+
+        /**
+         * Finalizes the HMAC computation.
+         * Note that the finalize operation is effectively a destructive, read-once operation.
+         *
+         * @param {WordArray|string} messageUpdate (Optional) A final message update.
+         *
+         * @return {WordArray} The HMAC.
+         *
+         * @example
+         *
+         *     var hmac = hmacHasher.finalize();
+         *     var hmac = hmacHasher.finalize('message');
+         *     var hmac = hmacHasher.finalize(wordArray);
+         */
+        finalize: function (messageUpdate) {
+            // Shortcut
+            var hasher = this._hasher;
+
+            // Compute HMAC
+            var innerHash = hasher.finalize(messageUpdate);
+            hasher.reset();
+            var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));
+
+            return hmac;
+        }
+    });
+}());
diff --git a/src/core/js/lib/crypto-js/mode-ctr.js b/src/core/js/lib/crypto-js/mode-ctr.js
new file mode 100755
index 0000000..e8a9bb9
--- /dev/null
+++ b/src/core/js/lib/crypto-js/mode-ctr.js
@@ -0,0 +1,44 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * Counter block mode.
+ */
+CryptoJS.mode.CTR = (function () {
+    var CTR = CryptoJS.lib.BlockCipherMode.extend();
+
+    var Encryptor = CTR.Encryptor = CTR.extend({
+        processBlock: function (words, offset) {
+            // Shortcuts
+            var cipher = this._cipher
+            var blockSize = cipher.blockSize;
+            var iv = this._iv;
+            var counter = this._counter;
+
+            // Generate keystream
+            if (iv) {
+                counter = this._counter = iv.slice(0);
+
+                // Remove IV for subsequent blocks
+                this._iv = undefined;
+            }
+            var keystream = counter.slice(0);
+            cipher.encryptBlock(keystream, 0);
+
+            // Increment counter
+            counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0
+
+            // Encrypt
+            for (var i = 0; i < blockSize; i++) {
+                words[offset + i] ^= keystream[i];
+            }
+        }
+    });
+
+    CTR.Decryptor = Encryptor;
+
+    return CTR;
+}());
diff --git a/src/core/js/lib/crypto-js/pad-nopadding.js b/src/core/js/lib/crypto-js/pad-nopadding.js
new file mode 100755
index 0000000..b168370
--- /dev/null
+++ b/src/core/js/lib/crypto-js/pad-nopadding.js
@@ -0,0 +1,16 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+/**
+ * A noop padding strategy.
+ */
+CryptoJS.pad.NoPadding = {
+    pad: function () {
+    },
+
+    unpad: function () {
+    }
+};
diff --git a/src/core/js/lib/crypto-js/pbkdf2.js b/src/core/js/lib/crypto-js/pbkdf2.js
new file mode 100755
index 0000000..46040ee
--- /dev/null
+++ b/src/core/js/lib/crypto-js/pbkdf2.js
@@ -0,0 +1,131 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var WordArray = C_lib.WordArray;
+    var C_algo = C.algo;
+    var SHA1 = C_algo.SHA1;
+    var HMAC = C_algo.HMAC;
+
+    /**
+     * Password-Based Key Derivation Function 2 algorithm.
+     */
+    var PBKDF2 = C_algo.PBKDF2 = Base.extend({
+        /**
+         * Configuration options.
+         *
+         * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
+         * @property {Hasher} hasher The hasher to use. Default: SHA1
+         * @property {number} iterations The number of iterations to perform. Default: 1
+         */
+        cfg: Base.extend({
+            keySize: 128/32,
+            hasher: SHA1,
+            iterations: 1
+        }),
+
+        /**
+         * Initializes a newly created key derivation function.
+         *
+         * @param {Object} cfg (Optional) The configuration options to use for the derivation.
+         *
+         * @example
+         *
+         *     var kdf = CryptoJS.algo.PBKDF2.create();
+         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 });
+         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 });
+         */
+        init: function (cfg) {
+            this.cfg = this.cfg.extend(cfg);
+        },
+
+        /**
+         * Computes the Password-Based Key Derivation Function 2.
+         *
+         * @param {WordArray|string} password The password.
+         * @param {WordArray|string} salt A salt.
+         *
+         * @return {WordArray} The derived key.
+         *
+         * @example
+         *
+         *     var key = kdf.compute(password, salt);
+         */
+        compute: function (password, salt) {
+            // Shortcut
+            var cfg = this.cfg;
+
+            // Init HMAC
+            var hmac = HMAC.create(cfg.hasher, password);
+
+            // Initial values
+            var derivedKey = WordArray.create();
+            var blockIndex = WordArray.create([0x00000001]);
+
+            // Shortcuts
+            var derivedKeyWords = derivedKey.words;
+            var blockIndexWords = blockIndex.words;
+            var keySize = cfg.keySize;
+            var iterations = cfg.iterations;
+
+            // Generate key
+            while (derivedKeyWords.length < keySize) {
+                var block = hmac.update(salt).finalize(blockIndex);
+                hmac.reset();
+
+                // Shortcuts
+                var blockWords = block.words;
+                var blockWordsLength = blockWords.length;
+
+                // Iterations
+                var intermediate = block;
+                for (var i = 1; i < iterations; i++) {
+                    intermediate = hmac.finalize(intermediate);
+                    hmac.reset();
+
+                    // Shortcut
+                    var intermediateWords = intermediate.words;
+
+                    // XOR intermediate with block
+                    for (var j = 0; j < blockWordsLength; j++) {
+                        blockWords[j] ^= intermediateWords[j];
+                    }
+                }
+
+                derivedKey.concat(block);
+                blockIndexWords[0]++;
+            }
+            derivedKey.sigBytes = keySize * 4;
+
+            return derivedKey;
+        }
+    });
+
+    /**
+     * Computes the Password-Based Key Derivation Function 2.
+     *
+     * @param {WordArray|string} password The password.
+     * @param {WordArray|string} salt A salt.
+     * @param {Object} cfg (Optional) The configuration options to use for this computation.
+     *
+     * @return {WordArray} The derived key.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var key = CryptoJS.PBKDF2(password, salt);
+     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 });
+     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 });
+     */
+    C.PBKDF2 = function (password, salt, cfg) {
+        return PBKDF2.create(cfg).compute(password, salt);
+    };
+}());
diff --git a/src/core/js/lib/crypto-js/sha1.js b/src/core/js/lib/crypto-js/sha1.js
new file mode 100755
index 0000000..e10a9a2
--- /dev/null
+++ b/src/core/js/lib/crypto-js/sha1.js
@@ -0,0 +1,136 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var Hasher = C_lib.Hasher;
+    var C_algo = C.algo;
+
+    // Reusable object
+    var W = [];
+
+    /**
+     * SHA-1 hash algorithm.
+     */
+    var SHA1 = C_algo.SHA1 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new WordArray.init([
+                0x67452301, 0xefcdab89,
+                0x98badcfe, 0x10325476,
+                0xc3d2e1f0
+            ]);
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcut
+            var H = this._hash.words;
+
+            // Working variables
+            var a = H[0];
+            var b = H[1];
+            var c = H[2];
+            var d = H[3];
+            var e = H[4];
+
+            // Computation
+            for (var i = 0; i < 80; i++) {
+                if (i < 16) {
+                    W[i] = M[offset + i] | 0;
+                } else {
+                    var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
+                    W[i] = (n << 1) | (n >>> 31);
+                }
+
+                var t = ((a << 5) | (a >>> 27)) + e + W[i];
+                if (i < 20) {
+                    t += ((b & c) | (~b & d)) + 0x5a827999;
+                } else if (i < 40) {
+                    t += (b ^ c ^ d) + 0x6ed9eba1;
+                } else if (i < 60) {
+                    t += ((b & c) | (b & d) | (c & d)) - 0x70e44324;
+                } else /* if (i < 80) */ {
+                    t += (b ^ c ^ d) - 0x359d3e2a;
+                }
+
+                e = d;
+                d = c;
+                c = (b << 30) | (b >>> 2);
+                b = a;
+                a = t;
+            }
+
+            // Intermediate hash value
+            H[0] = (H[0] + a) | 0;
+            H[1] = (H[1] + b) | 0;
+            H[2] = (H[2] + c) | 0;
+            H[3] = (H[3] + d) | 0;
+            H[4] = (H[4] + e) | 0;
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Return final computed hash
+            return this._hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        }
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA1('message');
+     *     var hash = CryptoJS.SHA1(wordArray);
+     */
+    C.SHA1 = Hasher._createHelper(SHA1);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA1(message, key);
+     */
+    C.HmacSHA1 = Hasher._createHmacHelper(SHA1);
+}());
diff --git a/src/core/js/lib/crypto-js/sha256.js b/src/core/js/lib/crypto-js/sha256.js
new file mode 100755
index 0000000..a3790b0
--- /dev/null
+++ b/src/core/js/lib/crypto-js/sha256.js
@@ -0,0 +1,185 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function (Math) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var WordArray = C_lib.WordArray;
+    var Hasher = C_lib.Hasher;
+    var C_algo = C.algo;
+
+    // Initialization and round constants tables
+    var H = [];
+    var K = [];
+
+    // Compute constants
+    (function () {
+        function isPrime(n) {
+            var sqrtN = Math.sqrt(n);
+            for (var factor = 2; factor <= sqrtN; factor++) {
+                if (!(n % factor)) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        function getFractionalBits(n) {
+            return ((n - (n | 0)) * 0x100000000) | 0;
+        }
+
+        var n = 2;
+        var nPrime = 0;
+        while (nPrime < 64) {
+            if (isPrime(n)) {
+                if (nPrime < 8) {
+                    H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
+                }
+                K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
+
+                nPrime++;
+            }
+
+            n++;
+        }
+    }());
+
+    // Reusable object
+    var W = [];
+
+    /**
+     * SHA-256 hash algorithm.
+     */
+    var SHA256 = C_algo.SHA256 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new WordArray.init(H.slice(0));
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcut
+            var H = this._hash.words;
+
+            // Working variables
+            var a = H[0];
+            var b = H[1];
+            var c = H[2];
+            var d = H[3];
+            var e = H[4];
+            var f = H[5];
+            var g = H[6];
+            var h = H[7];
+
+            // Computation
+            for (var i = 0; i < 64; i++) {
+                if (i < 16) {
+                    W[i] = M[offset + i] | 0;
+                } else {
+                    var gamma0x = W[i - 15];
+                    var gamma0  = ((gamma0x << 25) | (gamma0x >>> 7))  ^
+                                  ((gamma0x << 14) | (gamma0x >>> 18)) ^
+                                   (gamma0x >>> 3);
+
+                    var gamma1x = W[i - 2];
+                    var gamma1  = ((gamma1x << 15) | (gamma1x >>> 17)) ^
+                                  ((gamma1x << 13) | (gamma1x >>> 19)) ^
+                                   (gamma1x >>> 10);
+
+                    W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
+                }
+
+                var ch  = (e & f) ^ (~e & g);
+                var maj = (a & b) ^ (a & c) ^ (b & c);
+
+                var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
+                var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7)  | (e >>> 25));
+
+                var t1 = h + sigma1 + ch + K[i] + W[i];
+                var t2 = sigma0 + maj;
+
+                h = g;
+                g = f;
+                f = e;
+                e = (d + t1) | 0;
+                d = c;
+                c = b;
+                b = a;
+                a = (t1 + t2) | 0;
+            }
+
+            // Intermediate hash value
+            H[0] = (H[0] + a) | 0;
+            H[1] = (H[1] + b) | 0;
+            H[2] = (H[2] + c) | 0;
+            H[3] = (H[3] + d) | 0;
+            H[4] = (H[4] + e) | 0;
+            H[5] = (H[5] + f) | 0;
+            H[6] = (H[6] + g) | 0;
+            H[7] = (H[7] + h) | 0;
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Return final computed hash
+            return this._hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        }
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA256('message');
+     *     var hash = CryptoJS.SHA256(wordArray);
+     */
+    C.SHA256 = Hasher._createHelper(SHA256);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA256(message, key);
+     */
+    C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
+}(Math));
diff --git a/src/core/js/lib/crypto-js/sha512.js b/src/core/js/lib/crypto-js/sha512.js
new file mode 100755
index 0000000..14a16ac
--- /dev/null
+++ b/src/core/js/lib/crypto-js/sha512.js
@@ -0,0 +1,309 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function () {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Hasher = C_lib.Hasher;
+    var C_x64 = C.x64;
+    var X64Word = C_x64.Word;
+    var X64WordArray = C_x64.WordArray;
+    var C_algo = C.algo;
+
+    function X64Word_create() {
+        return X64Word.create.apply(X64Word, arguments);
+    }
+
+    // Constants
+    var K = [
+        X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd),
+        X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc),
+        X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019),
+        X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118),
+        X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe),
+        X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2),
+        X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1),
+        X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694),
+        X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3),
+        X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65),
+        X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483),
+        X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5),
+        X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210),
+        X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4),
+        X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725),
+        X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70),
+        X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926),
+        X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df),
+        X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8),
+        X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b),
+        X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001),
+        X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30),
+        X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910),
+        X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8),
+        X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53),
+        X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8),
+        X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb),
+        X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3),
+        X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60),
+        X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec),
+        X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9),
+        X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b),
+        X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207),
+        X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178),
+        X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6),
+        X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b),
+        X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493),
+        X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c),
+        X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a),
+        X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817)
+    ];
+
+    // Reusable objects
+    var W = [];
+    (function () {
+        for (var i = 0; i < 80; i++) {
+            W[i] = X64Word_create();
+        }
+    }());
+
+    /**
+     * SHA-512 hash algorithm.
+     */
+    var SHA512 = C_algo.SHA512 = Hasher.extend({
+        _doReset: function () {
+            this._hash = new X64WordArray.init([
+                new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b),
+                new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1),
+                new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f),
+                new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179)
+            ]);
+        },
+
+        _doProcessBlock: function (M, offset) {
+            // Shortcuts
+            var H = this._hash.words;
+
+            var H0 = H[0];
+            var H1 = H[1];
+            var H2 = H[2];
+            var H3 = H[3];
+            var H4 = H[4];
+            var H5 = H[5];
+            var H6 = H[6];
+            var H7 = H[7];
+
+            var H0h = H0.high;
+            var H0l = H0.low;
+            var H1h = H1.high;
+            var H1l = H1.low;
+            var H2h = H2.high;
+            var H2l = H2.low;
+            var H3h = H3.high;
+            var H3l = H3.low;
+            var H4h = H4.high;
+            var H4l = H4.low;
+            var H5h = H5.high;
+            var H5l = H5.low;
+            var H6h = H6.high;
+            var H6l = H6.low;
+            var H7h = H7.high;
+            var H7l = H7.low;
+
+            // Working variables
+            var ah = H0h;
+            var al = H0l;
+            var bh = H1h;
+            var bl = H1l;
+            var ch = H2h;
+            var cl = H2l;
+            var dh = H3h;
+            var dl = H3l;
+            var eh = H4h;
+            var el = H4l;
+            var fh = H5h;
+            var fl = H5l;
+            var gh = H6h;
+            var gl = H6l;
+            var hh = H7h;
+            var hl = H7l;
+
+            // Rounds
+            for (var i = 0; i < 80; i++) {
+                // Shortcut
+                var Wi = W[i];
+
+                // Extend message
+                if (i < 16) {
+                    var Wih = Wi.high = M[offset + i * 2]     | 0;
+                    var Wil = Wi.low  = M[offset + i * 2 + 1] | 0;
+                } else {
+                    // Gamma0
+                    var gamma0x  = W[i - 15];
+                    var gamma0xh = gamma0x.high;
+                    var gamma0xl = gamma0x.low;
+                    var gamma0h  = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7);
+                    var gamma0l  = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25));
+
+                    // Gamma1
+                    var gamma1x  = W[i - 2];
+                    var gamma1xh = gamma1x.high;
+                    var gamma1xl = gamma1x.low;
+                    var gamma1h  = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6);
+                    var gamma1l  = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26));
+
+                    // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]
+                    var Wi7  = W[i - 7];
+                    var Wi7h = Wi7.high;
+                    var Wi7l = Wi7.low;
+
+                    var Wi16  = W[i - 16];
+                    var Wi16h = Wi16.high;
+                    var Wi16l = Wi16.low;
+
+                    var Wil = gamma0l + Wi7l;
+                    var Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0);
+                    var Wil = Wil + gamma1l;
+                    var Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0);
+                    var Wil = Wil + Wi16l;
+                    var Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0);
+
+                    Wi.high = Wih;
+                    Wi.low  = Wil;
+                }
+
+                var chh  = (eh & fh) ^ (~eh & gh);
+                var chl  = (el & fl) ^ (~el & gl);
+                var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch);
+                var majl = (al & bl) ^ (al & cl) ^ (bl & cl);
+
+                var sigma0h = ((ah >>> 28) | (al << 4))  ^ ((ah << 30)  | (al >>> 2)) ^ ((ah << 25) | (al >>> 7));
+                var sigma0l = ((al >>> 28) | (ah << 4))  ^ ((al << 30)  | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7));
+                var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9));
+                var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9));
+
+                // t1 = h + sigma1 + ch + K[i] + W[i]
+                var Ki  = K[i];
+                var Kih = Ki.high;
+                var Kil = Ki.low;
+
+                var t1l = hl + sigma1l;
+                var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0);
+                var t1l = t1l + chl;
+                var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0);
+                var t1l = t1l + Kil;
+                var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0);
+                var t1l = t1l + Wil;
+                var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0);
+
+                // t2 = sigma0 + maj
+                var t2l = sigma0l + majl;
+                var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0);
+
+                // Update working variables
+                hh = gh;
+                hl = gl;
+                gh = fh;
+                gl = fl;
+                fh = eh;
+                fl = el;
+                el = (dl + t1l) | 0;
+                eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0;
+                dh = ch;
+                dl = cl;
+                ch = bh;
+                cl = bl;
+                bh = ah;
+                bl = al;
+                al = (t1l + t2l) | 0;
+                ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0;
+            }
+
+            // Intermediate hash value
+            H0l = H0.low  = (H0l + al);
+            H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0));
+            H1l = H1.low  = (H1l + bl);
+            H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0));
+            H2l = H2.low  = (H2l + cl);
+            H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0));
+            H3l = H3.low  = (H3l + dl);
+            H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0));
+            H4l = H4.low  = (H4l + el);
+            H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0));
+            H5l = H5.low  = (H5l + fl);
+            H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0));
+            H6l = H6.low  = (H6l + gl);
+            H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0));
+            H7l = H7.low  = (H7l + hl);
+            H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0));
+        },
+
+        _doFinalize: function () {
+            // Shortcuts
+            var data = this._data;
+            var dataWords = data.words;
+
+            var nBitsTotal = this._nDataBytes * 8;
+            var nBitsLeft = data.sigBytes * 8;
+
+            // Add padding
+            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000);
+            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal;
+            data.sigBytes = dataWords.length * 4;
+
+            // Hash final blocks
+            this._process();
+
+            // Convert hash to 32-bit word array before returning
+            var hash = this._hash.toX32();
+
+            // Return final computed hash
+            return hash;
+        },
+
+        clone: function () {
+            var clone = Hasher.clone.call(this);
+            clone._hash = this._hash.clone();
+
+            return clone;
+        },
+
+        blockSize: 1024/32
+    });
+
+    /**
+     * Shortcut function to the hasher's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     *
+     * @return {WordArray} The hash.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hash = CryptoJS.SHA512('message');
+     *     var hash = CryptoJS.SHA512(wordArray);
+     */
+    C.SHA512 = Hasher._createHelper(SHA512);
+
+    /**
+     * Shortcut function to the HMAC's object interface.
+     *
+     * @param {WordArray|string} message The message to hash.
+     * @param {WordArray|string} key The secret key.
+     *
+     * @return {WordArray} The HMAC.
+     *
+     * @static
+     *
+     * @example
+     *
+     *     var hmac = CryptoJS.HmacSHA512(message, key);
+     */
+    C.HmacSHA512 = Hasher._createHmacHelper(SHA512);
+}());
diff --git a/src/core/js/lib/crypto-js/x64-core.js b/src/core/js/lib/crypto-js/x64-core.js
new file mode 100755
index 0000000..c5ffb29
--- /dev/null
+++ b/src/core/js/lib/crypto-js/x64-core.js
@@ -0,0 +1,290 @@
+/*
+CryptoJS v3.1.2
+code.google.com/p/crypto-js
+(c) 2009-2013 by Jeff Mott. All rights reserved.
+code.google.com/p/crypto-js/wiki/License
+*/
+(function (undefined) {
+    // Shortcuts
+    var C = CryptoJS;
+    var C_lib = C.lib;
+    var Base = C_lib.Base;
+    var X32WordArray = C_lib.WordArray;
+
+    /**
+     * x64 namespace.
+     */
+    var C_x64 = C.x64 = {};
+
+    /**
+     * A 64-bit word.
+     */
+    var X64Word = C_x64.Word = Base.extend({
+        /**
+         * Initializes a newly created 64-bit word.
+         *
+         * @param {number} high The high 32 bits.
+         * @param {number} low The low 32 bits.
+         *
+         * @example
+         *
+         *     var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607);
+         */
+        init: function (high, low) {
+            this.high = high;
+            this.low = low;
+        }
+
+        /**
+         * Bitwise NOTs this word.
+         *
+         * @return {X64Word} A new x64-Word object after negating.
+         *
+         * @example
+         *
+         *     var negated = x64Word.not();
+         */
+        // not: function () {
+            // var high = ~this.high;
+            // var low = ~this.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise ANDs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to AND with this word.
+         *
+         * @return {X64Word} A new x64-Word object after ANDing.
+         *
+         * @example
+         *
+         *     var anded = x64Word.and(anotherX64Word);
+         */
+        // and: function (word) {
+            // var high = this.high & word.high;
+            // var low = this.low & word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise ORs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to OR with this word.
+         *
+         * @return {X64Word} A new x64-Word object after ORing.
+         *
+         * @example
+         *
+         *     var ored = x64Word.or(anotherX64Word);
+         */
+        // or: function (word) {
+            // var high = this.high | word.high;
+            // var low = this.low | word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Bitwise XORs this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to XOR with this word.
+         *
+         * @return {X64Word} A new x64-Word object after XORing.
+         *
+         * @example
+         *
+         *     var xored = x64Word.xor(anotherX64Word);
+         */
+        // xor: function (word) {
+            // var high = this.high ^ word.high;
+            // var low = this.low ^ word.low;
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Shifts this word n bits to the left.
+         *
+         * @param {number} n The number of bits to shift.
+         *
+         * @return {X64Word} A new x64-Word object after shifting.
+         *
+         * @example
+         *
+         *     var shifted = x64Word.shiftL(25);
+         */
+        // shiftL: function (n) {
+            // if (n < 32) {
+                // var high = (this.high << n) | (this.low >>> (32 - n));
+                // var low = this.low << n;
+            // } else {
+                // var high = this.low << (n - 32);
+                // var low = 0;
+            // }
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Shifts this word n bits to the right.
+         *
+         * @param {number} n The number of bits to shift.
+         *
+         * @return {X64Word} A new x64-Word object after shifting.
+         *
+         * @example
+         *
+         *     var shifted = x64Word.shiftR(7);
+         */
+        // shiftR: function (n) {
+            // if (n < 32) {
+                // var low = (this.low >>> n) | (this.high << (32 - n));
+                // var high = this.high >>> n;
+            // } else {
+                // var low = this.high >>> (n - 32);
+                // var high = 0;
+            // }
+
+            // return X64Word.create(high, low);
+        // },
+
+        /**
+         * Rotates this word n bits to the left.
+         *
+         * @param {number} n The number of bits to rotate.
+         *
+         * @return {X64Word} A new x64-Word object after rotating.
+         *
+         * @example
+         *
+         *     var rotated = x64Word.rotL(25);
+         */
+        // rotL: function (n) {
+            // return this.shiftL(n).or(this.shiftR(64 - n));
+        // },
+
+        /**
+         * Rotates this word n bits to the right.
+         *
+         * @param {number} n The number of bits to rotate.
+         *
+         * @return {X64Word} A new x64-Word object after rotating.
+         *
+         * @example
+         *
+         *     var rotated = x64Word.rotR(7);
+         */
+        // rotR: function (n) {
+            // return this.shiftR(n).or(this.shiftL(64 - n));
+        // },
+
+        /**
+         * Adds this word with the passed word.
+         *
+         * @param {X64Word} word The x64-Word to add with this word.
+         *
+         * @return {X64Word} A new x64-Word object after adding.
+         *
+         * @example
+         *
+         *     var added = x64Word.add(anotherX64Word);
+         */
+        // add: function (word) {
+            // var low = (this.low + word.low) | 0;
+            // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0;
+            // var high = (this.high + word.high + carry) | 0;
+
+            // return X64Word.create(high, low);
+        // }
+    });
+
+    /**
+     * An array of 64-bit words.
+     *
+     * @property {Array} words The array of CryptoJS.x64.Word objects.
+     * @property {number} sigBytes The number of significant bytes in this word array.
+     */
+    var X64WordArray = C_x64.WordArray = Base.extend({
+        /**
+         * Initializes a newly created word array.
+         *
+         * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects.
+         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+         *
+         * @example
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create();
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create([
+         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+         *     ]);
+         *
+         *     var wordArray = CryptoJS.x64.WordArray.create([
+         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+         *     ], 10);
+         */
+        init: function (words, sigBytes) {
+            words = this.words = words || [];
+
+            if (sigBytes != undefined) {
+                this.sigBytes = sigBytes;
+            } else {
+                this.sigBytes = words.length * 8;
+            }
+        },
+
+        /**
+         * Converts this 64-bit word array to a 32-bit word array.
+         *
+         * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array.
+         *
+         * @example
+         *
+         *     var x32WordArray = x64WordArray.toX32();
+         */
+        toX32: function () {
+            // Shortcuts
+            var x64Words = this.words;
+            var x64WordsLength = x64Words.length;
+
+            // Convert
+            var x32Words = [];
+            for (var i = 0; i < x64WordsLength; i++) {
+                var x64Word = x64Words[i];
+                x32Words.push(x64Word.high);
+                x32Words.push(x64Word.low);
+            }
+
+            return X32WordArray.create(x32Words, this.sigBytes);
+        },
+
+        /**
+         * Creates a copy of this word array.
+         *
+         * @return {X64WordArray} The clone.
+         *
+         * @example
+         *
+         *     var clone = x64WordArray.clone();
+         */
+        clone: function () {
+            var clone = Base.clone.call(this);
+
+            // Clone "words" array
+            var words = clone.words = this.words.slice(0);
+
+            // Clone each X64Word object
+            var wordsLength = words.length;
+            for (var i = 0; i < wordsLength; i++) {
+                words[i] = words[i].clone();
+            }
+
+            return clone;
+        }
+    });
+}());
diff --git a/src/core/js/lib/elliptic.js b/src/core/js/lib/elliptic.js
new file mode 100755
index 0000000..da9bcbf
--- /dev/null
+++ b/src/core/js/lib/elliptic.js
@@ -0,0 +1,208 @@
+// curve25519
+
+;(function (root, factory) {
+
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = factory(require('./bigint.js'))
+  } else {
+    root.Curve25519 = factory(root.BigInt)
+  }
+
+}(this, function (BigInt) {
+'use strict';
+
+// In order to generate a public value:
+//	priv = BigInt.randBigInt(256)
+//	pub = scalarMult(priv, basePoint)
+//
+// In order to perform key agreement:
+//	shared = scalarMult(myPrivate, theirPublic)
+
+var Curve25519 = function () {}
+
+// p25519 is the curve25519 prime: 2^255 - 19
+Curve25519.p25519 = BigInt.str2bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed', 16)
+// p25519Minus2 = 2^255 - 21
+var p25519Minus2 = BigInt.str2bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeb', 16)
+// a is a parameter of the elliptic curve
+var a = BigInt.str2bigInt('486662', 10)
+// basePoint is the generator of the elliptic curve group
+var basePoint = BigInt.str2bigInt('9', 10)
+
+// These variables are names for small, bigint constants.
+var four = BigInt.str2bigInt('4', 10)
+
+// groupAdd adds two elements of the elliptic curve group in Montgomery form.
+function groupAdd(x1, xn, zn, xm, zm) {
+	// x₃ = 4(x·x′ - z·z′)² · z1
+	var xx = BigInt.multMod(xn, xm, Curve25519.p25519)
+	var zz = BigInt.multMod(zn, zm, Curve25519.p25519)
+	var d
+	if (BigInt.greater(xx, zz)) {
+		d = BigInt.sub(xx, zz)
+	} else {
+		d = BigInt.sub(zz, xx)
+	}
+	var sq = BigInt.multMod(d, d, Curve25519.p25519)
+	var outx = BigInt.multMod(sq, four, Curve25519.p25519)
+
+	// z₃ = 4(x·z′ - z·x′)² · x1
+	var xz = BigInt.multMod(xm, zn, Curve25519.p25519)
+	var zx = BigInt.multMod(zm, xn, Curve25519.p25519)
+	if (BigInt.greater(xz, zx)) {
+		d = BigInt.sub(xz, zx)
+	} else {
+		d = BigInt.sub(zx, xz)
+	}
+	sq = BigInt.multMod(d, d, Curve25519.p25519)
+	var sq2 = BigInt.multMod(sq, x1, Curve25519.p25519)
+	var outz = BigInt.multMod(sq2, four, Curve25519.p25519)
+
+	return [outx, outz]
+}
+
+// groupDouble doubles a point in the elliptic curve group.
+function groupDouble(x, z) {
+	// x₂ = (x² - z²)²
+	var xx = BigInt.multMod(x, x, Curve25519.p25519)
+	var zz = BigInt.multMod(z, z, Curve25519.p25519)
+	var d
+	if (BigInt.greater(xx, zz)) {
+		d = BigInt.sub(xx, zz)
+	} else {
+		d = BigInt.sub(zz, xx)
+	}
+	var outx = BigInt.multMod(d, d, Curve25519.p25519)
+
+	// z₂ = 4xz·(x² + Axz + z²)
+	var s = BigInt.add(xx, zz)
+	var xz = BigInt.multMod(x, z, Curve25519.p25519)
+	var axz = BigInt.mult(xz, a)
+	s = BigInt.add(s, axz)
+	var fourxz = BigInt.mult(xz, four)
+	var outz = BigInt.multMod(fourxz, s, Curve25519.p25519)
+
+	return [outx, outz]
+}
+
+// scalarMult calculates i*base in the elliptic curve.
+Curve25519.scalarMult = function (scalar, base) {
+	var x1 = BigInt.str2bigInt('1', 10)
+	var z1 = BigInt.str2bigInt('0', 10)
+	var x2 = base
+	var z2 = BigInt.str2bigInt('1', 10)
+	var point
+
+	// Highest bit is one
+	point = groupAdd(base, x1, z1, x2, z2)
+	x1 = point[0]
+	z1 = point[1]
+	point = groupDouble(x2, z2)
+	x2 = point[0]
+	z2 = point[1]
+
+	for (var i = 253; i >= 3; i--) {
+		if (BigInt.getBit(scalar, i)) {
+			point = groupAdd(base, x1, z1, x2, z2)
+			x1 = point[0]
+			z1 = point[1]
+			point = groupDouble(x2, z2)
+			x2 = point[0]
+			z2 = point[1]
+		} else {
+			point = groupAdd(base, x1, z1, x2, z2)
+			x2 = point[0]
+			z2 = point[1]
+			point = groupDouble(x1, z1)
+			x1 = point[0]
+			z1 = point[1]
+		}
+	}
+
+	// Lowest 3 bits are zero
+	for (i = 2; i >= 0; i--) {
+		point = groupDouble(x1, z1)
+		x1 = point[0]
+		z1 = point[1]
+	}
+
+	var z1inv = BigInt.powMod(z1, p25519Minus2, Curve25519.p25519)
+	var x = BigInt.multMod(z1inv, x1, Curve25519.p25519)
+
+	return x
+}
+
+
+// P256
+
+// var priv = BigInt.randBigInt(256)
+// var pub = scalarMultP256(p256Gx, p256Gy, priv)
+// var message = BigInt.str2bigInt('2349623424239482634', 10)
+
+// p256 is the p256 prime
+// var p256 = BigInt.str2bigInt('115792089210356248762697446949407573530086143415290314195533631308867097853951', 10)
+// n256 is the number of points in the group
+// var n256 = BigInt.str2bigInt('115792089210356248762697446949407573529996955224135760342422259061068512044369', 10)
+// b256 is a parameter of the curve
+// var b256 = BigInt.str2bigInt('5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b', 16)
+// p256Gx and p256Gy is the generator of the group
+// var p256Gx = BigInt.str2bigInt('6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296', 16)
+// var p256Gy = BigInt.str2bigInt('4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5', 16)
+
+Curve25519.privateKeyToString = function(p){
+	return BigInt.bigInt2str(p, 64)
+}
+
+Curve25519.privateKeyFromString = function(s){
+	return BigInt.str2bigInt(s, 64)
+}
+
+Curve25519.sigToString = function(p){
+	return JSON.stringify([BigInt.bigInt2str(p[0], 64), BigInt.bigInt2str(p[1], 64)])
+}
+
+Curve25519.sigFromString = function(s){
+	var p = JSON.parse(s)
+	p[0] = BigInt.str2bigInt(p[0], 64)
+	p[1] = BigInt.str2bigInt(p[1], 64)
+	return p
+}
+
+Curve25519.publicKeyToString = function(p){
+	return JSON.stringify([BigInt.bigInt2str(p[0], 64), BigInt.bigInt2str(p[1], 64)])
+}
+
+Curve25519.publicKeyFromString = function(s){
+	var p = JSON.parse(s)
+	p[0] = BigInt.str2bigInt(p[0], 64)
+	p[1] = BigInt.str2bigInt(p[1], 64)
+	return p
+}
+
+// isOnCurve returns true if the given point is on the curve.
+// function isOnCurve(x, y) {
+// 	// y² = x³ - 3x + b
+// 	var yy = BigInt.multMod(y, y, p256)
+// 	var xxx = BigInt.multMod(x, BigInt.mult(x, x), p256)
+// 	var threex = BigInt.multMod(three, x, p256)
+// 	var s = BigInt.add(xxx, b256)
+// 	if (BigInt.greater(threex, s)) {
+// 		return false
+// 	}
+// 	s = BigInt.sub(s, threex)
+// 	return BigInt.equals(s, yy)
+// }
+
+
+Curve25519.ecDH = function(priv, pub) {
+	if (typeof pub === 'undefined') {
+		return Curve25519.scalarMult(priv, basePoint)
+	}
+	else {
+		return Curve25519.scalarMult(priv, pub)
+	}
+}
+
+return Curve25519
+
+}))
diff --git a/src/core/js/lib/eventemitter.js b/src/core/js/lib/eventemitter.js
new file mode 100644
index 0000000..c16414b
--- /dev/null
+++ b/src/core/js/lib/eventemitter.js
@@ -0,0 +1,472 @@
+/*!
+ * EventEmitter v4.2.7 - git.io/ee
+ * Oliver Caldwell
+ * MIT license
+ * @preserve
+ */
+
+(function () {
+	'use strict';
+
+	/**
+	 * Class for managing events.
+	 * Can be extended to provide event functionality in other classes.
+	 *
+	 * @class EventEmitter Manages event registering and emitting.
+	 */
+	function EventEmitter() {}
+
+	// Shortcuts to improve speed and size
+	var proto = EventEmitter.prototype;
+	var exports = this;
+	var originalGlobalValue = exports.EventEmitter;
+
+	/**
+	 * Finds the index of the listener for the event in it's storage array.
+	 *
+	 * @param {Function[]} listeners Array of listeners to search through.
+	 * @param {Function} listener Method to look for.
+	 * @return {Number} Index of the specified listener, -1 if not found
+	 * @api private
+	 */
+	function indexOfListener(listeners, listener) {
+		var i = listeners.length;
+		while (i--) {
+			if (listeners[i].listener === listener) {
+				return i;
+			}
+		}
+
+		return -1;
+	}
+
+	/**
+	 * Alias a method while keeping the context correct, to allow for overwriting of target method.
+	 *
+	 * @param {String} name The name of the target method.
+	 * @return {Function} The aliased method
+	 * @api private
+	 */
+	function alias(name) {
+		return function aliasClosure() {
+			return this[name].apply(this, arguments);
+		};
+	}
+
+	/**
+	 * Returns the listener array for the specified event.
+	 * Will initialise the event object and listener arrays if required.
+	 * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
+	 * Each property in the object response is an array of listener functions.
+	 *
+	 * @param {String|RegExp} evt Name of the event to return the listeners from.
+	 * @return {Function[]|Object} All listener functions for the event.
+	 */
+	proto.getListeners = function getListeners(evt) {
+		var events = this._getEvents();
+		var response;
+		var key;
+
+		// Return a concatenated array of all matching events if
+		// the selector is a regular expression.
+		if (evt instanceof RegExp) {
+			response = {};
+			for (key in events) {
+				if (events.hasOwnProperty(key) && evt.test(key)) {
+					response[key] = events[key];
+				}
+			}
+		}
+		else {
+			response = events[evt] || (events[evt] = []);
+		}
+
+		return response;
+	};
+
+	/**
+	 * Takes a list of listener objects and flattens it into a list of listener functions.
+	 *
+	 * @param {Object[]} listeners Raw listener objects.
+	 * @return {Function[]} Just the listener functions.
+	 */
+	proto.flattenListeners = function flattenListeners(listeners) {
+		var flatListeners = [];
+		var i;
+
+		for (i = 0; i < listeners.length; i += 1) {
+			flatListeners.push(listeners[i].listener);
+		}
+
+		return flatListeners;
+	};
+
+	/**
+	 * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
+	 *
+	 * @param {String|RegExp} evt Name of the event to return the listeners from.
+	 * @return {Object} All listener functions for an event in an object.
+	 */
+	proto.getListenersAsObject = function getListenersAsObject(evt) {
+		var listeners = this.getListeners(evt);
+		var response;
+
+		if (listeners instanceof Array) {
+			response = {};
+			response[evt] = listeners;
+		}
+
+		return response || listeners;
+	};
+
+	/**
+	 * Adds a listener function to the specified event.
+	 * The listener will not be added if it is a duplicate.
+	 * If the listener returns true then it will be removed after it is called.
+	 * If you pass a regular expression as the event name then the listener will be added to all events that match it.
+	 *
+	 * @param {String|RegExp} evt Name of the event to attach the listener to.
+	 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.addListener = function addListener(evt, listener) {
+		var listeners = this.getListenersAsObject(evt);
+		var listenerIsWrapped = typeof listener === 'object';
+		var key;
+
+		for (key in listeners) {
+			if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
+				listeners[key].push(listenerIsWrapped ? listener : {
+					listener: listener,
+					once: false
+				});
+			}
+		}
+
+		return this;
+	};
+
+	/**
+	 * Alias of addListener
+	 */
+	proto.on = alias('addListener');
+
+	/**
+	 * Semi-alias of addListener. It will add a listener that will be
+	 * automatically removed after it's first execution.
+	 *
+	 * @param {String|RegExp} evt Name of the event to attach the listener to.
+	 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.addOnceListener = function addOnceListener(evt, listener) {
+		return this.addListener(evt, {
+			listener: listener,
+			once: true
+		});
+	};
+
+	/**
+	 * Alias of addOnceListener.
+	 */
+	proto.once = alias('addOnceListener');
+
+	/**
+	 * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad.
+	 * You need to tell it what event names should be matched by a regex.
+	 *
+	 * @param {String} evt Name of the event to create.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.defineEvent = function defineEvent(evt) {
+		this.getListeners(evt);
+		return this;
+	};
+
+	/**
+	 * Uses defineEvent to define multiple events.
+	 *
+	 * @param {String[]} evts An array of event names to define.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.defineEvents = function defineEvents(evts) {
+		for (var i = 0; i < evts.length; i += 1) {
+			this.defineEvent(evts[i]);
+		}
+		return this;
+	};
+
+	/**
+	 * Removes a listener function from the specified event.
+	 * When passed a regular expression as the event name, it will remove the listener from all events that match it.
+	 *
+	 * @param {String|RegExp} evt Name of the event to remove the listener from.
+	 * @param {Function} listener Method to remove from the event.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.removeListener = function removeListener(evt, listener) {
+		var listeners = this.getListenersAsObject(evt);
+		var index;
+		var key;
+
+		for (key in listeners) {
+			if (listeners.hasOwnProperty(key)) {
+				index = indexOfListener(listeners[key], listener);
+
+				if (index !== -1) {
+					listeners[key].splice(index, 1);
+				}
+			}
+		}
+
+		return this;
+	};
+
+	/**
+	 * Alias of removeListener
+	 */
+	proto.off = alias('removeListener');
+
+	/**
+	 * Adds listeners in bulk using the manipulateListeners method.
+	 * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added.
+	 * You can also pass it a regular expression to add the array of listeners to all events that match it.
+	 * Yeah, this function does quite a bit. That's probably a bad thing.
+	 *
+	 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
+	 * @param {Function[]} [listeners] An optional array of listener functions to add.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.addListeners = function addListeners(evt, listeners) {
+		// Pass through to manipulateListeners
+		return this.manipulateListeners(false, evt, listeners);
+	};
+
+	/**
+	 * Removes listeners in bulk using the manipulateListeners method.
+	 * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
+	 * You can also pass it an event name and an array of listeners to be removed.
+	 * You can also pass it a regular expression to remove the listeners from all events that match it.
+	 *
+	 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
+	 * @param {Function[]} [listeners] An optional array of listener functions to remove.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.removeListeners = function removeListeners(evt, listeners) {
+		// Pass through to manipulateListeners
+		return this.manipulateListeners(true, evt, listeners);
+	};
+
+	/**
+	 * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
+	 * The first argument will determine if the listeners are removed (true) or added (false).
+	 * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
+	 * You can also pass it an event name and an array of listeners to be added/removed.
+	 * You can also pass it a regular expression to manipulate the listeners of all events that match it.
+	 *
+	 * @param {Boolean} remove True if you want to remove listeners, false if you want to add.
+	 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
+	 * @param {Function[]} [listeners] An optional array of listener functions to add/remove.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) {
+		var i;
+		var value;
+		var single = remove ? this.removeListener : this.addListener;
+		var multiple = remove ? this.removeListeners : this.addListeners;
+
+		// If evt is an object then pass each of it's properties to this method
+		if (typeof evt === 'object' && !(evt instanceof RegExp)) {
+			for (i in evt) {
+				if (evt.hasOwnProperty(i) && (value = evt[i])) {
+					// Pass the single listener straight through to the singular method
+					if (typeof value === 'function') {
+						single.call(this, i, value);
+					}
+					else {
+						// Otherwise pass back to the multiple function
+						multiple.call(this, i, value);
+					}
+				}
+			}
+		}
+		else {
+			// So evt must be a string
+			// And listeners must be an array of listeners
+			// Loop over it and pass each one to the multiple method
+			i = listeners.length;
+			while (i--) {
+				single.call(this, evt, listeners[i]);
+			}
+		}
+
+		return this;
+	};
+
+	/**
+	 * Removes all listeners from a specified event.
+	 * If you do not specify an event then all listeners will be removed.
+	 * That means every event will be emptied.
+	 * You can also pass a regex to remove all events that match it.
+	 *
+	 * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.removeEvent = function removeEvent(evt) {
+		var type = typeof evt;
+		var events = this._getEvents();
+		var key;
+
+		// Remove different things depending on the state of evt
+		if (type === 'string') {
+			// Remove all listeners for the specified event
+			delete events[evt];
+		}
+		else if (evt instanceof RegExp) {
+			// Remove all events matching the regex.
+			for (key in events) {
+				if (events.hasOwnProperty(key) && evt.test(key)) {
+					delete events[key];
+				}
+			}
+		}
+		else {
+			// Remove all listeners in all events
+			delete this._events;
+		}
+
+		return this;
+	};
+
+	/**
+	 * Alias of removeEvent.
+	 *
+	 * Added to mirror the node API.
+	 */
+	proto.removeAllListeners = alias('removeEvent');
+
+	/**
+	 * Emits an event of your choice.
+	 * When emitted, every listener attached to that event will be executed.
+	 * If you pass the optional argument array then those arguments will be passed to every listener upon execution.
+	 * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
+	 * So they will not arrive within the array on the other side, they will be separate.
+	 * You can also pass a regular expression to emit to all events that match it.
+	 *
+	 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
+	 * @param {Array} [args] Optional array of arguments to be passed to each listener.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.emitEvent = function emitEvent(evt, args) {
+		var listeners = this.getListenersAsObject(evt);
+		var listener;
+		var i;
+		var key;
+		var response;
+
+		for (key in listeners) {
+			if (listeners.hasOwnProperty(key)) {
+				i = listeners[key].length;
+
+				while (i--) {
+					// If the listener returns true then it shall be removed from the event
+					// The function is executed either with a basic call or an apply if there is an args array
+					listener = listeners[key][i];
+
+					if (listener.once === true) {
+						this.removeListener(evt, listener.listener);
+					}
+
+					response = listener.listener.apply(this, args || []);
+
+					if (response === this._getOnceReturnValue()) {
+						this.removeListener(evt, listener.listener);
+					}
+				}
+			}
+		}
+
+		return this;
+	};
+
+	/**
+	 * Alias of emitEvent
+	 */
+	proto.trigger = alias('emitEvent');
+
+	/**
+	 * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on.
+	 * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it.
+	 *
+	 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
+	 * @param {...*} Optional additional arguments to be passed to each listener.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.emit = function emit(evt) {
+		var args = Array.prototype.slice.call(arguments, 1);
+		return this.emitEvent(evt, args);
+	};
+
+	/**
+	 * Sets the current value to check against when executing listeners. If a
+	 * listeners return value matches the one set here then it will be removed
+	 * after execution. This value defaults to true.
+	 *
+	 * @param {*} value The new value to check for when executing listeners.
+	 * @return {Object} Current instance of EventEmitter for chaining.
+	 */
+	proto.setOnceReturnValue = function setOnceReturnValue(value) {
+		this._onceReturnValue = value;
+		return this;
+	};
+
+	/**
+	 * Fetches the current value to check against when executing listeners. If
+	 * the listeners return value matches this one then it should be removed
+	 * automatically. It will return true by default.
+	 *
+	 * @return {*|Boolean} The current value to check for or the default, true.
+	 * @api private
+	 */
+	proto._getOnceReturnValue = function _getOnceReturnValue() {
+		if (this.hasOwnProperty('_onceReturnValue')) {
+			return this._onceReturnValue;
+		}
+		else {
+			return true;
+		}
+	};
+
+	/**
+	 * Fetches the events object and creates one if required.
+	 *
+	 * @return {Object} The events storage object.
+	 * @api private
+	 */
+	proto._getEvents = function _getEvents() {
+		return this._events || (this._events = {});
+	};
+
+	/**
+	 * Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version.
+	 *
+	 * @return {Function} Non conflicting EventEmitter class.
+	 */
+	EventEmitter.noConflict = function noConflict() {
+		exports.EventEmitter = originalGlobalValue;
+		return EventEmitter;
+	};
+
+	// Expose the class either via AMD, CommonJS or the global object
+	if (typeof define === 'function' && define.amd) {
+		define(function () {
+			return EventEmitter;
+		});
+	}
+	else if (typeof module === 'object' && module.exports){
+		module.exports = EventEmitter;
+	}
+	else {
+		this.EventEmitter = EventEmitter;
+	}
+}.call(this));
\ No newline at end of file
diff --git a/src/core/js/lib/jquery/jquery.basicslider.js b/src/core/js/lib/jquery/jquery.basicslider.js
new file mode 100755
index 0000000..1de78e3
--- /dev/null
+++ b/src/core/js/lib/jquery/jquery.basicslider.js
@@ -0,0 +1,719 @@
+/*
+ * Basic jQuery Slider plug-in v.1.3
+ *
+ * http://www.basic-slider.com
+ *
+ * Authored by John Cobb
+ * http://www.johncobb.name
+ * @john0514
+ *
+ * Copyright 2011, John Cobb
+ * License: GNU General Public License, version 3 (GPL-3.0)
+ * http://www.opensource.org/licenses/gpl-3.0.html
+ *
+ */
+
+;(function($) {
+
+    "use strict";
+
+    $.fn.bjqs = function(o) {
+
+        // slider default settings
+        var defaults        = {
+
+            // w + h to enforce consistency
+            width           : 700,
+            height          : 300,
+
+            // transition valuess
+            animtype        : 'fade',
+            animduration    : 450,      // length of transition
+            animspeed       : 4000,     // delay between transitions
+            automatic       : true,     // enable/disable automatic slide rotation
+
+            // control and marker configuration
+            showcontrols    : true,     // enable/disable next + previous UI elements
+            centercontrols  : true,     // vertically center controls
+            nexttext        : 'Next',   // text/html inside next UI element
+            prevtext        : 'Prev',   // text/html inside previous UI element
+            showmarkers     : true,     // enable/disable individual slide UI markers
+            centermarkers   : true,     // horizontally center markers
+
+            // interaction values
+            keyboardnav     : true,     // enable/disable keyboard navigation
+            hoverpause      : true,     // enable/disable pause slides on hover
+
+            // presentational options
+            usecaptions     : true,     // enable/disable captions using img title attribute
+            randomstart     : false,     // start from a random slide
+            responsive      : false     // enable responsive behaviour
+
+        };
+
+        // create settings from defauls and user options
+        var settings        = $.extend({}, defaults, o);
+
+        // slider elements
+        var $wrapper        = this,
+            $slider         = $wrapper.find('ul.bjqs'),
+            $slides         = $slider.children('li'),
+
+            // control elements
+            $c_wrapper      = null,
+            $c_fwd          = null,
+            $c_prev         = null,
+
+            // marker elements
+            $m_wrapper      = null,
+            $m_markers      = null,
+
+            // elements for slide animation
+            $canvas         = null,
+            $clone_first    = null,
+            $clone_last     = null;
+
+        // state management object
+        var state           = {
+            slidecount      : $slides.length,   // total number of slides
+            animating       : false,            // bool: is transition is progress
+            paused          : false,            // bool: is the slider paused
+            currentslide    : 1,                // current slide being viewed (not 0 based)
+            nextslide       : 0,                // slide to view next (not 0 based)
+            currentindex    : 0,                // current slide being viewed (0 based)
+            nextindex       : 0,                // slide to view next (0 based)
+            interval        : null              // interval for automatic rotation
+        };
+
+        var responsive      = {
+            width           : null,
+            height          : null,
+            ratio           : null
+        };
+
+        // helpful variables
+        var vars            = {
+            fwd             : 'forward',
+            prev            : 'previous'
+        };
+
+        // run through options and initialise settings
+        var init = function() {
+
+            // differentiate slider li from content li
+            $slides.addClass('bjqs-slide');
+
+            // conf dimensions, responsive or static
+            if( settings.responsive ){
+                conf_responsive();
+            }
+            else{
+                conf_static();
+            }
+
+            // configurations only avaliable if more than 1 slide
+            if( state.slidecount > 1 ){
+
+                // enable random start
+                if (settings.randomstart){
+                    conf_random();
+                }
+
+                // create and show controls
+                if( settings.showcontrols ){
+                    conf_controls();
+                }
+
+                // create and show markers
+                if( settings.showmarkers ){
+                    conf_markers();
+                }
+
+                // enable slidenumboard navigation
+                if( settings.keyboardnav ){
+                    conf_keynav();
+                }
+
+                // enable pause on hover
+                if (settings.hoverpause && settings.automatic){
+                    conf_hoverpause();
+                }
+
+                // conf slide animation
+                if (settings.animtype === 'slide'){
+                    conf_slide();
+                }
+
+            } else {
+                // Stop automatic animation, because we only have one slide!
+                settings.automatic = false;
+            }
+
+            if(settings.usecaptions){
+                conf_captions();
+            }
+
+            // TODO: need to accomodate random start for slide transition setting
+            if(settings.animtype === 'slide' && !settings.randomstart){
+                state.currentindex = 1;
+                state.currentslide = 2;
+            }
+
+            // slide components are hidden by default, show them now
+            $slider.show();
+            $slides.eq(state.currentindex).show();
+
+            // Finally, if automatic is set to true, kick off the interval
+            if(settings.automatic){
+                state.interval = setInterval(function () {
+                    go(vars.fwd, false);
+                }, settings.animspeed);
+            }
+
+        };
+
+        var conf_responsive = function() {
+
+            responsive.width    = $wrapper.outerWidth();
+            responsive.ratio    = responsive.width/settings.width,
+            responsive.height   = settings.height * responsive.ratio;
+
+            if(settings.animtype === 'fade'){
+
+                // initial setup
+                $slides.css({
+                    'height'        : settings.height,
+                    'width'         : '100%'
+                });
+                $slides.children('img').css({
+                    'height'        : settings.height,
+                    'width'         : '100%'
+                });
+                $slider.css({
+                    'height'        : settings.height,
+                    'width'         : '100%'
+                });
+                $wrapper.css({
+                    'height'        : settings.height,
+                    'max-width'     : settings.width,
+                    'position'      : 'relative'
+                });
+
+                if(responsive.width < settings.width){
+
+                    $slides.css({
+                        'height'        : responsive.height
+                    });
+                    $slides.children('img').css({
+                        'height'        : responsive.height
+                    });
+                    $slider.css({
+                        'height'        : responsive.height
+                    });
+                    $wrapper.css({
+                        'height'        : responsive.height
+                    });
+
+                }
+
+                $(window).resize(function() {
+
+                    // calculate and update dimensions
+                    responsive.width    = $wrapper.outerWidth();
+                    responsive.ratio    = responsive.width/settings.width,
+                    responsive.height   = settings.height * responsive.ratio;
+
+                    $slides.css({
+                        'height'        : responsive.height
+                    });
+                    $slides.children('img').css({
+                        'height'        : responsive.height
+                    });
+                    $slider.css({
+                        'height'        : responsive.height
+                    });
+                    $wrapper.css({
+                        'height'        : responsive.height
+                    });
+
+                });
+
+            }
+
+            if(settings.animtype === 'slide'){
+
+                // initial setup
+                $slides.css({
+                    'height'        : settings.height,
+                    'width'         : settings.width
+                });
+                $slides.children('img').css({
+                    'height'        : settings.height,
+                    'width'         : settings.width
+                });
+                $slider.css({
+                    'height'        : settings.height,
+                    'width'         : settings.width * settings.slidecount
+                });
+                $wrapper.css({
+                    'height'        : settings.height,
+                    'max-width'     : settings.width,
+                    'position'      : 'relative'
+                });
+
+                if(responsive.width < settings.width){
+
+                    $slides.css({
+                        'height'        : responsive.height
+                    });
+                    $slides.children('img').css({
+                        'height'        : responsive.height
+                    });
+                    $slider.css({
+                        'height'        : responsive.height
+                    });
+                    $wrapper.css({
+                        'height'        : responsive.height
+                    });
+
+                }
+
+                $(window).resize(function() {
+
+                    // calculate and update dimensions
+                    responsive.width    = $wrapper.outerWidth(),
+                    responsive.ratio    = responsive.width/settings.width,
+                    responsive.height   = settings.height * responsive.ratio;
+
+                    $slides.css({
+                        'height'        : responsive.height,
+                        'width'         : responsive.width
+                    });
+                    $slides.children('img').css({
+                        'height'        : responsive.height,
+                        'width'         : responsive.width
+                    });
+                    $slider.css({
+                        'height'        : responsive.height,
+                        'width'         : responsive.width * settings.slidecount
+                    });
+                    $wrapper.css({
+                        'height'        : responsive.height
+                    });
+                    $canvas.css({
+                        'height'        : responsive.height,
+                        'width'         : responsive.width
+                    });
+
+                    resize_complete(function(){
+                        go(false,state.currentslide);
+                    }, 200, "some unique string");
+
+                });
+
+            }
+
+        };
+
+        var resize_complete = (function () {
+
+            var timers = {};
+
+            return function (callback, ms, uniqueId) {
+                if (!uniqueId) {
+                    uniqueId = "Don't call this twice without a uniqueId";
+                }
+                if (timers[uniqueId]) {
+                    clearTimeout (timers[uniqueId]);
+                }
+                timers[uniqueId] = setTimeout(callback, ms);
+            };
+
+        })();
+
+        // enforce fixed sizing on slides, slider and wrapper
+        var conf_static = function() {
+
+            $slides.css({
+                'height'    : settings.height,
+                'width'     : settings.width
+            });
+            $slider.css({
+                'height'    : settings.height,
+                'width'     : settings.width
+            });
+            $wrapper.css({
+                'height'    : settings.height,
+                'width'     : settings.width,
+                'position'  : 'relative'
+            });
+
+        };
+
+        var conf_slide = function() {
+
+            // create two extra elements which are clones of the first and last slides
+            $clone_first    = $slides.eq(0).clone();
+            $clone_last     = $slides.eq(state.slidecount-1).clone();
+
+            // add them to the DOM where we need them
+            $clone_first.attr({'data-clone' : 'last', 'data-slide' : 0}).appendTo($slider).show();
+            $clone_last.attr({'data-clone' : 'first', 'data-slide' : 0}).prependTo($slider).show();
+
+            // update the elements object
+            $slides             = $slider.children('li');
+            state.slidecount    = $slides.length;
+
+            // create a 'canvas' element which is neccessary for the slide animation to work
+            $canvas = $('<div class="bjqs-wrapper"></div>');
+
+            // if the slider is responsive && the calculated width is less than the max width
+            if(settings.responsive && (responsive.width < settings.width)){
+
+                $canvas.css({
+                    'width'     : responsive.width,
+                    'height'    : responsive.height,
+                    'overflow'  : 'hidden',
+                    'position'  : 'relative'
+                });
+
+                // update the dimensions to the slider to accomodate all the slides side by side
+                $slider.css({
+                    'width'     : responsive.width * (state.slidecount + 2),
+                    'left'      : -responsive.width * state.currentslide
+                });
+
+            }
+            else {
+
+                $canvas.css({
+                    'width'     : settings.width,
+                    'height'    : settings.height,
+                    'overflow'  : 'hidden',
+                    'position'  : 'relative'
+                });
+
+                // update the dimensions to the slider to accomodate all the slides side by side
+                $slider.css({
+                    'width'     : settings.width * (state.slidecount + 2),
+                    'left'      : -settings.width * state.currentslide
+                });
+
+            }
+
+            // add some inline styles which will align our slides for left-right sliding
+            $slides.css({
+                'float'         : 'left',
+                'position'      : 'relative',
+                'display'       : 'list-item'
+            });
+
+            // 'everything.. in it's right place'
+            $canvas.prependTo($wrapper);
+            $slider.appendTo($canvas);
+
+        };
+
+        var conf_controls = function() {
+
+            // create the elements for the controls
+            $c_wrapper  = $('<ul class="bjqs-controls"></ul>');
+            $c_fwd      = $('<li class="bjqs-next"><a href="#" data-direction="'+ vars.fwd +'">' + settings.nexttext + '</a></li>');
+            $c_prev     = $('<li class="bjqs-prev"><a href="#" data-direction="'+ vars.prev +'">' + settings.prevtext + '</a></li>');
+
+            // bind click events
+            $c_wrapper.on('click','a',function(e){
+
+                e.preventDefault();
+                var direction = $(this).attr('data-direction');
+
+                if(!state.animating){
+
+                    if(direction === vars.fwd){
+                        go(vars.fwd,false);
+                    }
+
+                    if(direction === vars.prev){
+                        go(vars.prev,false);
+                    }
+
+                }
+
+            });
+
+            // put 'em all together
+            $c_prev.appendTo($c_wrapper);
+            $c_fwd.appendTo($c_wrapper);
+            $c_wrapper.appendTo($wrapper);
+
+            // vertically center the controls
+            if (settings.centercontrols) {
+
+                $c_wrapper.addClass('v-centered');
+
+                // calculate offset % for vertical positioning
+                var offset_px   = ($wrapper.height() - $c_fwd.children('a').outerHeight()) / 2,
+                    ratio       = (offset_px / settings.height) * 100,
+                    offset      = ratio + '%';
+
+                $c_fwd.find('a').css('top', offset);
+                $c_prev.find('a').css('top', offset);
+
+            }
+
+        };
+
+        var conf_markers = function() {
+
+            // create a wrapper for our markers
+            $m_wrapper = $('<ol class="bjqs-markers"></ol>');
+
+            // for every slide, create a marker
+            $.each($slides, function(key, slide){
+
+                var slidenum    = key + 1,
+                    gotoslide   = key + 1;
+
+                if(settings.animtype === 'slide'){
+                    // + 2 to account for clones
+                    gotoslide = key + 2;
+                }
+
+                var marker = $('<li><a href="#">'+ slidenum +'</a></li>');
+
+                // set the first marker to be active
+                if(slidenum === state.currentslide){ marker.addClass('active-marker'); }
+
+                // bind the click event
+                marker.on('click','a',function(e){
+                    e.preventDefault();
+                    if(!state.animating && state.currentslide !== gotoslide){
+                        go(false,gotoslide);
+                    }
+                });
+
+                // add the marker to the wrapper
+                marker.appendTo($m_wrapper);
+
+            });
+
+            $m_wrapper.appendTo($wrapper);
+            $m_markers = $m_wrapper.find('li');
+
+            // center the markers
+            if (settings.centermarkers) {
+                $m_wrapper.addClass('h-centered');
+                var offset = (settings.width - $m_wrapper.width()) / 2;
+                $m_wrapper.css('left', offset);
+            }
+
+        };
+
+        var conf_keynav = function() {
+
+            $(document).keyup(function (event) {
+
+                if (!state.paused) {
+                    clearInterval(state.interval);
+                    state.paused = true;
+                }
+
+                if (!state.animating) {
+                    if (event.keyCode === 39) {
+                        event.preventDefault();
+                        go(vars.fwd, false);
+                    } else if (event.keyCode === 37) {
+                        event.preventDefault();
+                        go(vars.prev, false);
+                    }
+                }
+
+                if (state.paused && settings.automatic) {
+                    state.interval = setInterval(function () {
+                        go(vars.fwd);
+                    }, settings.animspeed);
+                    state.paused = false;
+                }
+
+            });
+
+        };
+
+        var conf_hoverpause = function() {
+
+            $wrapper.hover(function () {
+                if (!state.paused) {
+                    clearInterval(state.interval);
+                    state.paused = true;
+                }
+            }, function () {
+                if (state.paused) {
+                    state.interval = setInterval(function () {
+                        go(vars.fwd, false);
+                    }, settings.animspeed);
+                    state.paused = false;
+                }
+            });
+
+        };
+
+        var conf_captions = function() {
+
+            $.each($slides, function (key, slide) {
+
+                var caption = $(slide).children('img:first-child').attr('title');
+
+                // Account for images wrapped in links
+                if(!caption){
+                    caption = $(slide).children('a').find('img:first-child').attr('title');
+                }
+
+                if (caption) {
+                    caption = $('<p class="bjqs-caption">' + caption + '</p>');
+                    caption.appendTo($(slide));
+                }
+
+            });
+
+        };
+
+        var conf_random = function() {
+
+            var rand            = Math.floor(Math.random() * state.slidecount) + 1;
+            state.currentslide  = rand;
+            state.currentindex  = rand-1;
+
+        };
+
+        var set_next = function(direction) {
+
+            if(direction === vars.fwd){
+
+                if($slides.eq(state.currentindex).next().length){
+                    state.nextindex = state.currentindex + 1;
+                    state.nextslide = state.currentslide + 1;
+                }
+                else{
+                    state.nextindex = 0;
+                    state.nextslide = 1;
+                }
+
+            }
+            else{
+
+                if($slides.eq(state.currentindex).prev().length){
+                    state.nextindex = state.currentindex - 1;
+                    state.nextslide = state.currentslide - 1;
+                }
+                else{
+                    state.nextindex = state.slidecount - 1;
+                    state.nextslide = state.slidecount;
+                }
+
+            }
+
+        };
+
+        var go = function(direction, position) {
+
+            // only if we're not already doing things
+            if(!state.animating){
+
+                // doing things
+                state.animating = true;
+
+                if(position){
+                    state.nextslide = position;
+                    state.nextindex = position-1;
+                }
+                else{
+                    set_next(direction);
+                }
+
+                // fade animation
+                if(settings.animtype === 'fade'){
+
+                    if(settings.showmarkers){
+                        $m_markers.removeClass('active-marker');
+                        $m_markers.eq(state.nextindex).addClass('active-marker');
+                    }
+
+                    // fade out current
+                    $slides.eq(state.currentindex).fadeOut(settings.animduration);
+                    // fade in next
+                    $slides.eq(state.nextindex).fadeIn(settings.animduration, function(){
+
+                        // update state variables
+                        state.animating = false;
+                        state.currentslide = state.nextslide;
+                        state.currentindex = state.nextindex;
+
+                    });
+
+                }
+
+                // slide animation
+                if(settings.animtype === 'slide'){
+
+                    if(settings.showmarkers){
+
+                        var markerindex = state.nextindex-1;
+
+                        if(markerindex === state.slidecount-2){
+                            markerindex = 0;
+                        }
+                        else if(markerindex === -1){
+                            markerindex = state.slidecount-3;
+                        }
+
+                        $m_markers.removeClass('active-marker');
+                        $m_markers.eq(markerindex).addClass('active-marker');
+                    }
+
+                    // if the slider is responsive && the calculated width is less than the max width
+                    if(settings.responsive && ( responsive.width < settings.width ) ){
+                        state.slidewidth = responsive.width;
+                    }
+                    else{
+                        state.slidewidth = settings.width;
+                    }
+
+                    $slider.animate({'left': -state.nextindex * state.slidewidth }, settings.animduration, function(){
+
+                        state.currentslide = state.nextslide;
+                        state.currentindex = state.nextindex;
+
+                        // is the current slide a clone?
+                        if($slides.eq(state.currentindex).attr('data-clone') === 'last'){
+
+                            // affirmative, at the last slide (clone of first)
+                            $slider.css({'left': -state.slidewidth });
+                            state.currentslide = 2;
+                            state.currentindex = 1;
+
+                        }
+                        else if($slides.eq(state.currentindex).attr('data-clone') === 'first'){
+
+                            // affirmative, at the fist slide (clone of last)
+                            $slider.css({'left': -state.slidewidth *(state.slidecount - 2)});
+                            state.currentslide = state.slidecount - 1;
+                            state.currentindex = state.slidecount - 2;
+
+                        }
+
+                        state.animating = false;
+
+                    });
+
+                }
+
+            }
+
+        };
+
+        // lets get the party started :)
+        init();
+
+    };
+
+})(jQuery);
diff --git a/src/core/js/lib/jquery/jquery.color.js b/src/core/js/lib/jquery/jquery.color.js
new file mode 100644
index 0000000..d77c422
--- /dev/null
+++ b/src/core/js/lib/jquery/jquery.color.js
@@ -0,0 +1,2 @@
+/*! jQuery Color v at 2.1.2 http://github.com/jquery/jquery-color | jquery.org/license */
+(function(a,b){function m(a,b,c){var d=h[b.type]||{};return a==null?c||!b.def?null:b.def:(a=d.floor?~~a:parseFloat(a),isNaN(a)?b.def:d.mod?(a+d.mod)%d.mod:0>a?0:d.max<a?d.max:a)}function n(b){var c=f(),d=c._rgba=[];return b=b.toLowerCase(),l(e,function(a,e){var f,h=e.re.exec(b),i=h&&e.parse(h),j=e.space||"rgba";if(i)return f=c[j](i),c[g[j].cache]=f[g[j].cache],d=c._rgba=f._rgba,!1}),d.length?(d.join()==="0,0,0,0"&&a.extend(d,k.transparent),c):k[b]}function o(a,b,c){return c=(c+1)%1,c*6<1 [...]
\ No newline at end of file
diff --git a/src/core/js/lib/jquery/jquery.filterbydata.js b/src/core/js/lib/jquery/jquery.filterbydata.js
new file mode 100644
index 0000000..17ccf92
--- /dev/null
+++ b/src/core/js/lib/jquery/jquery.filterbydata.js
@@ -0,0 +1,13 @@
+(function($) {
+	$.fn.filterByData = function(prop, val) {
+		var $self = this
+		if (typeof val === 'undefined') {
+			return $self.filter(function() {
+				return typeof $(this).data(prop) !== 'undefined'
+			})
+		}
+		return $self.filter(function() {
+			return $(this).data(prop) == val
+		})
+	}
+})(window.jQuery)
\ No newline at end of file
diff --git a/src/core/js/lib/jquery/jquery.js b/src/core/js/lib/jquery/jquery.js
new file mode 100644
index 0000000..e5ace11
--- /dev/null
+++ b/src/core/js/lib/jquery/jquery.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r= [...]
+},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a); [...]
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$ [...]
diff --git a/src/core/js/lib/jquery/jquery.utip.js b/src/core/js/lib/jquery/jquery.utip.js
new file mode 100755
index 0000000..c2a6708
--- /dev/null
+++ b/src/core/js/lib/jquery/jquery.utip.js
@@ -0,0 +1,110 @@
+// uTip, unobtrusive tooltips for jQuery
+// Version 0.1.1
+// (c) Syme (git @ symeapp)
+// Released under the MIT license
+
+(function($) {
+
+  var Utip = {};
+
+  Utip.create = function(e){
+
+    var $this = $(this);
+
+    // Get data attributes
+    var content   = $this.attr('data-utip'),
+        gravity   = $this.attr('data-utip-gravity') || 'n', // North if not specified
+        hoverable = !!$this.attr('data-utip-hoverable');
+        style   = $(this).attr('data-utip-style') // Ability to add custom styling for a utip
+
+    // Remove previous utips if previous hoverTimer didn't finish
+    $('#utip').remove();
+
+    // Create utip element and add it to body
+    var $utip = $('<div id="utip" />').attr('data-gravity', gravity).html(content);
+    $('body').prepend($utip);
+
+    // Detect custom styling and apply it if it exists.
+    // Example of a valid data-utip-style: {color: '#0F0', width: '50px'}
+    if ( style ) {
+      try {
+        style = JSON.parse(style)
+        for (var s in style) {
+          $utip.css(s, style[s])
+        }
+      }
+      catch(err) {
+        console.log('utip:', 'could not parse custom style')
+      }
+    }
+
+    // Calculate dimensions
+    var elOffset        = $this.offset(),
+        elDimensions    = { width: $this.outerWidth(), height: $this.outerHeight() },
+        utipDimensions  = { width: $utip.outerWidth(), height: $utip.outerHeight() };
+
+    // Position tooltip according to gravity
+    var utipOffset = Utip.gravities(elOffset, elDimensions, utipDimensions)[gravity];
+    $utip.css(utipOffset);
+
+    if ( hoverable ) {
+
+      // Bind removal on mouseleave, but allow hovering on tooltip
+      var hoverTimer;
+      $this.add($utip).hover(function(){
+        if (hoverTimer) clearTimeout(hoverTimer);
+      }, function(){
+        hoverTimer = setTimeout(function(){ $utip.remove(); }, 100);
+      });
+
+    } else {
+
+      // Remove tooltip as soon as mouse leave
+      $this.one('mouseleave', function(){ $utip.remove(); });
+
+    }
+
+
+  };
+
+  // Returns gravities table given:
+  // 1. Element offset     (elO): { top:   int, left:   int }
+  // 2. Element dimensions (elD): { width: int, height: int }
+  // 3. Tooltip dimensions (utD): { width: int, height: int }
+  Utip.gravities = function(elO, elD, utD) {
+
+    var axes = {
+      h: {
+        l: elO.left - utD.width,
+        w: elO.left - utD.width + elD.width / 2,
+        c: elO.left - utD.width / 2 + elD.width / 2,
+        e: elO.left + elD.width / 2,
+        r: elO.left + elD.width
+      },
+
+      v: {
+        n: elO.top - utD.height,
+        c: elO.top - utD.height / 2 + elD.height / 2,
+        s: elO.top + elD.height
+      }
+    };
+
+    return {
+      nw: { top: axes.v.n, left: axes.h.w },
+      n:  { top: axes.v.n, left: axes.h.c },
+      ne: { top: axes.v.n, left: axes.h.e },
+      e:  { top: axes.v.c, left: axes.h.r },
+      se: { top: axes.v.s, left: axes.h.e },
+      s:  { top: axes.v.s, left: axes.h.c },
+      sw: { top: axes.v.s, left: axes.h.w },
+      w:  { top: axes.v.c, left: axes.h.l }
+    };
+
+  };
+
+  $.fn.utip = function() {
+    $(document).on('mouseenter', this.selector, Utip.create);
+    return this;
+  };
+
+})(jQuery);
\ No newline at end of file
diff --git a/src/core/js/lib/mousetrap.js b/src/core/js/lib/mousetrap.js
new file mode 100644
index 0000000..114a222
--- /dev/null
+++ b/src/core/js/lib/mousetrap.js
@@ -0,0 +1,9 @@
+/* mousetrap v1.4.6 craig.is/killing/mice */
+(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].leng [...]
+l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnV [...]
+b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}funct [...]
+c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{t [...]
+c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`" [...]
+"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c [...]
+unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level [...]
+b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
diff --git a/src/core/js/lib/mustache.js b/src/core/js/lib/mustache.js
new file mode 100644
index 0000000..a48baed
--- /dev/null
+++ b/src/core/js/lib/mustache.js
@@ -0,0 +1,570 @@
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+
+/*global define: false*/
+
+(function (root, factory) {
+  if (typeof exports === "object" && exports) {
+    factory(exports); // CommonJS
+  } else {
+    var mustache = {};
+    factory(mustache);
+    if (typeof define === "function" && define.amd) {
+      define(mustache); // AMD
+    } else {
+      root.Mustache = mustache; // <script>
+    }
+  }
+}(this, function (mustache) {
+
+  // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
+  // See https://github.com/janl/mustache.js/issues/189
+  var RegExp_test = RegExp.prototype.test;
+  function testRegExp(re, string) {
+    return RegExp_test.call(re, string);
+  }
+
+  var nonSpaceRe = /\S/;
+  function isWhitespace(string) {
+    return !testRegExp(nonSpaceRe, string);
+  }
+
+  var Object_toString = Object.prototype.toString;
+  var isArray = Array.isArray || function (object) {
+    return Object_toString.call(object) === '[object Array]';
+  };
+
+  function isFunction(object) {
+    return typeof object === 'function';
+  }
+
+  function escapeRegExp(string) {
+    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+  }
+
+  var entityMap = {
+    "&": "&",
+    "<": "<",
+    ">": ">",
+    '"': '"',
+    "'": ''',
+    "/": '&#x2F;'
+  };
+
+  function escapeHtml(string) {
+    return String(string).replace(/[&<>"'\/]/g, function (s) {
+      return entityMap[s];
+    });
+  }
+
+  function escapeTags(tags) {
+    if (!isArray(tags) || tags.length !== 2) {
+      throw new Error('Invalid tags: ' + tags);
+    }
+
+    return [
+      new RegExp(escapeRegExp(tags[0]) + "\\s*"),
+      new RegExp("\\s*" + escapeRegExp(tags[1]))
+    ];
+  }
+
+  var whiteRe = /\s*/;
+  var spaceRe = /\s+/;
+  var equalsRe = /\s*=/;
+  var curlyRe = /\s*\}/;
+  var tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+  /**
+   * Breaks up the given `template` string into a tree of tokens. If the `tags`
+   * argument is given here it must be an array with two string values: the
+   * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
+   * course, the default is to use mustaches (i.e. mustache.tags).
+   *
+   * A token is an array with at least 4 elements. The first element is the
+   * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
+   * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
+   * all text that appears outside a symbol this element is "text".
+   *
+   * The second element of a token is its "value". For mustache tags this is
+   * whatever else was inside the tag besides the opening symbol. For text tokens
+   * this is the text itself.
+   *
+   * The third and fourth elements of the token are the start and end indices,
+   * respectively, of the token in the original template.
+   *
+   * Tokens that are the root node of a subtree contain two more elements: 1) an
+   * array of tokens in the subtree and 2) the index in the original template at
+   * which the closing tag for that section begins.
+   */
+  function parseTemplate(template, tags) {
+    tags = tags || mustache.tags;
+    template = template || '';
+
+    if (typeof tags === 'string') {
+      tags = tags.split(spaceRe);
+    }
+
+    var tagRes = escapeTags(tags);
+    var scanner = new Scanner(template);
+
+    var sections = [];     // Stack to hold section tokens
+    var tokens = [];       // Buffer to hold the tokens
+    var spaces = [];       // Indices of whitespace tokens on the current line
+    var hasTag = false;    // Is there a {{tag}} on the current line?
+    var nonSpace = false;  // Is there a non-space char on the current line?
+
+    // Strips all whitespace tokens array for the current line
+    // if there was a {{#tag}} on it and otherwise only space.
+    function stripSpace() {
+      if (hasTag && !nonSpace) {
+        while (spaces.length) {
+          delete tokens[spaces.pop()];
+        }
+      } else {
+        spaces = [];
+      }
+
+      hasTag = false;
+      nonSpace = false;
+    }
+
+    var start, type, value, chr, token, openSection;
+    while (!scanner.eos()) {
+      start = scanner.pos;
+
+      // Match any text between tags.
+      value = scanner.scanUntil(tagRes[0]);
+      if (value) {
+        for (var i = 0, len = value.length; i < len; ++i) {
+          chr = value.charAt(i);
+
+          if (isWhitespace(chr)) {
+            spaces.push(tokens.length);
+          } else {
+            nonSpace = true;
+          }
+
+          tokens.push(['text', chr, start, start + 1]);
+          start += 1;
+
+          // Check for whitespace on the current line.
+          if (chr === '\n') {
+            stripSpace();
+          }
+        }
+      }
+
+      // Match the opening tag.
+      if (!scanner.scan(tagRes[0])) break;
+      hasTag = true;
+
+      // Get the tag type.
+      type = scanner.scan(tagRe) || 'name';
+      scanner.scan(whiteRe);
+
+      // Get the tag value.
+      if (type === '=') {
+        value = scanner.scanUntil(equalsRe);
+        scanner.scan(equalsRe);
+        scanner.scanUntil(tagRes[1]);
+      } else if (type === '{') {
+        value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1])));
+        scanner.scan(curlyRe);
+        scanner.scanUntil(tagRes[1]);
+        type = '&';
+      } else {
+        value = scanner.scanUntil(tagRes[1]);
+      }
+
+      // Match the closing tag.
+      if (!scanner.scan(tagRes[1])) {
+        throw new Error('Unclosed tag at ' + scanner.pos);
+      }
+
+      token = [ type, value, start, scanner.pos ];
+      tokens.push(token);
+
+      if (type === '#' || type === '^') {
+        sections.push(token);
+      } else if (type === '/') {
+        // Check section nesting.
+        openSection = sections.pop();
+
+        if (!openSection) {
+          throw new Error('Unopened section "' + value + '" at ' + start);
+        }
+        if (openSection[1] !== value) {
+          throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
+        }
+      } else if (type === 'name' || type === '{' || type === '&') {
+        nonSpace = true;
+      } else if (type === '=') {
+        // Set the tags for the next time around.
+        tagRes = escapeTags(tags = value.split(spaceRe));
+      }
+    }
+
+    // Make sure there are no open sections when we're done.
+    openSection = sections.pop();
+    if (openSection) {
+      throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
+    }
+
+    return nestTokens(squashTokens(tokens));
+  }
+
+  /**
+   * Combines the values of consecutive text tokens in the given `tokens` array
+   * to a single token.
+   */
+  function squashTokens(tokens) {
+    var squashedTokens = [];
+
+    var token, lastToken;
+    for (var i = 0, len = tokens.length; i < len; ++i) {
+      token = tokens[i];
+
+      if (token) {
+        if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
+          lastToken[1] += token[1];
+          lastToken[3] = token[3];
+        } else {
+          squashedTokens.push(token);
+          lastToken = token;
+        }
+      }
+    }
+
+    return squashedTokens;
+  }
+
+  /**
+   * Forms the given array of `tokens` into a nested tree structure where
+   * tokens that represent a section have two additional items: 1) an array of
+   * all tokens that appear in that section and 2) the index in the original
+   * template that represents the end of that section.
+   */
+  function nestTokens(tokens) {
+    var nestedTokens = [];
+    var collector = nestedTokens;
+    var sections = [];
+
+    var token, section;
+    for (var i = 0, len = tokens.length; i < len; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+      case '^':
+        collector.push(token);
+        sections.push(token);
+        collector = token[4] = [];
+        break;
+      case '/':
+        section = sections.pop();
+        section[5] = token[2];
+        collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
+        break;
+      default:
+        collector.push(token);
+      }
+    }
+
+    return nestedTokens;
+  }
+
+  /**
+   * A simple string scanner that is used by the template parser to find
+   * tokens in template strings.
+   */
+  function Scanner(string) {
+    this.string = string;
+    this.tail = string;
+    this.pos = 0;
+  }
+
+  /**
+   * Returns `true` if the tail is empty (end of string).
+   */
+  Scanner.prototype.eos = function () {
+    return this.tail === "";
+  };
+
+  /**
+   * Tries to match the given regular expression at the current position.
+   * Returns the matched text if it can match, the empty string otherwise.
+   */
+  Scanner.prototype.scan = function (re) {
+    var match = this.tail.match(re);
+
+    if (match && match.index === 0) {
+      var string = match[0];
+      this.tail = this.tail.substring(string.length);
+      this.pos += string.length;
+      return string;
+    }
+
+    return "";
+  };
+
+  /**
+   * Skips all text until the given regular expression can be matched. Returns
+   * the skipped string, which is the entire tail if no match can be made.
+   */
+  Scanner.prototype.scanUntil = function (re) {
+    var index = this.tail.search(re), match;
+
+    switch (index) {
+    case -1:
+      match = this.tail;
+      this.tail = "";
+      break;
+    case 0:
+      match = "";
+      break;
+    default:
+      match = this.tail.substring(0, index);
+      this.tail = this.tail.substring(index);
+    }
+
+    this.pos += match.length;
+
+    return match;
+  };
+
+  /**
+   * Represents a rendering context by wrapping a view object and
+   * maintaining a reference to the parent context.
+   */
+  function Context(view, parentContext) {
+    this.view = view == null ? {} : view;
+    this.cache = { '.': this.view };
+    this.parent = parentContext;
+  }
+
+  /**
+   * Creates a new context using the given view with this context
+   * as the parent.
+   */
+  Context.prototype.push = function (view) {
+    return new Context(view, this);
+  };
+
+  /**
+   * Returns the value of the given name in this context, traversing
+   * up the context hierarchy if the value is absent in this context's view.
+   */
+  Context.prototype.lookup = function (name) {
+    var value;
+    if (name in this.cache) {
+      value = this.cache[name];
+    } else {
+      var context = this;
+
+      while (context) {
+        if (name.indexOf('.') > 0) {
+          value = context.view;
+
+          var names = name.split('.'), i = 0;
+          while (value != null && i < names.length) {
+            value = value[names[i++]];
+          }
+        } else {
+          value = context.view[name];
+        }
+
+        if (value != null) break;
+
+        context = context.parent;
+      }
+
+      this.cache[name] = value;
+    }
+
+    if (isFunction(value)) {
+      value = value.call(this.view);
+    }
+
+    return value;
+  };
+
+  /**
+   * A Writer knows how to take a stream of tokens and render them to a
+   * string, given a context. It also maintains a cache of templates to
+   * avoid the need to parse the same template twice.
+   */
+  function Writer() {
+    this.cache = {};
+  }
+
+  /**
+   * Clears all cached templates in this writer.
+   */
+  Writer.prototype.clearCache = function () {
+    this.cache = {};
+  };
+
+  /**
+   * Parses and caches the given `template` and returns the array of tokens
+   * that is generated from the parse.
+   */
+  Writer.prototype.parse = function (template, tags) {
+    var cache = this.cache;
+    var tokens = cache[template];
+
+    if (tokens == null) {
+      tokens = cache[template] = parseTemplate(template, tags);
+    }
+
+    return tokens;
+  };
+
+  /**
+   * High-level method that is used to render the given `template` with
+   * the given `view`.
+   *
+   * The optional `partials` argument may be an object that contains the
+   * names and templates of partials that are used in the template. It may
+   * also be a function that is used to load partial templates on the fly
+   * that takes a single argument: the name of the partial.
+   */
+  Writer.prototype.render = function (template, view, partials) {
+    var tokens = this.parse(template);
+    var context = (view instanceof Context) ? view : new Context(view);
+    return this.renderTokens(tokens, context, partials, template);
+  };
+
+  /**
+   * Low-level method that renders the given array of `tokens` using
+   * the given `context` and `partials`.
+   *
+   * Note: The `originalTemplate` is only ever used to extract the portion
+   * of the original template that was contained in a higher-order section.
+   * If the template doesn't use higher-order sections, this argument may
+   * be omitted.
+   */
+  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+    var buffer = '';
+
+    // This function is used to render an arbitrary template
+    // in the current context by higher-order sections.
+    var self = this;
+    function subRender(template) {
+      return self.render(template, context, partials);
+    }
+
+    var token, value;
+    for (var i = 0, len = tokens.length; i < len; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+        value = context.lookup(token[1]);
+        if (!value) continue;
+
+        if (isArray(value)) {
+          for (var j = 0, jlen = value.length; j < jlen; ++j) {
+            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+          }
+        } else if (typeof value === 'object' || typeof value === 'string') {
+          buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+        } else if (isFunction(value)) {
+          if (typeof originalTemplate !== 'string') {
+            throw new Error('Cannot use higher-order sections without the original template');
+          }
+
+          // Extract the portion of the original template that the section contains.
+          value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+
+          if (value != null) buffer += value;
+        } else {
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+        }
+
+        break;
+      case '^':
+        value = context.lookup(token[1]);
+
+        // Use JavaScript's definition of falsy. Include empty arrays.
+        // See https://github.com/janl/mustache.js/issues/186
+        if (!value || (isArray(value) && value.length === 0)) {
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+        }
+
+        break;
+      case '>':
+        if (!partials) continue;
+        value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+        if (value != null) buffer += this.renderTokens(this.parse(value), context, partials, value);
+        break;
+      case '&':
+        value = context.lookup(token[1]);
+        if (value != null) buffer += value;
+        break;
+      case 'name':
+        value = context.lookup(token[1]);
+        if (value != null) buffer += mustache.escape(value);
+        break;
+      case 'text':
+        buffer += token[1];
+        break;
+      }
+    }
+
+    return buffer;
+  };
+
+  mustache.name = "mustache.js";
+  mustache.version = "0.8.1";
+  mustache.tags = [ "{{", "}}" ];
+
+  // All high-level mustache.* functions use this writer.
+  var defaultWriter = new Writer();
+
+  /**
+   * Clears all cached templates in the default writer.
+   */
+  mustache.clearCache = function () {
+    return defaultWriter.clearCache();
+  };
+
+  /**
+   * Parses and caches the given template in the default writer and returns the
+   * array of tokens it contains. Doing this ahead of time avoids the need to
+   * parse templates on the fly as they are rendered.
+   */
+  mustache.parse = function (template, tags) {
+    return defaultWriter.parse(template, tags);
+  };
+
+  /**
+   * Renders the `template` with the given `view` and `partials` using the
+   * default writer.
+   */
+  mustache.render = function (template, view, partials) {
+    return defaultWriter.render(template, view, partials);
+  };
+
+  // This is here for backwards compatibility with 0.4.x.
+  mustache.to_html = function (template, view, partials, send) {
+    var result = mustache.render(template, view, partials);
+
+    if (isFunction(send)) {
+      send(result);
+    } else {
+      return result;
+    }
+  };
+
+  // Export the escaping function so that the user may override it.
+  // See https://github.com/janl/mustache.js/issues/244
+  mustache.escape = escapeHtml;
+
+  // Export these mainly for testing, but also for advanced usage.
+  mustache.Scanner = Scanner;
+  mustache.Context = Context;
+  mustache.Writer = Writer;
+
+}));
\ No newline at end of file
diff --git a/src/core/js/lib/otr.js b/src/core/js/lib/otr.js
new file mode 100644
index 0000000..3aa32c6
--- /dev/null
+++ b/src/core/js/lib/otr.js
@@ -0,0 +1,2601 @@
+/*!
+
+  otr.js v0.2.12 - 2014-04-15
+  (c) 2014 - Arlo Breault <arlolra at gmail.com>
+  Freely distributed under the MPL v2.0 license.
+
+  This file is concatenated for the browser.
+  Please see: https://github.com/arlolra/otr
+
+*/
+
+;(function (root, factory) {
+
+  if (typeof define === 'function' && define.amd) {
+    define([
+        "bigint"
+      , "crypto"
+      , "eventemitter"
+    ], function (BigInt, CryptoJS, EventEmitter) {
+      var root = {
+          BigInt: BigInt
+        , CryptoJS: CryptoJS
+        , EventEmitter: EventEmitter
+        , OTR: {}
+        , DSA: {}
+      }
+      return factory.call(root)
+    })
+  } else {
+    root.OTR = {}
+    root.DSA = {}
+    factory.call(root)
+  }
+
+}(this, function () {
+
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var CONST = {
+
+    // diffie-heilman
+      N : 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF'
+    , G : '2'
+
+    // otr message states
+    , MSGSTATE_PLAINTEXT : 0
+    , MSGSTATE_ENCRYPTED : 1
+    , MSGSTATE_FINISHED  : 2
+
+    // otr auth states
+    , AUTHSTATE_NONE               : 0
+    , AUTHSTATE_AWAITING_DHKEY     : 1
+    , AUTHSTATE_AWAITING_REVEALSIG : 2
+    , AUTHSTATE_AWAITING_SIG       : 3
+
+    // whitespace tags
+    , WHITESPACE_TAG    : '\x20\x09\x20\x20\x09\x09\x09\x09\x20\x09\x20\x09\x20\x09\x20\x20'
+    , WHITESPACE_TAG_V2 : '\x20\x20\x09\x09\x20\x20\x09\x20'
+    , WHITESPACE_TAG_V3 : '\x20\x20\x09\x09\x20\x20\x09\x09'
+
+    // otr tags
+    , OTR_TAG       : '?OTR'
+    , OTR_VERSION_1 : '\x00\x01'
+    , OTR_VERSION_2 : '\x00\x02'
+    , OTR_VERSION_3 : '\x00\x03'
+
+    // smp machine states
+    , SMPSTATE_EXPECT0 : 0
+    , SMPSTATE_EXPECT1 : 1
+    , SMPSTATE_EXPECT2 : 2
+    , SMPSTATE_EXPECT3 : 3
+    , SMPSTATE_EXPECT4 : 4
+
+    // unstandard status codes
+    , STATUS_SEND_QUERY  : 0
+    , STATUS_AKE_INIT    : 1
+    , STATUS_AKE_SUCCESS : 2
+    , STATUS_END_OTR     : 3
+
+  }
+
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = CONST
+  } else {
+    root.OTR.CONST = CONST
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var HLP = {}, CryptoJS, BigInt
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = HLP = {}
+    CryptoJS = require('../vendor/crypto.js')
+    BigInt = require('../vendor/bigint.js')
+  } else {
+    if (root.OTR) root.OTR.HLP = HLP
+    if (root.DSA) root.DSA.HLP = HLP
+    CryptoJS = root.CryptoJS
+    BigInt = root.BigInt
+  }
+
+  // data types (byte lengths)
+  var DTS = {
+      BYTE  : 1
+    , SHORT : 2
+    , INT   : 4
+    , CTR   : 8
+    , MAC   : 20
+    , SIG   : 40
+  }
+
+  // otr message wrapper begin and end
+  var WRAPPER_BEGIN = "?OTR"
+    , WRAPPER_END   = "."
+
+  var TWO = BigInt.str2bigInt('2', 10)
+
+  HLP.debug = function (msg) {
+    // used as HLP.debug.call(ctx, msg)
+    if ( this.debug &&
+         typeof this.debug !== 'function' &&
+         typeof console !== 'undefined'
+    ) console.log(msg)
+  }
+
+  HLP.extend = function (child, parent) {
+    for (var key in parent) {
+      if (Object.hasOwnProperty.call(parent, key))
+        child[key] = parent[key]
+    }
+    function Ctor() { this.constructor = child }
+    Ctor.prototype = parent.prototype
+    child.prototype = new Ctor()
+    child.__super__ = parent.prototype
+  }
+
+  // constant-time string comparison
+  HLP.compare = function (str1, str2) {
+    if (str1.length !== str2.length)
+      return false
+    var i = 0, result = 0
+    for (; i < str1.length; i++)
+      result |= str1[i].charCodeAt(0) ^ str2[i].charCodeAt(0)
+    return result === 0
+  }
+
+  HLP.randomExponent = function () {
+    return BigInt.randBigInt(1536)
+  }
+
+  HLP.smpHash = function (version, fmpi, smpi) {
+    var sha256 = CryptoJS.algo.SHA256.create()
+    sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(version, DTS.BYTE)))
+    sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(fmpi)))
+    if (smpi) sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(smpi)))
+    var hash = sha256.finalize()
+    return HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))
+  }
+
+  HLP.makeMac = function (aesctr, m) {
+    var pass = CryptoJS.enc.Latin1.parse(m)
+    var mac = CryptoJS.HmacSHA256(CryptoJS.enc.Latin1.parse(aesctr), pass)
+    return HLP.mask(mac.toString(CryptoJS.enc.Latin1), 0, 160)
+  }
+
+  HLP.make1Mac = function (aesctr, m) {
+    var pass = CryptoJS.enc.Latin1.parse(m)
+    var mac = CryptoJS.HmacSHA1(CryptoJS.enc.Latin1.parse(aesctr), pass)
+    return mac.toString(CryptoJS.enc.Latin1)
+  }
+
+  HLP.encryptAes = function (msg, c, iv) {
+    var opts = {
+        mode: CryptoJS.mode.CTR
+      , iv: CryptoJS.enc.Latin1.parse(iv)
+      , padding: CryptoJS.pad.NoPadding
+    }
+    var aesctr = CryptoJS.AES.encrypt(
+        msg
+      , CryptoJS.enc.Latin1.parse(c)
+      , opts
+    )
+    var aesctr_decoded = CryptoJS.enc.Base64.parse(aesctr.toString())
+    return CryptoJS.enc.Latin1.stringify(aesctr_decoded)
+  }
+
+  HLP.decryptAes = function (msg, c, iv) {
+    msg = CryptoJS.enc.Latin1.parse(msg)
+    var opts = {
+        mode: CryptoJS.mode.CTR
+      , iv: CryptoJS.enc.Latin1.parse(iv)
+      , padding: CryptoJS.pad.NoPadding
+    }
+    return CryptoJS.AES.decrypt(
+        CryptoJS.enc.Base64.stringify(msg)
+      , CryptoJS.enc.Latin1.parse(c)
+      , opts
+    )
+  }
+
+  HLP.multPowMod = function (a, b, c, d, e) {
+    return BigInt.multMod(BigInt.powMod(a, b, e), BigInt.powMod(c, d, e), e)
+  }
+
+  HLP.ZKP = function (v, c, d, e) {
+    return BigInt.equals(c, HLP.smpHash(v, d, e))
+  }
+
+  // greater than, or equal
+  HLP.GTOE = function (a, b) {
+    return (BigInt.equals(a, b) || BigInt.greater(a, b))
+  }
+
+  HLP.between = function (x, a, b) {
+    return (BigInt.greater(x, a) && BigInt.greater(b, x))
+  }
+
+  HLP.checkGroup = function (g, N_MINUS_2) {
+    return HLP.GTOE(g, TWO) && HLP.GTOE(N_MINUS_2, g)
+  }
+
+  HLP.h1 = function (b, secbytes) {
+    var sha1 = CryptoJS.algo.SHA1.create()
+    sha1.update(CryptoJS.enc.Latin1.parse(b))
+    sha1.update(CryptoJS.enc.Latin1.parse(secbytes))
+    return (sha1.finalize()).toString(CryptoJS.enc.Latin1)
+  }
+
+  HLP.h2 = function (b, secbytes) {
+    var sha256 = CryptoJS.algo.SHA256.create()
+    sha256.update(CryptoJS.enc.Latin1.parse(b))
+    sha256.update(CryptoJS.enc.Latin1.parse(secbytes))
+    return (sha256.finalize()).toString(CryptoJS.enc.Latin1)
+  }
+
+  HLP.mask = function (bytes, start, n) {
+    return bytes.substr(start / 8, n / 8)
+  }
+
+  var _toString = String.fromCharCode;
+  HLP.packBytes = function (val, bytes) {
+    val = val.toString(16)
+    var nex, res = ''  // big-endian, unsigned long
+    for (; bytes > 0; bytes--) {
+      nex = val.length ? val.substr(-2, 2) : '0'
+      val = val.substr(0, val.length - 2)
+      res = _toString(parseInt(nex, 16)) + res
+    }
+    return res
+  }
+
+  HLP.packINT = function (d) {
+    return HLP.packBytes(d, DTS.INT)
+  }
+
+  HLP.packCtr = function (d) {
+    return HLP.padCtr(HLP.packBytes(d, DTS.CTR))
+  }
+
+  HLP.padCtr = function (ctr) {
+    return ctr + '\x00\x00\x00\x00\x00\x00\x00\x00'
+  }
+
+  HLP.unpackCtr = function (d) {
+    d = HLP.toByteArray(d.substring(0, 8))
+    return HLP.unpack(d)
+  }
+
+  HLP.unpack = function (arr) {
+    var val = 0, i = 0, len = arr.length
+    for (; i < len; i++) {
+      val = (val * 256) + arr[i]
+    }
+    return val
+  }
+
+  HLP.packData = function (d) {
+    return HLP.packINT(d.length) + d
+  }
+
+  HLP.bits2bigInt = function (bits) {
+    bits = HLP.toByteArray(bits)
+    return BigInt.ba2bigInt(bits)
+  }
+
+  HLP.packMPI = function (mpi) {
+    return HLP.packData(BigInt.bigInt2bits(BigInt.trim(mpi, 0)))
+  }
+
+  HLP.packSHORT = function (short) {
+    return HLP.packBytes(short, DTS.SHORT)
+  }
+
+  HLP.unpackSHORT = function (short) {
+    short = HLP.toByteArray(short)
+    return HLP.unpack(short)
+  }
+
+  HLP.packTLV = function (type, value) {
+    return HLP.packSHORT(type) + HLP.packSHORT(value.length) + value
+  }
+
+  HLP.readLen = function (msg) {
+    msg = HLP.toByteArray(msg.substring(0, 4))
+    return HLP.unpack(msg)
+  }
+
+  HLP.readData = function (data) {
+    var n = HLP.unpack(data.splice(0, 4))
+    return [n, data]
+  }
+
+  HLP.readMPI = function (data) {
+    data = HLP.toByteArray(data)
+    data = HLP.readData(data)
+    return BigInt.ba2bigInt(data[1])
+  }
+
+  HLP.packMPIs = function (arr) {
+    return arr.reduce(function (prv, cur) {
+      return prv + HLP.packMPI(cur)
+    }, '')
+  }
+
+  HLP.unpackMPIs = function (num, mpis) {
+    var i = 0, arr = []
+    for (; i < num; i++) arr.push('MPI')
+    return (HLP.splitype(arr, mpis)).map(function (m) {
+      return HLP.readMPI(m)
+    })
+  }
+
+  HLP.wrapMsg = function (msg, fs, v3, our_it, their_it) {
+    msg = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(msg))
+    msg = WRAPPER_BEGIN + ":" + msg + WRAPPER_END
+
+    var its
+    if (v3) {
+      its = '|'
+      its += (HLP.readLen(our_it)).toString(16)
+      its += '|'
+      its += (HLP.readLen(their_it)).toString(16)
+    }
+
+    if (!fs) return [null, msg]
+
+    var n = Math.ceil(msg.length / fs)
+    if (n > 65535) return ['Too many fragments']
+    if (n == 1) return [null, msg]
+
+    var k, bi, ei, frag, mf, mfs = []
+    for (k = 1; k <= n; k++) {
+      bi = (k - 1) * fs
+      ei = k * fs
+      frag = msg.slice(bi, ei)
+      mf = WRAPPER_BEGIN
+      if (v3) mf += its
+      mf += ',' + k + ','
+      mf += n + ','
+      mf += frag + ','
+      mfs.push(mf)
+    }
+
+    return [null, mfs]
+  }
+
+  HLP.splitype = function splitype(arr, msg) {
+    var data = []
+    arr.forEach(function (a) {
+      var str
+      switch (a) {
+        case 'PUBKEY':
+          str = splitype(['SHORT', 'MPI', 'MPI', 'MPI', 'MPI'], msg).join('')
+          break
+        case 'DATA':  // falls through
+        case 'MPI':
+          str = msg.substring(0, HLP.readLen(msg) + 4)
+          break
+        default:
+          str = msg.substring(0, DTS[a])
+      }
+      data.push(str)
+      msg = msg.substring(str.length)
+    })
+    return data
+  }
+
+  // https://github.com/msgpack/msgpack-javascript/blob/master/msgpack.js
+
+  var _bin2num = (function () {
+    var i = 0, _bin2num = {}
+    for (; i < 0x100; ++i) {
+      _bin2num[String.fromCharCode(i)] = i  // "\00" -> 0x00
+    }
+    for (i = 0x80; i < 0x100; ++i) {  // [Webkit][Gecko]
+      _bin2num[String.fromCharCode(0xf700 + i)] = i  // "\f780" -> 0x80
+    }
+    return _bin2num
+  }())
+
+  HLP.toByteArray = function (data) {
+    var rv = []
+      , ary = data.split("")
+      , i = -1
+      , iz = ary.length
+      , remain = iz % 8
+
+    while (remain--) {
+      ++i
+      rv[i] = _bin2num[ary[i]]
+    }
+    remain = iz >> 3
+    while (remain--) {
+      rv.push(_bin2num[ary[++i]], _bin2num[ary[++i]],
+              _bin2num[ary[++i]], _bin2num[ary[++i]],
+              _bin2num[ary[++i]], _bin2num[ary[++i]],
+              _bin2num[ary[++i]], _bin2num[ary[++i]])
+    }
+    return rv
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var CryptoJS, BigInt, Worker, WWPath, HLP
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = DSA
+    CryptoJS = require('../vendor/crypto.js')
+    BigInt = require('../vendor/bigint.js')
+    WWPath = require('path').join(__dirname, '/dsa-webworker.js')
+    HLP = require('./helpers.js')
+  } else {
+    // copy over and expose internals
+    Object.keys(root.DSA).forEach(function (k) {
+      DSA[k] = root.DSA[k]
+    })
+    root.DSA = DSA
+    CryptoJS = root.CryptoJS
+    BigInt = root.BigInt
+    Worker = root.Worker
+    WWPath = 'dsa-webworker.js'
+    HLP = DSA.HLP
+  }
+
+  var ZERO = BigInt.str2bigInt('0', 10)
+    , ONE = BigInt.str2bigInt('1', 10)
+    , TWO = BigInt.str2bigInt('2', 10)
+    , KEY_TYPE = '\x00\x00'
+
+  var DEBUG = false
+  function timer() {
+    var start = (new Date()).getTime()
+    return function (s) {
+      if (!DEBUG || typeof console === 'undefined') return
+      var t = (new Date()).getTime()
+      console.log(s + ': ' + (t - start))
+      start = t
+    }
+  }
+
+  function makeRandom(min, max) {
+    var c = BigInt.randBigInt(BigInt.bitSize(max))
+    if (!HLP.between(c, min, max)) return makeRandom(min, max)
+    return c
+  }
+
+  // altered BigInt.randProbPrime()
+  // n rounds of Miller Rabin (after trial division with small primes)
+  var rpprb = []
+  function isProbPrime(k, n) {
+    var i, B = 30000, l = BigInt.bitSize(k)
+    var primes = BigInt.primes
+
+    if (primes.length === 0)
+      primes = BigInt.findPrimes(B)
+
+    if (rpprb.length != k.length)
+      rpprb = BigInt.dup(k)
+
+    // check ans for divisibility by small primes up to B
+    for (i = 0; (i < primes.length) && (primes[i] <= B); i++)
+      if (BigInt.modInt(k, primes[i]) === 0 && !BigInt.equalsInt(k, primes[i]))
+        return 0
+
+    // do n rounds of Miller Rabin, with random bases less than k
+    for (i = 0; i < n; i++) {
+      BigInt.randBigInt_(rpprb, l, 0)
+      while(!BigInt.greater(k, rpprb))  // pick a random rpprb that's < k
+        BigInt.randBigInt_(rpprb, l, 0)
+      if (!BigInt.millerRabin(k, rpprb))
+        return 0
+    }
+
+    return 1
+  }
+
+  var bit_lengths = {
+      '1024': { N: 160, repeat: 40 }  // 40x should give 2^-80 confidence
+    , '2048': { N: 224, repeat: 56 }
+  }
+
+  var primes = {}
+
+  // follows go lang http://golang.org/src/pkg/crypto/dsa/dsa.go
+  // fips version was removed in 0c99af0df3e7
+  function generatePrimes(bit_length) {
+
+    var t = timer()  // for debugging
+
+    // number of MR tests to perform
+    var repeat = bit_lengths[bit_length].repeat
+
+    var N = bit_lengths[bit_length].N
+
+    var LM1 = BigInt.twoToThe(bit_length - 1)
+    var bl4 = 4 * bit_length
+    var brk = false
+
+    var q, p, rem, counter
+    for (;;) {
+
+      q = BigInt.randBigInt(N, 1)
+      q[0] |= 1
+
+      if (!isProbPrime(q, repeat)) continue
+      t('q')
+
+      for (counter = 0; counter < bl4; counter++) {
+        p = BigInt.randBigInt(bit_length, 1)
+        p[0] |= 1
+
+        rem = BigInt.mod(p, q)
+        rem = BigInt.sub(rem, ONE)
+        p = BigInt.sub(p, rem)
+
+        if (BigInt.greater(LM1, p)) continue
+        if (!isProbPrime(p, repeat)) continue
+
+        t('p')
+        primes[bit_length] = { p: p, q: q }
+        brk = true
+        break
+      }
+
+      if (brk) break
+    }
+
+    var h = BigInt.dup(TWO)
+    var pm1 = BigInt.sub(p, ONE)
+    var e = BigInt.multMod(pm1, BigInt.inverseMod(q, p), p)
+
+    var g
+    for (;;) {
+      g = BigInt.powMod(h, e, p)
+      if (BigInt.equals(g, ONE)) {
+        h = BigInt.add(h, ONE)
+        continue
+      }
+      primes[bit_length].g = g
+      t('g')
+      return
+    }
+
+    throw new Error('Unreachable!')
+  }
+
+  function DSA(obj, opts) {
+    if (!(this instanceof DSA)) return new DSA(obj, opts)
+
+    // options
+    opts = opts || {}
+
+    // inherit
+    if (obj) {
+      var self = this
+      ;['p', 'q', 'g', 'y', 'x'].forEach(function (prop) {
+        self[prop] = obj[prop]
+      })
+      this.type = obj.type || KEY_TYPE
+      return
+    }
+
+    // default to 1024
+    var bit_length = parseInt(opts.bit_length ? opts.bit_length : 1024, 10)
+
+    if (!bit_lengths[bit_length])
+      throw new Error('Unsupported bit length.')
+
+    // set primes
+    if (!primes[bit_length])
+      generatePrimes(bit_length)
+
+    this.p = primes[bit_length].p
+    this.q = primes[bit_length].q
+    this.g = primes[bit_length].g
+
+    // key type
+    this.type = KEY_TYPE
+
+    // private key
+    this.x = makeRandom(ZERO, this.q)
+
+    // public keys (p, q, g, y)
+    this.y = BigInt.powMod(this.g, this.x, this.p)
+
+    // nocache?
+    if (opts.nocache) primes[bit_length] = null
+  }
+
+  DSA.prototype = {
+
+    constructor: DSA,
+
+    packPublic: function () {
+      var str = this.type
+      str += HLP.packMPI(this.p)
+      str += HLP.packMPI(this.q)
+      str += HLP.packMPI(this.g)
+      str += HLP.packMPI(this.y)
+      return str
+    },
+
+    packPrivate: function () {
+      var str = this.packPublic() + HLP.packMPI(this.x)
+      str = CryptoJS.enc.Latin1.parse(str)
+      return str.toString(CryptoJS.enc.Base64)
+    },
+
+    // http://www.imperialviolet.org/2013/06/15/suddendeathentropy.html
+    generateNonce: function (m) {
+      var priv = BigInt.bigInt2bits(BigInt.trim(this.x, 0))
+      var rand = BigInt.bigInt2bits(BigInt.randBigInt(256))
+
+      var sha256 = CryptoJS.algo.SHA256.create()
+      sha256.update(CryptoJS.enc.Latin1.parse(priv))
+      sha256.update(m)
+      sha256.update(CryptoJS.enc.Latin1.parse(rand))
+
+      var hash = sha256.finalize()
+      hash = HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))
+      BigInt.rightShift_(hash, 256 - BigInt.bitSize(this.q))
+
+      return HLP.between(hash, ZERO, this.q) ? hash : this.generateNonce(m)
+    },
+
+    sign: function (m) {
+      m = CryptoJS.enc.Latin1.parse(m)
+      var b = BigInt.str2bigInt(m.toString(CryptoJS.enc.Hex), 16)
+      var k, r = ZERO, s = ZERO
+      while (BigInt.isZero(s) || BigInt.isZero(r)) {
+        k = this.generateNonce(m)
+        r = BigInt.mod(BigInt.powMod(this.g, k, this.p), this.q)
+        if (BigInt.isZero(r)) continue
+        s = BigInt.inverseMod(k, this.q)
+        s = BigInt.mult(s, BigInt.add(b, BigInt.mult(this.x, r)))
+        s = BigInt.mod(s, this.q)
+      }
+      return [r, s]
+    },
+
+    fingerprint: function () {
+      var pk = this.packPublic()
+      if (this.type === KEY_TYPE) pk = pk.substring(2)
+      pk = CryptoJS.enc.Latin1.parse(pk)
+      return CryptoJS.SHA1(pk).toString(CryptoJS.enc.Hex)
+    }
+
+  }
+
+  DSA.parsePublic = function (str, priv) {
+    var fields = ['SHORT', 'MPI', 'MPI', 'MPI', 'MPI']
+    if (priv) fields.push('MPI')
+    str = HLP.splitype(fields, str)
+    var obj = {
+        type: str[0]
+      , p: HLP.readMPI(str[1])
+      , q: HLP.readMPI(str[2])
+      , g: HLP.readMPI(str[3])
+      , y: HLP.readMPI(str[4])
+    }
+    if (priv) obj.x = HLP.readMPI(str[5])
+    return new DSA(obj)
+  }
+
+  function tokenizeStr(str) {
+    var start, end
+
+    start = str.indexOf("(")
+    end = str.lastIndexOf(")")
+
+    if (start < 0 || end < 0)
+      throw new Error("Malformed S-Expression")
+
+    str = str.substring(start + 1, end)
+
+    var splt = str.search(/\s/)
+    var obj = {
+        type: str.substring(0, splt)
+      , val: []
+    }
+
+    str = str.substring(splt + 1, end)
+    start = str.indexOf("(")
+
+    if (start < 0) obj.val.push(str)
+    else {
+
+      var i, len, ss, es
+      while (start > -1) {
+        i = start + 1
+        len = str.length
+        for (ss = 1, es = 0; i < len && es < ss; i++) {
+          if (str[i] === "(") ss++
+          if (str[i] === ")") es++
+        }
+        obj.val.push(tokenizeStr(str.substring(start, ++i)))
+        str = str.substring(++i)
+        start = str.indexOf("(")
+      }
+
+    }
+    return obj
+  }
+
+  function parseLibotr(obj) {
+    if (!obj.type) throw new Error("Parse error.")
+
+    var o, val
+    if (obj.type === "privkeys") {
+      o = []
+      obj.val.forEach(function (i) {
+        o.push(parseLibotr(i))
+      })
+      return o
+    }
+
+    o = {}
+    obj.val.forEach(function (i) {
+
+      val = i.val[0]
+      if (typeof val === "string") {
+
+        if (val.indexOf("#") === 0) {
+          val = val.substring(1, val.lastIndexOf("#"))
+          val = BigInt.str2bigInt(val, 16)
+        }
+
+      } else {
+        val = parseLibotr(i)
+      }
+
+      o[i.type] = val
+    })
+
+    return o
+  }
+
+  DSA.parsePrivate = function (str, libotr) {
+    if (!libotr) {
+      str = CryptoJS.enc.Base64.parse(str)
+      str = str.toString(CryptoJS.enc.Latin1)
+      return DSA.parsePublic(str, true)
+    }
+    // only returning the first key found
+    return parseLibotr(tokenizeStr(str))[0]["private-key"].dsa
+  }
+
+  DSA.verify = function (key, m, r, s) {
+    if (!HLP.between(r, ZERO, key.q) || !HLP.between(s, ZERO, key.q))
+      return false
+
+    var hm = CryptoJS.enc.Latin1.parse(m)  // CryptoJS.SHA1(m)
+    hm = BigInt.str2bigInt(hm.toString(CryptoJS.enc.Hex), 16)
+
+    var w = BigInt.inverseMod(s, key.q)
+    var u1 = BigInt.multMod(hm, w, key.q)
+    var u2 = BigInt.multMod(r, w, key.q)
+
+    u1 = BigInt.powMod(key.g, u1, key.p)
+    u2 = BigInt.powMod(key.y, u2, key.p)
+
+    var v = BigInt.mod(BigInt.multMod(u1, u2, key.p), key.q)
+
+    return BigInt.equals(v, r)
+  }
+
+  DSA.createInWebWorker = function (options, cb) {
+    var opts = {
+        path: WWPath
+      , seed: BigInt.getSeed
+    }
+    if (options && typeof options === 'object')
+      Object.keys(options).forEach(function (k) {
+        opts[k] = options[k]
+      })
+
+    // load optional dep. in node
+    if (typeof module !== 'undefined' && module.exports)
+      Worker = require('webworker-threads').Worker
+
+    var worker = new Worker(opts.path)
+    worker.onmessage = function (e) {
+      var data = e.data
+      switch (data.type) {
+        case "debug":
+          if (!DEBUG || typeof console === 'undefined') return
+          console.log(data.val)
+          break;
+        case "data":
+          worker.terminate()
+          cb(DSA.parsePrivate(data.val))
+          break;
+        default:
+          throw new Error("Unrecognized type.")
+      }
+    }
+    worker.postMessage({
+        seed: opts.seed()
+      , imports: opts.imports
+      , debug: DEBUG
+    })
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var Parse = {}, CryptoJS, CONST, HLP
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = Parse
+    CryptoJS = require('../vendor/crypto.js')
+    CONST = require('./const.js')
+    HLP = require('./helpers.js')
+  } else {
+    root.OTR.Parse = Parse
+    CryptoJS = root.CryptoJS
+    CONST = root.OTR.CONST
+    HLP = root.OTR.HLP
+  }
+
+  // whitespace tags
+  var tags = {}
+  tags[CONST.WHITESPACE_TAG_V2] = CONST.OTR_VERSION_2
+  tags[CONST.WHITESPACE_TAG_V3] = CONST.OTR_VERSION_3
+
+  Parse.parseMsg = function (otr, msg) {
+
+    var ver = []
+
+    // is this otr?
+    var start = msg.indexOf(CONST.OTR_TAG)
+    if (!~start) {
+
+      // restart fragments
+      this.initFragment(otr)
+
+      // whitespace tags
+      ind = msg.indexOf(CONST.WHITESPACE_TAG)
+
+      if (~ind) {
+
+        msg = msg.split('')
+        msg.splice(ind, 16)
+
+        var tag, len = msg.length
+        for (; ind < len;) {
+          tag = msg.slice(ind, ind + 8).join('')
+          if (Object.hasOwnProperty.call(tags, tag)) {
+            msg.splice(ind, 8)
+            ver.push(tags[tag])
+            continue
+          }
+          ind += 8
+        }
+
+        msg = msg.join('')
+
+      }
+
+      return { msg: msg, ver: ver }
+    }
+
+    var ind = start + CONST.OTR_TAG.length
+    var com = msg[ind]
+
+    // message fragment
+    if (com === ',' || com === '|') {
+      return this.msgFragment(otr, msg.substring(ind + 1), (com === '|'))
+    }
+
+    this.initFragment(otr)
+
+    // query message
+    if (~['?', 'v'].indexOf(com)) {
+
+      // version 1
+      if (msg[ind] === '?') {
+        ver.push(CONST.OTR_VERSION_1)
+        ind += 1
+      }
+
+      // other versions
+      var vers = {
+          '2': CONST.OTR_VERSION_2
+        , '3': CONST.OTR_VERSION_3
+      }
+      var qs = msg.substring(ind + 1)
+      var qi = qs.indexOf('?')
+
+      if (qi >= 1) {
+        qs = qs.substring(0, qi).split('')
+        if (msg[ind] === 'v') {
+          qs.forEach(function (q) {
+            if (Object.hasOwnProperty.call(vers, q)) ver.push(vers[q])
+          })
+        }
+      }
+
+      return { cls: 'query', ver: ver }
+    }
+
+    // otr message
+    if (com === ':') {
+
+      ind += 1
+
+      var info = msg.substring(ind, ind + 4)
+      if (info.length < 4) return { msg: msg }
+      info = CryptoJS.enc.Base64.parse(info).toString(CryptoJS.enc.Latin1)
+
+      var version = info.substring(0, 2)
+      var type = info.substring(2)
+
+      // supporting otr versions 2 and 3
+      if (!otr['ALLOW_V' + HLP.unpackSHORT(version)]) return { msg: msg }
+
+      ind += 4
+
+      var end = msg.substring(ind).indexOf('.')
+      if (!~end) return { msg: msg }
+
+      msg = CryptoJS.enc.Base64.parse(msg.substring(ind, ind + end))
+      msg = CryptoJS.enc.Latin1.stringify(msg)
+
+      // instance tags
+      var instance_tags
+      if (version === CONST.OTR_VERSION_3) {
+        instance_tags = msg.substring(0, 8)
+        msg = msg.substring(8)
+      }
+
+      var cls
+      if (~['\x02', '\x0a', '\x11', '\x12'].indexOf(type)) {
+        cls = 'ake'
+      } else if (type === '\x03') {
+        cls = 'data'
+      }
+
+      return {
+          version: version
+        , type: type
+        , msg: msg
+        , cls: cls
+        , instance_tags: instance_tags
+      }
+    }
+
+    // error message
+    if (msg.substring(ind, ind + 7) === ' Error:') {
+      if (otr.ERROR_START_AKE) {
+        otr.sendQueryMsg()
+      }
+      return { msg: msg.substring(ind + 7), cls: 'error' }
+    }
+
+    return { msg: msg }
+  }
+
+  Parse.initFragment = function (otr) {
+    otr.fragment = { s: '', j: 0, k: 0 }
+  }
+
+  Parse.msgFragment = function (otr, msg, v3) {
+
+    msg = msg.split(',')
+
+    // instance tags
+    if (v3) {
+      var its = msg.shift().split('|')
+      var their_it = HLP.packINT(parseInt(its[0], 16))
+      var our_it = HLP.packINT(parseInt(its[1], 16))
+      if (otr.checkInstanceTags(their_it + our_it)) return  // ignore
+    }
+
+    if (msg.length < 4 ||
+      isNaN(parseInt(msg[0], 10)) ||
+      isNaN(parseInt(msg[1], 10))
+    ) return
+
+    var k = parseInt(msg[0], 10)
+    var n = parseInt(msg[1], 10)
+    msg = msg[2]
+
+    if (n < k || n === 0 || k === 0) {
+      this.initFragment(otr)
+      return
+    }
+
+    if (k === 1) {
+      this.initFragment(otr)
+      otr.fragment = { k: 1, n: n, s: msg }
+    } else if (n === otr.fragment.n && k === (otr.fragment.k + 1)) {
+      otr.fragment.s += msg
+      otr.fragment.k += 1
+    } else {
+      this.initFragment(otr)
+    }
+
+    if (n === k) {
+      msg = otr.fragment.s
+      this.initFragment(otr)
+      return this.parseMsg(otr, msg)
+    }
+
+    return
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var CryptoJS, BigInt, CONST, HLP, DSA
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = AKE
+    CryptoJS = require('../vendor/crypto.js')
+    BigInt = require('../vendor/bigint.js')
+    CONST = require('./const.js')
+    HLP = require('./helpers.js')
+    DSA = require('./dsa.js')
+  } else {
+    root.OTR.AKE = AKE
+    CryptoJS = root.CryptoJS
+    BigInt = root.BigInt
+    CONST = root.OTR.CONST
+    HLP = root.OTR.HLP
+    DSA = root.DSA
+  }
+
+  // diffie-hellman modulus
+  // see group 5, RFC 3526
+  var N = BigInt.str2bigInt(CONST.N, 16)
+  var N_MINUS_2 = BigInt.sub(N, BigInt.str2bigInt('2', 10))
+
+  function hMac(gx, gy, pk, kid, m) {
+    var pass = CryptoJS.enc.Latin1.parse(m)
+    var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, pass)
+    hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gx)))
+    hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gy)))
+    hmac.update(CryptoJS.enc.Latin1.parse(pk))
+    hmac.update(CryptoJS.enc.Latin1.parse(kid))
+    return (hmac.finalize()).toString(CryptoJS.enc.Latin1)
+  }
+
+  // AKE constructor
+  function AKE(otr) {
+    if (!(this instanceof AKE)) return new AKE(otr)
+
+    // otr instance
+    this.otr = otr
+
+    // our keys
+    this.our_dh = otr.our_old_dh
+    this.our_keyid = otr.our_keyid - 1
+
+    // their keys
+    this.their_y = null
+    this.their_keyid = null
+    this.their_priv_pk = null
+
+    // state
+    this.ssid = null
+    this.transmittedRS = false
+    this.r = null
+
+    // bind methods
+    var self = this
+    ;['sendMsg'].forEach(function (meth) {
+      self[meth] = self[meth].bind(self)
+    })
+  }
+
+  AKE.prototype = {
+
+    constructor: AKE,
+
+    createKeys: function(g) {
+      var s = BigInt.powMod(g, this.our_dh.privateKey, N)
+      var secbytes = HLP.packMPI(s)
+      this.ssid = HLP.mask(HLP.h2('\x00', secbytes), 0, 64)  // first 64-bits
+      var tmp = HLP.h2('\x01', secbytes)
+      this.c = HLP.mask(tmp, 0, 128)  // first 128-bits
+      this.c_prime = HLP.mask(tmp, 128, 128)  // second 128-bits
+      this.m1 = HLP.h2('\x02', secbytes)
+      this.m2 = HLP.h2('\x03', secbytes)
+      this.m1_prime = HLP.h2('\x04', secbytes)
+      this.m2_prime = HLP.h2('\x05', secbytes)
+    },
+
+    verifySignMac: function (mac, aesctr, m2, c, their_y, our_dh_pk, m1, ctr) {
+      // verify mac
+      var vmac = HLP.makeMac(aesctr, m2)
+      if (!HLP.compare(mac, vmac))
+        return ['MACs do not match.']
+
+      // decrypt x
+      var x = HLP.decryptAes(aesctr.substring(4), c, ctr)
+      x = HLP.splitype(['PUBKEY', 'INT', 'SIG'], x.toString(CryptoJS.enc.Latin1))
+
+      var m = hMac(their_y, our_dh_pk, x[0], x[1], m1)
+      var pub = DSA.parsePublic(x[0])
+
+      var r = HLP.bits2bigInt(x[2].substring(0, 20))
+      var s = HLP.bits2bigInt(x[2].substring(20))
+
+      // verify sign m
+      if (!DSA.verify(pub, m, r, s)) return ['Cannot verify signature of m.']
+
+      return [null, HLP.readLen(x[1]), pub]
+    },
+
+    makeM: function (their_y, m1, c, m2) {
+      var pk = this.otr.priv.packPublic()
+      var kid = HLP.packINT(this.our_keyid)
+      var m = hMac(this.our_dh.publicKey, their_y, pk, kid, m1)
+      m = this.otr.priv.sign(m)
+      var msg = pk + kid
+      msg += BigInt.bigInt2bits(m[0], 20)  // pad to 20 bytes
+      msg += BigInt.bigInt2bits(m[1], 20)
+      msg = CryptoJS.enc.Latin1.parse(msg)
+      var aesctr = HLP.packData(HLP.encryptAes(msg, c, HLP.packCtr(0)))
+      var mac = HLP.makeMac(aesctr, m2)
+      return aesctr + mac
+    },
+
+    akeSuccess: function (version) {
+      HLP.debug.call(this.otr, 'success')
+
+      if (BigInt.equals(this.their_y, this.our_dh.publicKey))
+        return this.otr.error('equal keys - we have a problem.', true)
+
+      this.otr.our_old_dh = this.our_dh
+      this.otr.their_priv_pk = this.their_priv_pk
+
+      if (!(
+        (this.their_keyid === this.otr.their_keyid &&
+         BigInt.equals(this.their_y, this.otr.their_y)) ||
+        (this.their_keyid === (this.otr.their_keyid - 1) &&
+         BigInt.equals(this.their_y, this.otr.their_old_y))
+      )) {
+
+        this.otr.their_y = this.their_y
+        this.otr.their_old_y = null
+        this.otr.their_keyid = this.their_keyid
+
+        // rotate keys
+        this.otr.sessKeys[0] = [ new this.otr.DHSession(
+            this.otr.our_dh
+          , this.otr.their_y
+        ), null ]
+        this.otr.sessKeys[1] = [ new this.otr.DHSession(
+            this.otr.our_old_dh
+          , this.otr.their_y
+        ), null ]
+
+      }
+
+      // ake info
+      this.otr.ssid = this.ssid
+      this.otr.transmittedRS = this.transmittedRS
+      this.otr_version = version
+
+      // go encrypted
+      this.otr.authstate = CONST.AUTHSTATE_NONE
+      this.otr.msgstate = CONST.MSGSTATE_ENCRYPTED
+
+      // null out values
+      this.r = null
+      this.myhashed = null
+      this.dhcommit = null
+      this.encrypted = null
+      this.hashed = null
+
+      this.otr.trigger('status', [CONST.STATUS_AKE_SUCCESS])
+
+      // send stored msgs
+      this.otr.sendStored()
+    },
+
+    handleAKE: function (msg) {
+      var send, vsm, type
+      var version = msg.version
+
+      switch (msg.type) {
+
+        case '\x02':
+          HLP.debug.call(this.otr, 'd-h key message')
+
+          msg = HLP.splitype(['DATA', 'DATA'], msg.msg)
+
+          if (this.otr.authstate === CONST.AUTHSTATE_AWAITING_DHKEY) {
+            var ourHash = HLP.readMPI(this.myhashed)
+            var theirHash = HLP.readMPI(msg[1])
+            if (BigInt.greater(ourHash, theirHash)) {
+              type = '\x02'
+              send = this.dhcommit
+              break  // ignore
+            } else {
+              // forget
+              this.our_dh = this.otr.dh()
+              this.otr.authstate = CONST.AUTHSTATE_NONE
+              this.r = null
+              this.myhashed = null
+            }
+          } else if (
+            this.otr.authstate === CONST.AUTHSTATE_AWAITING_SIG
+          ) this.our_dh = this.otr.dh()
+
+          this.otr.authstate = CONST.AUTHSTATE_AWAITING_REVEALSIG
+
+          this.encrypted = msg[0].substring(4)
+          this.hashed = msg[1].substring(4)
+
+          type = '\x0a'
+          send = HLP.packMPI(this.our_dh.publicKey)
+          break
+
+        case '\x0a':
+          HLP.debug.call(this.otr, 'reveal signature message')
+
+          msg = HLP.splitype(['MPI'], msg.msg)
+
+          if (this.otr.authstate !== CONST.AUTHSTATE_AWAITING_DHKEY) {
+            if (this.otr.authstate === CONST.AUTHSTATE_AWAITING_SIG) {
+              if (!BigInt.equals(this.their_y, HLP.readMPI(msg[0]))) return
+            } else {
+              return  // ignore
+            }
+          }
+
+          this.otr.authstate = CONST.AUTHSTATE_AWAITING_SIG
+
+          this.their_y = HLP.readMPI(msg[0])
+
+          // verify gy is legal 2 <= gy <= N-2
+          if (!HLP.checkGroup(this.their_y, N_MINUS_2))
+            return this.otr.error('Illegal g^y.', true)
+
+          this.createKeys(this.their_y)
+
+          type = '\x11'
+          send = HLP.packMPI(this.r)
+          send += this.makeM(this.their_y, this.m1, this.c, this.m2)
+
+          this.m1 = null
+          this.m2 = null
+          this.c = null
+          break
+
+        case '\x11':
+          HLP.debug.call(this.otr, 'signature message')
+
+          if (this.otr.authstate !== CONST.AUTHSTATE_AWAITING_REVEALSIG)
+            return  // ignore
+
+          msg = HLP.splitype(['DATA', 'DATA', 'MAC'], msg.msg)
+
+          this.r = HLP.readMPI(msg[0])
+
+          // decrypt their_y
+          var key = CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r, 16))
+          key = CryptoJS.enc.Latin1.stringify(key)
+
+          var gxmpi = HLP.decryptAes(this.encrypted, key, HLP.packCtr(0))
+          gxmpi = gxmpi.toString(CryptoJS.enc.Latin1)
+
+          this.their_y = HLP.readMPI(gxmpi)
+
+          // verify hash
+          var hash = CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi))
+
+          if (!HLP.compare(this.hashed, hash.toString(CryptoJS.enc.Latin1)))
+            return this.otr.error('Hashed g^x does not match.', true)
+
+          // verify gx is legal 2 <= g^x <= N-2
+          if (!HLP.checkGroup(this.their_y, N_MINUS_2))
+            return this.otr.error('Illegal g^x.', true)
+
+          this.createKeys(this.their_y)
+
+          vsm = this.verifySignMac(
+              msg[2]
+            , msg[1]
+            , this.m2
+            , this.c
+            , this.their_y
+            , this.our_dh.publicKey
+            , this.m1
+            , HLP.packCtr(0)
+          )
+          if (vsm[0]) return this.otr.error(vsm[0], true)
+
+          // store their key
+          this.their_keyid = vsm[1]
+          this.their_priv_pk = vsm[2]
+
+          send = this.makeM(
+              this.their_y
+            , this.m1_prime
+            , this.c_prime
+            , this.m2_prime
+          )
+
+          this.m1 = null
+          this.m2 = null
+          this.m1_prime = null
+          this.m2_prime = null
+          this.c = null
+          this.c_prime = null
+
+          this.sendMsg(version, '\x12', send)
+          this.akeSuccess(version)
+          return
+
+        case '\x12':
+          HLP.debug.call(this.otr, 'data message')
+
+          if (this.otr.authstate !== CONST.AUTHSTATE_AWAITING_SIG)
+            return  // ignore
+
+          msg = HLP.splitype(['DATA', 'MAC'], msg.msg)
+
+          vsm = this.verifySignMac(
+              msg[1]
+            , msg[0]
+            , this.m2_prime
+            , this.c_prime
+            , this.their_y
+            , this.our_dh.publicKey
+            , this.m1_prime
+            , HLP.packCtr(0)
+          )
+          if (vsm[0]) return this.otr.error(vsm[0], true)
+
+          // store their key
+          this.their_keyid = vsm[1]
+          this.their_priv_pk = vsm[2]
+
+          this.m1_prime = null
+          this.m2_prime = null
+          this.c_prime = null
+
+          this.transmittedRS = true
+          this.akeSuccess(version)
+          return
+
+        default:
+          return  // ignore
+
+      }
+
+      this.sendMsg(version, type, send)
+    },
+
+    sendMsg: function (version, type, msg) {
+      var send = version + type
+      var v3 = (version === CONST.OTR_VERSION_3)
+
+      // instance tags for v3
+      if (v3) {
+        HLP.debug.call(this.otr, 'instance tags')
+        send += this.otr.our_instance_tag
+        send += this.otr.their_instance_tag
+      }
+
+      send += msg
+
+      // fragment message if necessary
+      send = HLP.wrapMsg(
+          send
+        , this.otr.fragment_size
+        , v3
+        , this.otr.our_instance_tag
+        , this.otr.their_instance_tag
+      )
+      if (send[0]) return this.otr.error(send[0])
+
+      this.otr.io(send[1])
+    },
+
+    initiateAKE: function (version) {
+      HLP.debug.call(this.otr, 'd-h commit message')
+
+      this.otr.trigger('status', [CONST.STATUS_AKE_INIT])
+
+      this.otr.authstate = CONST.AUTHSTATE_AWAITING_DHKEY
+
+      var gxmpi = HLP.packMPI(this.our_dh.publicKey)
+      gxmpi = CryptoJS.enc.Latin1.parse(gxmpi)
+
+      this.r = BigInt.randBigInt(128)
+      var key = CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r, 16))
+      key = CryptoJS.enc.Latin1.stringify(key)
+
+      this.myhashed = CryptoJS.SHA256(gxmpi)
+      this.myhashed = HLP.packData(this.myhashed.toString(CryptoJS.enc.Latin1))
+
+      this.dhcommit = HLP.packData(HLP.encryptAes(gxmpi, key, HLP.packCtr(0)))
+      this.dhcommit += this.myhashed
+
+      this.sendMsg(version, '\x02', this.dhcommit)
+    }
+
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var CryptoJS, BigInt,  EventEmitter, CONST, HLP
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = SM
+    CryptoJS = require('../vendor/crypto.js')
+    BigInt = require('../vendor/bigint.js')
+    EventEmitter = require('../vendor/eventemitter.js')
+    CONST = require('./const.js')
+    HLP = require('./helpers.js')
+  } else {
+    root.OTR.SM = SM
+    CryptoJS = root.CryptoJS
+    BigInt = root.BigInt
+    EventEmitter = root.EventEmitter
+    CONST = root.OTR.CONST
+    HLP = root.OTR.HLP
+  }
+
+  // diffie-hellman modulus and generator
+  // see group 5, RFC 3526
+  var G = BigInt.str2bigInt(CONST.G, 10)
+  var N = BigInt.str2bigInt(CONST.N, 16)
+  var N_MINUS_2 = BigInt.sub(N, BigInt.str2bigInt('2', 10))
+
+  // to calculate D's for zero-knowledge proofs
+  var Q = BigInt.sub(N, BigInt.str2bigInt('1', 10))
+  BigInt.divInt_(Q, 2)  // meh
+
+  function SM(reqs) {
+    if (!(this instanceof SM)) return new SM(reqs)
+
+    this.version = 1
+
+    this.our_fp = reqs.our_fp
+    this.their_fp = reqs.their_fp
+    this.ssid = reqs.ssid
+
+    this.debug = !!reqs.debug
+
+    // initial state
+    this.init()
+  }
+
+  // inherit from EE
+  HLP.extend(SM, EventEmitter)
+
+  // set the initial values
+  // also used when aborting
+  SM.prototype.init = function () {
+    this.smpstate = CONST.SMPSTATE_EXPECT1
+    this.secret = null
+  }
+
+  SM.prototype.makeSecret = function (our, secret) {
+    var sha256 = CryptoJS.algo.SHA256.create()
+    sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(this.version, 1)))
+    sha256.update(CryptoJS.enc.Hex.parse(our ? this.our_fp : this.their_fp))
+    sha256.update(CryptoJS.enc.Hex.parse(our ? this.their_fp : this.our_fp))
+    sha256.update(CryptoJS.enc.Latin1.parse(this.ssid))
+    sha256.update(CryptoJS.enc.Latin1.parse(secret))
+    var hash = sha256.finalize()
+    this.secret = HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))
+  }
+
+  SM.prototype.makeG2s = function () {
+    this.a2 = HLP.randomExponent()
+    this.a3 = HLP.randomExponent()
+    this.g2a = BigInt.powMod(G, this.a2, N)
+    this.g3a = BigInt.powMod(G, this.a3, N)
+    if ( !HLP.checkGroup(this.g2a, N_MINUS_2) ||
+         !HLP.checkGroup(this.g3a, N_MINUS_2)
+    ) this.makeG2s()
+  }
+
+  SM.prototype.computeGs = function (g2a, g3a) {
+    this.g2 = BigInt.powMod(g2a, this.a2, N)
+    this.g3 = BigInt.powMod(g3a, this.a3, N)
+  }
+
+  SM.prototype.computePQ = function (r) {
+    this.p = BigInt.powMod(this.g3, r, N)
+    this.q = HLP.multPowMod(G, r, this.g2, this.secret, N)
+  }
+
+  SM.prototype.computeR = function () {
+    this.r = BigInt.powMod(this.QoQ, this.a3, N)
+  }
+
+  SM.prototype.computeRab = function (r) {
+    return BigInt.powMod(r, this.a3, N)
+  }
+
+  SM.prototype.computeC = function (v, r) {
+    return HLP.smpHash(v, BigInt.powMod(G, r, N))
+  }
+
+  SM.prototype.computeD = function (r, a, c) {
+    return BigInt.subMod(r, BigInt.multMod(a, c, Q), Q)
+  }
+
+  // the bulk of the work
+  SM.prototype.handleSM = function (msg) {
+    var send, r2, r3, r7, t1, t2, t3, t4, rab, tmp2, cR, d7, ms, trust
+
+    var expectStates = {
+        2: CONST.SMPSTATE_EXPECT1
+      , 3: CONST.SMPSTATE_EXPECT2
+      , 4: CONST.SMPSTATE_EXPECT3
+      , 5: CONST.SMPSTATE_EXPECT4
+      , 7: CONST.SMPSTATE_EXPECT1
+    }
+
+    if (msg.type === 6) {
+      this.init()
+      this.trigger('abort')
+      return
+    }
+
+    // abort! there was an error
+    if (this.smpstate !== expectStates[msg.type])
+      return this.abort()
+
+    switch (this.smpstate) {
+
+      case CONST.SMPSTATE_EXPECT1:
+        HLP.debug.call(this, 'smp tlv 2')
+
+        // user specified question
+        var ind, question
+        if (msg.type === 7) {
+          ind = msg.msg.indexOf('\x00')
+          question = msg.msg.substring(0, ind)
+          msg.msg = msg.msg.substring(ind + 1)
+        }
+
+        // 0:g2a, 1:c2, 2:d2, 3:g3a, 4:c3, 5:d3
+        ms = HLP.readLen(msg.msg.substr(0, 4))
+        if (ms !== 6) return this.abort()
+        msg = HLP.unpackMPIs(6, msg.msg.substring(4))
+
+        if ( !HLP.checkGroup(msg[0], N_MINUS_2) ||
+             !HLP.checkGroup(msg[3], N_MINUS_2)
+        ) return this.abort()
+
+        // verify znp's
+        if (!HLP.ZKP(1, msg[1], HLP.multPowMod(G, msg[2], msg[0], msg[1], N)))
+          return this.abort()
+
+        if (!HLP.ZKP(2, msg[4], HLP.multPowMod(G, msg[5], msg[3], msg[4], N)))
+          return this.abort()
+
+        this.g3ao = msg[3]  // save for later
+
+        this.makeG2s()
+
+        // zero-knowledge proof that the exponents
+        // associated with g2a & g3a are known
+        r2 = HLP.randomExponent()
+        r3 = HLP.randomExponent()
+        this.c2 = this.computeC(3, r2)
+        this.c3 = this.computeC(4, r3)
+        this.d2 = this.computeD(r2, this.a2, this.c2)
+        this.d3 = this.computeD(r3, this.a3, this.c3)
+
+        this.computeGs(msg[0], msg[3])
+
+        this.smpstate = CONST.SMPSTATE_EXPECT0
+
+        // assume utf8 question
+        question = CryptoJS.enc.Latin1
+          .parse(question)
+          .toString(CryptoJS.enc.Utf8)
+
+        // invoke question
+        this.trigger('question', [question])
+        return
+
+      case CONST.SMPSTATE_EXPECT2:
+        HLP.debug.call(this, 'smp tlv 3')
+
+        // 0:g2a, 1:c2, 2:d2, 3:g3a, 4:c3, 5:d3, 6:p, 7:q, 8:cP, 9:d5, 10:d6
+        ms = HLP.readLen(msg.msg.substr(0, 4))
+        if (ms !== 11) return this.abort()
+        msg = HLP.unpackMPIs(11, msg.msg.substring(4))
+
+        if ( !HLP.checkGroup(msg[0], N_MINUS_2) ||
+             !HLP.checkGroup(msg[3], N_MINUS_2) ||
+             !HLP.checkGroup(msg[6], N_MINUS_2) ||
+             !HLP.checkGroup(msg[7], N_MINUS_2)
+        ) return this.abort()
+
+        // verify znp of c3 / c3
+        if (!HLP.ZKP(3, msg[1], HLP.multPowMod(G, msg[2], msg[0], msg[1], N)))
+          return this.abort()
+
+        if (!HLP.ZKP(4, msg[4], HLP.multPowMod(G, msg[5], msg[3], msg[4], N)))
+          return this.abort()
+
+        this.g3ao = msg[3]  // save for later
+
+        this.computeGs(msg[0], msg[3])
+
+        // verify znp of cP
+        t1 = HLP.multPowMod(this.g3, msg[9], msg[6], msg[8], N)
+        t2 = HLP.multPowMod(G, msg[9], this.g2, msg[10], N)
+        t2 = BigInt.multMod(t2, BigInt.powMod(msg[7], msg[8], N), N)
+
+        if (!HLP.ZKP(5, msg[8], t1, t2))
+          return this.abort()
+
+        var r4 = HLP.randomExponent()
+        this.computePQ(r4)
+
+        // zero-knowledge proof that P & Q
+        // were generated according to the protocol
+        var r5 = HLP.randomExponent()
+        var r6 = HLP.randomExponent()
+        var tmp = HLP.multPowMod(G, r5, this.g2, r6, N)
+        var cP = HLP.smpHash(6, BigInt.powMod(this.g3, r5, N), tmp)
+        var d5 = this.computeD(r5, r4, cP)
+        var d6 = this.computeD(r6, this.secret, cP)
+
+        // store these
+        this.QoQ = BigInt.divMod(this.q, msg[7], N)
+        this.PoP = BigInt.divMod(this.p, msg[6], N)
+
+        this.computeR()
+
+        // zero-knowledge proof that R
+        // was generated according to the protocol
+        r7 = HLP.randomExponent()
+        tmp2 = BigInt.powMod(this.QoQ, r7, N)
+        cR = HLP.smpHash(7, BigInt.powMod(G, r7, N), tmp2)
+        d7 = this.computeD(r7, this.a3, cR)
+
+        this.smpstate = CONST.SMPSTATE_EXPECT4
+
+        send = HLP.packINT(8) + HLP.packMPIs([
+            this.p
+          , this.q
+          , cP
+          , d5
+          , d6
+          , this.r
+          , cR
+          , d7
+        ])
+
+        // TLV
+        send = HLP.packTLV(4, send)
+        break
+
+      case CONST.SMPSTATE_EXPECT3:
+        HLP.debug.call(this, 'smp tlv 4')
+
+        // 0:p, 1:q, 2:cP, 3:d5, 4:d6, 5:r, 6:cR, 7:d7
+        ms = HLP.readLen(msg.msg.substr(0, 4))
+        if (ms !== 8) return this.abort()
+        msg = HLP.unpackMPIs(8, msg.msg.substring(4))
+
+        if ( !HLP.checkGroup(msg[0], N_MINUS_2) ||
+             !HLP.checkGroup(msg[1], N_MINUS_2) ||
+             !HLP.checkGroup(msg[5], N_MINUS_2)
+        ) return this.abort()
+
+        // verify znp of cP
+        t1 = HLP.multPowMod(this.g3, msg[3], msg[0], msg[2], N)
+        t2 = HLP.multPowMod(G, msg[3], this.g2, msg[4], N)
+        t2 = BigInt.multMod(t2, BigInt.powMod(msg[1], msg[2], N), N)
+
+        if (!HLP.ZKP(6, msg[2], t1, t2))
+          return this.abort()
+
+        // verify znp of cR
+        t3 = HLP.multPowMod(G, msg[7], this.g3ao, msg[6], N)
+        this.QoQ = BigInt.divMod(msg[1], this.q, N)  // save Q over Q
+        t4 = HLP.multPowMod(this.QoQ, msg[7], msg[5], msg[6], N)
+
+        if (!HLP.ZKP(7, msg[6], t3, t4))
+          return this.abort()
+
+        this.computeR()
+
+        // zero-knowledge proof that R
+        // was generated according to the protocol
+        r7 = HLP.randomExponent()
+        tmp2 = BigInt.powMod(this.QoQ, r7, N)
+        cR = HLP.smpHash(8, BigInt.powMod(G, r7, N), tmp2)
+        d7 = this.computeD(r7, this.a3, cR)
+
+        send = HLP.packINT(3) + HLP.packMPIs([ this.r, cR, d7 ])
+        send = HLP.packTLV(5, send)
+
+        rab = this.computeRab(msg[5])
+        trust = !!BigInt.equals(rab, BigInt.divMod(msg[0], this.p, N))
+
+        this.trigger('trust', [trust, 'answered'])
+        this.init()
+        break
+
+      case CONST.SMPSTATE_EXPECT4:
+        HLP.debug.call(this, 'smp tlv 5')
+
+        // 0:r, 1:cR, 2:d7
+        ms = HLP.readLen(msg.msg.substr(0, 4))
+        if (ms !== 3) return this.abort()
+        msg = HLP.unpackMPIs(3, msg.msg.substring(4))
+
+        if (!HLP.checkGroup(msg[0], N_MINUS_2)) return this.abort()
+
+        // verify znp of cR
+        t3 = HLP.multPowMod(G, msg[2], this.g3ao, msg[1], N)
+        t4 = HLP.multPowMod(this.QoQ, msg[2], msg[0], msg[1], N)
+        if (!HLP.ZKP(8, msg[1], t3, t4))
+          return this.abort()
+
+        rab = this.computeRab(msg[0])
+        trust = !!BigInt.equals(rab, this.PoP)
+
+        this.trigger('trust', [trust, 'asked'])
+        this.init()
+        return
+
+    }
+
+    this.sendMsg(send)
+  }
+
+  // send a message
+  SM.prototype.sendMsg = function (send) {
+    this.trigger('send', [this.ssid, '\x00' + send])
+  }
+
+  SM.prototype.rcvSecret = function (secret, question) {
+    HLP.debug.call(this, 'receive secret')
+
+    var fn, our = false
+    if (this.smpstate === CONST.SMPSTATE_EXPECT0) {
+      fn = this.answer
+    } else {
+      fn = this.initiate
+      our = true
+    }
+
+    this.makeSecret(our, secret)
+    fn.call(this, question)
+  }
+
+  SM.prototype.answer = function () {
+    HLP.debug.call(this, 'smp answer')
+
+    var r4 = HLP.randomExponent()
+    this.computePQ(r4)
+
+    // zero-knowledge proof that P & Q
+    // were generated according to the protocol
+    var r5 = HLP.randomExponent()
+    var r6 = HLP.randomExponent()
+    var tmp = HLP.multPowMod(G, r5, this.g2, r6, N)
+    var cP = HLP.smpHash(5, BigInt.powMod(this.g3, r5, N), tmp)
+    var d5 = this.computeD(r5, r4, cP)
+    var d6 = this.computeD(r6, this.secret, cP)
+
+    this.smpstate = CONST.SMPSTATE_EXPECT3
+
+    var send = HLP.packINT(11) + HLP.packMPIs([
+        this.g2a
+      , this.c2
+      , this.d2
+      , this.g3a
+      , this.c3
+      , this.d3
+      , this.p
+      , this.q
+      , cP
+      , d5
+      , d6
+    ])
+
+    this.sendMsg(HLP.packTLV(3, send))
+  }
+
+  SM.prototype.initiate = function (question) {
+    HLP.debug.call(this, 'smp initiate')
+
+    if (this.smpstate !== CONST.SMPSTATE_EXPECT1)
+      this.abort()  // abort + restart
+
+    this.makeG2s()
+
+    // zero-knowledge proof that the exponents
+    // associated with g2a & g3a are known
+    var r2 = HLP.randomExponent()
+    var r3 = HLP.randomExponent()
+    this.c2 = this.computeC(1, r2)
+    this.c3 = this.computeC(2, r3)
+    this.d2 = this.computeD(r2, this.a2, this.c2)
+    this.d3 = this.computeD(r3, this.a3, this.c3)
+
+    // set the next expected state
+    this.smpstate = CONST.SMPSTATE_EXPECT2
+
+    var send = ''
+    var type = 2
+
+    if (question) {
+      send += question
+      send += '\x00'
+      type = 7
+    }
+
+    send += HLP.packINT(6) + HLP.packMPIs([
+        this.g2a
+      , this.c2
+      , this.d2
+      , this.g3a
+      , this.c3
+      , this.d3
+    ])
+
+    this.sendMsg(HLP.packTLV(type, send))
+  }
+
+  SM.prototype.abort = function () {
+    this.init()
+    this.sendMsg(HLP.packTLV(6, ''))
+    this.trigger('abort')
+  }
+
+}).call(this)
+;(function () {
+  "use strict";
+
+  var root = this
+
+  var CryptoJS, BigInt, EventEmitter, Worker, SMWPath
+    , CONST, HLP, Parse, AKE, SM, DSA
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = OTR
+    CryptoJS = require('../vendor/crypto.js')
+    BigInt = require('../vendor/bigint.js')
+    EventEmitter = require('../vendor/eventemitter.js')
+    SMWPath = require('path').join(__dirname, '/sm-webworker.js')
+    CONST = require('./const.js')
+    HLP = require('./helpers.js')
+    Parse = require('./parse.js')
+    AKE = require('./ake.js')
+    SM = require('./sm.js')
+    DSA = require('./dsa.js')
+    // expose CONST for consistency with docs
+    OTR.CONST = CONST
+  } else {
+    // copy over and expose internals
+    Object.keys(root.OTR).forEach(function (k) {
+      OTR[k] = root.OTR[k]
+    })
+    root.OTR = OTR
+    CryptoJS = root.CryptoJS
+    BigInt = root.BigInt
+    EventEmitter = root.EventEmitter
+    Worker = root.Worker
+    SMWPath = 'sm-webworker.js'
+    CONST = OTR.CONST
+    HLP = OTR.HLP
+    Parse = OTR.Parse
+    AKE = OTR.AKE
+    SM = OTR.SM
+    DSA = root.DSA
+  }
+
+  // diffie-hellman modulus and generator
+  // see group 5, RFC 3526
+  var G = BigInt.str2bigInt(CONST.G, 10)
+  var N = BigInt.str2bigInt(CONST.N, 16)
+
+  // JavaScript integers
+  var MAX_INT = Math.pow(2, 53) - 1  // doubles
+  var MAX_UINT = Math.pow(2, 31) - 1  // bitwise operators
+
+  // OTR contructor
+  function OTR(options) {
+    if (!(this instanceof OTR)) return new OTR(options)
+
+    // options
+    options = options || {}
+
+    // private keys
+    if (options.priv && !(options.priv instanceof DSA))
+      throw new Error('Requires long-lived DSA key.')
+
+    this.priv = options.priv ? options.priv : new DSA()
+
+    this.fragment_size = options.fragment_size || 0
+    if (this.fragment_size < 0)
+      throw new Error('Fragment size must be a positive integer.')
+
+    this.send_interval = options.send_interval || 0
+    if (this.send_interval < 0)
+      throw new Error('Send interval must be a positive integer.')
+
+    this.outgoing = []
+
+    // instance tag
+    this.our_instance_tag = options.instance_tag || OTR.makeInstanceTag()
+
+    // debug
+    this.debug = !!options.debug
+
+    // smp in webworker options
+    // this is still experimental and undocumented
+    this.smw = options.smw
+
+    // init vals
+    this.init()
+
+    // bind methods
+    var self = this
+    ;['sendMsg', 'receiveMsg'].forEach(function (meth) {
+      self[meth] = self[meth].bind(self)
+    })
+
+    EventEmitter.call(this)
+  }
+
+  // inherit from EE
+  HLP.extend(OTR, EventEmitter)
+
+  // add to prototype
+  OTR.prototype.init = function () {
+
+    this.msgstate = CONST.MSGSTATE_PLAINTEXT
+    this.authstate = CONST.AUTHSTATE_NONE
+
+    this.ALLOW_V2 = true
+    this.ALLOW_V3 = true
+
+    this.REQUIRE_ENCRYPTION = false
+    this.SEND_WHITESPACE_TAG = false
+    this.WHITESPACE_START_AKE = false
+    this.ERROR_START_AKE = false
+
+    Parse.initFragment(this)
+
+    // their keys
+    this.their_y = null
+    this.their_old_y = null
+    this.their_keyid = 0
+    this.their_priv_pk = null
+    this.their_instance_tag = '\x00\x00\x00\x00'
+
+    // our keys
+    this.our_dh = this.dh()
+    this.our_old_dh = this.dh()
+    this.our_keyid = 2
+
+    // session keys
+    this.sessKeys = [ new Array(2), new Array(2) ]
+
+    // saved
+    this.storedMgs = []
+    this.oldMacKeys = []
+
+    // smp
+    this.sm = null  // initialized after AKE
+
+    // when ake is complete
+    // save their keys and the session
+    this._akeInit()
+
+    // receive plaintext message since switching to plaintext
+    // used to decide when to stop sending pt tags when SEND_WHITESPACE_TAG
+    this.receivedPlaintext = false
+
+  }
+
+  OTR.prototype._akeInit = function () {
+    this.ake = new AKE(this)
+    this.transmittedRS = false
+    this.ssid = null
+  }
+
+  // smp over webworker
+  OTR.prototype._SMW = function (otr, reqs) {
+    this.otr = otr
+    var opts = {
+        path: SMWPath
+      , seed: BigInt.getSeed
+    }
+    if (typeof otr.smw === 'object')
+      Object.keys(otr.smw).forEach(function (k) {
+        opts[k] = otr.smw[k]
+      })
+
+    // load optional dep. in node
+    if (typeof module !== 'undefined' && module.exports)
+      Worker = require('webworker-threads').Worker
+
+    this.worker = new Worker(opts.path)
+    var self = this
+    this.worker.onmessage = function (e) {
+      var d = e.data
+      if (!d) return
+      self.trigger(d.method, d.args)
+    }
+    this.worker.postMessage({
+        type: 'seed'
+      , seed: opts.seed()
+      , imports: opts.imports
+    })
+    this.worker.postMessage({
+        type: 'init'
+      , reqs: reqs
+    })
+  }
+
+  // inherit from EE
+  HLP.extend(OTR.prototype._SMW, EventEmitter)
+
+  // shim sm methods
+  ;['handleSM', 'rcvSecret', 'abort'].forEach(function (m) {
+    OTR.prototype._SMW.prototype[m] = function () {
+      this.worker.postMessage({
+          type: 'method'
+        , method: m
+        , args: Array.prototype.slice.call(arguments, 0)
+      })
+    }
+  })
+
+  OTR.prototype._smInit = function () {
+    var reqs = {
+        ssid: this.ssid
+      , our_fp: this.priv.fingerprint()
+      , their_fp: this.their_priv_pk.fingerprint()
+      , debug: this.debug
+    }
+    if (this.smw) {
+      if (this.sm) this.sm.worker.terminate()  // destroy prev webworker
+      this.sm = new this._SMW(this, reqs)
+    } else {
+      this.sm = new SM(reqs)
+    }
+    var self = this
+    ;['trust', 'abort', 'question'].forEach(function (e) {
+      self.sm.on(e, function () {
+        self.trigger('smp', [e].concat(Array.prototype.slice.call(arguments)))
+      })
+    })
+    this.sm.on('send', function (ssid, send) {
+      if (self.ssid === ssid) {
+        send = self.prepareMsg(send)
+        self.io(send)
+      }
+    })
+  }
+
+  OTR.prototype.io = function (msg, meta) {
+
+    // buffer
+    msg = ([].concat(msg)).map(function(m){
+       return { msg: m, meta: meta }
+    })
+    this.outgoing = this.outgoing.concat(msg)
+
+    var self = this
+    ;(function send(first) {
+      if (!first) {
+        if (!self.outgoing.length) return
+        var elem = self.outgoing.shift()
+        self.trigger('io', [elem.msg, elem.meta])
+      }
+      setTimeout(send, first ? 0 : self.send_interval)
+    }(true))
+
+  }
+
+  OTR.prototype.dh = function dh() {
+    var keys = { privateKey: BigInt.randBigInt(320) }
+    keys.publicKey = BigInt.powMod(G, keys.privateKey, N)
+    return keys
+  }
+
+  // session constructor
+  OTR.prototype.DHSession = function DHSession(our_dh, their_y) {
+    if (!(this instanceof DHSession)) return new DHSession(our_dh, their_y)
+
+    // shared secret
+    var s = BigInt.powMod(their_y, our_dh.privateKey, N)
+    var secbytes = HLP.packMPI(s)
+
+    // session id
+    this.id = HLP.mask(HLP.h2('\x00', secbytes), 0, 64)  // first 64-bits
+
+    // are we the high or low end of the connection?
+    var sq = BigInt.greater(our_dh.publicKey, their_y)
+    var sendbyte = sq ? '\x01' : '\x02'
+    var rcvbyte  = sq ? '\x02' : '\x01'
+
+    // sending and receiving keys
+    this.sendenc = HLP.mask(HLP.h1(sendbyte, secbytes), 0, 128)  // f16 bytes
+    this.sendmac = CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.sendenc))
+    this.sendmac = this.sendmac.toString(CryptoJS.enc.Latin1)
+
+    this.rcvenc = HLP.mask(HLP.h1(rcvbyte, secbytes), 0, 128)
+    this.rcvmac = CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.rcvenc))
+    this.rcvmac = this.rcvmac.toString(CryptoJS.enc.Latin1)
+    this.rcvmacused = false
+
+    // extra symmetric key
+    this.extra_symkey = HLP.h2('\xff', secbytes)
+
+    // counters
+    this.send_counter = 0
+    this.rcv_counter = 0
+  }
+
+  OTR.prototype.rotateOurKeys = function () {
+
+    // reveal old mac keys
+    var self = this
+    this.sessKeys[1].forEach(function (sk) {
+      if (sk && sk.rcvmacused) self.oldMacKeys.push(sk.rcvmac)
+    })
+
+    // rotate our keys
+    this.our_old_dh = this.our_dh
+    this.our_dh = this.dh()
+    this.our_keyid += 1
+
+    this.sessKeys[1][0] = this.sessKeys[0][0]
+    this.sessKeys[1][1] = this.sessKeys[0][1]
+    this.sessKeys[0] = [
+        this.their_y ?
+            new this.DHSession(this.our_dh, this.their_y) : null
+      , this.their_old_y ?
+            new this.DHSession(this.our_dh, this.their_old_y) : null
+    ]
+
+  }
+
+  OTR.prototype.rotateTheirKeys = function (their_y) {
+
+    // increment their keyid
+    this.their_keyid += 1
+
+    // reveal old mac keys
+    var self = this
+    this.sessKeys.forEach(function (sk) {
+      if (sk[1] && sk[1].rcvmacused) self.oldMacKeys.push(sk[1].rcvmac)
+    })
+
+    // rotate their keys / session
+    this.their_old_y = this.their_y
+    this.sessKeys[0][1] = this.sessKeys[0][0]
+    this.sessKeys[1][1] = this.sessKeys[1][0]
+
+    // new keys / sessions
+    this.their_y = their_y
+    this.sessKeys[0][0] = new this.DHSession(this.our_dh, this.their_y)
+    this.sessKeys[1][0] = new this.DHSession(this.our_old_dh, this.their_y)
+
+  }
+
+  OTR.prototype.prepareMsg = function (msg, esk) {
+    if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED || this.their_keyid === 0)
+      return this.error('Not ready to encrypt.')
+
+    var sessKeys = this.sessKeys[1][0]
+
+    if (sessKeys.send_counter >= MAX_INT)
+      return this.error('Should have rekeyed by now.')
+
+    sessKeys.send_counter += 1
+
+    var ctr = HLP.packCtr(sessKeys.send_counter)
+
+    var send = this.ake.otr_version + '\x03'  // version and type
+    var v3 = (this.ake.otr_version === CONST.OTR_VERSION_3)
+
+    if (v3) {
+      send += this.our_instance_tag
+      send += this.their_instance_tag
+    }
+
+    send += '\x00'  // flag
+    send += HLP.packINT(this.our_keyid - 1)
+    send += HLP.packINT(this.their_keyid)
+    send += HLP.packMPI(this.our_dh.publicKey)
+    send += ctr.substring(0, 8)
+
+    if (Math.ceil(msg.length / 8) >= MAX_UINT)  // * 16 / 128
+      return this.error('Message is too long.')
+
+    var aes = HLP.encryptAes(
+        CryptoJS.enc.Latin1.parse(msg)
+      , sessKeys.sendenc
+      , ctr
+    )
+
+    send += HLP.packData(aes)
+    send += HLP.make1Mac(send, sessKeys.sendmac)
+    send += HLP.packData(this.oldMacKeys.splice(0).join(''))
+
+    send = HLP.wrapMsg(
+        send
+      , this.fragment_size
+      , v3
+      , this.our_instance_tag
+      , this.their_instance_tag
+    )
+    if (send[0]) return this.error(send[0])
+
+    // emit extra symmetric key
+    if (esk) this.trigger('file', ['send', sessKeys.extra_symkey, esk])
+
+    return send[1]
+  }
+
+  OTR.prototype.handleDataMsg = function (msg) {
+    var vt = msg.version + msg.type
+
+    if (this.ake.otr_version === CONST.OTR_VERSION_3)
+      vt += msg.instance_tags
+
+    var types = ['BYTE', 'INT', 'INT', 'MPI', 'CTR', 'DATA', 'MAC', 'DATA']
+    msg = HLP.splitype(types, msg.msg)
+
+    // ignore flag
+    var ign = (msg[0] === '\x01')
+
+    if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED || msg.length !== 8) {
+      if (!ign) this.error('Received an unreadable encrypted message.', true)
+      return
+    }
+
+    var our_keyid = this.our_keyid - HLP.readLen(msg[2])
+    var their_keyid = this.their_keyid - HLP.readLen(msg[1])
+
+    if (our_keyid < 0 || our_keyid > 1) {
+      if (!ign) this.error('Not of our latest keys.', true)
+      return
+    }
+
+    if (their_keyid < 0 || their_keyid > 1) {
+      if (!ign) this.error('Not of your latest keys.', true)
+      return
+    }
+
+    var their_y = their_keyid ? this.their_old_y : this.their_y
+
+    if (their_keyid === 1 && !their_y) {
+      if (!ign) this.error('Do not have that key.')
+      return
+    }
+
+    var sessKeys = this.sessKeys[our_keyid][their_keyid]
+
+    var ctr = HLP.unpackCtr(msg[4])
+    if (ctr <= sessKeys.rcv_counter) {
+      if (!ign) this.error('Counter in message is not larger.')
+      return
+    }
+    sessKeys.rcv_counter = ctr
+
+    // verify mac
+    vt += msg.slice(0, 6).join('')
+    var vmac = HLP.make1Mac(vt, sessKeys.rcvmac)
+
+    if (!HLP.compare(msg[6], vmac)) {
+      if (!ign) this.error('MACs do not match.')
+      return
+    }
+    sessKeys.rcvmacused = true
+
+    var out = HLP.decryptAes(
+        msg[5].substring(4)
+      , sessKeys.rcvenc
+      , HLP.padCtr(msg[4])
+    )
+    out = out.toString(CryptoJS.enc.Latin1)
+
+    if (!our_keyid) this.rotateOurKeys()
+    if (!their_keyid) this.rotateTheirKeys(HLP.readMPI(msg[3]))
+
+    // parse TLVs
+    var ind = out.indexOf('\x00')
+    if (~ind) {
+      this.handleTLVs(out.substring(ind + 1), sessKeys)
+      out = out.substring(0, ind)
+    }
+
+    out = CryptoJS.enc.Latin1.parse(out)
+    return out.toString(CryptoJS.enc.Utf8)
+  }
+
+  OTR.prototype.handleTLVs = function (tlvs, sessKeys) {
+    var type, len, msg
+    for (; tlvs.length; ) {
+      type = HLP.unpackSHORT(tlvs.substr(0, 2))
+      len = HLP.unpackSHORT(tlvs.substr(2, 2))
+
+      msg = tlvs.substr(4, len)
+
+      // TODO: handle pathological cases better
+      if (msg.length < len) break
+
+      switch (type) {
+        case 1:
+          // Disconnected
+          this.msgstate = CONST.MSGSTATE_FINISHED
+          this.trigger('status', [CONST.STATUS_END_OTR])
+          break
+        case 2: case 3: case 4:
+        case 5: case 6: case 7:
+          // SMP
+          if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED) {
+            if (this.sm) this.sm.abort()
+            return
+          }
+          if (!this.sm) this._smInit()
+          this.sm.handleSM({ msg: msg, type: type })
+          break
+        case 8:
+          // utf8 filenames
+          msg = msg.substring(4) // remove 4-byte indication
+          msg = CryptoJS.enc.Latin1.parse(msg)
+          msg = msg.toString(CryptoJS.enc.Utf8)
+
+          // Extra Symkey
+          this.trigger('file', ['receive', sessKeys.extra_symkey, msg])
+          break
+      }
+
+      tlvs = tlvs.substring(4 + len)
+    }
+  }
+
+  OTR.prototype.smpSecret = function (secret, question) {
+    if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED)
+      return this.error('Must be encrypted for SMP.')
+
+    if (typeof secret !== 'string' || secret.length < 1)
+      return this.error('Secret is required.')
+
+    if (!this.sm) this._smInit()
+
+    // utf8 inputs
+    secret = CryptoJS.enc.Utf8.parse(secret).toString(CryptoJS.enc.Latin1)
+    question = CryptoJS.enc.Utf8.parse(question).toString(CryptoJS.enc.Latin1)
+
+    this.sm.rcvSecret(secret, question)
+  }
+
+  OTR.prototype.sendQueryMsg = function () {
+    var versions = {}
+      , msg = CONST.OTR_TAG
+
+    if (this.ALLOW_V2) versions['2'] = true
+    if (this.ALLOW_V3) versions['3'] = true
+
+    // but we don't allow v1
+    // if (versions['1']) msg += '?'
+
+    var vs = Object.keys(versions)
+    if (vs.length) {
+      msg += 'v'
+      vs.forEach(function (v) {
+        if (v !== '1') msg += v
+      })
+      msg += '?'
+    }
+
+    this.io(msg)
+    this.trigger('status', [CONST.STATUS_SEND_QUERY])
+  }
+
+  OTR.prototype.sendMsg = function (msg, meta) {
+    if ( this.REQUIRE_ENCRYPTION ||
+         this.msgstate !== CONST.MSGSTATE_PLAINTEXT
+    ) {
+      msg = CryptoJS.enc.Utf8.parse(msg)
+      msg = msg.toString(CryptoJS.enc.Latin1)
+    }
+
+    switch (this.msgstate) {
+      case CONST.MSGSTATE_PLAINTEXT:
+        if (this.REQUIRE_ENCRYPTION) {
+          this.storedMgs.push({msg: msg, meta: meta})
+          this.sendQueryMsg()
+          return
+        }
+        if (this.SEND_WHITESPACE_TAG && !this.receivedPlaintext) {
+          msg += CONST.WHITESPACE_TAG  // 16 byte tag
+          if (this.ALLOW_V3) msg += CONST.WHITESPACE_TAG_V3
+          if (this.ALLOW_V2) msg += CONST.WHITESPACE_TAG_V2
+        }
+        break
+      case CONST.MSGSTATE_FINISHED:
+        this.storedMgs.push({msg: msg, meta: meta})
+        this.error('Message cannot be sent at this time.')
+        return
+      case CONST.MSGSTATE_ENCRYPTED:
+        msg = this.prepareMsg(msg)
+        break
+      default:
+        throw new Error('Unknown message state.')
+    }
+
+    if (msg) this.io(msg, meta)
+  }
+
+  OTR.prototype.receiveMsg = function (msg) {
+
+    // parse type
+    msg = Parse.parseMsg(this, msg)
+
+    if (!msg) return
+
+    switch (msg.cls) {
+      case 'error':
+        this.error(msg.msg)
+        return
+      case 'ake':
+        if ( msg.version === CONST.OTR_VERSION_3 &&
+          this.checkInstanceTags(msg.instance_tags)
+        ) return  // ignore
+        this.ake.handleAKE(msg)
+        return
+      case 'data':
+        if ( msg.version === CONST.OTR_VERSION_3 &&
+          this.checkInstanceTags(msg.instance_tags)
+        ) return  // ignore
+        msg.msg = this.handleDataMsg(msg)
+        msg.encrypted = true
+        break
+      case 'query':
+        if (this.msgstate === CONST.MSGSTATE_ENCRYPTED) this._akeInit()
+        this.doAKE(msg)
+        break
+      default:
+        // check for encrypted
+        if ( this.REQUIRE_ENCRYPTION ||
+             this.msgstate !== CONST.MSGSTATE_PLAINTEXT
+        ) this.error('Received an unencrypted message.')
+
+        // received a plaintext message
+        // stop sending the whitespace tag
+        this.receivedPlaintext = true
+
+        // received a whitespace tag
+        if (this.WHITESPACE_START_AKE && msg.ver.length > 0)
+          this.doAKE(msg)
+    }
+
+    if (msg.msg) this.trigger('ui', [msg.msg, !!msg.encrypted])
+  }
+
+  OTR.prototype.checkInstanceTags = function (it) {
+    var their_it = HLP.readLen(it.substr(0, 4))
+    var our_it = HLP.readLen(it.substr(4, 4))
+
+    if (our_it && our_it !== HLP.readLen(this.our_instance_tag))
+      return true
+
+    if (HLP.readLen(this.their_instance_tag)) {
+      if (HLP.readLen(this.their_instance_tag) !== their_it) return true
+    } else {
+      if (their_it < 100) return true
+      this.their_instance_tag = HLP.packINT(their_it)
+    }
+  }
+
+  OTR.prototype.doAKE = function (msg) {
+    if (this.ALLOW_V3 && ~msg.ver.indexOf(CONST.OTR_VERSION_3)) {
+      this.ake.initiateAKE(CONST.OTR_VERSION_3)
+    } else if (this.ALLOW_V2 && ~msg.ver.indexOf(CONST.OTR_VERSION_2)) {
+      this.ake.initiateAKE(CONST.OTR_VERSION_2)
+    } else {
+      // is this an error?
+      this.error('OTR conversation requested, ' +
+        'but no compatible protocol version found.')
+    }
+  }
+
+  OTR.prototype.error = function (err, send) {
+    if (send) {
+      if (!this.debug) err = "An OTR error has occurred."
+      err = '?OTR Error:' + err
+      this.io(err)
+      return
+    }
+    this.trigger('error', [err])
+  }
+
+  OTR.prototype.sendStored = function () {
+    var self = this
+    ;(this.storedMgs.splice(0)).forEach(function (elem) {
+      var msg = self.prepareMsg(elem.msg)
+      self.io(msg, elem.meta)
+    })
+  }
+
+  OTR.prototype.sendFile = function (filename) {
+    if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED)
+      return this.error('Not ready to encrypt.')
+
+    if (this.ake.otr_version !== CONST.OTR_VERSION_3)
+      return this.error('Protocol v3 required.')
+
+    if (!filename) return this.error('Please specify a filename.')
+
+    // utf8 filenames
+    var l1name = CryptoJS.enc.Utf8.parse(filename)
+    l1name = l1name.toString(CryptoJS.enc.Latin1)
+
+    if (l1name.length >= 65532) return this.error('filename is too long.')
+
+    var msg = '\x00'  // null byte
+    msg += '\x00\x08'  // type 8 tlv
+    msg += HLP.packSHORT(4 + l1name.length)  // length of value
+    msg += '\x00\x00\x00\x01'  // four bytes indicating file
+    msg += l1name
+
+    msg = this.prepareMsg(msg, filename)
+    this.io(msg)
+  }
+
+  OTR.prototype.endOtr = function () {
+    if (this.msgstate === CONST.MSGSTATE_ENCRYPTED) {
+      this.sendMsg('\x00\x00\x01\x00\x00')
+      if (this.sm) {
+        if (this.smw) this.sm.worker.terminate()  // destroy webworker
+        this.sm = null
+      }
+    }
+    this.msgstate = CONST.MSGSTATE_PLAINTEXT
+    this.receivedPlaintext = false
+    this.trigger('status', [CONST.STATUS_END_OTR])
+  }
+
+  // attach methods
+
+  OTR.makeInstanceTag = function () {
+    var num = BigInt.randBigInt(32)
+    if (BigInt.greater(BigInt.str2bigInt('100', 16), num))
+      return OTR.makeInstanceTag()
+    return HLP.packINT(parseInt(BigInt.bigInt2str(num, 10), 10))
+  }
+
+}).call(this)
+
+
+  return {
+      OTR: this.OTR
+    , DSA: this.DSA
+  }
+
+}))
\ No newline at end of file
diff --git a/src/core/js/lib/salsa20.js b/src/core/js/lib/salsa20.js
new file mode 100644
index 0000000..d8495d3
--- /dev/null
+++ b/src/core/js/lib/salsa20.js
@@ -0,0 +1,284 @@
+// Salsa20 implementation
+// Contributed by Dmitry Chestnykh
+// 21-01-2013
+
+var Salsa20
+
+(function () {
+  'use strict';
+  Salsa20 = function (key, nonce) {
+		// Constants.
+		this.rounds = 20 // number of Salsa rounds
+		this.sigmaWords = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
+
+		// State.
+		this.keyWords = []		   // key words
+		this.nonceWords = [0, 0]	 // nonce words
+		this.counterWords = [0, 0]   // block counter words
+
+		// Output buffer.
+		this.block = []		// output block of 64 bytes
+		this.blockUsed = 64	 // number of block bytes used
+
+		this.setKey(key)
+		this.setNonce(nonce)
+	}
+
+	// setKey sets the key to the given 32-byte array.
+	Salsa20.prototype.setKey = function(key) {
+		for (var i = 0, j = 0; i < 8; i++, j += 4) {
+			this.keyWords[i] = (key[j] & 0xff)		|
+							  ((key[j+1] & 0xff)<<8)  |
+							  ((key[j+2] & 0xff)<<16) |
+							  ((key[j+3] & 0xff)<<24)
+		}
+		this._reset()
+	}
+
+	// setNonce sets the nonce to the given 8-byte array.
+	Salsa20.prototype.setNonce = function(nonce) {
+		this.nonceWords[0] = (nonce[0] & 0xff)	  |
+							((nonce[1] & 0xff)<<8)  |
+							((nonce[2] & 0xff)<<16) |
+							((nonce[3] & 0xff)<<24)
+		this.nonceWords[1] = (nonce[4] & 0xff)	  |
+							((nonce[5] & 0xff)<<8)  |
+							((nonce[6] & 0xff)<<16) |
+							((nonce[7] & 0xff)<<24)
+		this._reset()
+	}
+
+	// getBytes returns the next numberOfBytes bytes of stream.
+	Salsa20.prototype.getBytes = function(numberOfBytes) {
+		var out = new Array(numberOfBytes)
+		for (var i = 0; i < numberOfBytes; i++) {
+			if (this.blockUsed === 64) {
+				this._generateBlock()
+				this._incrementCounter()
+				this.blockUsed = 0
+			}
+			out[i] = this.block[this.blockUsed]
+			this.blockUsed++
+		}
+		return out
+	}
+
+	Salsa20.prototype.getHexString = function(numberOfBytes) {
+		var hex=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']
+		var out = []
+		var bytes = this.getBytes(numberOfBytes)
+		for(var i = 0; i < bytes.length; i++) {
+			out.push(hex[(bytes[i] >> 4) & 15])
+			out.push(hex[bytes[i] & 15])
+		}
+		return out.join('')
+	}
+
+	// Private methods.
+	Salsa20.prototype._reset = function() {
+		this.counterWords[0] = 0
+		this.counterWords[1] = 0
+		this.blockUsed = 64
+	}
+
+	// _incrementCounter increments block counter.
+	Salsa20.prototype._incrementCounter = function() {
+		// Note: maximum 2^64 blocks.
+		this.counterWords[0] = (this.counterWords[0] + 1) & 0xffffffff
+		if (this.counterWords[0] === 0) {
+			this.counterWords[1] = (this.counterWords[1] + 1) & 0xffffffff
+		}
+	}
+
+	// _generateBlock generates 64 bytes from key, nonce, and counter,
+	// and puts the result into this.block.
+	Salsa20.prototype._generateBlock = function() {
+		var j0 = this.sigmaWords[0],
+			j1 = this.keyWords[0],
+			j2 = this.keyWords[1],
+			j3 = this.keyWords[2],
+			j4 = this.keyWords[3],
+			j5 = this.sigmaWords[1],
+			j6 = this.nonceWords[0],
+			j7 = this.nonceWords[1],
+			j8 = this.counterWords[0],
+			j9 = this.counterWords[1],
+			j10 = this.sigmaWords[2],
+			j11 = this.keyWords[4],
+			j12 = this.keyWords[5],
+			j13 = this.keyWords[6],
+			j14 = this.keyWords[7],
+			j15 = this.sigmaWords[3]
+
+			var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7,
+				x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, x15 = j15
+
+			var u
+
+			for (var i = 0; i < this.rounds; i += 2) {
+				u = x0 + x12
+				x4 ^= (u<<7) | (u>>>(32-7))
+				u = x4 + x0
+				x8 ^= (u<<9) | (u>>>(32-9))
+				u = x8 + x4
+				x12 ^= (u<<13) | (u>>>(32-13))
+				u = x12 + x8
+				x0 ^= (u<<18) | (u>>>(32-18))
+
+				u = x5 + x1
+				x9 ^= (u<<7) | (u>>>(32-7))
+				u = x9 + x5
+				x13 ^= (u<<9) | (u>>>(32-9))
+				u = x13 + x9
+				x1 ^= (u<<13) | (u>>>(32-13))
+				u = x1 + x13
+				x5 ^= (u<<18) | (u>>>(32-18))
+
+				u = x10 + x6
+				x14 ^= (u<<7) | (u>>>(32-7))
+				u = x14 + x10
+				x2 ^= (u<<9) | (u>>>(32-9))
+				u = x2 + x14
+				x6 ^= (u<<13) | (u>>>(32-13))
+				u = x6 + x2
+				x10 ^= (u<<18) | (u>>>(32-18))
+
+				u = x15 + x11
+				x3 ^= (u<<7) | (u>>>(32-7))
+				u = x3 + x15
+				x7 ^= (u<<9) | (u>>>(32-9))
+				u = x7 + x3
+				x11 ^= (u<<13) | (u>>>(32-13))
+				u = x11 + x7
+				x15 ^= (u<<18) | (u>>>(32-18))
+
+				u = x0 + x3
+				x1 ^= (u<<7) | (u>>>(32-7))
+				u = x1 + x0
+				x2 ^= (u<<9) | (u>>>(32-9))
+				u = x2 + x1
+				x3 ^= (u<<13) | (u>>>(32-13))
+				u = x3 + x2
+				x0 ^= (u<<18) | (u>>>(32-18))
+
+				u = x5 + x4
+				x6 ^= (u<<7) | (u>>>(32-7))
+				u = x6 + x5
+				x7 ^= (u<<9) | (u>>>(32-9))
+				u = x7 + x6
+				x4 ^= (u<<13) | (u>>>(32-13))
+				u = x4 + x7
+				x5 ^= (u<<18) | (u>>>(32-18))
+
+				u = x10 + x9
+				x11 ^= (u<<7) | (u>>>(32-7))
+				u = x11 + x10
+				x8 ^= (u<<9) | (u>>>(32-9))
+				u = x8 + x11
+				x9 ^= (u<<13) | (u>>>(32-13))
+				u = x9 + x8
+				x10 ^= (u<<18) | (u>>>(32-18))
+
+				u = x15 + x14
+				x12 ^= (u<<7) | (u>>>(32-7))
+				u = x12 + x15
+				x13 ^= (u<<9) | (u>>>(32-9))
+				u = x13 + x12
+				x14 ^= (u<<13) | (u>>>(32-13))
+				u = x14 + x13
+				x15 ^= (u<<18) | (u>>>(32-18))
+			}
+
+			x0 += j0
+			x1 += j1
+			x2 += j2
+			x3 += j3
+			x4 += j4
+			x5 += j5
+			x6 += j6
+			x7 += j7
+			x8 += j8
+			x9 += j9
+			x10 += j10
+			x11 += j11
+			x12 += j12
+			x13 += j13
+			x14 += j14
+			x15 += j15
+
+			this.block[ 0] = ( x0 >>>  0) & 0xff
+			this.block[ 1] = ( x0 >>>  8) & 0xff
+			this.block[ 2] = ( x0 >>> 16) & 0xff
+			this.block[ 3] = ( x0 >>> 24) & 0xff
+			this.block[ 4] = ( x1 >>>  0) & 0xff
+			this.block[ 5] = ( x1 >>>  8) & 0xff
+			this.block[ 6] = ( x1 >>> 16) & 0xff
+			this.block[ 7] = ( x1 >>> 24) & 0xff
+			this.block[ 8] = ( x2 >>>  0) & 0xff
+			this.block[ 9] = ( x2 >>>  8) & 0xff
+			this.block[10] = ( x2 >>> 16) & 0xff
+			this.block[11] = ( x2 >>> 24) & 0xff
+			this.block[12] = ( x3 >>>  0) & 0xff
+			this.block[13] = ( x3 >>>  8) & 0xff
+			this.block[14] = ( x3 >>> 16) & 0xff
+			this.block[15] = ( x3 >>> 24) & 0xff
+			this.block[16] = ( x4 >>>  0) & 0xff
+			this.block[17] = ( x4 >>>  8) & 0xff
+			this.block[18] = ( x4 >>> 16) & 0xff
+			this.block[19] = ( x4 >>> 24) & 0xff
+			this.block[20] = ( x5 >>>  0) & 0xff
+			this.block[21] = ( x5 >>>  8) & 0xff
+			this.block[22] = ( x5 >>> 16) & 0xff
+			this.block[23] = ( x5 >>> 24) & 0xff
+			this.block[24] = ( x6 >>>  0) & 0xff
+			this.block[25] = ( x6 >>>  8) & 0xff
+			this.block[26] = ( x6 >>> 16) & 0xff
+			this.block[27] = ( x6 >>> 24) & 0xff
+			this.block[28] = ( x7 >>>  0) & 0xff
+			this.block[29] = ( x7 >>>  8) & 0xff
+			this.block[30] = ( x7 >>> 16) & 0xff
+			this.block[31] = ( x7 >>> 24) & 0xff
+			this.block[32] = ( x8 >>>  0) & 0xff
+			this.block[33] = ( x8 >>>  8) & 0xff
+			this.block[34] = ( x8 >>> 16) & 0xff
+			this.block[35] = ( x8 >>> 24) & 0xff
+			this.block[36] = ( x9 >>>  0) & 0xff
+			this.block[37] = ( x9 >>>  8) & 0xff
+			this.block[38] = ( x9 >>> 16) & 0xff
+			this.block[39] = ( x9 >>> 24) & 0xff
+			this.block[40] = (x10 >>>  0) & 0xff
+			this.block[41] = (x10 >>>  8) & 0xff
+			this.block[42] = (x10 >>> 16) & 0xff
+			this.block[43] = (x10 >>> 24) & 0xff
+			this.block[44] = (x11 >>>  0) & 0xff
+			this.block[45] = (x11 >>>  8) & 0xff
+			this.block[46] = (x11 >>> 16) & 0xff
+			this.block[47] = (x11 >>> 24) & 0xff
+			this.block[48] = (x12 >>>  0) & 0xff
+			this.block[49] = (x12 >>>  8) & 0xff
+			this.block[50] = (x12 >>> 16) & 0xff
+			this.block[51] = (x12 >>> 24) & 0xff
+			this.block[52] = (x13 >>>  0) & 0xff
+			this.block[53] = (x13 >>>  8) & 0xff
+			this.block[54] = (x13 >>> 16) & 0xff
+			this.block[55] = (x13 >>> 24) & 0xff
+			this.block[56] = (x14 >>>  0) & 0xff
+			this.block[57] = (x14 >>>  8) & 0xff
+			this.block[58] = (x14 >>> 16) & 0xff
+			this.block[59] = (x14 >>> 24) & 0xff
+			this.block[60] = (x15 >>>  0) & 0xff
+			this.block[61] = (x15 >>>  8) & 0xff
+			this.block[62] = (x15 >>> 16) & 0xff
+			this.block[63] = (x15 >>> 24) & 0xff
+   }
+
+   // CommonJS
+	if (typeof module ===  'object' && module.exports) {
+	   module.exports = Salsa20
+	}
+   // Browser
+	else if (typeof global === 'object') {
+		global.Salsa20 = Salsa20
+	}
+
+})()
diff --git a/src/core/js/lib/strophe/strophe.ibb.js b/src/core/js/lib/strophe/strophe.ibb.js
new file mode 100644
index 0000000..e2a9014
--- /dev/null
+++ b/src/core/js/lib/strophe/strophe.ibb.js
@@ -0,0 +1,166 @@
+/*global Strophe $iq $ */
+/*
+
+  (c) 2013 - Arlo Breault <arlolra at gmail.com>
+  Freely distributed under the MPL v2.0 license.
+
+  File: strophe.ibb.js
+  XEP-0047: In-Band Bytestreams
+  http://xmpp.org/extensions/xep-0047.html
+
+*/
+
+;(function () {
+  "use strict";
+
+  function noop() {}
+
+  Strophe.addConnectionPlugin('ibb', {
+
+    _c: null,
+    _cb: null,
+
+    init: function (c) {  
+      this._c = c;
+      Strophe.addNamespace('IBB', 'http://jabber.org/protocol/ibb');
+      c.addHandler(this._receive.bind(this), Strophe.NS.IBB, 'iq', 'set');
+    },
+
+    _createErr: function (to, id, type, name) {
+      var iq = $iq({
+        type: 'error',
+        to: to,
+        id: id
+      }).c('error', {
+        type: type
+      }).c(name, {
+        xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
+      })
+      return iq;
+    },
+
+    _receive: function (m) {
+
+      var $m = $(m);
+      var from = $m.attr('from');
+      var id = $m.attr('id')
+
+      // support ibb?
+      // proceed?
+      // prefer smaller chunks?
+
+      var iq = $iq({
+        type: 'result',
+        to: from,
+        id: id
+      });
+      this._send(iq, noop, noop);
+
+      var child = $m.children().get(0);
+      var type = child.tagName.toLowerCase();
+      var sid = $(child).attr('sid');
+
+      var data, seq;
+      if (type === 'data') {
+        data = $(child).text();
+        seq = $(child).attr('seq');
+      }
+
+      // callback message
+      if (typeof this._cb === 'function') {
+        this._cb(type, from, sid, data, seq);
+      }
+
+      return true;  // keep handler active
+
+    },
+
+    _success: function (cb) {
+      cb(null);
+    },
+
+    _fail: function (cb, stanza) {
+      var err = 'timed out';
+      if (stanza) {
+        err = $('error', stanza)
+                .children()
+                .get(0)
+                .tagName
+                .toLowerCase();
+      }
+      cb(new Error(err));
+    },
+
+    _send: function (iq, success, fail) {
+      this._c.sendIQ(iq, success, fail, 60 * 1000);
+    },
+
+    open: function (to, sid, bs, cb) {
+
+      if (parseInt(bs ? bs : 0, 10) > 65535)
+        return cb(new Error('Block-size too large.'))
+
+      // construct iq
+      var iq = $iq({
+        type: 'set',
+        to: to,
+        id: this._c.getUniqueId('ibb')
+      }).c('open', {
+        xmlns: Strophe.NS.IBB,
+        stanza: 'iq',
+        sid: sid,
+        'block-size': bs || '4096'
+      });
+
+      this._send(iq,
+        this._success.bind(this, cb),
+        this._fail.bind(this, cb)
+      );
+
+    },
+
+    data: function (to, sid, seq, data, cb) {
+
+      var iq = $iq({
+        type: 'set',
+        to: to,
+        id: this._c.getUniqueId('ibb')
+      }).c('data', {
+        xmlns: Strophe.NS.IBB,
+        seq: seq.toString(),
+        sid: sid
+      }).t(data);
+
+      this._send(iq,
+        this._success.bind(this, cb),
+        this._fail.bind(this, cb)
+      );
+
+    },
+
+    close: function (to, sid, cb) {
+
+      // construct iq
+      var iq = $iq({
+        type: 'set',
+        to: to,
+        id: this._c.getUniqueId('ibb')
+      }).c('close', {
+        xmlns: Strophe.NS.IBB,
+        sid: sid
+      });
+
+      this._send(iq,
+        this._success.bind(this, cb),
+        this._fail.bind(this, cb)
+      );
+
+    },
+
+    addIBBHandler: function (fn) {
+      this._cb = fn;
+    }
+
+  });
+
+}());
\ No newline at end of file
diff --git a/src/core/js/lib/strophe/strophe.js b/src/core/js/lib/strophe/strophe.js
new file mode 100644
index 0000000..5bee382
--- /dev/null
+++ b/src/core/js/lib/strophe/strophe.js
@@ -0,0 +1,4201 @@
+// This code was written by Tyler Akins and has been placed in the
+// public domain.  It would be nice if you left this header intact.
+// Base64 code from Tyler Akins -- http://rumkin.com
+
+var Base64 = (function () {
+    var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+    var obj = {
+        /**
+         * Encodes a string in base64
+         * @param {String} input The string to encode in base64.
+         */
+        encode: function (input) {
+            var output = "";
+            var chr1, chr2, chr3;
+            var enc1, enc2, enc3, enc4;
+            var i = 0;
+
+            do {
+                chr1 = input.charCodeAt(i++);
+                chr2 = input.charCodeAt(i++);
+                chr3 = input.charCodeAt(i++);
+
+                enc1 = chr1 >> 2;
+                enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+                enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+                enc4 = chr3 & 63;
+
+                if (isNaN(chr2)) {
+                    enc3 = enc4 = 64;
+                } else if (isNaN(chr3)) {
+                    enc4 = 64;
+                }
+
+                output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
+                    keyStr.charAt(enc3) + keyStr.charAt(enc4);
+            } while (i < input.length);
+
+            return output;
+        },
+
+        /**
+         * Decodes a base64 string.
+         * @param {String} input The string to decode.
+         */
+        decode: function (input) {
+            var output = "";
+            var chr1, chr2, chr3;
+            var enc1, enc2, enc3, enc4;
+            var i = 0;
+
+            // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+            input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+            do {
+                enc1 = keyStr.indexOf(input.charAt(i++));
+                enc2 = keyStr.indexOf(input.charAt(i++));
+                enc3 = keyStr.indexOf(input.charAt(i++));
+                enc4 = keyStr.indexOf(input.charAt(i++));
+
+                chr1 = (enc1 << 2) | (enc2 >> 4);
+                chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+                chr3 = ((enc3 & 3) << 6) | enc4;
+
+                output = output + String.fromCharCode(chr1);
+
+                if (enc3 != 64) {
+                    output = output + String.fromCharCode(chr2);
+                }
+                if (enc4 != 64) {
+                    output = output + String.fromCharCode(chr3);
+                }
+            } while (i < input.length);
+
+            return output;
+        }
+    };
+
+    return obj;
+})();
+/*
+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
+ * in FIPS PUB 180-1
+ * Version 2.1a Copyright Paul Johnston 2000 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for details.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
+var b64pad  = "="; /* base-64 pad character. "=" for strict RFC compliance   */
+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
+function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
+function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
+function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
+function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
+function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function sha1_vm_test()
+{
+  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
+}
+
+/*
+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
+ */
+function core_sha1(x, len)
+{
+  /* append padding */
+  x[len >> 5] |= 0x80 << (24 - len % 32);
+  x[((len + 64 >> 9) << 4) + 15] = len;
+
+  var w = new Array(80);
+  var a =  1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d =  271733878;
+  var e = -1009589776;
+
+  var i, j, t, olda, oldb, oldc, oldd, olde;
+  for (i = 0; i < x.length; i += 16)
+  {
+    olda = a;
+    oldb = b;
+    oldc = c;
+    oldd = d;
+    olde = e;
+
+    for (j = 0; j < 80; j++)
+    {
+      if (j < 16) { w[j] = x[i + j]; }
+      else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); }
+      t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
+                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
+      e = d;
+      d = c;
+      c = rol(b, 30);
+      b = a;
+      a = t;
+    }
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+    e = safe_add(e, olde);
+  }
+  return [a, b, c, d, e];
+}
+
+/*
+ * Perform the appropriate triplet combination function for the current
+ * iteration
+ */
+function sha1_ft(t, b, c, d)
+{
+  if (t < 20) { return (b & c) | ((~b) & d); }
+  if (t < 40) { return b ^ c ^ d; }
+  if (t < 60) { return (b & c) | (b & d) | (c & d); }
+  return b ^ c ^ d;
+}
+
+/*
+ * Determine the appropriate additive constant for the current iteration
+ */
+function sha1_kt(t)
+{
+  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
+         (t < 60) ? -1894007588 : -899497514;
+}
+
+/*
+ * Calculate the HMAC-SHA1 of a key and some data
+ */
+function core_hmac_sha1(key, data)
+{
+  var bkey = str2binb(key);
+  if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); }
+
+  var ipad = new Array(16), opad = new Array(16);
+  for (var i = 0; i < 16; i++)
+  {
+    ipad[i] = bkey[i] ^ 0x36363636;
+    opad[i] = bkey[i] ^ 0x5C5C5C5C;
+  }
+
+  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
+  return core_sha1(opad.concat(hash), 512 + 160);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function rol(num, cnt)
+{
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+ * Convert an 8-bit or 16-bit string to an array of big-endian words
+ * In 8-bit function, characters >255 have their hi-byte silently ignored.
+ */
+function str2binb(str)
+{
+  var bin = [];
+  var mask = (1 << chrsz) - 1;
+  for (var i = 0; i < str.length * chrsz; i += chrsz)
+  {
+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
+  }
+  return bin;
+}
+
+/*
+ * Convert an array of big-endian words to a string
+ */
+function binb2str(bin)
+{
+  var str = "";
+  var mask = (1 << chrsz) - 1;
+  for (var i = 0; i < bin.length * 32; i += chrsz)
+  {
+    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
+  }
+  return str;
+}
+
+/*
+ * Convert an array of big-endian words to a hex string.
+ */
+function binb2hex(binarray)
+{
+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+  var str = "";
+  for (var i = 0; i < binarray.length * 4; i++)
+  {
+    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
+           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
+  }
+  return str;
+}
+
+/*
+ * Convert an array of big-endian words to a base-64 string
+ */
+function binb2b64(binarray)
+{
+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  var str = "";
+  var triplet, j;
+  for (var i = 0; i < binarray.length * 4; i += 3)
+  {
+    triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16) |
+              (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) |
+               ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
+    for (j = 0; j < 4; j++)
+    {
+      if (i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
+      else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
+    }
+  }
+  return str;
+}
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+var MD5 = (function () {
+    /*
+     * Configurable variables. You may need to tweak these to be compatible with
+     * the server-side, but the defaults work in most cases.
+     */
+    var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase */
+    var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance */
+    var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode */
+
+    /*
+     * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+     * to work around bugs in some JS interpreters.
+     */
+    var safe_add = function (x, y) {
+        var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+        var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+        return (msw << 16) | (lsw & 0xFFFF);
+    };
+
+    /*
+     * Bitwise rotate a 32-bit number to the left.
+     */
+    var bit_rol = function (num, cnt) {
+        return (num << cnt) | (num >>> (32 - cnt));
+    };
+
+    /*
+     * Convert a string to an array of little-endian words
+     * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
+     */
+    var str2binl = function (str) {
+        var bin = [];
+        var mask = (1 << chrsz) - 1;
+        for(var i = 0; i < str.length * chrsz; i += chrsz)
+        {
+            bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
+        }
+        return bin;
+    };
+
+    /*
+     * Convert an array of little-endian words to a string
+     */
+    var binl2str = function (bin) {
+        var str = "";
+        var mask = (1 << chrsz) - 1;
+        for(var i = 0; i < bin.length * 32; i += chrsz)
+        {
+            str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
+        }
+        return str;
+    };
+
+    /*
+     * Convert an array of little-endian words to a hex string.
+     */
+    var binl2hex = function (binarray) {
+        var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+        var str = "";
+        for(var i = 0; i < binarray.length * 4; i++)
+        {
+            str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
+                hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
+        }
+        return str;
+    };
+
+    /*
+     * Convert an array of little-endian words to a base-64 string
+     */
+    var binl2b64 = function (binarray) {
+        var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+        var str = "";
+        var triplet, j;
+        for(var i = 0; i < binarray.length * 4; i += 3)
+        {
+            triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16) |
+                (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
+                ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
+            for(j = 0; j < 4; j++)
+            {
+                if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
+                else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
+            }
+        }
+        return str;
+    };
+
+    /*
+     * These functions implement the four basic operations the algorithm uses.
+     */
+    var md5_cmn = function (q, a, b, x, s, t) {
+        return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
+    };
+
+    var md5_ff = function (a, b, c, d, x, s, t) {
+        return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+    };
+
+    var md5_gg = function (a, b, c, d, x, s, t) {
+        return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+    };
+
+    var md5_hh = function (a, b, c, d, x, s, t) {
+        return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+    };
+
+    var md5_ii = function (a, b, c, d, x, s, t) {
+        return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+    };
+
+    /*
+     * Calculate the MD5 of an array of little-endian words, and a bit length
+     */
+    var core_md5 = function (x, len) {
+        /* append padding */
+        x[len >> 5] |= 0x80 << ((len) % 32);
+        x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+        var a =  1732584193;
+        var b = -271733879;
+        var c = -1732584194;
+        var d =  271733878;
+
+        var olda, oldb, oldc, oldd;
+        for (var i = 0; i < x.length; i += 16)
+        {
+            olda = a;
+            oldb = b;
+            oldc = c;
+            oldd = d;
+
+            a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+            d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+            c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
+            b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+            a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+            d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
+            c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+            b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+            a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
+            d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+            c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+            b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+            a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
+            d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+            c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+            b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
+
+            a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+            d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+            c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
+            b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+            a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+            d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
+            c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+            b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+            a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
+            d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+            c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+            b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
+            a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+            d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+            c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
+            b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+            a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+            d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+            c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
+            b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+            a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+            d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
+            c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+            b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+            a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
+            d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+            c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+            b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
+            a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+            d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+            c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
+            b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+            a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+            d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
+            c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+            b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+            a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
+            d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+            c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+            b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+            a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
+            d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+            c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+            b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
+            a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+            d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+            c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
+            b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+            a = safe_add(a, olda);
+            b = safe_add(b, oldb);
+            c = safe_add(c, oldc);
+            d = safe_add(d, oldd);
+        }
+        return [a, b, c, d];
+    };
+
+
+    /*
+     * Calculate the HMAC-MD5, of a key and some data
+     */
+    var core_hmac_md5 = function (key, data) {
+        var bkey = str2binl(key);
+        if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
+
+        var ipad = new Array(16), opad = new Array(16);
+        for(var i = 0; i < 16; i++)
+        {
+            ipad[i] = bkey[i] ^ 0x36363636;
+            opad[i] = bkey[i] ^ 0x5C5C5C5C;
+        }
+
+        var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
+        return core_md5(opad.concat(hash), 512 + 128);
+    };
+
+    var obj = {
+        /*
+         * These are the functions you'll usually want to call.
+         * They take string arguments and return either hex or base-64 encoded
+         * strings.
+         */
+        hexdigest: function (s) {
+            return binl2hex(core_md5(str2binl(s), s.length * chrsz));
+        },
+
+        b64digest: function (s) {
+            return binl2b64(core_md5(str2binl(s), s.length * chrsz));
+        },
+
+        hash: function (s) {
+            return binl2str(core_md5(str2binl(s), s.length * chrsz));
+        },
+
+        hmac_hexdigest: function (key, data) {
+            return binl2hex(core_hmac_md5(key, data));
+        },
+
+        hmac_b64digest: function (key, data) {
+            return binl2b64(core_hmac_md5(key, data));
+        },
+
+        hmac_hash: function (key, data) {
+            return binl2str(core_hmac_md5(key, data));
+        },
+
+        /*
+         * Perform a simple self-test to see if the VM is working
+         */
+        test: function () {
+            return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
+        }
+    };
+
+    return obj;
+})();
+/*
+    This program is distributed under the terms of the MIT license.
+    Please see the LICENSE file for details.
+
+    Copyright 2006-2008, OGG, LLC
+*/
+
+/* jslint configuration: */
+/*global document, window, setTimeout, clearTimeout, console,
+    XMLHttpRequest, ActiveXObject,
+    Base64, MD5,
+    Strophe, $build, $msg, $iq, $pres */
+
+/** File: strophe.js
+ *  A JavaScript library for XMPP BOSH.
+ *
+ *  This is the JavaScript version of the Strophe library.  Since JavaScript
+ *  has no facilities for persistent TCP connections, this library uses
+ *  Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate
+ *  a persistent, stateful, two-way connection to an XMPP server.  More
+ *  information on BOSH can be found in XEP 124.
+ */
+
+/** PrivateFunction: Function.prototype.bind
+ *  Bind a function to an instance.
+ *
+ *  This Function object extension method creates a bound method similar
+ *  to those in Python.  This means that the 'this' object will point
+ *  to the instance you want.  See
+ *  <a href='https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind'>MDC's bind() documentation</a> and
+ *  <a href='http://benjamin.smedbergs.us/blog/2007-01-03/bound-functions-and-function-imports-in-javascript/'>Bound Functions and Function Imports in JavaScript</a>
+ *  for a complete explanation.
+ *
+ *  This extension already exists in some browsers (namely, Firefox 3), but
+ *  we provide it to support those that don't.
+ *
+ *  Parameters:
+ *    (Object) obj - The object that will become 'this' in the bound function.
+ *    (Object) argN - An option argument that will be prepended to the
+ *      arguments given for the function call
+ *
+ *  Returns:
+ *    The bound function.
+ */
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (obj /*, arg1, arg2, ... */)
+    {
+        var func = this;
+        var _slice = Array.prototype.slice;
+        var _concat = Array.prototype.concat;
+        var _args = _slice.call(arguments, 1);
+
+        return function () {
+            return func.apply(obj ? obj : this,
+                              _concat.call(_args,
+                                           _slice.call(arguments, 0)));
+        };
+    };
+}
+
+/** PrivateFunction: Array.prototype.indexOf
+ *  Return the index of an object in an array.
+ *
+ *  This function is not supplied by some JavaScript implementations, so
+ *  we provide it if it is missing.  This code is from:
+ *  http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
+ *
+ *  Parameters:
+ *    (Object) elt - The object to look for.
+ *    (Integer) from - The index from which to start looking. (optional).
+ *
+ *  Returns:
+ *    The index of elt in the array or -1 if not found.
+ */
+if (!Array.prototype.indexOf)
+{
+    Array.prototype.indexOf = function(elt /*, from*/)
+    {
+        var len = this.length;
+
+        var from = Number(arguments[1]) || 0;
+        from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+        if (from < 0) {
+            from += len;
+        }
+
+        for (; from < len; from++) {
+            if (from in this && this[from] === elt) {
+                return from;
+            }
+        }
+
+        return -1;
+    };
+}
+
+/* All of the Strophe globals are defined in this special function below so
+ * that references to the globals become closures.  This will ensure that
+ * on page reload, these references will still be available to callbacks
+ * that are still executing.
+ */
+
+(function (callback) {
+var Strophe;
+
+/** Function: $build
+ *  Create a Strophe.Builder.
+ *  This is an alias for 'new Strophe.Builder(name, attrs)'.
+ *
+ *  Parameters:
+ *    (String) name - The root element name.
+ *    (Object) attrs - The attributes for the root element in object notation.
+ *
+ *  Returns:
+ *    A new Strophe.Builder object.
+ */
+function $build(name, attrs) { return new Strophe.Builder(name, attrs); }
+/** Function: $msg
+ *  Create a Strophe.Builder with a <message/> element as the root.
+ *
+ *  Parmaeters:
+ *    (Object) attrs - The <message/> element attributes in object notation.
+ *
+ *  Returns:
+ *    A new Strophe.Builder object.
+ */
+function $msg(attrs) { return new Strophe.Builder("message", attrs); }
+/** Function: $iq
+ *  Create a Strophe.Builder with an <iq/> element as the root.
+ *
+ *  Parameters:
+ *    (Object) attrs - The <iq/> element attributes in object notation.
+ *
+ *  Returns:
+ *    A new Strophe.Builder object.
+ */
+function $iq(attrs) { return new Strophe.Builder("iq", attrs); }
+/** Function: $pres
+ *  Create a Strophe.Builder with a <presence/> element as the root.
+ *
+ *  Parameters:
+ *    (Object) attrs - The <presence/> element attributes in object notation.
+ *
+ *  Returns:
+ *    A new Strophe.Builder object.
+ */
+function $pres(attrs) { return new Strophe.Builder("presence", attrs); }
+
+/** Class: Strophe
+ *  An object container for all Strophe library functions.
+ *
+ *  This class is just a container for all the objects and constants
+ *  used in the library.  It is not meant to be instantiated, but to
+ *  provide a namespace for library objects, constants, and functions.
+ */
+Strophe = {
+    /** Constant: VERSION
+     *  The version of the Strophe library. Unreleased builds will have
+     *  a version of head-HASH where HASH is a partial revision.
+     */
+    VERSION: "a8e5e94",
+
+    /** Constants: XMPP Namespace Constants
+     *  Common namespace constants from the XMPP RFCs and XEPs.
+     *
+     *  NS.HTTPBIND - HTTP BIND namespace from XEP 124.
+     *  NS.BOSH - BOSH namespace from XEP 206.
+     *  NS.CLIENT - Main XMPP client namespace.
+     *  NS.AUTH - Legacy authentication namespace.
+     *  NS.ROSTER - Roster operations namespace.
+     *  NS.PROFILE - Profile namespace.
+     *  NS.DISCO_INFO - Service discovery info namespace from XEP 30.
+     *  NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
+     *  NS.MUC - Multi-User Chat namespace from XEP 45.
+     *  NS.SASL - XMPP SASL namespace from RFC 3920.
+     *  NS.STREAM - XMPP Streams namespace from RFC 3920.
+     *  NS.BIND - XMPP Binding namespace from RFC 3920.
+     *  NS.SESSION - XMPP Session namespace from RFC 3920.
+     *  NS.XHTML_IM - XHTML-IM namespace from XEP 71.
+     *  NS.XHTML - XHTML body namespace from XEP 71.
+     */
+    NS: {
+        HTTPBIND: "http://jabber.org/protocol/httpbind",
+        BOSH: "urn:xmpp:xbosh",
+        CLIENT: "jabber:client",
+        AUTH: "jabber:iq:auth",
+        ROSTER: "jabber:iq:roster",
+        PROFILE: "jabber:iq:profile",
+        DISCO_INFO: "http://jabber.org/protocol/disco#info",
+        DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
+        MUC: "http://jabber.org/protocol/muc",
+        SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
+        STREAM: "http://etherx.jabber.org/streams",
+        BIND: "urn:ietf:params:xml:ns:xmpp-bind",
+        SESSION: "urn:ietf:params:xml:ns:xmpp-session",
+        VERSION: "jabber:iq:version",
+        STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas",
+        XHTML_IM: "http://jabber.org/protocol/xhtml-im",
+        XHTML: "http://www.w3.org/1999/xhtml"
+    },
+
+
+    /** Constants: XHTML_IM Namespace
+     *  contains allowed tags, tag attributes, and css properties.
+     *  Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset.
+     *  See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended
+     *  allowed tags and their attributes.
+     */
+    XHTML: {
+		tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'],
+		attributes: {
+			'a':          ['href'],
+			'blockquote': ['style'],
+			'br':         [],
+			'cite':       ['style'],
+			'em':         [],
+			'img':        ['src', 'alt', 'style', 'height', 'width'],
+			'li':         ['style'],
+			'ol':         ['style'],
+			'p':          ['style'],
+			'span':       ['style'],
+			'strong':     [],
+			'ul':         ['style'],
+			'body':       []
+		},
+		css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'],
+		validTag: function(tag)
+		{
+			for(var i = 0; i < Strophe.XHTML.tags.length; i++) {
+				if(tag == Strophe.XHTML.tags[i]) {
+					return true;
+				}
+			}
+			return false;
+		},
+		validAttribute: function(tag, attribute)
+		{
+			if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) {
+				for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) {
+					if(attribute == Strophe.XHTML.attributes[tag][i]) {
+						return true;
+					}
+				}
+			}
+			return false;
+		},
+		validCSS: function(style)
+		{
+			for(var i = 0; i < Strophe.XHTML.css.length; i++) {
+				if(style == Strophe.XHTML.css[i]) {
+					return true;
+				}
+			}
+			return false;
+		}
+    },
+
+    /** Function: addNamespace
+     *  This function is used to extend the current namespaces in
+     *	Strophe.NS.  It takes a key and a value with the key being the
+     *	name of the new namespace, with its actual value.
+     *	For example:
+     *	Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub");
+     *
+     *  Parameters:
+     *    (String) name - The name under which the namespace will be
+     *      referenced under Strophe.NS
+     *    (String) value - The actual namespace.
+     */
+    addNamespace: function (name, value)
+    {
+	    Strophe.NS[name] = value;
+    },
+
+    /** Constants: Connection Status Constants
+     *  Connection status constants for use by the connection handler
+     *  callback.
+     *
+     *  Status.ERROR - An error has occurred
+     *  Status.CONNECTING - The connection is currently being made
+     *  Status.CONNFAIL - The connection attempt failed
+     *  Status.AUTHENTICATING - The connection is authenticating
+     *  Status.AUTHFAIL - The authentication attempt failed
+     *  Status.CONNECTED - The connection has succeeded
+     *  Status.DISCONNECTED - The connection has been terminated
+     *  Status.DISCONNECTING - The connection is currently being terminated
+     *  Status.ATTACHED - The connection has been attached
+     */
+    Status: {
+        ERROR: 0,
+        CONNECTING: 1,
+        CONNFAIL: 2,
+        AUTHENTICATING: 3,
+        AUTHFAIL: 4,
+        CONNECTED: 5,
+        DISCONNECTED: 6,
+        DISCONNECTING: 7,
+        ATTACHED: 8
+    },
+
+    /** Constants: Log Level Constants
+     *  Logging level indicators.
+     *
+     *  LogLevel.DEBUG - Debug output
+     *  LogLevel.INFO - Informational output
+     *  LogLevel.WARN - Warnings
+     *  LogLevel.ERROR - Errors
+     *  LogLevel.FATAL - Fatal errors
+     */
+    LogLevel: {
+        DEBUG: 0,
+        INFO: 1,
+        WARN: 2,
+        ERROR: 3,
+        FATAL: 4
+    },
+
+    /** PrivateConstants: DOM Element Type Constants
+     *  DOM element types.
+     *
+     *  ElementType.NORMAL - Normal element.
+     *  ElementType.TEXT - Text data element.
+     *  ElementType.FRAGMENT - XHTML fragment element.
+     */
+    ElementType: {
+        NORMAL: 1,
+        TEXT: 3,
+        CDATA: 4,
+        FRAGMENT: 11
+    },
+
+    /** PrivateConstants: Timeout Values
+     *  Timeout values for error states.  These values are in seconds.
+     *  These should not be changed unless you know exactly what you are
+     *  doing.
+     *
+     *  TIMEOUT - Timeout multiplier. A waiting request will be considered
+     *      failed after Math.floor(TIMEOUT * wait) seconds have elapsed.
+     *      This defaults to 1.1, and with default wait, 66 seconds.
+     *  SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where
+     *      Strophe can detect early failure, it will consider the request
+     *      failed if it doesn't return after
+     *      Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed.
+     *      This defaults to 0.1, and with default wait, 6 seconds.
+     */
+    TIMEOUT: 1.1,
+    SECONDARY_TIMEOUT: 0.1,
+
+    /** Function: forEachChild
+     *  Map a function over some or all child elements of a given element.
+     *
+     *  This is a small convenience function for mapping a function over
+     *  some or all of the children of an element.  If elemName is null, all
+     *  children will be passed to the function, otherwise only children
+     *  whose tag names match elemName will be passed.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The element to operate on.
+     *    (String) elemName - The child element tag name filter.
+     *    (Function) func - The function to apply to each child.  This
+     *      function should take a single argument, a DOM element.
+     */
+    forEachChild: function (elem, elemName, func)
+    {
+        var i, childNode;
+
+        for (i = 0; i < elem.childNodes.length; i++) {
+            childNode = elem.childNodes[i];
+            if (childNode.nodeType == Strophe.ElementType.NORMAL &&
+                (!elemName || this.isTagEqual(childNode, elemName))) {
+                func(childNode);
+            }
+        }
+    },
+
+    /** Function: isTagEqual
+     *  Compare an element's tag name with a string.
+     *
+     *  This function is case insensitive.
+     *
+     *  Parameters:
+     *    (XMLElement) el - A DOM element.
+     *    (String) name - The element name.
+     *
+     *  Returns:
+     *    true if the element's tag name matches _el_, and false
+     *    otherwise.
+     */
+    isTagEqual: function (el, name)
+    {
+        return el.tagName.toLowerCase() == name.toLowerCase();
+    },
+
+    /** PrivateVariable: _xmlGenerator
+     *  _Private_ variable that caches a DOM document to
+     *  generate elements.
+     */
+    _xmlGenerator: null,
+
+    /** PrivateFunction: _makeGenerator
+     *  _Private_ function that creates a dummy XML DOM document to serve as
+     *  an element and text node generator.
+     */
+    _makeGenerator: function () {
+        var doc;
+
+        // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload.
+        // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be
+		// less than 10 in the case of IE9 and below.
+        if (document.implementation.createDocument === undefined ||
+			document.implementation.createDocument && document.documentMode && document.documentMode < 10) {
+            doc = this._getIEXmlDom();
+            doc.appendChild(doc.createElement('strophe'));
+        } else {
+            doc = document.implementation
+                .createDocument('jabber:client', 'strophe', null);
+        }
+
+        return doc;
+    },
+
+    /** Function: xmlGenerator
+     *  Get the DOM document to generate elements.
+     *
+     *  Returns:
+     *    The currently used DOM document.
+     */
+    xmlGenerator: function () {
+        if (!Strophe._xmlGenerator) {
+            Strophe._xmlGenerator = Strophe._makeGenerator();
+        }
+        return Strophe._xmlGenerator;
+    },
+
+    /** PrivateFunction: _getIEXmlDom
+     *  Gets IE xml doc object
+     *
+     *  Returns:
+     *    A Microsoft XML DOM Object
+     *  See Also:
+     *    http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx
+     */
+    _getIEXmlDom : function() {
+        var doc = null;
+        var docStrings = [
+            "Msxml2.DOMDocument.6.0",
+            "Msxml2.DOMDocument.5.0",
+            "Msxml2.DOMDocument.4.0",
+            "MSXML2.DOMDocument.3.0",
+            "MSXML2.DOMDocument",
+            "MSXML.DOMDocument",
+            "Microsoft.XMLDOM"
+        ];
+
+        for (var d = 0; d < docStrings.length; d++) {
+            if (doc === null) {
+                try {
+                    doc = new ActiveXObject(docStrings[d]);
+                } catch (e) {
+                    doc = null;
+                }
+            } else {
+                break;
+            }
+        }
+
+        return doc;
+    },
+
+    /** Function: xmlElement
+     *  Create an XML DOM element.
+     *
+     *  This function creates an XML DOM element correctly across all
+     *  implementations. Note that these are not HTML DOM elements, which
+     *  aren't appropriate for XMPP stanzas.
+     *
+     *  Parameters:
+     *    (String) name - The name for the element.
+     *    (Array|Object) attrs - An optional array or object containing
+     *      key/value pairs to use as element attributes. The object should
+     *      be in the format {'key': 'value'} or {key: 'value'}. The array
+     *      should have the format [['key1', 'value1'], ['key2', 'value2']].
+     *    (String) text - The text child data for the element.
+     *
+     *  Returns:
+     *    A new XML DOM element.
+     */
+    xmlElement: function (name)
+    {
+        if (!name) { return null; }
+
+        var node = Strophe.xmlGenerator().createElement(name);
+
+        // FIXME: this should throw errors if args are the wrong type or
+        // there are more than two optional args
+        var a, i, k;
+        for (a = 1; a < arguments.length; a++) {
+            if (!arguments[a]) { continue; }
+            if (typeof(arguments[a]) == "string" ||
+                typeof(arguments[a]) == "number") {
+                node.appendChild(Strophe.xmlTextNode(arguments[a]));
+            } else if (typeof(arguments[a]) == "object" &&
+                       typeof(arguments[a].sort) == "function") {
+                for (i = 0; i < arguments[a].length; i++) {
+                    if (typeof(arguments[a][i]) == "object" &&
+                        typeof(arguments[a][i].sort) == "function") {
+                        node.setAttribute(arguments[a][i][0],
+                                          arguments[a][i][1]);
+                    }
+                }
+            } else if (typeof(arguments[a]) == "object") {
+                for (k in arguments[a]) {
+                    if (arguments[a].hasOwnProperty(k)) {
+                        node.setAttribute(k, arguments[a][k]);
+                    }
+                }
+            }
+        }
+
+        return node;
+    },
+
+    /*  Function: xmlescape
+     *  Excapes invalid xml characters.
+     *
+     *  Parameters:
+     *     (String) text - text to escape.
+     *
+     *	Returns:
+     *      Escaped text.
+     */
+    xmlescape: function(text)
+    {
+        text = text.replace(/\&/g, "&");
+        text = text.replace(/</g,  "<");
+        text = text.replace(/>/g,  ">");
+        text = text.replace(/'/g,  "'");
+        text = text.replace(/"/g,  """);
+        return text;
+    },
+
+    /** Function: xmlTextNode
+     *  Creates an XML DOM text node.
+     *
+     *  Provides a cross implementation version of document.createTextNode.
+     *
+     *  Parameters:
+     *    (String) text - The content of the text node.
+     *
+     *  Returns:
+     *    A new XML DOM text node.
+     */
+    xmlTextNode: function (text)
+    {
+        return Strophe.xmlGenerator().createTextNode(text);
+    },
+
+    /** Function: xmlHtmlNode
+     *  Creates an XML DOM html node.
+     *
+     *  Parameters:
+     *    (String) html - The content of the html node.
+     *
+     *  Returns:
+     *    A new XML DOM text node.
+     */
+    xmlHtmlNode: function (html)
+    {
+        //ensure text is escaped
+        if (window.DOMParser) {
+            parser = new DOMParser();
+            node = parser.parseFromString(html, "text/xml");
+        } else {
+            node = new ActiveXObject("Microsoft.XMLDOM");
+            node.async="false";
+            node.loadXML(html);
+        }
+        return node;
+    },
+
+    /** Function: getText
+     *  Get the concatenation of all text children of an element.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - A DOM element.
+     *
+     *  Returns:
+     *    A String with the concatenated text of all text element children.
+     */
+    getText: function (elem)
+    {
+        if (!elem) { return null; }
+
+        var str = "";
+        if (elem.childNodes.length === 0 && elem.nodeType ==
+            Strophe.ElementType.TEXT) {
+            str += elem.nodeValue;
+        }
+
+        for (var i = 0; i < elem.childNodes.length; i++) {
+            if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) {
+                str += elem.childNodes[i].nodeValue;
+            }
+        }
+
+        return Strophe.xmlescape(str);
+    },
+
+    /** Function: copyElement
+     *  Copy an XML DOM element.
+     *
+     *  This function copies a DOM element and all its descendants and returns
+     *  the new copy.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - A DOM element.
+     *
+     *  Returns:
+     *    A new, copied DOM element tree.
+     */
+    copyElement: function (elem)
+    {
+        var i, el;
+        if (elem.nodeType == Strophe.ElementType.NORMAL) {
+            el = Strophe.xmlElement(elem.tagName);
+
+            for (i = 0; i < elem.attributes.length; i++) {
+                el.setAttribute(elem.attributes[i].nodeName.toLowerCase(),
+                                elem.attributes[i].value);
+            }
+
+            for (i = 0; i < elem.childNodes.length; i++) {
+                el.appendChild(Strophe.copyElement(elem.childNodes[i]));
+            }
+        } else if (elem.nodeType == Strophe.ElementType.TEXT) {
+            el = Strophe.xmlGenerator().createTextNode(elem.nodeValue);
+        }
+
+        return el;
+    },
+
+
+    /** Function: createHtml
+     *  Copy an HTML DOM element into an XML DOM.
+     *
+     *  This function copies a DOM element and all its descendants and returns
+     *  the new copy.
+     *
+     *  Parameters:
+     *    (HTMLElement) elem - A DOM element.
+     *
+     *  Returns:
+     *    A new, copied DOM element tree.
+     */
+    createHtml: function (elem)
+    {
+        var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue, children, child;
+        if (elem.nodeType == Strophe.ElementType.NORMAL) {
+            tag = elem.nodeName.toLowerCase();
+            if(Strophe.XHTML.validTag(tag)) {
+                try {
+                    el = Strophe.xmlElement(tag);
+                    for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) {
+                        attribute = Strophe.XHTML.attributes[tag][i];
+                        value = elem.getAttribute(attribute);
+                        if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) {
+                            continue;
+                        }
+                        if(attribute == 'style' && typeof value == 'object') {
+                            if(typeof value.cssText != 'undefined') {
+                                value = value.cssText; // we're dealing with IE, need to get CSS out
+                            }
+                        }
+                        // filter out invalid css styles
+                        if(attribute == 'style') {
+                            css = [];
+                            cssAttrs = value.split(';');
+                            for(j = 0; j < cssAttrs.length; j++) {
+                                attr = cssAttrs[j].split(':');
+                                cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase();
+                                if(Strophe.XHTML.validCSS(cssName)) {
+                                    cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, "");
+                                    css.push(cssName + ': ' + cssValue);
+                                }
+                            }
+                            if(css.length > 0) {
+                                value = css.join('; ');
+                                el.setAttribute(attribute, value);
+                            }
+                        } else {
+                            el.setAttribute(attribute, value);
+                        }
+                    }
+
+                    for (i = 0; i < elem.childNodes.length; i++) {
+                        el.appendChild(Strophe.createHtml(elem.childNodes[i]));
+                    }
+                } catch(e) { // invalid elements
+                  el = Strophe.xmlTextNode('');
+                }
+            } else {
+                el = Strophe.xmlGenerator().createDocumentFragment();
+                for (i = 0; i < elem.childNodes.length; i++) {
+                    el.appendChild(Strophe.createHtml(elem.childNodes[i]));
+                }
+            }
+        } else if (elem.nodeType == Strophe.ElementType.FRAGMENT) {
+            el = Strophe.xmlGenerator().createDocumentFragment();
+            for (i = 0; i < elem.childNodes.length; i++) {
+                el.appendChild(Strophe.createHtml(elem.childNodes[i]));
+            }
+        } else if (elem.nodeType == Strophe.ElementType.TEXT) {
+            el = Strophe.xmlTextNode(elem.nodeValue);
+        }
+
+        return el;
+    },
+
+    /** Function: escapeNode
+     *  Escape the node part (also called local part) of a JID.
+     *
+     *  Parameters:
+     *    (String) node - A node (or local part).
+     *
+     *  Returns:
+     *    An escaped node (or local part).
+     */
+    escapeNode: function (node)
+    {
+        return node.replace(/^\s+|\s+$/g, '')
+            .replace(/\\/g,  "\\5c")
+            .replace(/ /g,   "\\20")
+            .replace(/\"/g,  "\\22")
+            .replace(/\&/g,  "\\26")
+            .replace(/\'/g,  "\\27")
+            .replace(/\//g,  "\\2f")
+            .replace(/:/g,   "\\3a")
+            .replace(/</g,   "\\3c")
+            .replace(/>/g,   "\\3e")
+            .replace(/@/g,   "\\40");
+    },
+
+    /** Function: unescapeNode
+     *  Unescape a node part (also called local part) of a JID.
+     *
+     *  Parameters:
+     *    (String) node - A node (or local part).
+     *
+     *  Returns:
+     *    An unescaped node (or local part).
+     */
+    unescapeNode: function (node)
+    {
+        return node.replace(/\\20/g, " ")
+            .replace(/\\22/g, '"')
+            .replace(/\\26/g, "&")
+            .replace(/\\27/g, "'")
+            .replace(/\\2f/g, "/")
+            .replace(/\\3a/g, ":")
+            .replace(/\\3c/g, "<")
+            .replace(/\\3e/g, ">")
+            .replace(/\\40/g, "@")
+            .replace(/\\5c/g, "\\");
+    },
+
+    /** Function: getNodeFromJid
+     *  Get the node portion of a JID String.
+     *
+     *  Parameters:
+     *    (String) jid - A JID.
+     *
+     *  Returns:
+     *    A String containing the node.
+     */
+    getNodeFromJid: function (jid)
+    {
+        if (jid.indexOf("@") < 0) { return null; }
+        return jid.split("@")[0];
+    },
+
+    /** Function: getDomainFromJid
+     *  Get the domain portion of a JID String.
+     *
+     *  Parameters:
+     *    (String) jid - A JID.
+     *
+     *  Returns:
+     *    A String containing the domain.
+     */
+    getDomainFromJid: function (jid)
+    {
+        var bare = Strophe.getBareJidFromJid(jid);
+        if (bare.indexOf("@") < 0) {
+            return bare;
+        } else {
+            var parts = bare.split("@");
+            parts.splice(0, 1);
+            return parts.join('@');
+        }
+    },
+
+    /** Function: getResourceFromJid
+     *  Get the resource portion of a JID String.
+     *
+     *  Parameters:
+     *    (String) jid - A JID.
+     *
+     *  Returns:
+     *    A String containing the resource.
+     */
+    getResourceFromJid: function (jid)
+    {
+        var s = jid.split("/");
+        if (s.length < 2) { return null; }
+        s.splice(0, 1);
+        return s.join('/');
+    },
+
+    /** Function: getBareJidFromJid
+     *  Get the bare JID from a JID String.
+     *
+     *  Parameters:
+     *    (String) jid - A JID.
+     *
+     *  Returns:
+     *    A String containing the bare JID.
+     */
+    getBareJidFromJid: function (jid)
+    {
+        return jid ? jid.split("/")[0] : null;
+    },
+
+    /** Function: log
+     *  User overrideable logging function.
+     *
+     *  This function is called whenever the Strophe library calls any
+     *  of the logging functions.  The default implementation of this
+     *  function does nothing.  If client code wishes to handle the logging
+     *  messages, it should override this with
+     *  > Strophe.log = function (level, msg) {
+     *  >   (user code here)
+     *  > };
+     *
+     *  Please note that data sent and received over the wire is logged
+     *  via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
+     *
+     *  The different levels and their meanings are
+     *
+     *    DEBUG - Messages useful for debugging purposes.
+     *    INFO - Informational messages.  This is mostly information like
+     *      'disconnect was called' or 'SASL auth succeeded'.
+     *    WARN - Warnings about potential problems.  This is mostly used
+     *      to report transient connection errors like request timeouts.
+     *    ERROR - Some error occurred.
+     *    FATAL - A non-recoverable fatal error occurred.
+     *
+     *  Parameters:
+     *    (Integer) level - The log level of the log message.  This will
+     *      be one of the values in Strophe.LogLevel.
+     *    (String) msg - The log message.
+     */
+    log: function (level, msg)
+    {
+        return;
+    },
+
+    /** Function: debug
+     *  Log a message at the Strophe.LogLevel.DEBUG level.
+     *
+     *  Parameters:
+     *    (String) msg - The log message.
+     */
+    debug: function(msg)
+    {
+        this.log(this.LogLevel.DEBUG, msg);
+    },
+
+    /** Function: info
+     *  Log a message at the Strophe.LogLevel.INFO level.
+     *
+     *  Parameters:
+     *    (String) msg - The log message.
+     */
+    info: function (msg)
+    {
+        this.log(this.LogLevel.INFO, msg);
+    },
+
+    /** Function: warn
+     *  Log a message at the Strophe.LogLevel.WARN level.
+     *
+     *  Parameters:
+     *    (String) msg - The log message.
+     */
+    warn: function (msg)
+    {
+        this.log(this.LogLevel.WARN, msg);
+    },
+
+    /** Function: error
+     *  Log a message at the Strophe.LogLevel.ERROR level.
+     *
+     *  Parameters:
+     *    (String) msg - The log message.
+     */
+    error: function (msg)
+    {
+        this.log(this.LogLevel.ERROR, msg);
+    },
+
+    /** Function: fatal
+     *  Log a message at the Strophe.LogLevel.FATAL level.
+     *
+     *  Parameters:
+     *    (String) msg - The log message.
+     */
+    fatal: function (msg)
+    {
+        this.log(this.LogLevel.FATAL, msg);
+    },
+
+    /** Function: serialize
+     *  Render a DOM element and all descendants to a String.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - A DOM element.
+     *
+     *  Returns:
+     *    The serialized element tree as a String.
+     */
+    serialize: function (elem)
+    {
+        var result;
+
+        if (!elem) { return null; }
+
+        if (typeof(elem.tree) === "function") {
+            elem = elem.tree();
+        }
+
+        var nodeName = elem.nodeName;
+        var i, child;
+
+        if (elem.getAttribute("_realname")) {
+            nodeName = elem.getAttribute("_realname");
+        }
+
+        result = "<" + nodeName;
+        for (i = 0; i < elem.attributes.length; i++) {
+               if(elem.attributes[i].nodeName != "_realname") {
+                 result += " " + elem.attributes[i].nodeName.toLowerCase() +
+                "='" + elem.attributes[i].value
+                    .replace(/&/g, "&")
+                       .replace(/\'/g, "'")
+                       .replace(/>/g, ">")
+                       .replace(/</g, "<") + "'";
+               }
+        }
+
+        if (elem.childNodes.length > 0) {
+            result += ">";
+            for (i = 0; i < elem.childNodes.length; i++) {
+                child = elem.childNodes[i];
+                switch( child.nodeType ){
+                  case Strophe.ElementType.NORMAL:
+                    // normal element, so recurse
+                    result += Strophe.serialize(child);
+                    break;
+                  case Strophe.ElementType.TEXT:
+                    // text element to escape values
+                    result += Strophe.xmlescape(child.nodeValue);
+                    break;
+                  case Strophe.ElementType.CDATA:
+                    // cdata section so don't escape values
+                    result += "<![CDATA["+child.nodeValue+"]]>";
+                }
+            }
+            result += "</" + nodeName + ">";
+        } else {
+            result += "/>";
+        }
+
+        return result;
+    },
+
+    /** PrivateVariable: _requestId
+     *  _Private_ variable that keeps track of the request ids for
+     *  connections.
+     */
+    _requestId: 0,
+
+    /** PrivateVariable: Strophe.connectionPlugins
+     *  _Private_ variable Used to store plugin names that need
+     *  initialization on Strophe.Connection construction.
+     */
+    _connectionPlugins: {},
+
+    /** Function: addConnectionPlugin
+     *  Extends the Strophe.Connection object with the given plugin.
+     *
+     *  Parameters:
+     *    (String) name - The name of the extension.
+     *    (Object) ptype - The plugin's prototype.
+     */
+    addConnectionPlugin: function (name, ptype)
+    {
+        Strophe._connectionPlugins[name] = ptype;
+    }
+};
+
+/** Class: Strophe.Builder
+ *  XML DOM builder.
+ *
+ *  This object provides an interface similar to JQuery but for building
+ *  DOM element easily and rapidly.  All the functions except for toString()
+ *  and tree() return the object, so calls can be chained.  Here's an
+ *  example using the $iq() builder helper.
+ *  > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
+ *  >     .c('query', {xmlns: 'strophe:example'})
+ *  >     .c('example')
+ *  >     .toString()
+ *  The above generates this XML fragment
+ *  > <iq to='you' from='me' type='get' id='1'>
+ *  >   <query xmlns='strophe:example'>
+ *  >     <example/>
+ *  >   </query>
+ *  > </iq>
+ *  The corresponding DOM manipulations to get a similar fragment would be
+ *  a lot more tedious and probably involve several helper variables.
+ *
+ *  Since adding children makes new operations operate on the child, up()
+ *  is provided to traverse up the tree.  To add two children, do
+ *  > builder.c('child1', ...).up().c('child2', ...)
+ *  The next operation on the Builder will be relative to the second child.
+ */
+
+/** Constructor: Strophe.Builder
+ *  Create a Strophe.Builder object.
+ *
+ *  The attributes should be passed in object notation.  For example
+ *  > var b = new Builder('message', {to: 'you', from: 'me'});
+ *  or
+ *  > var b = new Builder('messsage', {'xml:lang': 'en'});
+ *
+ *  Parameters:
+ *    (String) name - The name of the root element.
+ *    (Object) attrs - The attributes for the root element in object notation.
+ *
+ *  Returns:
+ *    A new Strophe.Builder.
+ */
+Strophe.Builder = function (name, attrs)
+{
+    // Set correct namespace for jabber:client elements
+    if (name == "presence" || name == "message" || name == "iq") {
+        if (attrs && !attrs.xmlns) {
+            attrs.xmlns = Strophe.NS.CLIENT;
+        } else if (!attrs) {
+            attrs = {xmlns: Strophe.NS.CLIENT};
+        }
+    }
+
+    // Holds the tree being built.
+    this.nodeTree = Strophe.xmlElement(name, attrs);
+
+    // Points to the current operation node.
+    this.node = this.nodeTree;
+};
+
+Strophe.Builder.prototype = {
+    /** Function: tree
+     *  Return the DOM tree.
+     *
+     *  This function returns the current DOM tree as an element object.  This
+     *  is suitable for passing to functions like Strophe.Connection.send().
+     *
+     *  Returns:
+     *    The DOM tree as a element object.
+     */
+    tree: function ()
+    {
+        return this.nodeTree;
+    },
+
+    /** Function: toString
+     *  Serialize the DOM tree to a String.
+     *
+     *  This function returns a string serialization of the current DOM
+     *  tree.  It is often used internally to pass data to a
+     *  Strophe.Request object.
+     *
+     *  Returns:
+     *    The serialized DOM tree in a String.
+     */
+    toString: function ()
+    {
+        return Strophe.serialize(this.nodeTree);
+    },
+
+    /** Function: up
+     *  Make the current parent element the new current element.
+     *
+     *  This function is often used after c() to traverse back up the tree.
+     *  For example, to add two children to the same element
+     *  > builder.c('child1', {}).up().c('child2', {});
+     *
+     *  Returns:
+     *    The Stophe.Builder object.
+     */
+    up: function ()
+    {
+        this.node = this.node.parentNode;
+        return this;
+    },
+
+    /** Function: attrs
+     *  Add or modify attributes of the current element.
+     *
+     *  The attributes should be passed in object notation.  This function
+     *  does not move the current element pointer.
+     *
+     *  Parameters:
+     *    (Object) moreattrs - The attributes to add/modify in object notation.
+     *
+     *  Returns:
+     *    The Strophe.Builder object.
+     */
+    attrs: function (moreattrs)
+    {
+        for (var k in moreattrs) {
+            if (moreattrs.hasOwnProperty(k)) {
+                this.node.setAttribute(k, moreattrs[k]);
+            }
+        }
+        return this;
+    },
+
+    /** Function: c
+     *  Add a child to the current element and make it the new current
+     *  element.
+     *
+     *  This function moves the current element pointer to the child,
+     *  unless text is provided.  If you need to add another child, it
+     *  is necessary to use up() to go back to the parent in the tree.
+     *
+     *  Parameters:
+     *    (String) name - The name of the child.
+     *    (Object) attrs - The attributes of the child in object notation.
+     *    (String) text - The text to add to the child.
+     *
+     *  Returns:
+     *    The Strophe.Builder object.
+     */
+    c: function (name, attrs, text)
+    {
+        var child = Strophe.xmlElement(name, attrs, text);
+        this.node.appendChild(child);
+        if (!text) {
+            this.node = child;
+        }
+        return this;
+    },
+
+    /** Function: cnode
+     *  Add a child to the current element and make it the new current
+     *  element.
+     *
+     *  This function is the same as c() except that instead of using a
+     *  name and an attributes object to create the child it uses an
+     *  existing DOM element object.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - A DOM element.
+     *
+     *  Returns:
+     *    The Strophe.Builder object.
+     */
+    cnode: function (elem)
+    {
+        var xmlGen = Strophe.xmlGenerator();
+        try {
+            var impNode = (xmlGen.importNode !== undefined);
+        }
+        catch (e) {
+            var impNode = false;
+        }
+        var newElem = impNode ?
+                      xmlGen.importNode(elem, true) :
+                      Strophe.copyElement(elem);
+        this.node.appendChild(newElem);
+        this.node = newElem;
+        return this;
+    },
+
+    /** Function: t
+     *  Add a child text element.
+     *
+     *  This *does not* make the child the new current element since there
+     *  are no children of text elements.
+     *
+     *  Parameters:
+     *    (String) text - The text data to append to the current element.
+     *
+     *  Returns:
+     *    The Strophe.Builder object.
+     */
+    t: function (text)
+    {
+        var child = Strophe.xmlTextNode(text);
+        this.node.appendChild(child);
+        return this;
+    },
+
+    /** Function: h
+     *  Replace current element contents with the HTML passed in.
+     *
+     *  This *does not* make the child the new current element
+     *
+     *  Parameters:
+     *    (String) html - The html to insert as contents of current element.
+     *
+     *  Returns:
+     *    The Strophe.Builder object.
+     */
+    h: function (html)
+    {
+        var fragment = document.createElement('body');
+
+        // force the browser to try and fix any invalid HTML tags
+        fragment.innerHTML = html;
+
+        // copy cleaned html into an xml dom
+        var xhtml = Strophe.createHtml(fragment);
+
+        while(xhtml.childNodes.length > 0) {
+            this.node.appendChild(xhtml.childNodes[0]);
+        }
+        return this;
+    }
+};
+
+/** PrivateClass: Strophe.Handler
+ *  _Private_ helper class for managing stanza handlers.
+ *
+ *  A Strophe.Handler encapsulates a user provided callback function to be
+ *  executed when matching stanzas are received by the connection.
+ *  Handlers can be either one-off or persistant depending on their
+ *  return value. Returning true will cause a Handler to remain active, and
+ *  returning false will remove the Handler.
+ *
+ *  Users will not use Strophe.Handler objects directly, but instead they
+ *  will use Strophe.Connection.addHandler() and
+ *  Strophe.Connection.deleteHandler().
+ */
+
+/** PrivateConstructor: Strophe.Handler
+ *  Create and initialize a new Strophe.Handler.
+ *
+ *  Parameters:
+ *    (Function) handler - A function to be executed when the handler is run.
+ *    (String) ns - The namespace to match.
+ *    (String) name - The element name to match.
+ *    (String) type - The element type to match.
+ *    (String) id - The element id attribute to match.
+ *    (String) from - The element from attribute to match.
+ *    (Object) options - Handler options
+ *
+ *  Returns:
+ *    A new Strophe.Handler object.
+ */
+Strophe.Handler = function (handler, ns, name, type, id, from, options)
+{
+    this.handler = handler;
+    this.ns = ns;
+    this.name = name;
+    this.type = type;
+    this.id = id;
+    this.options = options || {matchbare: false};
+
+    // default matchBare to false if undefined
+    if (!this.options.matchBare) {
+        this.options.matchBare = false;
+    }
+
+    if (this.options.matchBare) {
+        this.from = from ? Strophe.getBareJidFromJid(from) : null;
+    } else {
+        this.from = from;
+    }
+
+    // whether the handler is a user handler or a system handler
+    this.user = true;
+};
+
+Strophe.Handler.prototype = {
+    /** PrivateFunction: isMatch
+     *  Tests if a stanza matches the Strophe.Handler.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The XML element to test.
+     *
+     *  Returns:
+     *    true if the stanza matches and false otherwise.
+     */
+    isMatch: function (elem)
+    {
+        var nsMatch;
+        var from = null;
+
+        if (this.options.matchBare) {
+            from = Strophe.getBareJidFromJid(elem.getAttribute('from'));
+        } else {
+            from = elem.getAttribute('from');
+        }
+
+        nsMatch = false;
+        if (!this.ns) {
+            nsMatch = true;
+        } else {
+            var that = this;
+            Strophe.forEachChild(elem, null, function (elem) {
+                if (elem.getAttribute("xmlns") == that.ns) {
+                    nsMatch = true;
+                }
+            });
+
+            nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns;
+        }
+
+        if (nsMatch &&
+            (!this.name || Strophe.isTagEqual(elem, this.name)) &&
+            (!this.type || elem.getAttribute("type") == this.type) &&
+            (!this.id || elem.getAttribute("id") == this.id) &&
+            (!this.from || from == this.from)) {
+                return true;
+        }
+
+        return false;
+    },
+
+    /** PrivateFunction: run
+     *  Run the callback on a matching stanza.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The DOM element that triggered the
+     *      Strophe.Handler.
+     *
+     *  Returns:
+     *    A boolean indicating if the handler should remain active.
+     */
+    run: function (elem)
+    {
+        var result = null;
+        try {
+            result = this.handler(elem);
+        } catch (e) {
+            if (e.sourceURL) {
+                Strophe.fatal("error: " + this.handler +
+                              " " + e.sourceURL + ":" +
+                              e.line + " - " + e.name + ": " + e.message);
+            } else if (e.fileName) {
+                if (typeof(console) != "undefined") {
+                    console.trace();
+                    console.error(this.handler, " - error - ", e, e.message);
+                }
+                Strophe.fatal("error: " + this.handler + " " +
+                              e.fileName + ":" + e.lineNumber + " - " +
+                              e.name + ": " + e.message);
+            } else {
+                Strophe.fatal("error: " + e.message + "\n" + e.stack);
+            }
+
+            throw e;
+        }
+
+        return result;
+    },
+
+    /** PrivateFunction: toString
+     *  Get a String representation of the Strophe.Handler object.
+     *
+     *  Returns:
+     *    A String.
+     */
+    toString: function ()
+    {
+        return "{Handler: " + this.handler + "(" + this.name + "," +
+            this.id + "," + this.ns + ")}";
+    }
+};
+
+/** PrivateClass: Strophe.TimedHandler
+ *  _Private_ helper class for managing timed handlers.
+ *
+ *  A Strophe.TimedHandler encapsulates a user provided callback that
+ *  should be called after a certain period of time or at regular
+ *  intervals.  The return value of the callback determines whether the
+ *  Strophe.TimedHandler will continue to fire.
+ *
+ *  Users will not use Strophe.TimedHandler objects directly, but instead
+ *  they will use Strophe.Connection.addTimedHandler() and
+ *  Strophe.Connection.deleteTimedHandler().
+ */
+
+/** PrivateConstructor: Strophe.TimedHandler
+ *  Create and initialize a new Strophe.TimedHandler object.
+ *
+ *  Parameters:
+ *    (Integer) period - The number of milliseconds to wait before the
+ *      handler is called.
+ *    (Function) handler - The callback to run when the handler fires.  This
+ *      function should take no arguments.
+ *
+ *  Returns:
+ *    A new Strophe.TimedHandler object.
+ */
+Strophe.TimedHandler = function (period, handler)
+{
+    this.period = period;
+    this.handler = handler;
+
+    this.lastCalled = new Date().getTime();
+    this.user = true;
+};
+
+Strophe.TimedHandler.prototype = {
+    /** PrivateFunction: run
+     *  Run the callback for the Strophe.TimedHandler.
+     *
+     *  Returns:
+     *    true if the Strophe.TimedHandler should be called again, and false
+     *      otherwise.
+     */
+    run: function ()
+    {
+        this.lastCalled = new Date().getTime();
+        return this.handler();
+    },
+
+    /** PrivateFunction: reset
+     *  Reset the last called time for the Strophe.TimedHandler.
+     */
+    reset: function ()
+    {
+        this.lastCalled = new Date().getTime();
+    },
+
+    /** PrivateFunction: toString
+     *  Get a string representation of the Strophe.TimedHandler object.
+     *
+     *  Returns:
+     *    The string representation.
+     */
+    toString: function ()
+    {
+        return "{TimedHandler: " + this.handler + "(" + this.period +")}";
+    }
+};
+
+/** PrivateClass: Strophe.Request
+ *  _Private_ helper class that provides a cross implementation abstraction
+ *  for a BOSH related XMLHttpRequest.
+ *
+ *  The Strophe.Request class is used internally to encapsulate BOSH request
+ *  information.  It is not meant to be used from user's code.
+ */
+
+/** PrivateConstructor: Strophe.Request
+ *  Create and initialize a new Strophe.Request object.
+ *
+ *  Parameters:
+ *    (XMLElement) elem - The XML data to be sent in the request.
+ *    (Function) func - The function that will be called when the
+ *      XMLHttpRequest readyState changes.
+ *    (Integer) rid - The BOSH rid attribute associated with this request.
+ *    (Integer) sends - The number of times this same request has been
+ *      sent.
+ */
+Strophe.Request = function (elem, func, rid, sends)
+{
+    this.id = ++Strophe._requestId;
+    this.xmlData = elem;
+    this.data = Strophe.serialize(elem);
+    // save original function in case we need to make a new request
+    // from this one.
+    this.origFunc = func;
+    this.func = func;
+    this.rid = rid;
+    this.date = NaN;
+    this.sends = sends || 0;
+    this.abort = false;
+    this.dead = null;
+    this.age = function () {
+        if (!this.date) { return 0; }
+        var now = new Date();
+        return (now - this.date) / 1000;
+    };
+    this.timeDead = function () {
+        if (!this.dead) { return 0; }
+        var now = new Date();
+        return (now - this.dead) / 1000;
+    };
+    this.xhr = this._newXHR();
+};
+
+Strophe.Request.prototype = {
+    /** PrivateFunction: getResponse
+     *  Get a response from the underlying XMLHttpRequest.
+     *
+     *  This function attempts to get a response from the request and checks
+     *  for errors.
+     *
+     *  Throws:
+     *    "parsererror" - A parser error occured.
+     *
+     *  Returns:
+     *    The DOM element tree of the response.
+     */
+    getResponse: function ()
+    {
+        var node = null;
+        if (this.xhr.responseXML && this.xhr.responseXML.documentElement) {
+            node = this.xhr.responseXML.documentElement;
+            if (node.tagName == "parsererror") {
+                Strophe.error("invalid response received");
+                Strophe.error("responseText: " + this.xhr.responseText);
+                Strophe.error("responseXML: " +
+                              Strophe.serialize(this.xhr.responseXML));
+                throw "parsererror";
+            }
+        } else if (this.xhr.responseText) {
+            Strophe.error("invalid response received");
+            Strophe.error("responseText: " + this.xhr.responseText);
+            Strophe.error("responseXML: " +
+                          Strophe.serialize(this.xhr.responseXML));
+        }
+
+        return node;
+    },
+
+    /** PrivateFunction: _newXHR
+     *  _Private_ helper function to create XMLHttpRequests.
+     *
+     *  This function creates XMLHttpRequests across all implementations.
+     *
+     *  Returns:
+     *    A new XMLHttpRequest.
+     */
+    _newXHR: function ()
+    {
+        var xhr = null;
+        if (window.XMLHttpRequest) {
+            xhr = new XMLHttpRequest();
+            if (xhr.overrideMimeType) {
+                xhr.overrideMimeType("text/xml");
+            }
+        } else if (window.ActiveXObject) {
+            xhr = new ActiveXObject("Microsoft.XMLHTTP");
+        }
+
+        // use Function.bind() to prepend ourselves as an argument
+        xhr.onreadystatechange = this.func.bind(null, this);
+
+        return xhr;
+    }
+};
+
+/** Class: Strophe.Connection
+ *  XMPP Connection manager.
+ *
+ *  This class is the main part of Strophe.  It manages a BOSH connection
+ *  to an XMPP server and dispatches events to the user callbacks as
+ *  data arrives.  It supports SASL PLAIN, SASL DIGEST-MD5, and legacy
+ *  authentication.
+ *
+ *  After creating a Strophe.Connection object, the user will typically
+ *  call connect() with a user supplied callback to handle connection level
+ *  events like authentication failure, disconnection, or connection
+ *  complete.
+ *
+ *  The user will also have several event handlers defined by using
+ *  addHandler() and addTimedHandler().  These will allow the user code to
+ *  respond to interesting stanzas or do something periodically with the
+ *  connection.  These handlers will be active once authentication is
+ *  finished.
+ *
+ *  To send data to the connection, use send().
+ */
+
+/** Constructor: Strophe.Connection
+ *  Create and initialize a Strophe.Connection object.
+ *
+ *  Parameters:
+ *    (String) service - The BOSH service URL.
+ *
+ *  Returns:
+ *    A new Strophe.Connection object.
+ */
+Strophe.Connection = function (service)
+{
+    /* The path to the httpbind service. */
+    this.service = service;
+    /* The connected JID. */
+    this.jid = "";
+    /* the JIDs domain */
+    this.domain = null;
+    /* request id for body tags */
+    this.rid = Math.floor(Math.random() * 4294967295);
+    /* The current session ID. */
+    this.sid = null;
+    this.streamId = null;
+    /* stream:features */
+    this.features = null;
+
+    // SASL
+    this._sasl_data = [];
+    this.do_session = false;
+    this.do_bind = false;
+
+    // handler lists
+    this.timedHandlers = [];
+    this.handlers = [];
+    this.removeTimeds = [];
+    this.removeHandlers = [];
+    this.addTimeds = [];
+    this.addHandlers = [];
+
+    this._authentication = {};
+    this._idleTimeout = null;
+    this._disconnectTimeout = null;
+
+    this.do_authentication = true;
+    this.authenticated = false;
+    this.disconnecting = false;
+    this.connected = false;
+
+    this.errors = 0;
+
+    this.paused = false;
+
+    // default BOSH values
+    this.hold = 1;
+    this.wait = 60;
+    this.window = 5;
+
+    this._data = [];
+    this._requests = [];
+    this._uniqueId = Math.round(Math.random() * 10000);
+
+    this._sasl_success_handler = null;
+    this._sasl_failure_handler = null;
+    this._sasl_challenge_handler = null;
+
+    // Max retries before disconnecting
+    this.maxRetries = 5;
+
+    // setup onIdle callback every 1/10th of a second
+    this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+
+    // initialize plugins
+    for (var k in Strophe._connectionPlugins) {
+        if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+	    var ptype = Strophe._connectionPlugins[k];
+            // jslint complaints about the below line, but this is fine
+            var F = function () {};
+            F.prototype = ptype;
+            this[k] = new F();
+	    this[k].init(this);
+        }
+    }
+};
+
+Strophe.Connection.prototype = {
+    /** Function: reset
+     *  Reset the connection.
+     *
+     *  This function should be called after a connection is disconnected
+     *  before that connection is reused.
+     */
+    reset: function ()
+    {
+        this.rid = Math.floor(Math.random() * 4294967295);
+
+        this.sid = null;
+        this.streamId = null;
+
+        // SASL
+        this.do_session = false;
+        this.do_bind = false;
+
+        // handler lists
+        this.timedHandlers = [];
+        this.handlers = [];
+        this.removeTimeds = [];
+        this.removeHandlers = [];
+        this.addTimeds = [];
+        this.addHandlers = [];
+        this._authentication = {};
+
+        this.authenticated = false;
+        this.disconnecting = false;
+        this.connected = false;
+
+        this.errors = 0;
+
+        this._requests = [];
+        this._uniqueId = Math.round(Math.random()*10000);
+    },
+
+    /** Function: pause
+     *  Pause the request manager.
+     *
+     *  This will prevent Strophe from sending any more requests to the
+     *  server.  This is very useful for temporarily pausing while a lot
+     *  of send() calls are happening quickly.  This causes Strophe to
+     *  send the data in a single request, saving many request trips.
+     */
+    pause: function ()
+    {
+        this.paused = true;
+    },
+
+    /** Function: resume
+     *  Resume the request manager.
+     *
+     *  This resumes after pause() has been called.
+     */
+    resume: function ()
+    {
+        this.paused = false;
+    },
+
+    /** Function: getUniqueId
+     *  Generate a unique ID for use in <iq/> elements.
+     *
+     *  All <iq/> stanzas are required to have unique id attributes.  This
+     *  function makes creating these easy.  Each connection instance has
+     *  a counter which starts from zero, and the value of this counter
+     *  plus a colon followed by the suffix becomes the unique id. If no
+     *  suffix is supplied, the counter is used as the unique id.
+     *
+     *  Suffixes are used to make debugging easier when reading the stream
+     *  data, and their use is recommended.  The counter resets to 0 for
+     *  every new connection for the same reason.  For connections to the
+     *  same server that authenticate the same way, all the ids should be
+     *  the same, which makes it easy to see changes.  This is useful for
+     *  automated testing as well.
+     *
+     *  Parameters:
+     *    (String) suffix - A optional suffix to append to the id.
+     *
+     *  Returns:
+     *    A unique string to be used for the id attribute.
+     */
+    getUniqueId: function (suffix)
+    {
+        if (typeof(suffix) == "string" || typeof(suffix) == "number") {
+            return Cryptocat.random.encodedBytes(32, CryptoJS.enc.Hex) + ":" + suffix;
+        } else {
+            return Cryptocat.random.encodedBytes(32, CryptoJS.enc.Hex) + "";
+        }
+    },
+
+    /** Function: connect
+     *  Starts the connection process.
+     *
+     *  As the connection process proceeds, the user supplied callback will
+     *  be triggered multiple times with status updates.  The callback
+     *  should take two arguments - the status code and the error condition.
+     *
+     *  The status code will be one of the values in the Strophe.Status
+     *  constants.  The error condition will be one of the conditions
+     *  defined in RFC 3920 or the condition 'strophe-parsererror'.
+     *
+     *  Please see XEP 124 for a more detailed explanation of the optional
+     *  parameters below.
+     *
+     *  Parameters:
+     *    (String) jid - The user's JID.  This may be a bare JID,
+     *      or a full JID.  If a node is not supplied, SASL ANONYMOUS
+     *      authentication will be attempted.
+     *    (String) pass - The user's password.
+     *    (Function) callback - The connect callback function.
+     *    (Integer) wait - The optional HTTPBIND wait value.  This is the
+     *      time the server will wait before returning an empty result for
+     *      a request.  The default setting of 60 seconds is recommended.
+     *      Other settings will require tweaks to the Strophe.TIMEOUT value.
+     *    (Integer) hold - The optional HTTPBIND hold value.  This is the
+     *      number of connections the server will hold at one time.  This
+     *      should almost always be set to 1 (the default).
+	 *    (String) route
+     */
+    connect: function (jid, pass, callback, wait, hold, route)
+    {
+        this.jid = jid;
+        this.pass = pass;
+        this.connect_callback = callback;
+        this.disconnecting = false;
+        this.connected = false;
+        this.authenticated = false;
+        this.errors = 0;
+
+        this.wait = wait || this.wait;
+        this.hold = hold || this.hold;
+
+        // parse jid for domain and resource
+        this.domain = this.domain || Strophe.getDomainFromJid(this.jid);
+
+        // build the body tag
+        var body = this._buildBody().attrs({
+            to: this.domain,
+            "xml:lang": "en",
+            wait: this.wait,
+            hold: this.hold,
+            content: "text/xml; charset=utf-8",
+            ver: "1.6",
+            "xmpp:version": "1.0",
+            "xmlns:xmpp": Strophe.NS.BOSH
+        });
+
+		if(route){
+			body.attrs({
+				route: route
+			});
+		}
+
+        this._changeConnectStatus(Strophe.Status.CONNECTING, null);
+
+        var _connect_cb = this._connect_callback || this._connect_cb;
+        this._connect_callback = null;
+
+        this._requests.push(
+            new Strophe.Request(body.tree(),
+                                this._onRequestStateChange.bind(
+                                    this, _connect_cb.bind(this)),
+                                body.tree().getAttribute("rid")));
+        this._throttledRequestHandler();
+    },
+
+    /** Function: attach
+     *  Attach to an already created and authenticated BOSH session.
+     *
+     *  This function is provided to allow Strophe to attach to BOSH
+     *  sessions which have been created externally, perhaps by a Web
+     *  application.  This is often used to support auto-login type features
+     *  without putting user credentials into the page.
+     *
+     *  Parameters:
+     *    (String) jid - The full JID that is bound by the session.
+     *    (String) sid - The SID of the BOSH session.
+     *    (String) rid - The current RID of the BOSH session.  This RID
+     *      will be used by the next request.
+     *    (Function) callback The connect callback function.
+     *    (Integer) wait - The optional HTTPBIND wait value.  This is the
+     *      time the server will wait before returning an empty result for
+     *      a request.  The default setting of 60 seconds is recommended.
+     *      Other settings will require tweaks to the Strophe.TIMEOUT value.
+     *    (Integer) hold - The optional HTTPBIND hold value.  This is the
+     *      number of connections the server will hold at one time.  This
+     *      should almost always be set to 1 (the default).
+     *    (Integer) wind - The optional HTTBIND window value.  This is the
+     *      allowed range of request ids that are valid.  The default is 5.
+     */
+    attach: function (jid, sid, rid, callback, wait, hold, wind)
+    {
+        this.jid = jid;
+        this.sid = sid;
+        this.rid = rid;
+        this.connect_callback = callback;
+
+        this.domain = Strophe.getDomainFromJid(this.jid);
+
+        this.authenticated = true;
+        this.connected = true;
+
+        this.wait = wait || this.wait;
+        this.hold = hold || this.hold;
+        this.window = wind || this.window;
+
+        this._changeConnectStatus(Strophe.Status.ATTACHED, null);
+    },
+
+    /** Function: xmlInput
+     *  User overrideable function that receives XML data coming into the
+     *  connection.
+     *
+     *  The default function does nothing.  User code can override this with
+     *  > Strophe.Connection.xmlInput = function (elem) {
+     *  >   (user code)
+     *  > };
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The XML data received by the connection.
+     */
+    xmlInput: function (elem)
+    {
+        return;
+    },
+
+    /** Function: xmlOutput
+     *  User overrideable function that receives XML data sent to the
+     *  connection.
+     *
+     *  The default function does nothing.  User code can override this with
+     *  > Strophe.Connection.xmlOutput = function (elem) {
+     *  >   (user code)
+     *  > };
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The XMLdata sent by the connection.
+     */
+    xmlOutput: function (elem)
+    {
+        return;
+    },
+
+    /** Function: rawInput
+     *  User overrideable function that receives raw data coming into the
+     *  connection.
+     *
+     *  The default function does nothing.  User code can override this with
+     *  > Strophe.Connection.rawInput = function (data) {
+     *  >   (user code)
+     *  > };
+     *
+     *  Parameters:
+     *    (String) data - The data received by the connection.
+     */
+    rawInput: function (data)
+    {
+        return;
+    },
+
+    /** Function: rawOutput
+     *  User overrideable function that receives raw data sent to the
+     *  connection.
+     *
+     *  The default function does nothing.  User code can override this with
+     *  > Strophe.Connection.rawOutput = function (data) {
+     *  >   (user code)
+     *  > };
+     *
+     *  Parameters:
+     *    (String) data - The data sent by the connection.
+     */
+    rawOutput: function (data)
+    {
+        return;
+    },
+
+    /** Function: send
+     *  Send a stanza.
+     *
+     *  This function is called to push data onto the send queue to
+     *  go out over the wire.  Whenever a request is sent to the BOSH
+     *  server, all pending data is sent and the queue is flushed.
+     *
+     *  Parameters:
+     *    (XMLElement |
+     *     [XMLElement] |
+     *     Strophe.Builder) elem - The stanza to send.
+     */
+    send: function (elem)
+    {
+        if (elem === null) { return ; }
+        if (typeof(elem.sort) === "function") {
+            for (var i = 0; i < elem.length; i++) {
+                this._queueData(elem[i]);
+            }
+        } else if (typeof(elem.tree) === "function") {
+            this._queueData(elem.tree());
+        } else {
+            this._queueData(elem);
+        }
+
+        this._throttledRequestHandler();
+        clearTimeout(this._idleTimeout);
+        this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+    },
+
+    /** Function: flush
+     *  Immediately send any pending outgoing data.
+     *
+     *  Normally send() queues outgoing data until the next idle period
+     *  (100ms), which optimizes network use in the common cases when
+     *  several send()s are called in succession. flush() can be used to
+     *  immediately send all pending data.
+     */
+    flush: function ()
+    {
+        // cancel the pending idle period and run the idle function
+        // immediately
+        clearTimeout(this._idleTimeout);
+        this._onIdle();
+    },
+
+    /** Function: sendIQ
+     *  Helper function to send IQ stanzas.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The stanza to send.
+     *    (Function) callback - The callback function for a successful request.
+     *    (Function) errback - The callback function for a failed or timed
+     *      out request.  On timeout, the stanza will be null.
+     *    (Integer) timeout - The time specified in milliseconds for a
+     *      timeout to occur.
+     *
+     *  Returns:
+     *    The id used to send the IQ.
+    */
+    sendIQ: function(elem, callback, errback, timeout) {
+        var timeoutHandler = null;
+        var that = this;
+
+        if (typeof(elem.tree) === "function") {
+            elem = elem.tree();
+        }
+	var id = elem.getAttribute('id');
+
+	// inject id if not found
+	if (!id) {
+	    id = this.getUniqueId("sendIQ");
+	    elem.setAttribute("id", id);
+	}
+
+	var handler = this.addHandler(function (stanza) {
+	    // remove timeout handler if there is one
+            if (timeoutHandler) {
+                that.deleteTimedHandler(timeoutHandler);
+            }
+
+            var iqtype = stanza.getAttribute('type');
+	    if (iqtype == 'result') {
+		if (callback) {
+                    callback(stanza);
+                }
+	    } else if (iqtype == 'error') {
+		if (errback) {
+                    errback(stanza);
+                }
+	    } else {
+                throw {
+                    name: "StropheError",
+                    message: "Got bad IQ type of " + iqtype
+                };
+            }
+	}, null, 'iq', null, id);
+
+	// if timeout specified, setup timeout handler.
+	if (timeout) {
+	    timeoutHandler = this.addTimedHandler(timeout, function () {
+                // get rid of normal handler
+                that.deleteHandler(handler);
+
+	        // call errback on timeout with null stanza
+                if (errback) {
+		    errback(null);
+                }
+		return false;
+	    });
+	}
+
+	this.send(elem);
+
+	return id;
+    },
+
+    /** PrivateFunction: _queueData
+     *  Queue outgoing data for later sending.  Also ensures that the data
+     *  is a DOMElement.
+     */
+    _queueData: function (element) {
+        if (element === null ||
+            !element.tagName ||
+            !element.childNodes) {
+            throw {
+                name: "StropheError",
+                message: "Cannot queue non-DOMElement."
+            };
+        }
+
+        this._data.push(element);
+    },
+
+    /** PrivateFunction: _sendRestart
+     *  Send an xmpp:restart stanza.
+     */
+    _sendRestart: function ()
+    {
+        this._data.push("restart");
+
+        this._throttledRequestHandler();
+        clearTimeout(this._idleTimeout);
+        this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+    },
+
+    /** Function: addTimedHandler
+     *  Add a timed handler to the connection.
+     *
+     *  This function adds a timed handler.  The provided handler will
+     *  be called every period milliseconds until it returns false,
+     *  the connection is terminated, or the handler is removed.  Handlers
+     *  that wish to continue being invoked should return true.
+     *
+     *  Because of method binding it is necessary to save the result of
+     *  this function if you wish to remove a handler with
+     *  deleteTimedHandler().
+     *
+     *  Note that user handlers are not active until authentication is
+     *  successful.
+     *
+     *  Parameters:
+     *    (Integer) period - The period of the handler.
+     *    (Function) handler - The callback function.
+     *
+     *  Returns:
+     *    A reference to the handler that can be used to remove it.
+     */
+    addTimedHandler: function (period, handler)
+    {
+        var thand = new Strophe.TimedHandler(period, handler);
+        this.addTimeds.push(thand);
+        return thand;
+    },
+
+    /** Function: deleteTimedHandler
+     *  Delete a timed handler for a connection.
+     *
+     *  This function removes a timed handler from the connection.  The
+     *  handRef parameter is *not* the function passed to addTimedHandler(),
+     *  but is the reference returned from addTimedHandler().
+     *
+     *  Parameters:
+     *    (Strophe.TimedHandler) handRef - The handler reference.
+     */
+    deleteTimedHandler: function (handRef)
+    {
+        // this must be done in the Idle loop so that we don't change
+        // the handlers during iteration
+        this.removeTimeds.push(handRef);
+    },
+
+    /** Function: addHandler
+     *  Add a stanza handler for the connection.
+     *
+     *  This function adds a stanza handler to the connection.  The
+     *  handler callback will be called for any stanza that matches
+     *  the parameters.  Note that if multiple parameters are supplied,
+     *  they must all match for the handler to be invoked.
+     *
+     *  The handler will receive the stanza that triggered it as its argument.
+     *  The handler should return true if it is to be invoked again;
+     *  returning false will remove the handler after it returns.
+     *
+     *  As a convenience, the ns parameters applies to the top level element
+     *  and also any of its immediate children.  This is primarily to make
+     *  matching /iq/query elements easy.
+     *
+     *  The options argument contains handler matching flags that affect how
+     *  matches are determined. Currently the only flag is matchBare (a
+     *  boolean). When matchBare is true, the from parameter and the from
+     *  attribute on the stanza will be matched as bare JIDs instead of
+     *  full JIDs. To use this, pass {matchBare: true} as the value of
+     *  options. The default value for matchBare is false.
+     *
+     *  The return value should be saved if you wish to remove the handler
+     *  with deleteHandler().
+     *
+     *  Parameters:
+     *    (Function) handler - The user callback.
+     *    (String) ns - The namespace to match.
+     *    (String) name - The stanza name to match.
+     *    (String) type - The stanza type attribute to match.
+     *    (String) id - The stanza id attribute to match.
+     *    (String) from - The stanza from attribute to match.
+     *    (String) options - The handler options
+     *
+     *  Returns:
+     *    A reference to the handler that can be used to remove it.
+     */
+    addHandler: function (handler, ns, name, type, id, from, options)
+    {
+        var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
+        this.addHandlers.push(hand);
+        return hand;
+    },
+
+    /** Function: deleteHandler
+     *  Delete a stanza handler for a connection.
+     *
+     *  This function removes a stanza handler from the connection.  The
+     *  handRef parameter is *not* the function passed to addHandler(),
+     *  but is the reference returned from addHandler().
+     *
+     *  Parameters:
+     *    (Strophe.Handler) handRef - The handler reference.
+     */
+    deleteHandler: function (handRef)
+    {
+        // this must be done in the Idle loop so that we don't change
+        // the handlers during iteration
+        this.removeHandlers.push(handRef);
+    },
+
+    /** Function: disconnect
+     *  Start the graceful disconnection process.
+     *
+     *  This function starts the disconnection process.  This process starts
+     *  by sending unavailable presence and sending BOSH body of type
+     *  terminate.  A timeout handler makes sure that disconnection happens
+     *  even if the BOSH server does not respond.
+     *
+     *  The user supplied connection callback will be notified of the
+     *  progress as this process happens.
+     *
+     *  Parameters:
+     *    (String) reason - The reason the disconnect is occuring.
+     */
+    disconnect: function (reason)
+    {
+        this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
+
+        Strophe.info("Disconnect was called because: " + reason);
+        if (this.connected) {
+            // setup timeout handler
+            this._disconnectTimeout = this._addSysTimedHandler(
+                3000, this._onDisconnectTimeout.bind(this));
+            this._sendTerminate();
+        }
+    },
+
+    /** PrivateFunction: _changeConnectStatus
+     *  _Private_ helper function that makes sure plugins and the user's
+     *  callback are notified of connection status changes.
+     *
+     *  Parameters:
+     *    (Integer) status - the new connection status, one of the values
+     *      in Strophe.Status
+     *    (String) condition - the error condition or null
+     */
+    _changeConnectStatus: function (status, condition)
+    {
+        // notify all plugins listening for status changes
+        for (var k in Strophe._connectionPlugins) {
+            if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+                var plugin = this[k];
+                if (plugin.statusChanged) {
+                    try {
+                        plugin.statusChanged(status, condition);
+                    } catch (err) {
+                        Strophe.error("" + k + " plugin caused an exception " +
+                                      "changing status: " + err);
+                    }
+                }
+            }
+        }
+
+        // notify the user's callback
+        if (this.connect_callback) {
+            try {
+                this.connect_callback(status, condition);
+            } catch (e) {
+                Strophe.error("User connection callback caused an " +
+                              "exception: " + e);
+            }
+        }
+    },
+
+    /** PrivateFunction: _buildBody
+     *  _Private_ helper function to generate the <body/> wrapper for BOSH.
+     *
+     *  Returns:
+     *    A Strophe.Builder with a <body/> element.
+     */
+    _buildBody: function ()
+    {
+        var bodyWrap = $build('body', {
+            rid: this.rid++,
+            xmlns: Strophe.NS.HTTPBIND
+        });
+
+        if (this.sid !== null) {
+            bodyWrap.attrs({sid: this.sid});
+        }
+
+        return bodyWrap;
+    },
+
+    /** PrivateFunction: _removeRequest
+     *  _Private_ function to remove a request from the queue.
+     *
+     *  Parameters:
+     *    (Strophe.Request) req - The request to remove.
+     */
+    _removeRequest: function (req)
+    {
+        Strophe.debug("removing request");
+
+        var i;
+        for (i = this._requests.length - 1; i >= 0; i--) {
+            if (req == this._requests[i]) {
+                this._requests.splice(i, 1);
+            }
+        }
+
+        // IE6 fails on setting to null, so set to empty function
+        req.xhr.onreadystatechange = function () {};
+
+        this._throttledRequestHandler();
+    },
+
+    /** PrivateFunction: _restartRequest
+     *  _Private_ function to restart a request that is presumed dead.
+     *
+     *  Parameters:
+     *    (Integer) i - The index of the request in the queue.
+     */
+    _restartRequest: function (i)
+    {
+        var req = this._requests[i];
+        if (req.dead === null) {
+            req.dead = new Date();
+        }
+
+        this._processRequest(i);
+    },
+
+    /** PrivateFunction: _processRequest
+     *  _Private_ function to process a request in the queue.
+     *
+     *  This function takes requests off the queue and sends them and
+     *  restarts dead requests.
+     *
+     *  Parameters:
+     *    (Integer) i - The index of the request in the queue.
+     */
+    _processRequest: function (i)
+    {
+        var req = this._requests[i];
+        var reqStatus = -1;
+
+        try {
+            if (req.xhr.readyState == 4) {
+                reqStatus = req.xhr.status;
+            }
+        } catch (e) {
+            Strophe.error("caught an error in _requests[" + i +
+                          "], reqStatus: " + reqStatus);
+        }
+
+        if (typeof(reqStatus) == "undefined") {
+            reqStatus = -1;
+        }
+
+        // make sure we limit the number of retries
+        if (req.sends > this.maxRetries) {
+            this._onDisconnectTimeout();
+            return;
+        }
+
+        var time_elapsed = req.age();
+        var primaryTimeout = (!isNaN(time_elapsed) &&
+                              time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait));
+        var secondaryTimeout = (req.dead !== null &&
+                                req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait));
+        var requestCompletedWithServerError = (req.xhr.readyState == 4 &&
+                                               (reqStatus < 1 ||
+                                                reqStatus >= 500));
+        if (primaryTimeout || secondaryTimeout ||
+            requestCompletedWithServerError) {
+            if (secondaryTimeout) {
+                Strophe.error("Request " +
+                              this._requests[i].id +
+                              " timed out (secondary), restarting");
+            }
+            req.abort = true;
+            req.xhr.abort();
+            // setting to null fails on IE6, so set to empty function
+            req.xhr.onreadystatechange = function () {};
+            this._requests[i] = new Strophe.Request(req.xmlData,
+                                                    req.origFunc,
+                                                    req.rid,
+                                                    req.sends);
+            req = this._requests[i];
+        }
+
+        if (req.xhr.readyState === 0) {
+            Strophe.debug("request id " + req.id +
+                          "." + req.sends + " posting");
+
+            try {
+                req.xhr.open("POST", this.service, true);
+            } catch (e2) {
+                Strophe.error("XHR open failed.");
+                if (!this.connected) {
+                    this._changeConnectStatus(Strophe.Status.CONNFAIL,
+                                              "bad-service");
+                }
+                this.disconnect();
+                return;
+            }
+
+            // Fires the XHR request -- may be invoked immediately
+            // or on a gradually expanding retry window for reconnects
+            var sendFunc = function () {
+                req.date = new Date();
+                req.xhr.send(req.data);
+            };
+
+            // Implement progressive backoff for reconnects --
+            // First retry (send == 1) should also be instantaneous
+            if (req.sends > 1) {
+                // Using a cube of the retry number creates a nicely
+                // expanding retry window
+                var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait),
+                                       Math.pow(req.sends, 3)) * 1000;
+                setTimeout(sendFunc, backoff);
+            } else {
+                sendFunc();
+            }
+
+            req.sends++;
+
+            if (this.xmlOutput !== Strophe.Connection.prototype.xmlOutput) {
+                this.xmlOutput(req.xmlData);
+            }
+            if (this.rawOutput !== Strophe.Connection.prototype.rawOutput) {
+                this.rawOutput(req.data);
+            }
+        } else {
+            Strophe.debug("_processRequest: " +
+                          (i === 0 ? "first" : "second") +
+                          " request has readyState of " +
+                          req.xhr.readyState);
+        }
+    },
+
+    /** PrivateFunction: _throttledRequestHandler
+     *  _Private_ function to throttle requests to the connection window.
+     *
+     *  This function makes sure we don't send requests so fast that the
+     *  request ids overflow the connection window in the case that one
+     *  request died.
+     */
+    _throttledRequestHandler: function ()
+    {
+        if (!this._requests) {
+            Strophe.debug("_throttledRequestHandler called with " +
+                          "undefined requests");
+        } else {
+            Strophe.debug("_throttledRequestHandler called with " +
+                          this._requests.length + " requests");
+        }
+
+        if (!this._requests || this._requests.length === 0) {
+            return;
+        }
+
+        if (this._requests.length > 0) {
+            this._processRequest(0);
+        }
+
+        if (this._requests.length > 1 &&
+            Math.abs(this._requests[0].rid -
+                     this._requests[1].rid) < this.window) {
+            this._processRequest(1);
+        }
+    },
+
+    /** PrivateFunction: _onRequestStateChange
+     *  _Private_ handler for Strophe.Request state changes.
+     *
+     *  This function is called when the XMLHttpRequest readyState changes.
+     *  It contains a lot of error handling logic for the many ways that
+     *  requests can fail, and calls the request callback when requests
+     *  succeed.
+     *
+     *  Parameters:
+     *    (Function) func - The handler for the request.
+     *    (Strophe.Request) req - The request that is changing readyState.
+     */
+    _onRequestStateChange: function (func, req)
+    {
+        Strophe.debug("request id " + req.id +
+                      "." + req.sends + " state changed to " +
+                      req.xhr.readyState);
+
+        if (req.abort) {
+            req.abort = false;
+            return;
+        }
+
+        // request complete
+        var reqStatus;
+        if (req.xhr.readyState == 4) {
+            reqStatus = 0;
+            try {
+                reqStatus = req.xhr.status;
+            } catch (e) {
+                // ignore errors from undefined status attribute.  works
+                // around a browser bug
+            }
+
+            if (typeof(reqStatus) == "undefined") {
+                reqStatus = 0;
+            }
+
+            if (this.disconnecting) {
+                if (reqStatus >= 400) {
+                    this._hitError(reqStatus);
+                    return;
+                }
+            }
+
+            var reqIs0 = (this._requests[0] == req);
+            var reqIs1 = (this._requests[1] == req);
+
+            if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
+                // remove from internal queue
+                this._removeRequest(req);
+                Strophe.debug("request id " +
+                              req.id +
+                              " should now be removed");
+            }
+
+            // request succeeded
+            if (reqStatus == 200) {
+                // if request 1 finished, or request 0 finished and request
+                // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to
+                // restart the other - both will be in the first spot, as the
+                // completed request has been removed from the queue already
+                if (reqIs1 ||
+                    (reqIs0 && this._requests.length > 0 &&
+                     this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) {
+                    this._restartRequest(0);
+                }
+                // call handler
+                Strophe.debug("request id " +
+                              req.id + "." +
+                              req.sends + " got 200");
+                func(req);
+                this.errors = 0;
+            } else {
+                Strophe.error("request id " +
+                              req.id + "." +
+                              req.sends + " error " + reqStatus +
+                              " happened");
+                if (reqStatus === 0 ||
+                    (reqStatus >= 400 && reqStatus < 600) ||
+                    reqStatus >= 12000) {
+                    this._hitError(reqStatus);
+                    if (reqStatus >= 400 && reqStatus < 500) {
+                        this._changeConnectStatus(Strophe.Status.DISCONNECTING,
+                                                  null);
+                        this._doDisconnect();
+                    }
+                }
+            }
+
+            if (!((reqStatus > 0 && reqStatus < 500) ||
+                  req.sends > 5)) {
+                this._throttledRequestHandler();
+            }
+        }
+    },
+
+    /** PrivateFunction: _hitError
+     *  _Private_ function to handle the error count.
+     *
+     *  Requests are resent automatically until their error count reaches
+     *  5.  Each time an error is encountered, this function is called to
+     *  increment the count and disconnect if the count is too high.
+     *
+     *  Parameters:
+     *    (Integer) reqStatus - The request status.
+     */
+    _hitError: function (reqStatus)
+    {
+        this.errors++;
+        Strophe.warn("request errored, status: " + reqStatus +
+                     ", number of errors: " + this.errors);
+        if (this.errors > 4) {
+            this._onDisconnectTimeout();
+        }
+    },
+
+    /** PrivateFunction: _doDisconnect
+     *  _Private_ function to disconnect.
+     *
+     *  This is the last piece of the disconnection logic.  This resets the
+     *  connection and alerts the user's connection callback.
+     */
+    _doDisconnect: function ()
+    {
+        Strophe.info("_doDisconnect was called");
+        this.authenticated = false;
+        this.disconnecting = false;
+        this.sid = null;
+        this.streamId = null;
+        this.rid = Math.floor(Math.random() * 4294967295);
+
+        // tell the parent we disconnected
+        if (this.connected) {
+            this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
+            this.connected = false;
+        }
+
+        // delete handlers
+        this.handlers = [];
+        this.timedHandlers = [];
+        this.removeTimeds = [];
+        this.removeHandlers = [];
+        this.addTimeds = [];
+        this.addHandlers = [];
+    },
+
+    /** PrivateFunction: _dataRecv
+     *  _Private_ handler to processes incoming data from the the connection.
+     *
+     *  Except for _connect_cb handling the initial connection request,
+     *  this function handles the incoming data for all requests.  This
+     *  function also fires stanza handlers that match each incoming
+     *  stanza.
+     *
+     *  Parameters:
+     *    (Strophe.Request) req - The request that has data ready.
+     */
+    _dataRecv: function (req)
+    {
+        try {
+            var elem = req.getResponse();
+        } catch (e) {
+            if (e != "parsererror") { throw e; }
+            this.disconnect("strophe-parsererror");
+        }
+        if (elem === null) { return; }
+
+        if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
+            this.xmlInput(elem);
+        }
+        if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
+            this.rawInput(Strophe.serialize(elem));
+        }
+
+        // remove handlers scheduled for deletion
+        var i, hand;
+        while (this.removeHandlers.length > 0) {
+            hand = this.removeHandlers.pop();
+            i = this.handlers.indexOf(hand);
+            if (i >= 0) {
+                this.handlers.splice(i, 1);
+            }
+        }
+
+        // add handlers scheduled for addition
+        while (this.addHandlers.length > 0) {
+            this.handlers.push(this.addHandlers.pop());
+        }
+
+        // handle graceful disconnect
+        if (this.disconnecting && this._requests.length === 0) {
+            this.deleteTimedHandler(this._disconnectTimeout);
+            this._disconnectTimeout = null;
+            this._doDisconnect();
+            return;
+        }
+
+        var typ = elem.getAttribute("type");
+        var cond, conflict;
+        if (typ !== null && typ == "terminate") {
+            // Don't process stanzas that come in after disconnect
+            if (this.disconnecting) {
+                return;
+            }
+
+            // an error occurred
+            cond = elem.getAttribute("condition");
+            conflict = elem.getElementsByTagName("conflict");
+            if (cond !== null) {
+                if (cond == "remote-stream-error" && conflict.length > 0) {
+                    cond = "conflict";
+                }
+                this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+            } else {
+                this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+            }
+            this.disconnect();
+            return;
+        }
+
+        // send each incoming stanza through the handler chain
+        var that = this;
+        Strophe.forEachChild(elem, null, function (child) {
+            var i, newList;
+            // process handlers
+            newList = that.handlers;
+            that.handlers = [];
+            for (i = 0; i < newList.length; i++) {
+                var hand = newList[i];
+                // encapsulate 'handler.run' not to lose the whole handler list if
+                // one of the handlers throws an exception
+                try {
+                    if (hand.isMatch(child) &&
+                        (that.authenticated || !hand.user)) {
+                        if (hand.run(child)) {
+                            that.handlers.push(hand);
+                        }
+                    } else {
+                        that.handlers.push(hand);
+                    }
+                } catch(e) {
+                    //if the handler throws an exception, we consider it as false
+                }
+            }
+        });
+    },
+
+    /** PrivateFunction: _sendTerminate
+     *  _Private_ function to send initial disconnect sequence.
+     *
+     *  This is the first step in a graceful disconnect.  It sends
+     *  the BOSH server a terminate body and includes an unavailable
+     *  presence if authentication has completed.
+     */
+    _sendTerminate: function ()
+    {
+        Strophe.info("_sendTerminate was called");
+        var body = this._buildBody().attrs({type: "terminate"});
+
+        if (this.authenticated) {
+            body.c('presence', {
+                xmlns: Strophe.NS.CLIENT,
+                type: 'unavailable'
+            });
+        }
+
+        this.disconnecting = true;
+
+        var req = new Strophe.Request(body.tree(),
+                                      this._onRequestStateChange.bind(
+                                          this, this._dataRecv.bind(this)),
+                                      body.tree().getAttribute("rid"));
+
+        this._requests.push(req);
+        this._throttledRequestHandler();
+    },
+
+    /** PrivateFunction: _connect_cb
+     *  _Private_ handler for initial connection request.
+     *
+     *  This handler is used to process the initial connection request
+     *  response from the BOSH server. It is used to set up authentication
+     *  handlers and start the authentication process.
+     *
+     *  SASL authentication will be attempted if available, otherwise
+     *  the code will fall back to legacy authentication.
+     *
+     *  Parameters:
+     *    (Strophe.Request) req - The current request.
+     *    (Function) _callback - low level (xmpp) connect callback function.
+     *      Useful for plugins with their own xmpp connect callback (when their)
+     *      want to do something special).
+     */
+    _connect_cb: function (req, _callback)
+    {
+        Strophe.info("_connect_cb was called");
+
+        this.connected = true;
+        var bodyWrap = req.getResponse();
+        if (!bodyWrap) { return; }
+
+        if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
+            this.xmlInput(bodyWrap);
+        }
+        if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
+            this.rawInput(Strophe.serialize(bodyWrap));
+        }
+
+        var typ = bodyWrap.getAttribute("type");
+        var cond, conflict;
+        if (typ !== null && typ == "terminate") {
+            // an error occurred
+            cond = bodyWrap.getAttribute("condition");
+            conflict = bodyWrap.getElementsByTagName("conflict");
+            if (cond !== null) {
+                if (cond == "remote-stream-error" && conflict.length > 0) {
+                    cond = "conflict";
+                }
+                this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+            } else {
+                this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+            }
+            return;
+        }
+
+        // check to make sure we don't overwrite these if _connect_cb is
+        // called multiple times in the case of missing stream:features
+        if (!this.sid) {
+            this.sid = bodyWrap.getAttribute("sid");
+        }
+        if (!this.stream_id) {
+            this.stream_id = bodyWrap.getAttribute("authid");
+        }
+        var wind = bodyWrap.getAttribute('requests');
+        if (wind) { this.window = parseInt(wind, 10); }
+        var hold = bodyWrap.getAttribute('hold');
+        if (hold) { this.hold = parseInt(hold, 10); }
+        var wait = bodyWrap.getAttribute('wait');
+        if (wait) { this.wait = parseInt(wait, 10); }
+
+        this._authentication.sasl_scram_sha1 = false;
+        this._authentication.sasl_plain = false;
+        this._authentication.sasl_digest_md5 = false;
+        this._authentication.sasl_anonymous = false;
+        this._authentication.legacy_auth = false;
+
+
+        // Check for the stream:features tag
+        var hasFeatures = bodyWrap.getElementsByTagName("stream:features").length > 0;
+        if (!hasFeatures) {
+            hasFeatures = bodyWrap.getElementsByTagName("features").length > 0;
+        }
+        var mechanisms = bodyWrap.getElementsByTagName("mechanism");
+        var i, mech, auth_str, hashed_auth_str,
+            found_authentication = false;
+        if (hasFeatures && mechanisms.length > 0) {
+            var missmatchedmechs = 0;
+            for (i = 0; i < mechanisms.length; i++) {
+                mech = Strophe.getText(mechanisms[i]);
+                if (mech == 'SCRAM-SHA-1') {
+                    this._authentication.sasl_scram_sha1 = true;
+                } else if (mech == 'DIGEST-MD5') {
+                    this._authentication.sasl_digest_md5 = true;
+                } else if (mech == 'PLAIN') {
+                    this._authentication.sasl_plain = true;
+                } else if (mech == 'ANONYMOUS') {
+                    this._authentication.sasl_anonymous = true;
+                } else missmatchedmechs++;
+            }
+
+            this._authentication.legacy_auth =
+                bodyWrap.getElementsByTagName("auth").length > 0;
+
+            found_authentication =
+                this._authentication.legacy_auth ||
+                missmatchedmechs < mechanisms.length;
+        }
+        if (!found_authentication) {
+            _callback = _callback || this._connect_cb;
+            // we didn't get stream:features yet, so we need wait for it
+            // by sending a blank poll request
+            var body = this._buildBody();
+            this._requests.push(
+                new Strophe.Request(body.tree(),
+                                    this._onRequestStateChange.bind(
+                                        this, _callback.bind(this)),
+                                    body.tree().getAttribute("rid")));
+            this._throttledRequestHandler();
+            return;
+        }
+        if (this.do_authentication !== false)
+            this.authenticate();
+    },
+
+    /** Function: authenticate
+     * Set up authentication
+     *
+     *  Contiunues the initial connection request by setting up authentication
+     *  handlers and start the authentication process.
+     *
+     *  SASL authentication will be attempted if available, otherwise
+     *  the code will fall back to legacy authentication.
+     *
+     */
+    authenticate: function ()
+    {
+        if (Strophe.getNodeFromJid(this.jid) === null &&
+            this._authentication.sasl_anonymous) {
+            this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+            this._sasl_success_handler = this._addSysHandler(
+                this._sasl_success_cb.bind(this), null,
+                "success", null, null);
+            this._sasl_failure_handler = this._addSysHandler(
+                this._sasl_failure_cb.bind(this), null,
+                "failure", null, null);
+
+            this.send($build("auth", {
+                xmlns: Strophe.NS.SASL,
+                mechanism: "ANONYMOUS"
+            }).tree());
+        } else if (Strophe.getNodeFromJid(this.jid) === null) {
+            // we don't have a node, which is required for non-anonymous
+            // client connections
+            this._changeConnectStatus(Strophe.Status.CONNFAIL,
+                                      'x-strophe-bad-non-anon-jid');
+            this.disconnect();
+        } else if (this._authentication.sasl_scram_sha1) {
+            var cnonce = MD5.hexdigest(Math.random() * 1234567890);
+
+            var auth_str = "n=" + Strophe.getNodeFromJid(this.jid);
+            auth_str += ",r=";
+            auth_str += cnonce;
+
+            this._sasl_data["cnonce"] = cnonce;
+            this._sasl_data["client-first-message-bare"] = auth_str;
+
+            auth_str = "n,," + auth_str;
+
+            this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+            this._sasl_challenge_handler = this._addSysHandler(
+                this._sasl_scram_challenge_cb.bind(this), null,
+                "challenge", null, null);
+            this._sasl_failure_handler = this._addSysHandler(
+                this._sasl_failure_cb.bind(this), null,
+                "failure", null, null);
+
+            this.send($build("auth", {
+                xmlns: Strophe.NS.SASL,
+                mechanism: "SCRAM-SHA-1"
+            }).t(Base64.encode(auth_str)).tree());
+        } else if (this._authentication.sasl_digest_md5) {
+            this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+            this._sasl_challenge_handler = this._addSysHandler(
+                this._sasl_digest_challenge1_cb.bind(this), null,
+                "challenge", null, null);
+            this._sasl_failure_handler = this._addSysHandler(
+                this._sasl_failure_cb.bind(this), null,
+                "failure", null, null);
+
+            this.send($build("auth", {
+                xmlns: Strophe.NS.SASL,
+                mechanism: "DIGEST-MD5"
+            }).tree());
+        } else if (this._authentication.sasl_plain) {
+            // Build the plain auth string (barejid null
+            // username null password) and base 64 encoded.
+            auth_str = Strophe.getBareJidFromJid(this.jid);
+            auth_str = auth_str + "\u0000";
+            auth_str = auth_str + Strophe.getNodeFromJid(this.jid);
+            auth_str = auth_str + "\u0000";
+            auth_str = auth_str + this.pass;
+
+            this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+            this._sasl_success_handler = this._addSysHandler(
+                this._sasl_success_cb.bind(this), null,
+                "success", null, null);
+            this._sasl_failure_handler = this._addSysHandler(
+                this._sasl_failure_cb.bind(this), null,
+                "failure", null, null);
+
+            hashed_auth_str = Base64.encode(auth_str);
+            this.send($build("auth", {
+                xmlns: Strophe.NS.SASL,
+                mechanism: "PLAIN"
+            }).t(hashed_auth_str).tree());
+        } else {
+            this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+            this._addSysHandler(this._auth1_cb.bind(this), null, null,
+                                null, "_auth_1");
+
+            this.send($iq({
+                type: "get",
+                to: this.domain,
+                id: "_auth_1"
+            }).c("query", {
+                xmlns: Strophe.NS.AUTH
+            }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree());
+        }
+    },
+
+    /** PrivateFunction: _sasl_digest_challenge1_cb
+     *  _Private_ handler for DIGEST-MD5 SASL authentication.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The challenge stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_digest_challenge1_cb: function (elem)
+    {
+        var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
+
+        var challenge = Base64.decode(Strophe.getText(elem));
+        var cnonce = MD5.hexdigest("" + (Math.random() * 1234567890));
+        var realm = "";
+        var host = null;
+        var nonce = "";
+        var qop = "";
+        var matches;
+
+        // remove unneeded handlers
+        this.deleteHandler(this._sasl_failure_handler);
+
+        while (challenge.match(attribMatch)) {
+            matches = challenge.match(attribMatch);
+            challenge = challenge.replace(matches[0], "");
+            matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
+            switch (matches[1]) {
+            case "realm":
+                realm = matches[2];
+                break;
+            case "nonce":
+                nonce = matches[2];
+                break;
+            case "qop":
+                qop = matches[2];
+                break;
+            case "host":
+                host = matches[2];
+                break;
+            }
+        }
+
+        var digest_uri = "xmpp/" + this.domain;
+        if (host !== null) {
+            digest_uri = digest_uri + "/" + host;
+        }
+
+        var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) +
+                          ":" + realm + ":" + this.pass) +
+            ":" + nonce + ":" + cnonce;
+        var A2 = 'AUTHENTICATE:' + digest_uri;
+
+        var responseText = "";
+        responseText += 'username=' +
+            this._quote(Strophe.getNodeFromJid(this.jid)) + ',';
+        responseText += 'realm=' + this._quote(realm) + ',';
+        responseText += 'nonce=' + this._quote(nonce) + ',';
+        responseText += 'cnonce=' + this._quote(cnonce) + ',';
+        responseText += 'nc="00000001",';
+        responseText += 'qop="auth",';
+        responseText += 'digest-uri=' + this._quote(digest_uri) + ',';
+        responseText += 'response=' + this._quote(
+            MD5.hexdigest(MD5.hexdigest(A1) + ":" +
+                          nonce + ":00000001:" +
+                          cnonce + ":auth:" +
+                          MD5.hexdigest(A2))) + ',';
+        responseText += 'charset="utf-8"';
+
+        this._sasl_challenge_handler = this._addSysHandler(
+            this._sasl_digest_challenge2_cb.bind(this), null,
+            "challenge", null, null);
+        this._sasl_success_handler = this._addSysHandler(
+            this._sasl_success_cb.bind(this), null,
+            "success", null, null);
+        this._sasl_failure_handler = this._addSysHandler(
+            this._sasl_failure_cb.bind(this), null,
+            "failure", null, null);
+
+        this.send($build('response', {
+            xmlns: Strophe.NS.SASL
+        }).t(Base64.encode(responseText)).tree());
+
+        return false;
+    },
+
+    /** PrivateFunction: _quote
+     *  _Private_ utility function to backslash escape and quote strings.
+     *
+     *  Parameters:
+     *    (String) str - The string to be quoted.
+     *
+     *  Returns:
+     *    quoted string
+     */
+    _quote: function (str)
+    {
+        return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
+        //" end string workaround for emacs
+    },
+
+
+    /** PrivateFunction: _sasl_digest_challenge2_cb
+     *  _Private_ handler for second step of DIGEST-MD5 SASL authentication.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The challenge stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_digest_challenge2_cb: function (elem)
+    {
+        // remove unneeded handlers
+        this.deleteHandler(this._sasl_success_handler);
+        this.deleteHandler(this._sasl_failure_handler);
+
+        this._sasl_success_handler = this._addSysHandler(
+            this._sasl_success_cb.bind(this), null,
+            "success", null, null);
+        this._sasl_failure_handler = this._addSysHandler(
+            this._sasl_failure_cb.bind(this), null,
+            "failure", null, null);
+        this.send($build('response', {xmlns: Strophe.NS.SASL}).tree());
+        return false;
+    },
+
+    /** PrivateFunction: _sasl_scram_challenge_cb
+     *  _Private_ handler for SCRAM-SHA-1 SASL authentication.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The challenge stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_scram_challenge_cb: function (elem)
+    {
+        var nonce, salt, iter, Hi, U, U_old;
+        var clientKey, serverKey, clientSignature;
+        var responseText = "c=biws,";
+        var challenge = Base64.decode(Strophe.getText(elem));
+        var authMessage = this._sasl_data["client-first-message-bare"] + "," +
+            challenge + ",";
+        var cnonce = this._sasl_data["cnonce"]
+        var attribMatch = /([a-z]+)=([^,]+)(,|$)/;
+
+        // remove unneeded handlers
+        this.deleteHandler(this._sasl_failure_handler);
+
+        while (challenge.match(attribMatch)) {
+            matches = challenge.match(attribMatch);
+            challenge = challenge.replace(matches[0], "");
+            switch (matches[1]) {
+            case "r":
+                nonce = matches[2];
+                break;
+            case "s":
+                salt = matches[2];
+                break;
+            case "i":
+                iter = matches[2];
+                break;
+            }
+        }
+
+        if (!(nonce.substr(0, cnonce.length) === cnonce)) {
+            this._sasl_data = [];
+            return this._sasl_failure_cb(null);
+        }
+
+        responseText += "r=" + nonce;
+        authMessage += responseText;
+
+        salt = Base64.decode(salt);
+        salt += "\0\0\0\1";
+
+        Hi = U_old = core_hmac_sha1(this.pass, salt);
+        for (i = 1; i < iter; i++) {
+            U = core_hmac_sha1(this.pass, binb2str(U_old));
+            for (k = 0; k < 5; k++) {
+                Hi[k] ^= U[k];
+            }
+            U_old = U;
+        }
+        Hi = binb2str(Hi);
+
+        clientKey = core_hmac_sha1(Hi, "Client Key");
+        serverKey = str_hmac_sha1(Hi, "Server Key");
+        clientSignature = core_hmac_sha1(str_sha1(binb2str(clientKey)), authMessage);
+        this._sasl_data["server-signature"] = b64_hmac_sha1(serverKey, authMessage);
+
+        for (k = 0; k < 5; k++) {
+            clientKey[k] ^= clientSignature[k];
+        }
+
+        responseText += ",p=" + Base64.encode(binb2str(clientKey));
+
+        this._sasl_success_handler = this._addSysHandler(
+            this._sasl_success_cb.bind(this), null,
+            "success", null, null);
+        this._sasl_failure_handler = this._addSysHandler(
+            this._sasl_failure_cb.bind(this), null,
+            "failure", null, null);
+
+        this.send($build('response', {
+            xmlns: Strophe.NS.SASL
+        }).t(Base64.encode(responseText)).tree());
+
+        return false;
+    },
+
+    /** PrivateFunction: _auth1_cb
+     *  _Private_ handler for legacy authentication.
+     *
+     *  This handler is called in response to the initial <iq type='get'/>
+     *  for legacy authentication.  It builds an authentication <iq/> and
+     *  sends it, creating a handler (calling back to _auth2_cb()) to
+     *  handle the result
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The stanza that triggered the callback.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _auth1_cb: function (elem)
+    {
+        // build plaintext auth iq
+        var iq = $iq({type: "set", id: "_auth_2"})
+            .c('query', {xmlns: Strophe.NS.AUTH})
+            .c('username', {}).t(Strophe.getNodeFromJid(this.jid))
+            .up()
+            .c('password').t(this.pass);
+
+        if (!Strophe.getResourceFromJid(this.jid)) {
+            // since the user has not supplied a resource, we pick
+            // a default one here.  unlike other auth methods, the server
+            // cannot do this for us.
+            this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
+        }
+        iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
+
+        this._addSysHandler(this._auth2_cb.bind(this), null,
+                            null, null, "_auth_2");
+
+        this.send(iq.tree());
+
+        return false;
+    },
+
+    /** PrivateFunction: _sasl_success_cb
+     *  _Private_ handler for succesful SASL authentication.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The matching stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_success_cb: function (elem)
+    {
+        if (this._sasl_data["server-signature"]) {
+            var serverSignature;
+            var success = Base64.decode(Strophe.getText(elem));
+            var attribMatch = /([a-z]+)=([^,]+)(,|$)/;
+            matches = success.match(attribMatch);
+            if (matches[1] == "v") {
+                serverSignature = matches[2];
+            }
+	    if (serverSignature != this._sasl_data["server-signature"]) {
+		// remove old handlers
+		this.deleteHandler(this._sasl_failure_handler);
+		this._sasl_failure_handler = null;
+		if (this._sasl_challenge_handler) {
+			this.deleteHandler(this._sasl_challenge_handler);
+			this._sasl_challenge_handler = null;
+		}
+
+		this._sasl_data = [];
+		return this._sasl_failure_cb(null);
+	    }
+	}
+
+	Strophe.info("SASL authentication succeeded.");
+
+        // remove old handlers
+        this.deleteHandler(this._sasl_failure_handler);
+        this._sasl_failure_handler = null;
+        if (this._sasl_challenge_handler) {
+            this.deleteHandler(this._sasl_challenge_handler);
+            this._sasl_challenge_handler = null;
+        }
+
+        this._addSysHandler(this._sasl_auth1_cb.bind(this), null,
+                            "stream:features", null, null);
+
+        // we must send an xmpp:restart now
+        this._sendRestart();
+
+        return false;
+    },
+
+    /** PrivateFunction: _sasl_auth1_cb
+     *  _Private_ handler to start stream binding.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The matching stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_auth1_cb: function (elem)
+    {
+        // save stream:features for future usage
+        this.features = elem;
+
+        var i, child;
+
+        for (i = 0; i < elem.childNodes.length; i++) {
+            child = elem.childNodes[i];
+            if (child.nodeName == 'bind') {
+                this.do_bind = true;
+            }
+
+            if (child.nodeName == 'session') {
+                this.do_session = true;
+            }
+        }
+
+        if (!this.do_bind) {
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            return false;
+        } else {
+            this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
+                                null, "_bind_auth_2");
+
+            var resource = Strophe.getResourceFromJid(this.jid);
+            if (resource) {
+                this.send($iq({type: "set", id: "_bind_auth_2"})
+                          .c('bind', {xmlns: Strophe.NS.BIND})
+                          .c('resource', {}).t(resource).tree());
+            } else {
+                this.send($iq({type: "set", id: "_bind_auth_2"})
+                          .c('bind', {xmlns: Strophe.NS.BIND})
+                          .tree());
+            }
+        }
+
+        return false;
+    },
+
+    /** PrivateFunction: _sasl_bind_cb
+     *  _Private_ handler for binding result and session start.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The matching stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_bind_cb: function (elem)
+    {
+        if (elem.getAttribute("type") == "error") {
+            Strophe.info("SASL binding failed.");
+			var conflict = elem.getElementsByTagName("conflict"), condition;
+			if (conflict.length > 0) {
+				condition = 'conflict';
+			}
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition);
+            return false;
+        }
+
+        // TODO - need to grab errors
+        var bind = elem.getElementsByTagName("bind");
+        var jidNode;
+        if (bind.length > 0) {
+            // Grab jid
+            jidNode = bind[0].getElementsByTagName("jid");
+            if (jidNode.length > 0) {
+                this.jid = Strophe.getText(jidNode[0]);
+
+                if (this.do_session) {
+                    this._addSysHandler(this._sasl_session_cb.bind(this),
+                                        null, null, null, "_session_auth_2");
+
+                    this.send($iq({type: "set", id: "_session_auth_2"})
+                                  .c('session', {xmlns: Strophe.NS.SESSION})
+                                  .tree());
+                } else {
+                    this.authenticated = true;
+                    this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+                }
+            }
+        } else {
+            Strophe.info("SASL binding failed.");
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            return false;
+        }
+    },
+
+    /** PrivateFunction: _sasl_session_cb
+     *  _Private_ handler to finish successful SASL connection.
+     *
+     *  This sets Connection.authenticated to true on success, which
+     *  starts the processing of user handlers.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The matching stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_session_cb: function (elem)
+    {
+        if (elem.getAttribute("type") == "result") {
+            this.authenticated = true;
+            this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+        } else if (elem.getAttribute("type") == "error") {
+            Strophe.info("Session creation failed.");
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            return false;
+        }
+
+        return false;
+    },
+
+    /** PrivateFunction: _sasl_failure_cb
+     *  _Private_ handler for SASL authentication failure.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The matching stanza.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _sasl_failure_cb: function (elem)
+    {
+        // delete unneeded handlers
+        if (this._sasl_success_handler) {
+            this.deleteHandler(this._sasl_success_handler);
+            this._sasl_success_handler = null;
+        }
+        if (this._sasl_challenge_handler) {
+            this.deleteHandler(this._sasl_challenge_handler);
+            this._sasl_challenge_handler = null;
+        }
+
+        this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+        return false;
+    },
+
+    /** PrivateFunction: _auth2_cb
+     *  _Private_ handler to finish legacy authentication.
+     *
+     *  This handler is called when the result from the jabber:iq:auth
+     *  <iq/> stanza is returned.
+     *
+     *  Parameters:
+     *    (XMLElement) elem - The stanza that triggered the callback.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _auth2_cb: function (elem)
+    {
+        if (elem.getAttribute("type") == "result") {
+            this.authenticated = true;
+            this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+        } else if (elem.getAttribute("type") == "error") {
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            this.disconnect();
+        }
+
+        return false;
+    },
+
+    /** PrivateFunction: _addSysTimedHandler
+     *  _Private_ function to add a system level timed handler.
+     *
+     *  This function is used to add a Strophe.TimedHandler for the
+     *  library code.  System timed handlers are allowed to run before
+     *  authentication is complete.
+     *
+     *  Parameters:
+     *    (Integer) period - The period of the handler.
+     *    (Function) handler - The callback function.
+     */
+    _addSysTimedHandler: function (period, handler)
+    {
+        var thand = new Strophe.TimedHandler(period, handler);
+        thand.user = false;
+        this.addTimeds.push(thand);
+        return thand;
+    },
+
+    /** PrivateFunction: _addSysHandler
+     *  _Private_ function to add a system level stanza handler.
+     *
+     *  This function is used to add a Strophe.Handler for the
+     *  library code.  System stanza handlers are allowed to run before
+     *  authentication is complete.
+     *
+     *  Parameters:
+     *    (Function) handler - The callback function.
+     *    (String) ns - The namespace to match.
+     *    (String) name - The stanza name to match.
+     *    (String) type - The stanza type attribute to match.
+     *    (String) id - The stanza id attribute to match.
+     */
+    _addSysHandler: function (handler, ns, name, type, id)
+    {
+        var hand = new Strophe.Handler(handler, ns, name, type, id);
+        hand.user = false;
+        this.addHandlers.push(hand);
+        return hand;
+    },
+
+    /** PrivateFunction: _onDisconnectTimeout
+     *  _Private_ timeout handler for handling non-graceful disconnection.
+     *
+     *  If the graceful disconnect process does not complete within the
+     *  time allotted, this handler finishes the disconnect anyway.
+     *
+     *  Returns:
+     *    false to remove the handler.
+     */
+    _onDisconnectTimeout: function ()
+    {
+        Strophe.info("_onDisconnectTimeout was called");
+
+        // cancel all remaining requests and clear the queue
+        var req;
+        while (this._requests.length > 0) {
+            req = this._requests.pop();
+            req.abort = true;
+            req.xhr.abort();
+            // jslint complains, but this is fine. setting to empty func
+            // is necessary for IE6
+            req.xhr.onreadystatechange = function () {};
+        }
+
+        // actually disconnect
+        this._doDisconnect();
+
+        return false;
+    },
+
+    /** PrivateFunction: _onIdle
+     *  _Private_ handler to process events during idle cycle.
+     *
+     *  This handler is called every 100ms to fire timed handlers that
+     *  are ready and keep poll requests going.
+     */
+    _onIdle: function ()
+    {
+        var i, thand, since, newList;
+
+        // add timed handlers scheduled for addition
+        // NOTE: we add before remove in the case a timed handler is
+        // added and then deleted before the next _onIdle() call.
+        while (this.addTimeds.length > 0) {
+            this.timedHandlers.push(this.addTimeds.pop());
+        }
+
+        // remove timed handlers that have been scheduled for deletion
+        while (this.removeTimeds.length > 0) {
+            thand = this.removeTimeds.pop();
+            i = this.timedHandlers.indexOf(thand);
+            if (i >= 0) {
+                this.timedHandlers.splice(i, 1);
+            }
+        }
+
+        // call ready timed handlers
+        var now = new Date().getTime();
+        newList = [];
+        for (i = 0; i < this.timedHandlers.length; i++) {
+            thand = this.timedHandlers[i];
+            if (this.authenticated || !thand.user) {
+                since = thand.lastCalled + thand.period;
+                if (since - now <= 0) {
+                    if (thand.run()) {
+                        newList.push(thand);
+                    }
+                } else {
+                    newList.push(thand);
+                }
+            }
+        }
+        this.timedHandlers = newList;
+
+        var body, time_elapsed;
+
+        // if no requests are in progress, poll
+        if (this.authenticated && this._requests.length === 0 &&
+            this._data.length === 0 && !this.disconnecting) {
+            Strophe.info("no requests during idle cycle, sending " +
+                         "blank request");
+            this._data.push(null);
+        }
+
+        if (this._requests.length < 2 && this._data.length > 0 &&
+            !this.paused) {
+            body = this._buildBody();
+            for (i = 0; i < this._data.length; i++) {
+                if (this._data[i] !== null) {
+                    if (this._data[i] === "restart") {
+                        body.attrs({
+                            to: this.domain,
+                            "xml:lang": "en",
+                            "xmpp:restart": "true",
+                            "xmlns:xmpp": Strophe.NS.BOSH
+                        });
+                    } else {
+                        body.cnode(this._data[i]).up();
+                    }
+                }
+            }
+            delete this._data;
+            this._data = [];
+            this._requests.push(
+                new Strophe.Request(body.tree(),
+                                    this._onRequestStateChange.bind(
+                                        this, this._dataRecv.bind(this)),
+                                    body.tree().getAttribute("rid")));
+            this._processRequest(this._requests.length - 1);
+        }
+
+        if (this._requests.length > 0) {
+            time_elapsed = this._requests[0].age();
+            if (this._requests[0].dead !== null) {
+                if (this._requests[0].timeDead() >
+                    Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) {
+                    this._throttledRequestHandler();
+                }
+            }
+
+            if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) {
+                Strophe.warn("Request " +
+                             this._requests[0].id +
+                             " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) +
+                             " seconds since last activity");
+                this._throttledRequestHandler();
+            }
+        }
+
+        clearTimeout(this._idleTimeout);
+
+        // reactivate the timer only if connected
+        if (this.connected) {
+            this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+        }
+    }
+};
+
+if (callback) {
+    callback(Strophe, $build, $msg, $iq, $pres);
+}
+
+})(function () {
+    window.Strophe = arguments[0];
+    window.$build = arguments[1];
+    window.$msg = arguments[2];
+    window.$iq = arguments[3];
+    window.$pres = arguments[4];
+});
diff --git a/src/core/js/lib/strophe/strophe.muc.js b/src/core/js/lib/strophe/strophe.muc.js
new file mode 100755
index 0000000..a48aaf2
--- /dev/null
+++ b/src/core/js/lib/strophe/strophe.muc.js
@@ -0,0 +1,1013 @@
+// Generated by CoffeeScript 1.3.3
+/*
+ *Plugin to implement the MUC extension.
+   http://xmpp.org/extensions/xep-0045.html
+ *Previous Author:
+    Nathan Zorn <nathan.zorn at gmail.com>
+ *Complete CoffeeScript rewrite:
+    Andreas Guth <guth at dbis.rwth-aachen.de>
+*/
+
+var Occupant, RoomConfig, XmppRoom,
+  __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+Strophe.addConnectionPlugin('muc', {
+  _connection: null,
+  rooms: [],
+  /*Function
+  Initialize the MUC plugin. Sets the correct connection object and
+  extends the namesace.
+  */
+
+  init: function(conn) {
+    this._connection = conn;
+    this._muc_handler = null;
+    Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
+    Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
+    Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
+    return Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
+  },
+  /*Function
+  Join a multi-user chat room
+  Parameters:
+  (String) room - The multi-user chat room to join.
+  (String) nick - The nickname to use in the chat room. Optional
+  (Function) msg_handler_cb - The function call to handle messages from the
+  specified chat room.
+  (Function) pres_handler_cb - The function call back to handle presence
+  in the chat room.
+  (String) password - The optional password to use. (password protected
+  rooms only)
+  */
+
+  join: function(room, nick, msg_handler_cb, pres_handler_cb, roster_cb, password) {
+    var msg, room_nick, _base, _ref, _ref1,
+      _this = this;
+    room_nick = this.test_append_nick(room, nick);
+    msg = $pres({
+      from: this._connection.jid,
+      to: room_nick
+    }).c("x", {
+      xmlns: Strophe.NS.MUC
+    });
+    if (password != null) {
+      msg.cnode(Strophe.xmlElement("password", [], password));
+    }
+    if ((_ref = this._muc_handler) == null) {
+      this._muc_handler = this._connection.addHandler(function(stanza) {
+        var from, handler, handlers, id, roomname, x, xmlns, xquery, _i, _len;
+        from = stanza.getAttribute('from');
+        if (!from) {
+          return true;
+        }
+        roomname = from.split("/")[0];
+        if (!_this.rooms[roomname]) {
+          return true;
+        }
+        room = _this.rooms[roomname];
+        handlers = {};
+        if (stanza.nodeName === "message") {
+          handlers = room._message_handlers;
+        } else if (stanza.nodeName === "presence") {
+          xquery = stanza.getElementsByTagName("x");
+          if (xquery.length > 0) {
+            for (_i = 0, _len = xquery.length; _i < _len; _i++) {
+              x = xquery[_i];
+              xmlns = x.getAttribute("xmlns");
+              if (xmlns && xmlns.match(Strophe.NS.MUC)) {
+                handlers = room._presence_handlers;
+                break;
+              }
+            }
+          }
+        }
+        for (id in handlers) {
+          handler = handlers[id];
+          if (!handler(stanza, room)) {
+            delete handlers[id];
+          }
+        }
+        return true;
+      });
+    }
+    if ((_ref1 = (_base = this.rooms)[room]) == null) {
+      _base[room] = new XmppRoom(this, room, nick, password);
+    }
+    if (pres_handler_cb) {
+      this.rooms[room].addHandler('presence', pres_handler_cb);
+    }
+    if (msg_handler_cb) {
+      this.rooms[room].addHandler('message', msg_handler_cb);
+    }
+    if (roster_cb) {
+      this.rooms[room].addHandler('roster', roster_cb);
+    }
+    return this._connection.send(msg);
+  },
+  /*Function
+  Leave a multi-user chat room
+  Parameters:
+  (String) room - The multi-user chat room to leave.
+  (String) nick - The nick name used in the room.
+  (Function) handler_cb - Optional function to handle the successful leave.
+  (String) exit_msg - optional exit message.
+  Returns:
+  iqid - The unique id for the room leave.
+  */
+
+  leave: function(room, nick, handler_cb, exit_msg) {
+    var presence, presenceid, room_nick;
+    delete this.rooms[room];
+    if (this.rooms.length === 0) {
+      this._connection.deleteHandler(this._muc_handler);
+      this._muc_handler = null;
+    }
+    room_nick = this.test_append_nick(room, nick);
+    presenceid = this._connection.getUniqueId();
+    presence = $pres({
+      type: "unavailable",
+      id: presenceid,
+      from: this._connection.jid,
+      to: room_nick
+    });
+    if (exit_msg != null) {
+      presence.c("status", exit_msg);
+    }
+    if (handler_cb != null) {
+      this._connection.addHandler(handler_cb, null, "presence", null, presenceid);
+    }
+    this._connection.send(presence);
+    return presenceid;
+  },
+  /*Function
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) nick - The nick name used in the chat room.
+  (String) message - The plaintext message to send to the room.
+  (String) html_message - The message to send to the room with html markup.
+  (String) type - "groupchat" for group chat messages o
+                  "chat" for private chat messages
+  (String) notification - "composing" for composing message
+                          "active" for not composing message
+  Returns:
+  msgiq - the unique id used to send the message
+  */
+
+  message: function(room, nick, message, html_message, type, notification) {
+    var msg, msgid, parent, room_nick;
+    room_nick = this.test_append_nick(room, nick);
+    type = type || (nick != null ? "chat" : "groupchat");
+    msgid = this._connection.getUniqueId();
+    msg = $msg({
+      to: room_nick,
+      from: this._connection.jid,
+      type: type,
+      id: msgid
+    }).c("body", {
+      xmlns: Strophe.NS.CLIENT
+    }).t(message);
+    msg.up();
+    if (html_message != null) {
+      msg.c("html", {
+        xmlns: Strophe.NS.XHTML_IM
+      }).c("body", {
+        xmlns: Strophe.NS.XHTML
+      }).h(html_message);
+      if (msg.node.childNodes.length === 0) {
+        parent = msg.node.parentNode;
+        msg.up().up();
+        msg.node.removeChild(parent);
+      } else {
+        msg.up().up();
+      }
+    }
+    msg.c("x", {
+      xmlns: "jabber:x:event"
+    }).c(notification);
+    this._connection.send(msg);
+    return msgid;
+  },
+  /*Function
+  Convenience Function to send a Message to all Occupants
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) message - The plaintext message to send to the room.
+  (String) html_message - The message to send to the room with html markup.
+  Returns:
+  msgiq - the unique id used to send the message
+  */
+
+  groupchat: function(room, message, html_message) {
+    return this.message(room, null, message, html_message);
+  },
+  /*Function
+  Send a mediated invitation.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) receiver - The invitation's receiver.
+  (String) reason - Optional reason for joining the room.
+  Returns:
+  msgiq - the unique id used to send the invitation
+  */
+
+  invite: function(room, receiver, reason) {
+    var invitation, msgid;
+    msgid = this._connection.getUniqueId();
+    invitation = $msg({
+      from: this._connection.jid,
+      to: room,
+      id: msgid
+    }).c('x', {
+      xmlns: Strophe.NS.MUC_USER
+    }).c('invite', {
+      to: receiver
+    });
+    if (reason != null) {
+      invitation.c('reason', reason);
+    }
+    this._connection.send(invitation);
+    return msgid;
+  },
+  /*Function
+  Send a direct invitation.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) receiver - The invitation's receiver.
+  (String) reason - Optional reason for joining the room.
+  (String) password - Optional password for the room.
+  Returns:
+  msgiq - the unique id used to send the invitation
+  */
+
+  directInvite: function(room, receiver, reason, password) {
+    var attrs, invitation, msgid;
+    msgid = this._connection.getUniqueId();
+    attrs = {
+      xmlns: 'jabber:x:conference',
+      jid: room
+    };
+    if (reason != null) {
+      attrs.reason = reason;
+    }
+    if (password != null) {
+      attrs.password = password;
+    }
+    invitation = $msg({
+      from: this._connection.jid,
+      to: receiver,
+      id: msgid
+    }).c('x', attrs);
+    this._connection.send(invitation);
+    return msgid;
+  },
+  /*Function
+  Queries a room for a list of occupants
+  (String) room - The multi-user chat room name.
+  (Function) success_cb - Optional function to handle the info.
+  (Function) error_cb - Optional function to handle an error.
+  Returns:
+  id - the unique id used to send the info request
+  */
+
+  queryOccupants: function(room, success_cb, error_cb) {
+    var attrs, info;
+    attrs = {
+      xmlns: Strophe.NS.DISCO_ITEMS
+    };
+    info = $iq({
+      from: this._connection.jid,
+      to: room,
+      type: 'get'
+    }).c('query', attrs);
+    return this._connection.sendIQ(info, success_cb, error_cb);
+  },
+  /*Function
+  Start a room configuration.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (Function) handler_cb - Optional function to handle the config form.
+  Returns:
+  id - the unique id used to send the configuration request
+  */
+
+  configure: function(room, handler_cb, error_cb) {
+    var config, stanza;
+    config = $iq({
+      to: room,
+      type: "get"
+    }).c("query", {
+      xmlns: Strophe.NS.MUC_OWNER
+    });
+    stanza = config.tree();
+    return this._connection.sendIQ(stanza, handler_cb, error_cb);
+  },
+  /*Function
+  Cancel the room configuration
+  Parameters:
+  (String) room - The multi-user chat room name.
+  Returns:
+  id - the unique id used to cancel the configuration.
+  */
+
+  cancelConfigure: function(room) {
+    var config, stanza;
+    config = $iq({
+      to: room,
+      type: "set"
+    }).c("query", {
+      xmlns: Strophe.NS.MUC_OWNER
+    }).c("x", {
+      xmlns: "jabber:x:data",
+      type: "cancel"
+    });
+    stanza = config.tree();
+    return this._connection.sendIQ(stanza);
+  },
+  /*Function
+  Save a room configuration.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (Array) config- Form Object or an array of form elements used to configure the room.
+  Returns:
+  id - the unique id used to save the configuration.
+  */
+
+  saveConfiguration: function(room, config, success_cb, error_cb) {
+    var conf, iq, stanza, _i, _len;
+    iq = $iq({
+      to: room,
+      type: "set"
+    }).c("query", {
+      xmlns: Strophe.NS.MUC_OWNER
+    });
+    if (config instanceof Form) {
+      config.type = "submit";
+      iq.cnode(config.toXML());
+    } else {
+      iq.c("x", {
+        xmlns: "jabber:x:data",
+        type: "submit"
+      });
+      for (_i = 0, _len = config.length; _i < _len; _i++) {
+        conf = config[_i];
+        iq.cnode(conf).up();
+      }
+    }
+    stanza = iq.tree();
+    return this._connection.sendIQ(stanza, success_cb, error_cb);
+  },
+  /*Function
+  Parameters:
+  (String) room - The multi-user chat room name.
+  Returns:
+  id - the unique id used to create the chat room.
+  */
+
+  createInstantRoom: function(room, success_cb, error_cb) {
+    var roomiq;
+    roomiq = $iq({
+      to: room,
+      type: "set"
+    }).c("query", {
+      xmlns: Strophe.NS.MUC_OWNER
+    }).c("x", {
+      xmlns: "jabber:x:data",
+      type: "submit"
+    });
+    return this._connection.sendIQ(roomiq.tree(), success_cb, error_cb);
+  },
+  /*Function
+  Set the topic of the chat room.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) topic - Topic message.
+  */
+
+  setTopic: function(room, topic) {
+    var msg;
+    msg = $msg({
+      to: room,
+      from: this._connection.jid,
+      type: "groupchat"
+    }).c("subject", {
+      xmlns: "jabber:client"
+    }).t(topic);
+    return this._connection.send(msg.tree());
+  },
+  /*Function
+  Internal Function that Changes the role or affiliation of a member
+  of a MUC room. This function is used by modifyRole and modifyAffiliation.
+  The modification can only be done by a room moderator. An error will be
+  returned if the user doesn't have permission.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (Object) item - Object with nick and role or jid and affiliation attribute
+  (String) reason - Optional reason for the change.
+  (Function) handler_cb - Optional callback for success
+  (Function) error_cb - Optional callback for error
+  Returns:
+  iq - the id of the mode change request.
+  */
+
+  _modifyPrivilege: function(room, item, reason, handler_cb, error_cb) {
+    var iq;
+    iq = $iq({
+      to: room,
+      type: "set"
+    }).c("query", {
+      xmlns: Strophe.NS.MUC_ADMIN
+    }).cnode(item.node);
+    if (reason != null) {
+      iq.c("reason", reason);
+    }
+    return this._connection.sendIQ(iq.tree(), handler_cb, error_cb);
+  },
+  /*Function
+  Changes the role of a member of a MUC room.
+  The modification can only be done by a room moderator. An error will be
+  returned if the user doesn't have permission.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) nick - The nick name of the user to modify.
+  (String) role - The new role of the user.
+  (String) affiliation - The new affiliation of the user.
+  (String) reason - Optional reason for the change.
+  (Function) handler_cb - Optional callback for success
+  (Function) error_cb - Optional callback for error
+  Returns:
+  iq - the id of the mode change request.
+  */
+
+  modifyRole: function(room, nick, role, reason, handler_cb, error_cb) {
+    var item;
+    item = $build("item", {
+      nick: nick,
+      role: role
+    });
+    return this._modifyPrivilege(room, item, reason, handler_cb, error_cb);
+  },
+  kick: function(room, nick, reason, handler_cb, error_cb) {
+    return this.modifyRole(room, nick, 'none', reason, handler_cb, error_cb);
+  },
+  voice: function(room, nick, reason, handler_cb, error_cb) {
+    return this.modifyRole(room, nick, 'participant', reason, handler_cb, error_cb);
+  },
+  mute: function(room, nick, reason, handler_cb, error_cb) {
+    return this.modifyRole(room, nick, 'visitor', reason, handler_cb, error_cb);
+  },
+  op: function(room, nick, reason, handler_cb, error_cb) {
+    return this.modifyRole(room, nick, 'moderator', reason, handler_cb, error_cb);
+  },
+  deop: function(room, nick, reason, handler_cb, error_cb) {
+    return this.modifyRole(room, nick, 'participant', reason, handler_cb, error_cb);
+  },
+  /*Function
+  Changes the affiliation of a member of a MUC room.
+  The modification can only be done by a room moderator. An error will be
+  returned if the user doesn't have permission.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) jid  - The jid of the user to modify.
+  (String) affiliation - The new affiliation of the user.
+  (String) reason - Optional reason for the change.
+  (Function) handler_cb - Optional callback for success
+  (Function) error_cb - Optional callback for error
+  Returns:
+  iq - the id of the mode change request.
+  */
+
+  modifyAffiliation: function(room, jid, affiliation, reason, handler_cb, error_cb) {
+    var item;
+    item = $build("item", {
+      jid: jid,
+      affiliation: affiliation
+    });
+    return this._modifyPrivilege(room, item, reason, handler_cb, error_cb);
+  },
+  ban: function(room, jid, reason, handler_cb, error_cb) {
+    return this.modifyAffiliation(room, jid, 'outcast', reason, handler_cb, error_cb);
+  },
+  member: function(room, jid, reason, handler_cb, error_cb) {
+    return this.modifyAffiliation(room, jid, 'member', reason, handler_cb, error_cb);
+  },
+  revoke: function(room, jid, reason, handler_cb, error_cb) {
+    return this.modifyAffiliation(room, jid, 'none', reason, handler_cb, error_cb);
+  },
+  owner: function(room, jid, reason, handler_cb, error_cb) {
+    return this.modifyAffiliation(room, jid, 'owner', reason, handler_cb, error_cb);
+  },
+  admin: function(room, jid, reason, handler_cb, error_cb) {
+    return this.modifyAffiliation(room, jid, 'admin', reason, handler_cb, error_cb);
+  },
+  /*Function
+  Change the current users nick name.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) user - The new nick name.
+  */
+
+  changeNick: function(room, user) {
+    var presence, room_nick;
+    room_nick = this.test_append_nick(room, user);
+    presence = $pres({
+      from: this._connection.jid,
+      to: room_nick,
+      id: this._connection.getUniqueId()
+    });
+    return this._connection.send(presence.tree());
+  },
+  /*Function
+  Change the current users status.
+  Parameters:
+  (String) room - The multi-user chat room name.
+  (String) user - The current nick.
+  (String) show - The new show-text.
+  (String) status - The new status-text.
+  */
+
+  setStatus: function(room, user, show, status) {
+    var presence, room_nick;
+    room_nick = this.test_append_nick(room, user);
+    presence = $pres({
+      from: this._connection.jid,
+      to: room_nick
+    });
+    if (show != null) {
+      presence.c('show', show).up();
+    }
+    if (status != null) {
+      presence.c('status', status);
+    }
+    return this._connection.send(presence.tree());
+  },
+  /*Function
+  List all chat room available on a server.
+  Parameters:
+  (String) server - name of chat server.
+  (String) handle_cb - Function to call for room list return.
+  (String) error_cb - Function to call on error.
+  */
+
+  listRooms: function(server, handle_cb, error_cb) {
+    var iq;
+    iq = $iq({
+      to: server,
+      from: this._connection.jid,
+      type: "get"
+    }).c("query", {
+      xmlns: Strophe.NS.DISCO_ITEMS
+    });
+    return this._connection.sendIQ(iq, handle_cb, error_cb);
+  },
+  test_append_nick: function(room, nick) {
+    return room + (nick != null ? "/" + (Strophe.escapeNode(nick)) : "");
+  }
+});
+
+XmppRoom = (function() {
+
+  XmppRoom.prototype.roster = {};
+
+  XmppRoom.prototype._message_handlers = {};
+
+  XmppRoom.prototype._presence_handlers = {};
+
+  XmppRoom.prototype._roster_handlers = {};
+
+  XmppRoom.prototype._handler_ids = 0;
+
+  function XmppRoom(client, name, nick, password) {
+    this.client = client;
+    this.name = name;
+    this.nick = nick;
+    this.password = password;
+    this._roomRosterHandler = __bind(this._roomRosterHandler, this);
+
+    this._addOccupant = __bind(this._addOccupant, this);
+
+    if (client.muc) {
+      this.client = client.muc;
+    }
+    this.name = Strophe.getBareJidFromJid(name);
+    this.client.rooms[this.name] = this;
+    this.addHandler('presence', this._roomRosterHandler);
+  }
+
+  XmppRoom.prototype.join = function(msg_handler_cb, pres_handler_cb, roster_cb) {
+    return this.client.join(this.name, this.nick, msg_handler_cb, pres_handler_cb, roster_cb, this.password);
+  };
+
+  XmppRoom.prototype.leave = function(handler_cb, message) {
+    this.client.leave(this.name, this.nick, handler_cb, message);
+    return delete this.client.rooms[this.name];
+  };
+
+  XmppRoom.prototype.message = function(nick, message, html_message, type) {
+    return this.client.message(this.name, nick, message, html_message, type);
+  };
+
+  XmppRoom.prototype.groupchat = function(message, html_message) {
+    return this.client.groupchat(this.name, message, html_message);
+  };
+
+  XmppRoom.prototype.invite = function(receiver, reason) {
+    return this.client.invite(this.name, receiver, reason);
+  };
+
+  XmppRoom.prototype.directInvite = function(receiver, reason) {
+    return this.client.directInvite(this.name, receiver, reason, this.password);
+  };
+
+  XmppRoom.prototype.configure = function(handler_cb) {
+    return this.client.configure(this.name, handler_cb);
+  };
+
+  XmppRoom.prototype.cancelConfigure = function() {
+    return this.client.cancelConfigure(this.name);
+  };
+
+  XmppRoom.prototype.saveConfiguration = function(config) {
+    return this.client.saveConfiguration(this.name, config);
+  };
+
+  XmppRoom.prototype.queryOccupants = function(success_cb, error_cb) {
+    return this.client.queryOccupants(this.name, success_cb, error_cb);
+  };
+
+  XmppRoom.prototype.setTopic = function(topic) {
+    return this.client.setTopic(this.name, topic);
+  };
+
+  XmppRoom.prototype.modifyRole = function(nick, role, reason, success_cb, error_cb) {
+    return this.client.modifyRole(this.name, nick, role, reason, success_cb, error_cb);
+  };
+
+  XmppRoom.prototype.kick = function(nick, reason, handler_cb, error_cb) {
+    return this.client.kick(this.name, nick, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.voice = function(nick, reason, handler_cb, error_cb) {
+    return this.client.voice(this.name, nick, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.mute = function(nick, reason, handler_cb, error_cb) {
+    return this.client.mute(this.name, nick, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.op = function(nick, reason, handler_cb, error_cb) {
+    return this.client.op(this.name, nick, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.deop = function(nick, reason, handler_cb, error_cb) {
+    return this.client.deop(this.name, nick, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.modifyAffiliation = function(jid, affiliation, reason, success_cb, error_cb) {
+    return this.client.modifyAffiliation(this.name, jid, affiliation, reason, success_cb, error_cb);
+  };
+
+  XmppRoom.prototype.ban = function(jid, reason, handler_cb, error_cb) {
+    return this.client.ban(this.name, jid, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.member = function(jid, reason, handler_cb, error_cb) {
+    return this.client.member(this.name, jid, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.revoke = function(jid, reason, handler_cb, error_cb) {
+    return this.client.revoke(this.name, jid, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.owner = function(jid, reason, handler_cb, error_cb) {
+    return this.client.owner(this.name, jid, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.admin = function(jid, reason, handler_cb, error_cb) {
+    return this.client.admin(this.name, jid, reason, handler_cb, error_cb);
+  };
+
+  XmppRoom.prototype.changeNick = function(nick) {
+    this.nick = nick;
+    return this.client.changeNick(this.name, nick);
+  };
+
+  XmppRoom.prototype.setStatus = function(show, status) {
+    return this.client.setStatus(this.name, this.nick, show, status);
+  };
+
+  /*Function
+  Adds a handler to the MUC room.
+    Parameters:
+  (String) handler_type - 'message', 'presence' or 'roster'.
+  (Function) handler - The handler function.
+  Returns:
+  id - the id of handler.
+  */
+
+
+  XmppRoom.prototype.addHandler = function(handler_type, handler) {
+    var id;
+    id = this._handler_ids++;
+    switch (handler_type) {
+      case 'presence':
+        this._presence_handlers[id] = handler;
+        break;
+      case 'message':
+        this._message_handlers[id] = handler;
+        break;
+      case 'roster':
+        this._roster_handlers[id] = handler;
+        break;
+      default:
+        this._handler_ids--;
+        return null;
+    }
+    return id;
+  };
+
+  /*Function
+  Removes a handler from the MUC room.
+  This function takes ONLY ids returned by the addHandler function
+  of this room. passing handler ids returned by connection.addHandler
+  may brake things!
+    Parameters:
+  (number) id - the id of the handler
+  */
+
+
+  XmppRoom.prototype.removeHandler = function(id) {
+    delete this._presence_handlers[id];
+    delete this._message_handlers[id];
+    return delete this._roster_handlers[id];
+  };
+
+  /*Function
+  Creates and adds an Occupant to the Room Roster.
+    Parameters:
+  (Object) data - the data the Occupant is filled with
+  Returns:
+  occ - the created Occupant.
+  */
+
+
+  XmppRoom.prototype._addOccupant = function(data) {
+    var occ;
+    occ = new Occupant(data, this);
+    this.roster[occ.nick] = occ;
+    return occ;
+  };
+
+  /*Function
+  The standard handler that managed the Room Roster.
+    Parameters:
+  (Object) pres - the presence stanza containing user information
+  */
+
+
+  XmppRoom.prototype._roomRosterHandler = function(pres) {
+    var data, handler, id, newnick, nick, _ref;
+    data = XmppRoom._parsePresence(pres);
+    nick = data.nick;
+    newnick = data.newnick || null;
+    switch (data.type) {
+      case 'error':
+        return;
+      case 'unavailable':
+        if (newnick) {
+          data.nick = newnick;
+          if (this.roster.hasOwnProperty(nick) && this.roster.hasOwnProperty(newnick)) {
+            this.roster[nick].update(this.roster[newnick]);
+            this.roster[newnick] = this.roster[nick];
+          }
+          if (this.roster.hasOwnProperty(nick) && !this.roster.hasOwnProperty(newnick)) {
+            this.roster[newnick] = this.roster[nick].update(data);
+          }
+        }
+        delete this.roster[nick];
+        break;
+      default:
+        if (this.roster.hasOwnProperty(nick)) {
+          this.roster[nick].update(data);
+        } else {
+          this._addOccupant(data);
+        }
+    }
+    _ref = this._roster_handlers;
+    for (id in _ref) {
+      handler = _ref[id];
+      if (!handler(this.roster, this)) {
+        delete this._roster_handlers[id];
+      }
+    }
+    return true;
+  };
+
+  /*Function
+  Parses a presence stanza
+    Parameters:
+  (Object) data - the data extracted from the presence stanza
+  */
+
+
+  XmppRoom._parsePresence = function(pres) {
+    var a, c, c2, data, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
+    data = {};
+    a = pres.attributes;
+    data.nick = Strophe.getResourceFromJid(a.from.textContent);
+    data.type = ((_ref = a.type) != null ? _ref.textContent : void 0) || null;
+    data.states = [];
+    _ref1 = pres.childNodes;
+    for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+      c = _ref1[_i];
+      switch (c.nodeName) {
+        case "status":
+          data.status = c.textContent || null;
+          break;
+        case "show":
+          data.show = c.textContent || null;
+          break;
+        case "x":
+          a = c.attributes;
+          if (((_ref2 = a.xmlns) != null ? _ref2.textContent : void 0) === Strophe.NS.MUC_USER) {
+            _ref3 = c.childNodes;
+            for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
+              c2 = _ref3[_j];
+              switch (c2.nodeName) {
+                case "item":
+                  a = c2.attributes;
+                  data.affiliation = ((_ref4 = a.affiliation) != null ? _ref4.textContent : void 0) || null;
+                  data.role = ((_ref5 = a.role) != null ? _ref5.textContent : void 0) || null;
+                  data.jid = ((_ref6 = a.jid) != null ? _ref6.textContent : void 0) || null;
+                  data.newnick = ((_ref7 = a.nick) != null ? _ref7.textContent : void 0) || null;
+                  break;
+                case "status":
+                  if (c2.attributes.code) {
+                    data.states.push(c2.attributes.code.textContent);
+                  }
+              }
+            }
+          }
+      }
+    }
+    return data;
+  };
+
+  return XmppRoom;
+
+})();
+
+RoomConfig = (function() {
+
+  function RoomConfig(info) {
+    this.parse = __bind(this.parse, this);
+    if (info != null) {
+      this.parse(info);
+    }
+  }
+
+  RoomConfig.prototype.parse = function(result) {
+    var attr, attrs, child, field, identity, query, _i, _j, _k, _len, _len1, _len2, _ref;
+    query = result.getElementsByTagName("query")[0].childNodes;
+    this.identities = [];
+    this.features = [];
+    this.x = [];
+    for (_i = 0, _len = query.length; _i < _len; _i++) {
+      child = query[_i];
+      attrs = child.attributes;
+      switch (child.nodeName) {
+        case "identity":
+          identity = {};
+          for (_j = 0, _len1 = attrs.length; _j < _len1; _j++) {
+            attr = attrs[_j];
+            identity[attr.name] = attr.textContent;
+          }
+          this.identities.push(identity);
+          break;
+        case "feature":
+          this.features.push(attrs["var"].textContent);
+          break;
+        case "x":
+          attrs = child.childNodes[0].attributes;
+          if ((!attrs["var"].textContent === 'FORM_TYPE') || (!attrs.type.textContent === 'hidden')) {
+            break;
+          }
+          _ref = child.childNodes;
+          for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) {
+            field = _ref[_k];
+            if (!(!field.attributes.type)) {
+              continue;
+            }
+            attrs = field.attributes;
+            this.x.push({
+              "var": attrs["var"].textContent,
+              label: attrs.label.textContent || "",
+              value: field.firstChild.textContent || ""
+            });
+          }
+      }
+    }
+    return {
+      "identities": this.identities,
+      "features": this.features,
+      "x": this.x
+    };
+  };
+
+  return RoomConfig;
+
+})();
+
+Occupant = (function() {
+
+  function Occupant(data, room) {
+    this.room = room;
+    this.update = __bind(this.update, this);
+
+    this.admin = __bind(this.admin, this);
+
+    this.owner = __bind(this.owner, this);
+
+    this.revoke = __bind(this.revoke, this);
+
+    this.member = __bind(this.member, this);
+
+    this.ban = __bind(this.ban, this);
+
+    this.modifyAffiliation = __bind(this.modifyAffiliation, this);
+
+    this.deop = __bind(this.deop, this);
+
+    this.op = __bind(this.op, this);
+
+    this.mute = __bind(this.mute, this);
+
+    this.voice = __bind(this.voice, this);
+
+    this.kick = __bind(this.kick, this);
+
+    this.modifyRole = __bind(this.modifyRole, this);
+
+    this.update(data);
+  }
+
+  Occupant.prototype.modifyRole = function(role, reason, success_cb, error_cb) {
+    return this.room.modifyRole(this.nick, role, reason, success_cb, error_cb);
+  };
+
+  Occupant.prototype.kick = function(reason, handler_cb, error_cb) {
+    return this.room.kick(this.nick, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.voice = function(reason, handler_cb, error_cb) {
+    return this.room.voice(this.nick, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.mute = function(reason, handler_cb, error_cb) {
+    return this.room.mute(this.nick, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.op = function(reason, handler_cb, error_cb) {
+    return this.room.op(this.nick, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.deop = function(reason, handler_cb, error_cb) {
+    return this.room.deop(this.nick, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.modifyAffiliation = function(affiliation, reason, success_cb, error_cb) {
+    return this.room.modifyAffiliation(this.jid, affiliation, reason, success_cb, error_cb);
+  };
+
+  Occupant.prototype.ban = function(reason, handler_cb, error_cb) {
+    return this.room.ban(this.jid, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.member = function(reason, handler_cb, error_cb) {
+    return this.room.member(this.jid, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.revoke = function(reason, handler_cb, error_cb) {
+    return this.room.revoke(this.jid, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.owner = function(reason, handler_cb, error_cb) {
+    return this.room.owner(this.jid, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.admin = function(reason, handler_cb, error_cb) {
+    return this.room.admin(this.jid, reason, handler_cb, error_cb);
+  };
+
+  Occupant.prototype.update = function(data) {
+    this.nick = data.nick || null;
+    this.affiliation = data.affiliation || null;
+    this.role = data.role || null;
+    this.jid = data.jid || null;
+    this.status = data.status || null;
+    this.show = data.show || null;
+    return this;
+  };
+
+  return Occupant;
+
+})();
diff --git a/src/core/js/lib/strophe/strophe.ping.js b/src/core/js/lib/strophe/strophe.ping.js
new file mode 100644
index 0000000..9b7f049
--- /dev/null
+++ b/src/core/js/lib/strophe/strophe.ping.js
@@ -0,0 +1,53 @@
+Strophe.addConnectionPlugin('connectionmanager', {
+  
+	pingTime: 12000, // the time in ms between each ping
+	timeoutTime: 6000,	// the time in ms to wait for a ping to return
+	pingInterval: null,
+	connection: null,
+	failedPings: 0,
+	
+	init: function(connection) {
+		this.connection = connection
+	},
+	
+	// Called automatically by Strophe when the connection status changes
+	statusChanged: function(status) {
+		var self = this
+		switch(status) {
+			case Strophe.Status.CONNECTED:
+			case Strophe.Status.ATTACHED:
+				// start monitoring the connection
+				clearInterval(this.pingInterval)
+				this.pingInterval = setInterval(function(){ self.pingServer() }, this.pingTime)
+				break
+			case Strophe.Status.DISCONNECTED:
+				// stop monitoring the connection
+				this.failedPings = 0
+				clearInterval(this.pingInterval)
+				break
+		}
+	},
+	
+	pingServer: function() {
+		var self = this
+		var stanza = $iq({
+			id:	"ping",
+			type: "get",
+			to:	this.connection.domain
+		}).c("ping", {xmlns: 'urn:xmpp:ping'})
+		
+		this.connection.sendIQ(stanza, null, function(){ self.requestTimedOut() }, this.timeoutTime)
+	},
+	
+	requestTimedOut: function() {
+		this.failedPings++
+		console.log('FP')
+		if (this.failedPings === 2) {
+			this.failedPings = 0
+			clearInterval(this.pingInterval)
+			console.log('FP!')
+			this.connection.disconnect()
+		}
+	}
+
+})
\ No newline at end of file
diff --git a/src/core/js/lib/strophe/strophe.si-filetransfer.js b/src/core/js/lib/strophe/strophe.si-filetransfer.js
new file mode 100644
index 0000000..94ead89
--- /dev/null
+++ b/src/core/js/lib/strophe/strophe.si-filetransfer.js
@@ -0,0 +1,159 @@
+/*global Strophe $iq $ */
+/*
+
+  (c) 2013 - Arlo Breault <arlolra at gmail.com>
+  Freely distributed under the MPL v2.0 license.
+
+  File: strophe.si-filetransfer.js
+  XEP-0096: SI File Transfer
+  http://xmpp.org/extensions/xep-0096.html
+
+*/
+
+;(function () {
+  "use strict";
+
+  function noop() {}
+  
+  function inVals(stanza, ns) {
+    var ok = false;
+    var $mthds = $('si feature x field[var="stream-method"] value', stanza);
+    $mthds.each(function (i, m) {
+      if ($(m).text() === ns) ok = true;
+    });
+    return ok;
+  }
+
+  Strophe.addConnectionPlugin('si_filetransfer', {
+    
+    _c: null,
+    _cb: null,
+
+    init: function (c) {  
+
+      this._c = c;
+
+      Strophe.addNamespace('SI', 'http://jabber.org/protocol/si');
+      Strophe.addNamespace('SI_FILE_TRANSFER',
+        Strophe.NS.SI + '/profile/file-transfer');
+      Strophe.addNamespace('FEATURE_NEG',
+        'http://jabber.org/protocol/feature-neg');
+
+      c.addHandler(this._receive.bind(this), Strophe.NS.SI, 'iq', 'set');
+
+    },
+
+    _receive: function (m) {
+
+      var $m = $(m);
+      var from = $m.attr('from');
+      var id = $m.attr('id')
+      var sid = $('si', $m).attr('id');
+
+      var iq = $iq({
+        type: 'result',
+        to: from,
+        id: id
+      }).c('si', {
+        xmlns: Strophe.NS.SI,
+        id: sid
+      }).c('file', {
+        xmlns: Strophe.NS.SI_FILE_TRANSFER
+      }).up().c('feature', {
+        xmlns: Strophe.NS.FEATURE_NEG  
+      }).c('x', {
+        xmlns: 'jabber:x:data',
+        type: 'submit'
+      }).c('field', {
+        'var': 'stream-method'
+      });
+
+      // check for In-Band Bytestream plugin
+      // and IBB accepted
+      if ( Object.hasOwnProperty.call(this._c, 'ibb') &&
+           inVals(m, Strophe.NS.IBB)
+      ) iq.c('value').t(Strophe.NS.IBB);
+
+      this._send(iq, noop, noop);
+
+      var $file = $('file', $m);
+      var filename = $file.attr('name');
+      var size = $file.attr('size'); 
+      var mime = $('si', $m).attr('mime-type');
+
+      // callback message
+      if (typeof this._cb === 'function') {
+        this._cb(from, sid, filename, size, mime);
+      }
+
+      return true;
+
+    },
+
+    _success: function (cb, stanza) {
+      var err;
+
+      // search for ibb
+      if (!inVals(stanza, Strophe.NS.IBB))
+        err = new Error('In-Band Bytestream not supported');
+
+      cb(err);
+    },
+
+    _fail: function (cb, stanza) {
+      var err = 'timed out';
+      if (stanza) err = stanza;
+      cb(new Error(err));
+    },
+
+    _send: function (iq, success, fail) {
+      this._c.sendIQ(iq, success, fail, 60 * 1000);
+    },
+
+    send: function (to, sid, filename, size, mime, cb) {
+
+      // check for In-Band Bytestream plugin
+      if (!Object.hasOwnProperty.call(this._c, 'ibb')) {
+        Strophe.warn('The In-Band Bytestream plugin is required.');
+        return;
+      }
+
+      var iq = $iq({
+        type: 'set',
+        to: to,
+        id: this._c.getUniqueId('si-filetransfer')
+      }).c('si', {
+        xmlns: Strophe.NS.SI,
+        id: sid,
+        profile: Strophe.NS.SI_FILE_TRANSFER,
+        'mime-type': mime
+      }).c('file', {
+        xmlns: Strophe.NS.SI_FILE_TRANSFER,
+        name: filename,
+        size: size
+      }).up().c('feature', {
+        xmlns: Strophe.NS.FEATURE_NEG
+      }).c('x', {
+        xmlns: 'jabber:x:data',
+        type: 'form'
+      }).c('field', {
+        'var': 'stream-method',
+        type: 'list-single'
+      }).c('option')
+        .c('value')
+        .t(Strophe.NS.IBB);
+
+      this._send(iq,
+        this._success.bind(this, cb),
+        this._fail.bind(this, cb)
+      );
+
+    },
+
+    addFileHandler: function (fn) {
+      this._cb = fn;
+    }
+
+  });
+
+}());
\ No newline at end of file
diff --git a/src/core/js/lib/tinycon.js b/src/core/js/lib/tinycon.js
new file mode 100644
index 0000000..283f511
--- /dev/null
+++ b/src/core/js/lib/tinycon.js
@@ -0,0 +1,8 @@
+/*!
+  Tinycon - A small library for manipulating the Favicon
+  Tom Moor, http://tommoor.com
+  Copyright (c) 2012 Tom Moor
+  @license MIT Licensed
+  @version 0.6.3
+*/
+(function(){var Tinycon={};var currentFavicon=null;var originalFavicon=null;var faviconImage=null;var canvas=null;var options={};var r=window.devicePixelRatio||1;var size=16*r;var defaults={width:7,height:9,font:10*r+'px arial',colour:'#ffffff',background:'#F03D25',fallback:true,crossOrigin:true,abbreviate:true};var ua=(function(){var agent=navigator.userAgent.toLowerCase();return function(browser){return agent.indexOf(browser)!==-1}}());var browser={ie:ua('msie'),chrome:ua('chrome'),web [...]
\ No newline at end of file
diff --git a/src/core/js/workers/dsa.js b/src/core/js/workers/dsa.js
new file mode 100644
index 0000000..754578f
--- /dev/null
+++ b/src/core/js/workers/dsa.js
@@ -0,0 +1,32 @@
+/*global DSA */
+;(function (root) {
+	"use strict";
+
+	root.OTR = {}
+	root.DSA = {}
+
+	// default imports
+	var imports = [
+		'../lib/crypto-js.js',
+		'../lib/salsa20.js',
+		'../etc/random.js',
+		'../lib/bigint.js',
+		'../lib/eventemitter.js',
+		'../lib/otr.js'
+	]
+
+	function sendMsg(type, val) {
+		postMessage({ type: type, val: val })
+	}
+
+	onmessage = function (e) {
+		var data = e.data;
+		importScripts.apply(root, imports)
+		Cryptocat.random.setSeed(data.seed)
+		if (data.debug) { sendMsg('debug', 'DSA key creation started') }
+		var dsa = new DSA()
+		if (data.debug) { sendMsg('debug', 'DSA key creation finished') }
+		sendMsg('data', dsa.packPrivate())
+	}
+
+}(this))
\ No newline at end of file
diff --git a/src/core/js/workers/smp.js b/src/core/js/workers/smp.js
new file mode 100644
index 0000000..c5c8c21
--- /dev/null
+++ b/src/core/js/workers/smp.js
@@ -0,0 +1,44 @@
+;(function (root) {
+	'use strict';
+
+	root.OTR = {}
+
+	var imports = [
+		'../lib/crypto-js.js',
+		'../lib/salsa20.js',
+		'../etc/random.js',
+		'../lib/bigint.js',
+		'../lib/eventemitter.js',
+		'../lib/otr.js'
+	]
+
+	var wrapPostMessage = function(method) {
+		return function () {
+			postMessage({
+				method: method,
+				args: Array.prototype.slice.call(arguments, 0)
+			})
+		}
+	}
+
+	var sm
+	onmessage = function(e) {
+		var data = e.data
+		switch (data.type) {
+			case 'seed':
+				importScripts.apply(root, imports)
+				Cryptocat.random.setSeed(data.seed)
+				break
+			case 'init':
+				sm = new root.OTR.SM(data.reqs)
+				;['trust','question', 'send', 'abort'].forEach(function (m) {
+					sm.on(m, wrapPostMessage(m));
+				})
+				break
+			case 'method':
+				sm[data.method].apply(sm, data.args)
+				break
+		}
+	}
+
+}(this))
diff --git a/src/core/locale/ar.txt b/src/core/locale/ar.txt
new file mode 100755
index 0000000..bb6bc2c
--- /dev/null
+++ b/src/core/locale/ar.txt
@@ -0,0 +1,87 @@
+rtl
+Tahoma, DejaVu, "Helvetica Neue", Helvetica, Arial, Verdana
+محادثات خصوصية وآمنة لجميع الأشخاص.
+مرحبا بكم في Cryptocat. توجد هنا بعض النصائح المفيدة: <ul><li> إن Cryptocat ليس عصا سحرية. لا يجب عليك الوثوق بأي قطعة برمجية في حياتك. </li><li class="key">لا يستطيع Cryptocat حمايتك ضد الناس الغير جديرين بالثقة أو الاختراقات، و لا يمكنه أيضا جعل اتصالاتك مجهولة المصدر.</li></ul>
+مدونة
+خادم مخصص
+إعادة تعيين
+اسم المحادثة
+الاسم المستعار
+اتصل
+إدخل إسم لمحادثتك وأعطي الإسم للأشخاص التي ترغب أن تتكلم معها.
+ادخل اسم محادثة للانضمام.
+الرجاء ادخال اسم محادثة.
+إسم المكالمة يجب أن يكن حروف أو أرقام فقط.
+الرجاء ادخال اسم مستعار.
+اسمك المكالمة يجب أن يكن حروف أو أرقام فقط.
+الاسم المتسعار مستخدم.
+فشل التحقيق.
+الإتصال قد فشل.
+شكرا لكم لاستخدام كريبتوكات.
+جاري التسجيل...
+جاري الاتصال...
+موصول.
+الرجاء الضغط على أزرار لوحة مفاتيحك بكل ما يمكن من عشوائية لبضع ثوان.
+جاري توليد مفاتيح التشفير...
+محادثة جماعية. اضغط على مستخدم لمحادثة خاصة.
+بصمة OTR (للمحادثات الخاصة)
+بصمة المحادثة الجماعية:
+إعادة تعيين مفاتيح التشفير
+إعادة مفاتيحك سوف يفصلك من هذه المحادثة. بصماتك أيضاً ستتغير.
+موافق
+الحالة: متواجد
+الحالة: غير متواجد
+معلوماتي
+اخطارات سطح المكتب مفعلة
+اخطارات سطح المكتب غير مفعلة
+اخطارات صوتية مفعلة
+اخطارات صوتية غير مفعلة
+تذكر إسمي المستعار
+لا تحفظ إسمي المستعار
+تسجيل الخروج
+عرض المعلومات
+إرسال ملفّ مشفر
+مطالعة الصورة
+تحميل الملف
+محادثة
+لا تقبل إلّا الملفّات زِِيپ والصور. أكبر حجم ملف: (SIZE) MB.
+خطأ: يجب أن يكون الملف أرشيفاً من نوع ZIP أو صورة.
+خطأ: لا يمكن أن ِيتجاوز حجم الملفّ (SIZE) MB
+بدء محادثة فيديو
+اختتام محادثة فيديو
+هذا الشخص (NICKNAME) يرغب في بدء محادثة فيديو معك.
+إلغاء
+تجاهل
+اهتم
+يوثق
+تحقق من هوية هذا المستخدم من خلال طرح سؤال سري. يجب أن تكون الإجابات متطابقة تماما!
+السؤال السري
+الإجابة السرية
+يسأل
+يسأل ...
+فشل
+تم التحقق من الهوية.
+يود هذا الشخص (NICKNAME) أن يتحقق من هويتك. الرجاء الإجابة عن السؤال السري أدناه من أجل إثبات شخصيتك:
+يجب أن تتطابق إجابتك تماما مع الإجابة المطروحة من قبل هذا الشخص (NICKNAME).
+الإجابة
+تحذير : لقد توصلت برسالة غير قابلة للقراءة من طرف (لقب). يمكن أن يدل هذا عن مستخدم غير موثوق به أو عن رسالة لم يتم التوصل بها بالشكل الصحيح. من الممكن أيضا أن يكون إصدار كريپتوكات الذي بحوزتك قديما. المرجو التحقق من وجود تحديثات.
+تحذير: الإصدار الخاص بك من كريبتوكات قديم الطراز. يوصى بشدة التحديث إلى الإصدار الأحدث! لا تتابع بدون التحديث إلى الإصدار الأحدث.
+عفواً لم نتمكن من إرسال الرسالة إلي (NICKNAME)
+تم التويق بنجاح
+المستخدم غير موثق
+إضغة لمعرفة المزيد
+تعلم المزيد عن التوثيق
+كل مره تستخدم فيها كقيبتو كات, يجب عليك أن تتوثق من الاشخاص الذين تتحد معهم
+من ضمن الطرق التي يمكنك من خلالها أن توثق عن طريقها هي أن تسأل صديقك عن سؤال سري بحيث أنه فقط هو الذي يعرف جوابه
+يمكنك أن تتواصل معهم عن طريق وسيلة إتصال اَمنة مثل الهاتف, ثم تسألهم عن بصمته
+البصمات تستخدم في التوثق من الأشخاص, من الممكن أن تتغير ما بين محادثه و أخري
+بدون توثيق, قد يكون أحد بإنتحال شخصيتك أو أن يعترض المكالمة
+بصمة التوثيق الخاصة بهذا المستخدم قد تغيرت. ليس من الطبيعي أن يحدث هذا و ذالك  يدعو للشك. يرجي التوثق من هذا الشخص قبل الدردشة معه
+مكالمة جماعية
+فيسبوك
+إتصل بفيسبوك عبر كريبتوكات، وإحصل على مكالمات مشفرة مع كل صديق على فيسبوك إن كان يستخدم كريبتوكات أيضاً.
+إتصل بفيسبوك
+حالة المكالمة
+مشفرة.
+غير مشفرة!
+لم يتم تشفير هذه المحادثة لأن هذا المستخدم لا يستخدم كريبتوكات. إطلب من صديقك تحميل كريبتوكات للاستمتاع بلدردشة المشفرة.
\ No newline at end of file
diff --git a/src/core/locale/bg.txt b/src/core/locale/bg.txt
new file mode 100644
index 0000000..36cf47b
--- /dev/null
+++ b/src/core/locale/bg.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Частни Разговори за Всички.
+Добре дошли в Cryptocat. Ето някои полезни съвети: <ul><li>Cryptocat не е магическа отвара. Никога не трябва да доверявате живота си на даден софтуер. </li><li class="key">Cryptocat не може да ви защити срещу неблагонадеждни хора или кийлогъри и не прави вашата връзка анонимна.</li></ul>
+Блог
+Персонализиран сървър
+Нулиране
+Име на Разговор
+прякор
+свързване
+Въведете име за вашият разговор и го споделете с хората с които искате да говорите , или се присъединете към нашето <strong>лоби</strong> за да срещнете нови приятели!
+Напишете името на разговора да се свържете.
+Моля напишете име на разговор.
+Името на разговора може да се състои само от букви или цифри.
+Моля напишете прякор.
+Псевдонимът може да бъде само букви или цифри.
+Този прякор се използва.
+Неуспешно удостоверяване.
+Свързването е неуспешно.
+Благодарим ви че използвате Криптокат.
+Регистриране...
+Свързване...
+Свързан.
+Моля напишете нещо произволно за няколко секунди.
+Генериране на криптиращи ключове...
+Разговор на група. Моля кликнете върху име на потребител за частен разговор.
+ОТР подпис (за частен разговор):
+Подпис за групов разговор.
+Нулирай криптиращия ключ
+Нулирането на криптиращия ключ ще прекъсне вашата връзка. Вашите подписи също ще се променят.
+Продължи
+Статус: Свободен
+Статус: Зает
+Моите данни
+Екранни съобщения: Включени
+Екранни съобщения: Изключени
+Звукови съобщения: Включени
+Звукови съобщения: Изключени
+Запомни моя прякор
+Не запомняй моят прякор.
+Изход
+Покажи данни
+Изпрати криптиран файл
+виж снимката
+изтегли файла
+Разговор
+Приемат се само ZIP файлове и изображения. Максимален размер на файла: (SIZE) MB.
+Грешка: Моля, уверете се, че файлът е в ZIP формат или е изображение.
+Грешка: файлът не може да бъде по-голям от (SIZE) MB.
+Започване на видео чат
+Край на видео чат
+(NICKNAME) би искал(а) да започне видео чат с вас.
+Отменете
+Игнорирай
+Отмени игнорирането
+Удостоверяване
+Удостоверете самоличността на този потребител, като зададете таен въпрос. Отговорите трябва да съвпадат точно!
+Таен въпрос
+Таен отговор
+Задайте
+Задава...
+Неуспешен
+Самоличността е проверена.
+(NICKNAME) иска да удостоверите самоличността си. Моля, отговорете на тайния въпрос по-долу, за да се удостоверите:
+Вашият отговор трябва да съвпада точно с този, даден от (NICKNAME).
+Отговор
+Внимание: Вие получихте нечетимо съобщение от (NICKNAME). Това може да е индикация за неудостоверен потребител или неуспешно изпратено съобщение. Възможно е да използвате стара версия на Cryptocat. Моля проверете за нова версия.
+Внимание: Вашата версия на Cryptocat е остаряла. Обновяването до последната версия е силно препоръчително! Не продължавайте, преди да сте актуализирали до най-новата версия.
+Внимание: това съобщение не може да бъде изпратено до  (NICKNAME)
+Удостоверен
+Потребителя не е удостоверен.
+Кликнете за да научите по-вече...
+Научете по-вече за удостоверяването
+Всеки път когато водите разговор с Cryptocat, вие трябва да удостоверите потребителя с който разговаряте.
+Единият начин за удостоверяване е като използвате Cryptocat за да запитате потребителя таен въпрос на който той трябва да отговори.
+Можете да се свързвате и по други доверени начини, като по телефона например, и ги помолите да прочетат подписа си.
+Подписите са идентификатори които ви позволяват за удостоверявате потребители. Те могат да се променят между всеки Cryptocat разговор.
+Без удостоверяване, някой може да се представи за друга персона или да подслушва разговора ви.
+Подписа за удостоверяване на този контакт беше променен. Това не е нормално, и може да означава опит за вмешателство. Моля удостоверете този потребител преди да започнете да комуникирате с него.
+Групов чат
+Facebook
+Свържете се с Facebook Chat чрез Cryptocat. Създайте напълно криптирани разговори с приятелите си във Facebook, които също използват Cryptocat.
+Чат чрез Facebook
+Статус на разговора
+криптиран.
+не е криптиран!
+Този разговор не е криптиран, защото този контакт не използва Cryptocat. Помолете вашия приятел да изтегли Cryptocat, за да се насладите на криптиран чат.
\ No newline at end of file
diff --git a/src/core/locale/bo.txt b/src/core/locale/bo.txt
new file mode 100755
index 0000000..c3f250a
--- /dev/null
+++ b/src/core/locale/bo.txt
@@ -0,0 +1,68 @@
+ltr
+Microsoft Himalaya, Jomolhari, "Helvetica Neue", Helvetica, Arial, Verdana
+ཁེ་གཙང་དུ་ཚང་མའི་ཆེད་གླེང་མོལ།
+ཉེན་སྲུང་ཞི་མིའི་གླིང་ལ་ཕེབས་པར་དགའ་བསུ་ཞུ། འདི་རུ་ཕན་ཐོགས་ཅན་གྱི་ལམ་སྟོན་ཁ་ཤས་ཡོད། ཉེན་སྲུང་ཞི་མི་ནི་རྫུ་འཕྲུལ་གྱི་མདེའུ་མིན། ཁྱོད་ཀྱིས་ནམ་ཡང་ཁྱོད་ཀྱི་མི་ཚེ་དང་འབྲེལ་བའི་མཉེན་ཆས་ཀྱི་མ་ལག་གང་ལ་ཡང་ཡིད་རྟོན་བྱ་མི་རུང་། ཉེན་སྲུང་ཞི་མིས། ཁྱོད་རང་ཡིད་ཆེས་དཀའ་བའི་སྐྱེ་བོ་དེ་འདྲའི་རྩ་ནས་སྐྱོབ་མི་ཐུབ་ལ་དེས་ཁྱེད་རང་གི་འབྲེལ་ལམ་ཁག་མིང་མེད་ཡང་མི་བཟོ། 
+ཟིན་བྲིས།
+རང་སྒྲུབ་བྱས་པའི་དྲ་ཐོག་ཞབས་ཞུ།
+བསྐྱར་དུ་སྒྲིགས།
+གླེང་མོལ་གྱི་མིང་།
+གསང་མིང་། 
+མཐུད།
+ཁྱེད་རང་གི་གླེང་མོལ་གྱི་ཆེད་དུ་མིང་ནང་འཇུག་བྱས་ནས་སུ་དང་མཉམ་དུ་སྐད་ཆ་བཤད་འདོད་པ་དེར་བགོ་བཤའ་རྒྱོབས། ཡང་ན་<strong> ལ་ཞུགས་པའམ་<strong> བརྒྱུད་མི་གཞན་ལ་ཐུག 
+ཁྱེད་ཀྱི་འཛུལ་འདོད་ཡོད་པའི་གླེང་མོལ་གྱི་མིང་དེ་བཅུག 
+སྐུ་མཁྱེན། གླེང་མོལ་གྱི་མིང་ནང་འཇུག་བྱོས།
+གླེང་མོལ་གྱི་མིང་དེ་ངེས་པར་དུ་ཨང་ཡིག་གཉིས་ལྡན་དགོས།
+སྐུ་མཁྱེན། སྨྱུག་མིང་ནང་འཇུག་བྱོས།
+སྨྱུག་མིང་ངེས་པར་དུ་ཨང་ཡིག་གཉིས་ལྡན་དགོས།
+སྨྱུག་མིང་བེད་སྤྱོད་བྱེད་བཞིན་པ།
+ར་སྤྲོད་ཁུངས་སྐྱེལ་བྱེད་མ་ཐུབ།
+འབྲེལ་མཐུད་བྱེད་མི་ཐུབ།
+ཉེན་སྲུང་ཞི་མི་སྤྱོད་པར་ཐུགས་རྗེ་ཆེ།
+ཐོ་འགོད་བྱེད་བཞིན་ཡོད།
+འབྲེལ་མཐུད་བྱེད་བཞིན་ཡོད།
+འབྲེལ་མཐུད་བྱས་ཟིན།
+སྐུ་མཁྱེན། མཐེབ་གཞོང་ངོས་སུ་སྐར་ཆ་འགར་གང་འདོད་དུ་ཡི་གེ་འགའ་བཏགས།
+གསང་སྡོམ་གྱི་ལྡེ་མིག་བཟོ་བཞིན་ཡོད།
+མཉམ་འདུས་གླེང་མོལ།  སྒེར་གྱི་གླེང་མོལ་བྱེད་པར་བཀོལ་སྤྱོད་མཁན་གྱི་ཐོག་ལ་བསྣུན། 
+OTR མཛུབ་རིས། (སྒེར་གྱི་གླེང་མོལ་ཆེད།)
+མཉམ་འདུས་གླེང་མོལ་གྱི་མཛུབ་རྟགས། 
+ངའི་གསང་སྡོམ་གྱི་ལྡེ་མིག་དེ་བསྐྱར་དུ་སྒྲིགས།
+གསང་སྡོམ་གྱི་ལྡེ་མིག་དེ་བསྐྱར་སྒྲིགས་བྱེད་ན་ཁྱེད་དང་འབྲེལ་མཐུད་གཅོད་ཀྱི་རེད། ཁྱེད་ཀྱི་མཛུབ་རྟགས་ཀྱང་བརྗེ་བར་འགྱུར།
+མུ་མཐུད།
+གྲལ་གནས། : འདིར་ཡོད།
+གྲལ་གནས། : འདིར་མེད།
+ངའི་སྐོར།
+རྩིས་འཁོར་ཀླད་ཀྱི་འབྱོར་བརྡའི་གདམ་ག་སྤོར།
+རྩིས་འཁོར་ཀླད་ཀྱི་འབྱོར་བརྡའི་གདམ་ག་གཟིམས། 
+སྒྲ་ཟློས་འབྱོར་བརྡའི་གདམ་ག་སྤོར།
+སྒྲ་ཟློས་འབྱོར་བརྡའི་གདམ་ག་གཟིམས། 
+ངའི་སྨྱུག་མིང་དྲན་པ་བྱོས།
+ངའི་སྨྱུག་མིང་དྲན་མི་དགོས།
+ཕྱིར་ཐོན།
+འགྲེལ་བཤད་སྟོན།
+གསང་སྡོམ་ཡིག་ཆ་ཐོངས། 
+འདྲ་རིས་ལ་ལྟོས། 
+ཡིག་ཆ་ཕབ་ལེན་བྱོས། 
+གླེང་མོལ།
+ཨང་ཚབ་ཡིག་ཆ་དང་པར་རིས་དག་གཅིག་པོ་ངོས་ལེན་བྱས་འདུག ཡིག་ཆའི་ཚད་གཞི་ཆེ་ཤོས་དེ་MBརེད། 
+ནོར་འཁྲུལ། ཁྱེད་ཀྱི་ཡིག་ཆ་དེ་ཨང་ཚབ་ཡིག་ཆའམ་ཡང་ན་པར་རིས་ཡིན་པ་བྱོས། 
+ནོར་འཁྲུལ། ཡིག་ཆ་དེ་ MBཡི་ཚད་ལས་ཆེ་རུ་གཏོང་ཐུབ་གི་མི་འདུག 
+དྲ་བརྙན་གླེང་མོལ་འགོ་ཚུགས།
+དྲ་བརྙན་གླེང་མོལ་དེ་མཚམས་ཞོག
+གསང་མིང་ཅན་ཞིག་གིས་ཁྱོད་དང་ལྷན་དུ་བརྙན་པར་གླེང་མོལ་བྱེད་འདོད་འདུག 
+གསུབ།
+སྣང་མེད་བྱོས། 
+དོ་འཇོག་བྱོས། 
+ངོ་འཛིན་བྱོས། 
+གསང་བའི་དྲི་བ་ཞིག་བཏང་ནས་བེད་སྤྱོད་གཏོང་མཁན་འདིའི་ངོ་རྟགས་ར་སྤྲོད་བྱོས། 
+གསང་བའི་དྲི་བ། 
+གསང་ལན། 
+དྲིས། 
+འདྲི་བཞིན་ཡོད། 
+ལམ་ལྷོང་མ་བྱུང་།
+ངོ་རྟགས་ར་སྤྲོད་པ། 
+གསང་མིང་ཞིག་གིས་ཁྱོད་ཀྱི་ངོ་རྟགས་ར་སྤྲོད་བྱེད་འདོད་འདུག སྐུ་མཁྱེན། གཤམ་གྱི་གསང་བའི་དྲི་བ་དེར་ལན་བརྒྱབ་ནས་ཁྱེད་རང་ར་སྤྲོད་བྱོས། 
+ཁྱོད་ཀྱི་དྲི་ལན་གསང་མིང་དེས་སྤྲད་པའི་ལན་དང་ངེས་པར་དུ་མཐུན་དགོས། 
+ལན།
+ཉེན་བརྡ། ཁྱོད་ལ་གསང་མིང་ཞིག་བརྒྱུད་ནས་ཀློག་མི་ཐུབ་པའི་འཕྲིན་ཐུང་ཞིག་འབྱོར་འདུག འདིས་འཕྲིན་ཐུང་གཏོང་མཁན་དེ་རྫུན་མའམ་ཡང་ན་འཕྲིན་ཐུང་དེ་བདེ་བླགས་ངང་འབྱོར་མ་ཐུབ་པ་སྟོན། 
+ཉེན་བརྡ། ཁྱོད་ལ་ཡོད་པའི་ཉེན་སྲུང་ཞི་མོ་དེའི་དུས་ཡུན་རྫོགས་འདུག ཐོན་གསར་བ་དེ་གསར་སྣོན་བྱེད་རོགས། ཐོན་གསར་ཤོས་དེ་ཁ་སྣོན་མ་བྱས་པར་བེད་སྤྱོད་མ་གཏོང་། 
\ No newline at end of file
diff --git a/src/core/locale/ca.txt b/src/core/locale/ca.txt
new file mode 100755
index 0000000..4896118
--- /dev/null
+++ b/src/core/locale/ca.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Converses privades per a tothom.
+Benvingut a Cryptocat. Aquí van alguns consells útils:<ul><li>Cryptocat no és una solució màgica. Mai haurieu de posar la vostra vida en mans d'un programa.</li> <li class="key">Cryptocat no pot protegir-vos de persones amb males intencions ni keyloggers, així com tampoc anonimitza la conexió.</li></ul>
+Bloc
+Servidor personalitzat
+Reinicia
+nom de la conversa
+nom d'usuari
+connecta
+Escriu un nom per a la teva conversa i comparteix-la amb gent amb qui t'agradaria parlar.
+Introdueix el nom de la conversa per a unir-te.
+Si us plau, introduïu el nom de la conversa.
+El nom de la conversa només pot contenir números i lletres.
+Si us plau, introduïu un nom d'usuari
+El nom d'usuari només es pot formar amb números i lletres.
+El nom d'usuari ja està en us.
+Error d'identificació.
+Ha fallat la connexió.
+Gràcies per utilitzar Cryptocat.
+Registrant...
+Connectant...
+Connectat.
+Si us plau, escriviu aleatòriament amb el teclat durant uns segons.
+S'estan generant les claus de xifrat...
+Conversa de grup. Clica a un usuari per a iniciar una conversa privada.
+Empremta OTR (per a converses privades):
+Empremta de conversa de grup:
+Reinicia les meves claus de xifrat
+Reiniciar les teves claus de xifrat et desconnectarà. La teva emprempta digital també canviarà.
+Continua
+Estat: Disponible
+Estat: Absent
+La meva informació
+Notificacions d'escriptori activades
+Notificacions d'escriptori desactivades
+Notificacions d'àudio activades
+Notificacions d'àudio desactivades
+Recorda el meu nom d'usuari
+No recordis el meu nom d'usuari
+Desconnectar-se
+Mostra informació
+Envia fitxer xifrat
+Veure imatge
+Desar fitxer
+Conversa
+Només s'accepten arxius ZIP i imatges. Mida màxima d'arxiu: (SIZE) MB.
+Error: Assegura't que l'arxiu sigui un ZIP o una imatge.
+Error: L'arxiu no pot superar (SIZE) MB.
+Iniciar xat amb video
+Finalitzar xat amb video
+(NICKNAME) vol iniciar una videoconferència amb tu.
+Cancel·la
+Ignorar
+Des-ignorar
+Autenticar-se
+Verifica la identitat d'aquest usuari fent una pregunta secreta. La resposta ha de coincidir exactament!
+Pregunta secreta
+Resposta secreta
+Pregunta
+Preguntant...
+Ha fallat
+Identitat verificada.
+(NICKNAME) vol verificar la vostra identitat. Si us plau, contesteu la pregunta secreta per a autenticar-vos:
+La vostra resposta ha de coincidir exactament amb la proporcionada per (NICKNAME).
+Resposta
+Avís: Heu rebut un missatge il·legible de (NICKNAME). Això pot indicar un usuari no fiable o bé missatges que no s'han rebut correctament. També pot ser causat per una versió desactualitzada de Cryptocat. Si us plau, comproveu les actualitzacions.
+Avís: La vostra versió de Cryptocat està desactualitzada. És molt important que l'actualitzeu abans de continuar.
+Avís: no s'ha pogut enviar el missatge a (NICKNAME)
+Autenticat
+L'usuari no s'ha autenticat.
+Vull aprendre més...
+Apreneu més sobre l'autenticació
+Cada cop que teniu una conversació a través de Cryptocat, necessiteu autenticar aquells amb qui parleu.
+Una manera d'autenticar-vos és fer servir Cryptocat per fer una pregunta secreta al vostre amic que només ell sàpiga respondre.
+També els pots contactar a través d'un canal de confiança, com ara per telèfon, i preguntar-los les seves empremtes.
+Les empremtes són identificadors que us permeten autenticar persones. Poden canviar entre cada conversació de Cryptocat.
+Sense autenticació, algú podria suplantar la vostra identitar i interceptar les vostres comunicacions.
+Les empremtes d'autenticació d'aquest contacte han canviat. Això no hauria de passar i podria indicar un comportament sospitós. Haurieu d'autenticar aquest contacte abans de parlar-hi.
+Conversa de grup
+Facebook
+Connecta al xat de Facebook mitjançant Cryptocat. Estableix converses xifrades extrem a extrem amb els teus amics de Facebook que també fan servir Cryptocat.
+Xatejar mitjançant Facebook
+Estat de la conversa
+xifrat
+no xifrat!
+Aquesta conversa no està xifrada perque el contacte no està fent servir Cryptocat. Demana-li al tru amic que es descarregui Cryptocat per gaudir d'una conversa xifrada.
\ No newline at end of file
diff --git a/src/core/locale/cs.txt b/src/core/locale/cs.txt
new file mode 100755
index 0000000..276795e
--- /dev/null
+++ b/src/core/locale/cs.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Soukromý chat pro každého.
+Vítejte v Cryptocat. Zde je několik užitečných tipů: <ul><li> Cryptocat není zázrak - váš život by nikdy neměl záležet na jakémkoliv programu. </li><li class="key"> Cryptocat vás nechrání před nedůvěryhodnými lidmi, keyloggery a neanonymizuje vaše připojení. </li></ul>
+Blog
+Vlastní server
+obnovit
+jméno chatu
+přezdívka
+připojit
+Zadejte jméno vašeho chatu. Pak toto jméno dejte lidem, s kterými chcete chatovat.
+Zadejte jméno chatu ke kterému se chcete připojit.
+Zadejte jméno chatu.
+Název konverzace může obsahovat pouze písmena nebo čísla.
+Zadejte přezdívku.
+Přezdívka může obsahovat pouze písmena nebo čísla.
+Přezdívka již není k dispozici.
+Ověření selhalo.
+Připojení selhalo.
+Děkujeme za použití Cryptocatu.
+Registruji...
+Připojuji...
+Jste připojeni.
+Ťukejte na klávesnici mnoho náhodných znaků po dobu několika sekund.
+Generuji šifrovací klíče ...
+Skupinový chat. Klikněte na uživatele pro soukromý chat.
+OTR fingerprint (pro soukromý chat):
+Fingerprint skupinového chatu:
+obnovit moje šifrovací klíče
+Jakmile obnovíte šifrovací klíče, budete odpojeni. Váš fingerprint se také změní.
+pokračovat
+Status: k dispozici
+Status: pryč
+Moje info
+Desktopová oznámení zapnuta
+Desktopová oznámení vypnuta
+Zvuková oznámení zapnuta
+Zvuková oznámení vypnuta
+Pamatovat si mou přezdívku
+nepamatovat přezdívku
+odhlásit
+ukázat data
+poslat šifrovaný soubor
+zobrazit obrázek
+stáhnout soubor
+Chat
+Zdrojový řetězec Přeložit jenom ZIP soubory a obrázky jsou akceptovány. Maximální velikost souboru: (SIZE) MB.
+Chyba: Prosím, ujistěte se, že Vaš soubor je ZIP soubor nebo obrázek.
+Chyba:. Soubor nemůže být větší než (SIZE) MB.
+Spustit video chat
+Ukončení video chatu
+(NICKNAME) by s vámi chtěl začít video chat.
+Zrušit
+Ignorovat
+Přestat ignorovat
+Ověřit
+Ověřit identitu uživatele tajnou otázkou. Odpovědi musí přesně souhlasit!
+Tajná otázka
+Tajná odpověď
+Zeptat se
+Ptám se...
+Selhalo
+Identita ověřena.
+(NICKNAME) chce ověřit vaší identitu. Pro ověření prosím odpovězte na tajnou otázku níže:
+Vaše odpověď musí přesně souhlasit s odpovědí poskytnutou (NICKNAME)
+Odpověď
+Varování: Obdržel jste nečitelnou zprávu od (NICKNAME). Toto může napovídat o nedůvěryhodném uživateli či zprávě kterou nebylo možné obdržet. Další možností je že používáte neaktualní verzi Cryptocat. Prosíme zkontrolujte aktualizace.
+VAROVÁNÍ: Vaše verze Cryptocat je zastaralá. Silně doporučujeme aktualizaci! Nepokračujte bez aktualizace na nejnovější verzi.
+Varování: Tuto zprávu nelze zaslat (NICKNAME)
+Ověřené
+Uživatel není ověřený
+Pro více informací klikněte...
+Zjistěte více o ověřování
+Vždy když komunkujete s pomocí Cryptocat je potřeba, abyste ověřili osoby, se kterými mluvíte.
+Jednou z možností ověření uživatele je využití Cryptocat k položení tajné otázky Vašemu kamarádovi, na kterou by měl znát odpověď pouze on.
+Také je můžete kontaktovat pomocí důvěryhodného kanálu, například telefonu a požádat je o přečtení jejich otisku (fingerprintu).
+Otisky (Fingerprinty) jsou identifikátory které Vám umožní ověřit osoby. Mohou být měněny mezi každou Cryptocat konverzací.
+Bez ověření by se někdo mohl vydávat za někoho jiného, či zachycovat Vaši komunikaci.
+Ověřovací otisky (Fingerprinty) pro tento kontakt se změnily. Toto by se nemělo stávat a může to napovídat o podezdřelém chování. Prosím ověřte tento kontakt předtím, než s ním budete chatovat.
+Skupinový chat
+Facebook
+Připojte se k chatu na Facebooku prostřednictvím nástroje Cryptocat. Můžete nastavit obousměrně zašifrovaný chat s přáteli na Facebooku, kteří také používají doplněk Cryptocat.
+Chatovat prostřednictvím Facebooku
+Stav konverzace
+zašifrován.
+není šifrován.
+Tato konverzace není zašifrována, protože tento kontakt nepoužívá doplněk Cryptocat. Požádejte přítele, aby si stáhl doplněk Cryptocat, který umožní šifrovat chatování.
\ No newline at end of file
diff --git a/src/core/locale/da.txt b/src/core/locale/da.txt
new file mode 100755
index 0000000..db4e0c9
--- /dev/null
+++ b/src/core/locale/da.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Privat Samtale for Alle.
+Velkommen til Crytocat. Her er nogle hjælpsomme tips: <ul><li>Cryptocat er ikke magisk. Du bør aldrig stole fuldt og fast på noget software.</li><li class="key">Cryptocat kan ikke beskytte dig mod utroværdige mennesker eller key loggers, og gør ikke din forbindelse anonym.</li></ul>
+Blog
+Brugerdefineret server
+Nulstille
+samtale navn
+brugernavn
+forbind
+Intast et navn på din samtale og del den med dem du gerne vil tale med, eller deltag <strong>lobby</strong> for at tale med nye mennesker
+Indtast navnet på en samtale du vil forbindes til
+Indtast samtalens navn
+Samtale navn kan kun være bogstaver og tal.
+Indtast bruger navn
+Brugernavn kan kun være bogstaver og tal.
+Brugernavn er i brug.
+Godkendelse fejlede.
+Forbindelse fejlede.
+Tak fordi du brugte Cryptocat.
+Registrering...
+Forbinder...
+Forbundet.
+Tast så forskelligt taster som muligt i nogle sekunder.
+Genererer krypterings nøgle...
+Gruppe samtale. Klik på bruger navn for privat samtale.
+OTR signatur (for privat samtale):
+Gruppe samtale signatur:
+Nulstil mine krypterings nøgler
+Nulstilling af dine krypteringsnøgler vil afbryde samtalen. Din bruger indstilling vil også ændres
+Fortsæt
+Status: Ledig
+Status: Ikke tilstede
+Min Info
+Desktop Besked On
+Desktop besked Off
+Lyd Besked On
+Lyd Besked Off
+Husk mit brugernavn
+Husk ikke mit brugernavn
+Log ud
+Vis Info
+Send krypteret fil
+Se billede
+Download fil
+Samtale
+Kun ZIP filer og billeder accepteres. Max fil størrelse: (SIZE) MB.
+Fejl: Kun ZIP fil eller billede.
+Fejl: Fil kan ikke være større end (SIZE) MB.
+Start video samtale
+Afslut video samtale
+(NICKNAME) vil gerne starte en video samtale med dig.
+Fortryd
+Ignorerer
+Fjern ignorering
+Godkender
+Bekræft denne brugers identitet ved at spørge om det hemmelige spørgsmål. Svaret skal passe præcist!
+Hemmeligt spørgsmål
+Hemmeligt svar
+Spørg
+Spørger...
+Mislykkedes
+Identitet godkendt.
+(NICKNAME) ønsker at verificere din identitet. Svar venligst nedenstående hemmelige spørgsmål, for at autentificere dig selv:
+Dit svar skal svare præcist til det givet af (NICKNAME).
+Svar
+Advarsel: Du har modtaget en ulæselig meddelelse fra (NICKNAME). Dette kan indikere at brugeren ikke er troværdig eller beskeden ikke kunne modtages. Du kan også bruge en forældet version af Cryptocat. Check for opdateringer.
+Advarsel: Din version af Cryptocat er forældet. Vi anbefaler stærkt, at du opdaterer til den seneste version. Fortsæt ikke uden først at opdatere til den seneste version.
+Advarsel: denne besked kunne ikke sendes til (NICKNAME)
+Godkendt
+Bruger er ikke godkendt
+Klik her for at få mere at vide...
+Lær mere om godkendelse
+Hver gang du fører en Cryptocat samtale, behøver du at godkende den person du har samtale med
+En måde at godkende på, er at bruge Cryptocat til at spørge en ven et hemmeligt spørgsmål som kun personen kender svaret på.
+Du kan kontakte dem gennem sikker kanal som pr. telefon, og bede den læse deres fingerprint
+Fingerprint er identifikatorer der tillader dig at godkende personen. Disse kan skifte mellem hver Cryptocat samtale.
+Uden godkendelse kan andre udgives sig for en person og opfange jeres samtaler.
+Godkendelselses fingerprint for denne kontakt er forandret. Dette skulle ikke ske og kan indikere mistanke.
+Gruppe samtale
+Facebook
+Forbind til Facebook Chat via Cryptocat. Opsæt end-to-end krypteret chats med Facebook venner der også bruger Cryptocat.
+Chat via Facebook
+Samtale status
+krypteret.
+ikke krypteret !
+Denne samtale er ikke krypteret fordi denne kontakt ikke bruger Cryptocat. Bed din ven om at downloade Cryptocat for at få glæde af krypteret chat.
\ No newline at end of file
diff --git a/src/core/locale/de.txt b/src/core/locale/de.txt
new file mode 100755
index 0000000..09a0ed0
--- /dev/null
+++ b/src/core/locale/de.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Private Unterhaltungen für jeden.
+Willkommen bei Cryptocat. Ein paar hilfreiche Tipps: <ul><li>Cryptocat ist kein Wundermittel. Sie sollten Ihr Leben niemals einer Anwendung anvertrauen.</li><li class="key">Cryptocat kann Sie nicht vor vertrauensunwürdigen Menschen oder Keyloggern schützen und anonymisiert auch Ihre Internetverbindung nicht.</li></ul>
+Blog
+Benutzerdefinierter Server
+Zurücksetzen
+Name der Unterhaltung
+Spitzname
+Verbinden
+Einen Namen für Ihre Unterhaltung eingeben und diese Leuten freigeben, mit denen Sie sich unterhalten möchten, oder die <strong>Vorhalle</strong> besuchen, um neue Leute zu treffen!
+Den Namen einer Unterhaltung eingeben, um ihr beizutreten.
+Bitte den Namen einer Unterhaltung eingeben.
+Unterhaltungsname kann nur aus Buchstaben und Zahlen bestehen.
+Bitte einen Spitznamen eingeben.
+Spitzname kann nur aus Buchstaben und Zahlen bestehen.
+Dieser Spitzname wird bereits verwendet.
+Bestätigungsfehler.
+Verbindung fehlgeschlagen.
+Vielen Dank, dass Sie Cryptocat verwenden.
+Registrierung wird durchgeführt …
+Verbindung wird hergestellt …
+Verbunden.
+Bitte, so zufällig wie möglich, für ein paar Sekunden etwas auf der Tastatur eingeben.
+Verschlüsselungsschlüssel wird generiert …
+Gruppenunterhaltung. Für eine private Unterhaltung auf einen Benutzer klicken.
+OTR-Fingerabdruck (für private Unterhaltungen):
+Fingerabdruck der Gruppenunterhaltung:
+Meine Verschlüsselungsschlüssel zurücksetzen
+Das Zurücksetzen Ihrer Verschlüsselungsschlüssel wird die Verbindung trennen. Ihre Fingerabdrücke werden sich ebenfalls ändern.
+Fortsetzen
+Status: Verfügbar
+Status: Abwesend
+Meine Informationen
+Schreibtischbenachrichtigungen An
+Schreibtischbenachrichtigungen Aus
+Tonbenachrichtigung An
+Tonbenachrichtigung Aus
+Meinen Spitznamen merken
+Meinen Spitznamen nicht merken
+Abmelden
+Information anzeigen
+Verschlüsselte Datei senden
+Bild anzeigen
+Datei herunterladen
+Unterhaltung
+Es werden nur ZIP-Dateien und Bilder akzeptiert. Maximale Dateigröße: (SIZE) MB.
+Fehler: Ihre Datei muss eine ZIP-Datei oder ein Bild sein.
+Fehler: Datei darf nicht größer als (SIZE) MB sein.
+Video-Unterhaltung starten
+Video-Unterhaltung beenden
+(NICKNAME) möchte eine Video-Unterhaltung mit Ihnen starten.
+Abbrechen
+Ignorieren
+Ignorieren rückgängig machen
+Bestätigen
+Die Identität dieses Benutzers durch eine Sicherheitsfrage bestätigen. Die Antworten müssen exakt übereinstimmen!
+Sicherheitsfrage
+Geheime Antwort
+Fragen
+Frage wird gestellt …
+Fehlgeschlagen
+Identität bestätigt.
+(NICKNAME) möchte Ihre Identität überprüfen. Zum bestätigen bitte die unten stehende Sicherheitsfrage beantworten:
+Ihre Antwort muss exakt mit der von (NICKNAME) übereinstimmen.
+Antwort
+Achtung: Sie haben eine unlesbare Nachricht von (NICKNAME) erhalten. Das kann durch einen vertrauensunwürdigen Benutzer oder durch Netzwerkprobleme verursacht worden sein. Vielleicht verwenden Sie auch eine veraltete Version von Cryptocat. Bitte auf Aktualisierungen überprüfen.
+Achtung: Ihre Version von Cryptocat ist veraltet. Wir empfehlen Ihnen dringend, die neueste Version zu verwenden! Bevor Sie fortfahren bitte unbedingt auf die neueste Version aktualisieren.
+Achtung: Diese Nachricht konnte nicht zu (NICKNAME) gesendet werden.
+Bestätigt
+Benutzer ist nicht bestätigt.
+Anklicken, um mehr zu erfahren …
+Mehr über die Bestätigung erfahren
+Bei jeder Cryptocat-Unterhaltung, müssen Sie die Person, mit der Sie sprechen, bestätigen.
+Eine Möglichkeit der Bestätigung ist es, über Cryptocat Ihrem Freund eine geheime Frage zu stellen, die niemand anderer sonst beantworten kann.
+Sie können sich auch über einen vertrauenswürdigen Kanal, wie z.B. über das Telefon bitten, ihre Fingerabdrücke vorzulesen.
+Fingerabdrücke sind Kennungen, die es Ihnen erlauben, eine Person zu bestätigen. Diese können sich bei jeder Cryptocat-Unterhaltung ändern.
+Ohne Bestätigung könnte sich jemand als jemand anderes ausgeben, oder Ihre Kommunikation abhören.
+Die bestätigten Fingerabdrücke für diesen Kontakt haben sich geändert. Dies ist nicht vorgesehen und sieht nach einem verdächtigen Verhalten aus. Bitte bestätigen Sie diesen Kontakt, bevor Sie mit ihm unterhalten.
+Gruppenchat
+Facebook
+Verbinden Sie sich über Cryptocat mit Facebook Chat. Richten Sie durchgehend verschlüsselte Chats mit Facebook-Freunden ein, die ebenfalls Cryptocat verwenden
+Über Facebook chatten
+Unterhaltungsstatus
+verschlüsselt.
+nicht verschlüsselt!
+Diese Unterhaltung ist nicht verschlüselt, da dieser Kontakt nicht Cryptocat verwendet. Bitten Sie Ihre/n Freund/in, Cryptocat herunterzuladen, um verschlüsselten Chat zu nutzen.
\ No newline at end of file
diff --git a/src/core/locale/el.txt b/src/core/locale/el.txt
new file mode 100755
index 0000000..a43b770
--- /dev/null
+++ b/src/core/locale/el.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Ιδιωτικές Συνομιλίες για Όλους.
+Καλωσορίσατε στο Cryptocat. Ορίστε μερικές χρήσιμες συμβουλές: <ul><li> Το Cryptocat δεν είναι μία μαγική σφαίρα. Δεν πρέπει να εμπιστεύεστε ποτέ οποιοδήποτε πρόγραμμα με την ζωής σας.</li><li class="key">Το Cryptocat δεν μπορεί να σας προστατέψει ενάντια σε μη έμπιστους ανθρώπους ή προγράμματα υποκλοπής κωδικών, και δεν καθιστά ανώνυμη την σύνδεσή σας.</li></ul>
+Μπλόγκ
+Προσωπικός διακομιστής
+Επαναφορά
+όνομα συνομιλίας
+ψευδώνυμο
+σύνδεση
+Δώστε ένα όνομα  στην συζήτηση σας και μοιραστείτε το με τα άτομα που θέλετε να μιλήσετε
+Εισάγετε το όνομα μιας συνομιλίας για να συνδεθείτε.
+Παρακαλώ εισάγετε όνομα για τη συνομιλία.
+Το όνομα συνομιλίας μπορεί να είναι γράμματα ή αριθμοί μόνο.
+Παρακαλώ εισάγετε ένα ψευδώνυμο.
+Το ψευδώνυμο μπορεί να είναι μόνο γράμματα ή αριθμοί.
+Το ψευδώνυμο χρησιμοποιείται ήδη.
+Αποτυχία πιστοποίησης.
+Αποτυχία σύνδεσης.
+Ευχαριστούμε που χρησιμοποιείτε το Cryptocat.
+Εγγραφή...
+Σύνδεση...
+Συνδεδεμένη.
+Παρακαλώ πληκτρολογήστε όσο το δυνατό πιο τυχαίους χαρακτήρες στο πληκτρολόγιό σας για λίγο.
+Δημιουργία κρυπτογραφικών κλειδιών...
+Ομαδική συνομιλία. Κάντε κλικ σε κάποιο χρήστη για ιδιωτική συζήτηση.
+Αποτύπωμα OTR (για προσωπικές συνομιλίες):
+Αποτύπωμα ομαδικής συνομιλίας:
+Επαναφορά κλειδιά κρυπτογράφησης μου
+Επαναφορά κλειδιά κρυπτογράφησης σας θα σας αποσυνδέσει. Δακτυλικά αποτυπώματα σας θα αλλάξει.
+Συνεχίστε
+Κατάσταση: Διαθέσιμος/η
+Κατάσταση: Απομακρυσμένος/η
+Οι πληροφορίες μου
+Ειδοποιήσεις οθόνης Ενεργοποιημένες
+Ειδοποιήσεις οθόνης Απενεργοποιημένες
+Ηχητικές ειδοποιήσεις Ενεργοποιημένες
+Ηχητικές ειδοποιήσεις Απενεργοποιημένες
+Θυμηθείτε το ψευδώνυμό μου
+Να μη θυμάσαι το ψευδώνυμό μου
+Αποσύνδεση
+Προβολή Πληροφοριών
+Αποστολή κρυπτογραφημένου αρχείου
+προβολή εικόνας
+κατέβασμα αρχείου
+Συνομιλία
+Μόνο αρχεία ZIP και εικόνες γίνονται αποδεκτά. Μέγιστο μέγεθος αρχείου: (SIZE) MB.
+Σφάλμα: Παρακαλούμε βεβαιωθείτε ότι το αρχείο σας είναι αρχείο ZIP ή εικόνα.
+Σφάλμα: Το αρχείο δεν μπορεί να είναι μεγαλύτερο από (SIZE) MB.
+Αρχή βίντεο συνομιλίας
+Τέλος βίντεο συνομιλίας
+Ο χρήστης (NICKNAME) θα ήθελε να ξεκινήσει μία βίντεο κλήση μαζί σας.
+Ακύρωση
+Αγνόηση
+Απο-αγνόηση
+Έλεγχος Ταυτότητας
+Εξακριβώστε την ταυτότητα αυτού του χρήστη χρησιμοποιώντας μία μυστική ερώτηση. Οι απαντήσεις πρέπει να ταιριάζουν απολύτως!
+Μυστική ερώτηση
+Μυστική απάντηση
+Ρωτήστε
+Η ερώτηση πραγματοποιείται...
+Απέτυχε
+Ταυτότητα επαληθεύτηκε.
+Ο χρήστης (NICKNAME) θέλει να επιβεβαιώσει την ταυτότητά σας. Παρακαλούμε απαντήστε στην παρακάτω μυστική ερώτηση για να επαληθεύσετε την ταυτότητάς σας:
+Η απάντηση σας πρέπει να ταιριάζει επακριβώς με αυτήν που δόθηκε από τον χρήστη (NICKNAME).
+Απάντηση
+Προειδοποίηση: Έχετε λάβει ένα μη αναγνώσιμο μήνυμα από (NICKNAME). Αυτό μπορεί να είναι αποτελεί ένδειξη ενός αναξιόπιστου χρήστη ή μηνύματα που απέτυχε η λήψη τους. Μπορεί επίσης να τρέχετε μια παλιά έκδοση του Cryptocat. Παρακαλώ ελέγξετε για ενημερώσεις.
+Προσοχή: Η έκδοση του Cryptocat που έχετε είναι ξεπερασμένη. Η ενημέρωση στην τελευταία έκδοση συνιστάται! Μην προχωρήσετε χωρίς να κάνετε ενημέρωση στην τελευταία έκδοση.
+Προειδοποίηση: αυτό το μήνυμα δεν μπόρεσε να αποσταλεί στον/ην (NICKNAME)
+Πιστοποιημένο(ς)
+Ο χρήστης δεν είναι πιστοποιημένος.
+Κάντε κλικ για να μάθετε περισσότερα...
+Μάθετε περισσότερα περί πιστοποίησης
+Κάθε φορά που έχετε μια συζήτηση Cryptocat, πρέπει να πιστοποιείτε τα άτομα στα οποία μιλάτε.
+Ένας τρόπος να πιστοποιείτε είναι χρησιμοποιώντας το Cryptocat για να ρωτήσει τον φίλο σας μια μυστική ερώτηση που μόνο εκείνοι θα μπορούσαν να απατήσουν σε αυτή.
+Μπορείτε επίσης να επικοινωνήσετε μαζί τους μέσω ενός έμπιστου καναλιού, όπως μέσω τηλεφώνου, και να ζητάτε να σας διαβάσουν τους αριθμούς πιστοποιησής (αποτυπώματα/fingerprints) τους.
+Τα αποτυπώματα είναι (αριθμοί πιστοποίησης) είναι προσδιορισμοί που σας επιτρέπουν να πιστοποιείτε άτομα. Μπορούν να αλλάξουν μεταξύ κάθε συζήτησης Cryptocat.
+Χωρίς πιστοποίηση, κάποιος θα μπορούσε να υποδηθεί ή να παρακολουθήσει τις συνομιλίες σας.
+Τα αποτυπώματα πιστοποίησης για αυτόν τον σύνδεσμο έχουν αλλάξει. Αυτό υποτίθεται πως δεν πρέπει να συμβαίνει και μπορεί να υποδεικνύει ύποπτη συμπεριφορά. Παρακαλώ πιστοποιείστε αυτόν τον σύνδεσμο πριν μιλήσετε μαζί του.
+Ομαδική Συνομιλία
+Facebook
+Συνδεθείτε στο Facebook Chat μέσω του Cryptocat. Ρύθμιση από την αρχή ως το τέλος των κρυπτογραφημένων συζητήσεων των φίλων στο Facebook που χρησιμοποιούν επίσης Cryptocat.
+Συνομιλία μέσω Facebook
+Κατάσταση συνομιλίας
+κρυπτογραφημένη.
+δεν είναι κρυπτογραφημένο!
+Αυτή η συζήτηση δεν είναι κρυπτογραφημένη, επειδή αυτή η επαφή δεν χρησιμοποιεί Cryptocat. Ζητήστε από το φίλο σας να κατεβάσει το Cryptocat για να απολαύσετε κρυπτογραφημένη συνομιλία.
\ No newline at end of file
diff --git a/src/core/locale/en.txt b/src/core/locale/en.txt
new file mode 100644
index 0000000..feb67a9
--- /dev/null
+++ b/src/core/locale/en.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Private Conversations for Everyone.
+Welcome to Cryptocat. Here are some helpful tips: <ul><li>Cryptocat is not a magic bullet. You should never trust any piece of software with your life.</li><li class="key">Cryptocat can't protect you against untrustworthy people or key loggers, and does not anonymize your connection.</li></ul>
+Blog
+Custom server
+Reset
+conversation name
+nickname
+connect
+Enter a name for your conversation and share it with people you'd like to talk to, or join <strong>lobby</strong> to meet new people!
+Enter the name of a conversation to join.
+Please enter a conversation name.
+Conversation name can be letters or numbers only.
+Please enter a nickname.
+Nickname can be letters or numbers only.
+Nickname in use.
+Authentication failure.
+Connection failed.
+Thank you for using Cryptocat.
+Registering...
+Connecting...
+Connected.
+Please type on your keyboard as randomly as possible for a few seconds.
+Generating encryption keys...
+Group conversation. Click on a user for private chat.
+OTR fingerprint (for private conversations):
+Group conversation fingerprint:
+Reset my encryption keys
+Resetting your encryption keys will disconnect you. Your fingerprints will also change.
+Continue
+Status: Available
+Status: Away
+My Info
+Desktop Notifications On
+Desktop Notifications Off
+Audio Notifications On
+Audio Notifications Off
+Remember my nickname
+Don't remember my nickname
+Logout
+Display Info
+Send encrypted file
+view image
+download file
+Conversation
+Only ZIP files and images are accepted. Maximum file size: (SIZE) MB.
+Error: Please make sure your file is a ZIP file or an image.
+Error: File cannot be larger than (SIZE) MB.
+Start video chat
+End video chat
+(NICKNAME) would like to start a video chat with you.
+Cancel
+Ignore
+Unignore
+Authenticate
+Verify this user's identity by asking a secret question. Answers must match exactly!
+Secret question
+Secret answer
+Ask
+Asking...
+Failed
+Identity verified.
+(NICKNAME) wants to verify your identity. Please answer the below secret question to authenticate yourself:
+Your answer must exactly match the one given by (NICKNAME).
+Answer
+Warning: You have received an unreadable message from (NICKNAME). This may indicate an untrustworthy user or messages that failed to be received. You may also be running an outdated version of Cryptocat. Please check for updates.
+Warning: Your version of Cryptocat is outdated. Updating to the latest version is strongly recommended! Do not proceed without updating to the latest version.
+Warning: this message could not be sent to (NICKNAME)
+Authenticated
+User is not authenticated.
+Click to learn more...
+Learn more about authentication
+Every time you have a Cryptocat conversation, you need to authenticate the persons you are talking to.
+One way you can authenticate is by using Cryptocat to ask your friend a secret question that only they would know the answer to.
+You can also contact them via a trusted channel, such as by phone, and ask them to read their fingerprints.
+Fingerprints are identifiers that allow you to authenticate persons. They can change between every Cryptocat conversation.
+Without authentication, someone could be impersonating or intercepting your communications.
+The authentication fingerprints for this contact have changed. This is not supposed to happen and could indicate suspicious behaviour. Please authenticate this contact before chatting with them.
+Group Chat
+Facebook
+Connect to Facebook Chat via Cryptocat. Set up end-to-end encrypted chats with Facebook friends who are also using Cryptocat.
+Chat via Facebook
+Conversation status
+encrypted.
+not encrypted!
+This conversation is not encrypted because this contact is not using Cryptocat. Ask your friend to download Cryptocat to enjoy encrypted chat.
\ No newline at end of file
diff --git a/src/core/locale/es.txt b/src/core/locale/es.txt
new file mode 100755
index 0000000..ba44b92
--- /dev/null
+++ b/src/core/locale/es.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Conversaciones privadas para todos.
+Bienvenido a Cryptocat. Algunos consejos útiles: <ul><li>Cryptocat no es una panacea. No ponga su vida en manos de un programa informático.</li><li class="key">Cryptocat no puede protegerle de personas poco fiables y keyloggers. Tampoco anonimiza su conexión.</li></ul>
+Blog
+Servidor personalizado
+Reiniciar
+nombre de la conversación
+nombre de usuario
+conectar
+Introduzca un nombre para su conversación y compártalo con las personas con las que desea hablar, o únase al <strong>lobby</strong> para conocer nuevas personas.
+Introduzca el nombre de una conversación para unirse.
+Por favor, introduzca un nombre de conversación.
+El nombre de la conversación solo puede contener letras y números.
+Por favor, introduzca un nombre de usuario.
+Su nombre de usuario solo puede contener letras o números.
+El nombre de usuario se encuentra en uso.
+Error de autenticación.
+Conexión fallida.
+Gracias por usar Cryptocat.
+Registrando...
+Conectando...
+Conectado.
+Por favor, escribe aleatoriamente en tu teclado durante unos segundos.
+Generando claves de cifrado...
+Conversación de grupo. Haga clic sobre un usuario para iniciar una conversación privada.
+Huella OTR (para conversaciones privadas):
+Huella para conversaciones en grupo:
+Restablecer mis claves de cifrado
+Restablecer las claves de cifrado hará que se desconecte. Sus huellas digitales también cambiarán.
+Continuar
+Estado: Disponible
+Estado: Ausente
+Su información
+Notificaciones de escritorio activadas
+Notificaciones de escritorio desactivadas
+Notificaciones de audio activadas
+Notificaciones de audio desactivadas
+Recordar mi nombre de usuario
+No recordar mi nombre de usuario
+Desconectarse
+Mostrar Información
+Enviar archivo cifrado
+ver imagen
+descargar archivo
+Conversación
+Solo se aceptan archivos ZIP e imágenes. Tamaño máximo: (SIZE) MB.
+Error: Por favor, asegúrese de que su archivo es ZIP o una imagen.
+Error: El archivo no puede ser más grande que (SIZE) MB.
+Comenzar conversación de video
+Terminar conversación de video
+A (NICKNAME) le gustaría iniciar una conversación de vídeo con usted.
+Cancelar
+Ignorar
+No ignorar
+Autenticar
+Verifique la identidad de este usuario mediante una pregunta secreta. ¡Las respuestas tienen que coincidir exactamente!
+Pregunta secreta
+Respuesta secreta
+Preguntar
+Preguntando...
+Fallida
+Identidad verificada.
+(NICKNAME) quiere verificar su identidad. Por favor, conteste la siguiente pregunta para autenticarse:
+Su respuesta debe coincidir exactamente con la que ha escrito (NICKNAME).
+Responder
+Advertencia: Ha recibido un mensaje ilegible de (NICKNAME). Esto puede deberse a que el usuario no es confiable o a que los mensajes no fueron recibidos con éxito. También puede ser que esté usando una versión antigua de Cryptocat. Revise si hay actualizaciones.
+Advertencia: Su versión de Cryptocat no está actualizada. ¡La actualización a la última versión es muy recomendable! No proceda sin actualizar a la última versión.
+Advertencia: no se pudo enviar este mensaje a (NICKNAME)
+Autenticado
+El usuario no está autenticado.
+Haga clic para aprender más...
+Aprenda más sobre la autenticación
+Cada que tiene una conversación por Cryptocat, necesita que autenticar a las personas a quienes les está hablando.
+Una manera de autenticar es usar Cryptocat para preguntarle a su amigo una pregunta secreta cuya respuesta solo él conoce.
+También puede contactarlos por medio de un canal confiable, como por teléfono, y pedirles que lean sus huellas digitales.
+Las huellas digitales son identificadores que le permiten autenticar personas. Pueden cambiar entre cada conversación en Cryptocat.
+Sin autenticación, alguien puede suplantar o interceptar sus comunicaciones.
+Las huellas de autenticación para este contacto han cambiado. No se supone que eso suceda y pudiera indicar comportamiento sospechoso. Por favor, autentifique a este contacto antes de conversar con él.
+Charla grupal
+Facebook
+Conéctese a Facebook Chat a través de Cryptocat. Establezca charlas cifradas de extremo a extremo con amigos de Facebook que también estén usando Cryptocat.
+Charle a través de Facebook
+Estado de la conversación
+cifrado.
+¡sin cifrar!
+Esta conversación no está cifrada porque este contacto no está usando Cryptocat. Pídale a su amigo que descargue Cryptocat para poder disfrutar de una charla cifrada.
\ No newline at end of file
diff --git a/src/core/locale/et.txt b/src/core/locale/et.txt
new file mode 100644
index 0000000..44d4a47
--- /dev/null
+++ b/src/core/locale/et.txt
@@ -0,0 +1,68 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Privaatvestlus kõigile.
+Teretulemast Cryptocat'i. Paar abistavat nõuannet: <ul><li>Cryptocat ei ole hõbekuul. Ära kunagi usalda oma elu tarkvara kätesse.</li><li class="key">Cryptocat ei saa sind kaitsta ebausaldusväärsete inimeste ega nuhkvara eest ning ei anonümiseeri sinu ühendust.</li></ul>
+Blogi
+Kohandatud server
+Nulli
+vestluse nimi
+hüüdnimi
+ühenda
+Sisesta vestlusele nimi ning jaga seda inimestega, kelle sa rääkida tahad või liitu <strong>lobbiga</strong>, et kohtuda uute inimestega!
+Sisesta vestluse nimi millega ühineda.
+Palun sisesta vestluse nimi.
+Vestluse nimi peab olema tähtnumbriline.
+Palun sisesta hüüdnimi.
+Hüüdnimi peab olema tähtnumbriline.
+Hüüdnimi on kasutuses.
+Autentimisrike.
+Ühendus ebaõnnestus.
+Täname, et kasutate Cryptocat'i.
+Registreerimas...
+Ühendamas...
+Ühendatud.
+Palun trüki mõni sekund oma klaviatuuril nii suvaliselt kui võimalik.
+Loomas krüpteerimise võtmeid..
+Grupivestlus. Vajuta hüüdnimel privaatvestluseks.
+OTR sõrmejälg (privaatvestluste jaoks):
+Grupivestluse sõrmejälg:
+Nulli mu krüpteerimise võtmed
+Krüpteerimise võtmete nullimine katkestab su ühenduse. Ka sinu sõrmejäljed muutuvad.
+Jätka
+Staatus: Saadaval
+Staatus: Eemal
+Minu Info
+Töölaua Teated Sees
+Töölaua Teated Väljas
+Heliteated Sees
+Heliteated Väljas
+Mäleta mu hüüdnime
+Ära mäleta mu hüüdnime
+Logi välja
+Kuva Info
+Saada krüpteeritud fail
+vaata pilti
+lae fail alla
+Vestlus
+Ainult ZIP ja pildifailid on lubatud. Maksimaalne faili suurus: (SIZE) MB.
+Viga: Palun veendu, et sinu fail on .zip või pildifail.
+Viga: Faili ei saa olla suurem kui (SIZE) MB.
+Alusta videokõne
+Lõpeta videokõne
+(NICKNAME) tahab alustada sinuga video vestlust.
+Loobu
+Eira
+Tühista eiramine
+Autentimine
+Kinnita selle kasutaja identiteet, küsides salajast küsimust. Vastused peavad olema täpsed!
+Salaküsimus
+Salavastus
+Küsi
+Küsimas...
+Ebaõnnestus
+Identiteet kinnitatud.
+(NICKNAME) tahab kontrollida sinu identiteeti. Palun vastake allpool toodud salaküsimusele, et ennast autentida:
+Sinu vastus peab täpselt ühtima (NICKNAME) vastusega
+Vasta
+Hoiatus: (NICKNAME) on saatnud sulle loetamatu sõnumi. See võib viidata ebausaldusväärsele kasutajale või sõnumitele, mida ei ole võimalik vastu võtta.
+Hoiatus: Sinu Cryptocat versioon on vananenud. Uuendamine värskeimale versioonile on tungivalt soovitatav! Ärge jätkake ilma uuendamiseta.
\ No newline at end of file
diff --git a/src/core/locale/eu.txt b/src/core/locale/eu.txt
new file mode 100755
index 0000000..fc13cea
--- /dev/null
+++ b/src/core/locale/eu.txt
@@ -0,0 +1,79 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Elkarrizketa pribatuak guztionatzat.
+Ongi etorri Cryptocat-era. Hemen dituzu aholku erabilgarri batzuk: <ul><li>Cryptocat ez da magikoa. Ez zenuke inoiz zure bizitza software baten esku jarri behar.</li><li class="key">Cryptocat-ek ezin zaitu konfiantza merezi ez duten pertsonetatik edo keylogger-etatik babestu, eta ez du zure konexioa anonimo bihurtzen.</li></ul>
+Bloga
+Zerbitzari pertsonalizatua
+Berrezarri
+elkarrizketa-izena
+goitizena
+konektatu
+Idatzi zure elkarrizketarako izen bat eta parteka ezazu izen hori hitz egin nahiko zenukeen jendearekin. Bestela <strong>lobby</strong> elkarrizketan sartu jende berria ezagutzeko!
+Idatzi elkarrizketa baten izena elkarrizketa horretara sartzeko.
+Idatzi elkarrizketa-izen bat mesedez.
+Elkarrizketa-izena letraz eta zenbakiz bakarrik egon daiteke osatuta.
+Idatzi goitizen bat mesedez.
+Goitizena letraz eta zenbakiz bakarrik egon daiteke osatuta.
+Erabiltzen ari den goitizena.
+Egiaztaketa hutsegitea.
+Konexioak huts egin du.
+Eskerrik asko Cryptocat erabiltzeagatik.
+Erregistratzen...
+Konektatzen...
+Konektatuta.
+Segundu batzuz idatz ezazu teklatuan ahalik eta era ausazkoenean.
+Zifraketa-gakoak sortzen...
+Talde-elkarrizketa. Egin klik erabiltzaile baten gainean elkarrizketa pribatua izateko.
+OTR hatz-aztarna (elkarrizketa pribatuetarako):
+Talde elkarrizketaren hatz-aztarna:
+Berrezarri nire zifraketa-gakoak
+Zure zifraketa-gakoak berrezartzeak deskonektatu egingo zaitu. Zure hatz-aztarnak ere aldatuko dira.
+Jarraitu
+Egoera: Eskuragarri
+Egoera: Kanpoan
+Nire Informazioa
+Mahaigaineko jakinarazpenak gaituta
+Mahaigaineko jakinarazpenak desgaituta
+Soinu jakinarazpenak gaituta
+Soinu jakinarazpenak desgaituta
+Nire goititzena gogoratu
+Ez gogoratu nire goitizena
+Itxi saioa
+Erakutsi informazioa
+Bidali fitxategi zifratua
+ikusi irudia
+jaitsi fitxategia
+Elkarrizketa
+ZIP fitxategiak eta irudiak bakarrik onartzen dira. Gehienezko fitxategi-tamaina: (SIZE) MB.
+Errorea: Egiazta ezazu fitxategia ZIP motakoa edo irudia dela.
+Errorea: Fitxategia ezin da izan (SIZE) MB baino handiagoa.
+Hasi bideo-elkarrizketa
+Amaitu bideo-elkarrizketa
+(NICKNAME)-(e)k bideo-elkarrizketa bat hasi nahi du zurekin.
+Utzi
+Baztertu
+Ez baztertu
+Autentikatu
+Erabiltzaile honen nortasuna egiaztatu galdera sekretu baten bitartez. Erantzunek berdin-berdinak izan behar dute!
+Galdera sekretua
+Erantzun sekretua
+Galdetu
+Galdetzen...
+Huts egin du
+Nortasun egiaztatua.
+(NICKNAME)-(e)k zure nortasuna egiaztatu nahi du. Mesedez ondorengo galdera sekretua erantzun zure nortasuna egiaztatzeko:
+Zure erantzunak (NICKNAME)-(e)k emandakoaren berdina izan behar du.
+Erantzun
+Abisua: (NICKNAME)-(e)k irakurri ezin daitekeen mezu bat bidali dizu. Baliteke erabiltzaile fidagaitza izatea edo jasotzerakoan mezuak huts egin izana. Agian Cryptocat bertsioa zaharkitua erabiltzen egongo zara. Eguneraketarik dagoen egiaztatu, mesedez.
+Abisua: Zure Cryptocat bertsioa zaharkituta dago. Oso gomendagarria da azken bertsiora eguneratzea! Ez jarraitu azken bertsiora eguneratu gabe.
+Abisua: ezin izan da mezu hau (NICKNAME)-(r)i bidali
+Egiaztatua
+Erabiltzaile ez egiaztatua.
+Klik egin gehiago ikasteko...
+Ikasi gehiago egiaztaketari buruz
+Cryptocat elkarrizketa bat duzun bakoitzean, hizketan zauden pertsonak egiaztatu behar dituzu.
+Cryptocat era ziurtatuan erabiltzeko modu bat zure lagunei galdera sekretua egitea da, erantzuna beraiek bakarrik jakingo luketen bat.
+Beste modu bat, bide fidagarri batetik beraiek kontaktatzea da, adibidez telefonoz, eta beraien hatz-aztarna irakurtzeko eskatzea.
+Hatz-aztarnak pertsonak egiaztatzea ahalbidetzen duten identifikatzaileak dira. Cryptocat elkarrizketa bakoitzean aldatu daitezke.
+Egiaztaketarik gabe, norbait zure komunikazioak entzuten egon daiteke eta hizketan norekin ari zaren ere ezin duzu ziur egon.
+Kontaktu hau ziurtatzeko hatz-aztarnak aldatu egin dira. Horrelakorik ez zen gertatu beharko eta erabilera susmagarriaren seinale izan daiteke. Ziurtatu ezazu kontaktu hau berarekin elkarrizketa izan aurretik.
\ No newline at end of file
diff --git a/src/core/locale/fa.txt b/src/core/locale/fa.txt
new file mode 100755
index 0000000..b5784c1
--- /dev/null
+++ b/src/core/locale/fa.txt
@@ -0,0 +1,87 @@
+rtl
+Tahoma, DejaVu, "Helvetica Neue", Helvetica, Arial, Verdana
+گفتگوی خصوصی برای تمامی افراد.
+به کریپتوکت خوش آمدید. چند نکته‌ی مفید در مورد کریپتوکت:<ul><li> کریپتوکت گلوله‌ی سحر و جادو نیست. شما هرگز نباید به هیچ نرم‌افزاری در زندگی خود اعتماد کنید. </li><li class="key">کریپتوکت نمی‌تواند شما را در برابر افراد غیرقابل اعتماد و کی لاگرها محافظت کند و اتصال شما را ناشناس برقرار کند. </li></ul>
+وبلاگ
+سرور سفارشی
+تنظیم مجدد
+نام گفتگو
+نام مستعار
+اتصال
+نام مورد نظر برای گفتگویتان را وارد کنید و آن را با افرادی که می‌خواهید با آنها صحبت کنید، به اشتراک بگذارید، یا به <strong>لابی</strong> رفته و با اشخاص جدید آشنا شوید.
+نام گفتگو را برای شرکت در آن وارد نمایید.
+لطفا برای گفتگو، نام انتخاب نمایید.
+نام گفتگو می‌تواند از حروف و یا فقط اعداد باشد.
+لطفا نام مستعاری وارد کنید.
+نام گفتگو کننده می‌تواند از حروف و یا فقط اعداد باشد.
+این نام مستعار در حال استفاده است.
+تایید هویت با خطا همراه است.
+اتصال با مشکل مواجه شده است.
+از شما بابت استفاده از کریپتوکت متشکریم.
+در حال ثبت‌نام...
+در حال اتصال...
+متصل شد.
+لطفا به کمک صفحه کلید، به صورت تصادفی به مدت چند ثانیه تایپ کنید.
+در حال ایجاد کلیدهای کدگذاری...
+گفتگوی گروهی. برای گفتگوی خصوصی، نام کاربری را کلیک کنید.
+اثر انگشت محرمانه (برای گفتگوهای خصوصی):
+اثر انگشت گفتگوی گروهی:
+تنظیم مجدد کلیدهای رمزگذاری
+تنظیم مجدد کلیدهای رمزگذاری، دسترسی شما را قطع خواهد کرد. اثر انگشت‌های شما نیز تغییر خواهند کرد.
+ادامه
+وضعیت: در دسترس
+وضعیت: در دسترس نیست
+اطلاعات من
+پیغام‌رسان رومیزی فعال
+پیغام‌رسان رومیزی غیرفعال
+پیغام‌رسان صوتی فعال
+پیغام‌رسان صوتی غیرفعال
+نام مستعارم را به یاد داشته باش
+اسم مستعارم را به خاطر نسپار
+خروج
+نمایش اطلاعات
+ارسال فایل کدگذاری شده
+مشاهده عکس
+دانلود فایل
+گفتگو
+تنها فایل‌های فشرده (Zip) و تصویری قایل قبول هستند. حداکثر حجم فایل: (SIZE) MB.
+خطا: مطمئن شوید که فایل شما فشرده(Zip) شده و یا یک عکس است.
+خطا: فایل نمی‌تواند بزرگتر از (SIZE) MB باشد.
+آغاز گفتمان ویدیویی
+پایان گفتگوی ویدیویی
+کاربر (NICKNAME) تمایل دارد که با شما گفتگوی ویدیویی داشته باشد.
+لغو
+نادیده بگیر
+نادیده نگیر
+تایید کنید
+هویت این کاربر را با پرسیدن یک سوال سری تایید کنید. جواب ها باید حتما یکی باشند!
+سوال سری
+جواب سری
+پرسیدن
+در حال پرسش...
+ناموفق
+هویت تایید شد.
+(NICKNAME) میخواهد هویت شما را تایید کند. لطفا سوال سری زیر را برای تایید هویت خود جواب دهید:
+جواب شما باید دقیقا مطابق با جواب ارایه شده (NICKNAME) باشد.
+جواب
+اخطار: شما یک پیام غیرقابل خواندن از (NICKNAME) دریافت کرده‌اید. این پیام ممکن است به عنوان یک کاربر ناشناس شناخته شده باشد و یا پیامی که دریافت آن دچار مشکل شده است. علاوه بر این ممکن است که نسخه کریپتوکت شما به روز نباشد. لطفا از به روز بودن کریپتوکت اطمینان حاصل کنید.
+هشدار: نسخه کریپتوکت شما قدیمی است. ارتقا به نسخه جدید شدیدا پیشنهاد میشود! قبل ازارتقا به جدیدترین نسخه ادامه ندهید.
+اخطار: این پیام ممکن است به (NICKNAME) ارسال نشده باشد.
+تایید شده
+کاربر تایید نشده است.
+برای اطلاعات بیشتر کلیک کنید...
+در مورد تایید بیشتر بدانید
+برای هر بار گفتگو در کریپتوکت، شما باید افراد طرف گفتگو را تایید کنید.
+یک راه تایید این است که از دوستتان یک سوال بپرسید که فقط او جوابش را بداند.
+شما همچنین می‌توانید از راه مسیری مطمئن، مانند تلفن، با آنها تماس گرفته و از آنها بخواهید تا اثر انگشتشان را برایتان بخوانند.
+اثر انگشت‌ها شناسه‌هایی هستند که به شما امکان تایید هویت افراد را می‌دهند. آنها ممکن است در هر یک از مکالمه‌های کریپتوکت تغییر کنند.
+بدون تایید هویت، ممکن است فردی بتواند هویت خود را جعل کرده و یا ارتباط شما را قطع کند.
+اثر انگشتهای این فرد تغییر کرده است. رخ دادن چنین چیزی غیرمنتظره بوده و ممکن است نمایانگر رفتاری مشکوک باشد. لطفاً پیش از صحبت با این فرد، او را تایید کنید.
+گفتگوی گروهی
+فیس‌بوک
+وصل شدن به فیس‌بوک از طریق کریپت‌وکت. برقراری یک ارتباط نقطه به نقطه امن با دوستان فیس‌بوکی که آنها نیز از کریپت‌وکت استفاده می‌کنند.
+گفتگو از طریق فیس‌بوک.
+وضعیت گفتگو
+رمزگذاری شده.
+رمزگذاری نشده.
+این گفتگو رمزگذاری نشده است. و دلیل این هم آن است که این کاربر از کریپت‌وکت استفاده نمی‌کند. از دوست خود بخواهید کریپت‌وکت را دانلود کند تا از گفتگوی رمزگذاری شده لذت ببرد.
\ No newline at end of file
diff --git a/src/core/locale/fr.txt b/src/core/locale/fr.txt
new file mode 100755
index 0000000..025974e
--- /dev/null
+++ b/src/core/locale/fr.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Conversations privées pour tout le monde.
+Bienvenue chez Cryptocat. Voici quelques conseils utiles: <ul><li>Cryptocat n'est pas une solution miracle. Vous ne devriez jamais faire confiance à n'importe quel logiciel dans votre vie.</li><li class="key">Cryptocat ne peut pas vous protéger contre les personnes malhonnêtes ou les enregistreurs de frappe, et ne rend pas votre connexion anonyme.</li></ul>
+Blog
+Serveur dédié
+Réinitialiser
+nom de la conversation
+pseudonyme
+connexion
+Entrez un nom pour votre conversation et envoyez-le aux gens avec qui vous souhaitez parler, ou rejoignez <strong>lobby</strong> pour faire de nouvelles rencontres.
+Saisissez le nom d'une conversation à rejoindre.
+Entrez un nom de conversation s'il vous plaît.
+Nom de conversation doit être des lettres ou des chiffres.
+Entrez un pseudonyme s'il vous plaît.
+Votre pseudonyme doit être des lettres ou des chiffres.
+Ce pseudonyme est déjà utilisé.
+Erreur d'authentification.
+La connexion a échoué.
+Merci d'utiliser Cryptocat.
+Inscription en cours...
+Connexion en cours...
+Connecté.
+Veuillez taper sur votre clavier d'une manière aléatoire pendant quelques secondes.
+Génération des clés de chiffrement...
+Conversation de groupe. Cliquez sur un utilisateur pour commencer une conversation privée.
+Signature OTR (pour les conversations privées):
+Signature de la conversation de groupe:
+Réinitialiser mes clés de chiffrement
+Réinitialiser vos clés de chiffrement va vous déconnecter. Vos signatures seront également modifiées.
+Continuer
+État: Disponible
+État: Absent
+Mes Informations
+Notifications Activées
+Notifications Désactivées
+Notifications Audio Activées
+Notifications Audio Désactivées
+Se souvenir de mon pseudonyme
+Ne pas mémoriser mon pseudonyme
+Déconnexion
+Afficher mes informations
+Envoyer un fichier chiffré
+afficher l'image
+télécharger le fichier
+Conversation
+Seuls les fichiers ZIP et les images sont acceptés. Taille maximale: (SIZE) Mo.
+Erreur: Veuillez vérifier que le fichier soit bien un ZIP ou une image.
+Erreur: Le fichier ne peut pas dépasser (SIZE) Mo.
+Démarrer la visioconférence
+Terminer la visioconférence
+(NICKNAME) voudrait démarrer une visioconférence avec vous.
+Annuler
+Ignorer
+Ne pas ignorer
+Authentifier
+Vérifiez l'identité de l'utilisateur en posant une question secrète. Les réponses doivent correspondre exactement!
+Question secrète
+Réponse secrète
+Demander
+Demande en cours...
+Échec
+Identité vérifiée.
+(NICKNAME) veut vérifier votre identité. Veuillez répondre à la question secrète ci-dessous pour vous authentifier:
+Votre réponse doit correspondre exactement à celle donnée par (NICKNAME).
+Réponse
+Attention: Vous avez reçu un message illisible de (NICKNAME). Cela peut signifier que l'utilisateur n'est pas de confiance ou que certains messages n'ont pas été bien reçus. Il se peut aussi que vous utilisiez une ancienne version de Cryptocat. Veuillez vérifier s'il existe une mise à jour.
+Attention: Votre version de Cryptocat n'est plus à jour. Une mise à jour avec la dernière version est fortement recommandée! N'allez pas plus loin sans avoir mis à jour avec la dernière version.
+Attention: ce message n'a pas pu être transmis à (NICKNAME)
+Authentifié
+L'utilisateur n'est pas authentifié.
+Cliquez pour en savoir plus...
+En savoir plus sur l'authentification
+A chaque fois que vous avez une conversation via Cryptocat, vous devez authentifier les personnes avec lesquelles vous parler.
+Une façon de s'authentifier est de poser une question secrète via Cryptocat à votre ami dont il est le seul à connaître la réponse.
+Vous pouvez aussi les contacter via un canal de communication de confiance, comme par téléphone, et lui demander de lire son fingerprint/son empreinte.
+Les empreintes digitales sont des identifiants qui vous permettent d'authentifier les personnes. Elles peuvent changer entre chaque conversation.
+Sans authentification, quelqu'un pourrait se faire passer pour vous ou intercepter vos communications.
+L'empreinte d'authentification pour ce contact a changée. Ce n'est pas censé se produire et pourrait indiquer un comportement suspect. S'il vous plaît, veuillez authentifier ce contact avant de discuter avec lui.
+Conversation de Groupe
+Facebook
+Connectez-vous à Facebook Chat via Cryptocat. Mettez en place des chats cryptés avec vos amis Facebook qui utilisent Cryptocat aussi.
+Discuter via Facebook
+État de la conversation
+cryptée.
+pas cryptée!
+Cette conversation n'est pas cryptée car ce contact  n'utilise pas Cryptocat. Demandez à votre ami de télécharger Cryptocat pour profiter d'une conversation cryptée.
\ No newline at end of file
diff --git a/src/core/locale/he.txt b/src/core/locale/he.txt
new file mode 100755
index 0000000..ee43c7a
--- /dev/null
+++ b/src/core/locale/he.txt
@@ -0,0 +1,87 @@
+rtl
+"Helvetica Neue", Helvetica, Arial, Verdana
+שיחות חופשיות לכולם.
+ברוכים הבאים לקריפטוקאט. הנה כמה עצות מועילות: <ul><li> קריפטוקאט הוא לא כדור פלא. אתם אף פעם לא צריכים לסמוך על אף פיסת תוכנה עם החיים שלכם. </li><li class="key">קריפטוקאט לא יכול להגן עליכם נגד אנשים לא אמינים או רושמי הקשות, ולא הופך את החיבור שלכם לאנונימי.</li></ul>
+בלוג
+שרת מותאם אישית
+התחל מחדש
+שם השיחה
+כינוי
+התחבר
+הקלידו שם לשיחה ושתפו אותה עם האנשים שברצונכם לדבר איתם
+מלאו שם שיחה על מנת להצטרף
+נא למלא שם שיחה
+שם השיחה יכול להכיל אותיות או מספרים בלבד.
+נא למלא כינוי
+כינוי משתמש יכול להכיל אותיות או מספרים בלבד.
+הכינוי נמצא בשימוש.
+שגיאה בהזדהות.
+חיבור נכשל.
+תודה שהשתמשת בCryptocat.
+נרשם...
+מתחבר...
+מחובר.
+אנא הקלידו בצורה אקראית למשך מספר שניות.
+מייצר מפתחות הצפנה...
+שיחה קבוצתית. לחצו על משתמש לשיחה פרטית.
+חתימת OTR (לשיחות פרטיות):
+חתימת שיחה קבוצתית:
+ייצר מחדש את מפתחות ההצפנה שלי
+ייצור מחדש של מפתחת ההצפנה שלך יגרום לניתוק. החתימות שלך תשתננה בהתאם.
+המשך
+מצב: זמין
+מצב: לא זמין
+הפרטים שלי
+התראות שולחן עבודה פעילות
+התראות שולחן עבודה מבוטלות
+התראות קוליות פעילות
+התראות קוליות מבוטלות
+זכור את שם המשתמש שלי
+איני זוכר/ת את הכינוי שלי
+יציאה
+הצג מידע
+שלח קובץ מוצפן
+פתח תמונה
+הורד קובץ
+שיחה
+רק קבצי ZIP ותמונות מותרים. גודל קובץ מקסימלי: ' (SIZE) MB.
+שגיאה: אנא ודא כי הקובץ הינו מסוג ZIP או תמונה.
+שגיאה: הקובץ אינו יכול להיות גדול מ (SIZE) MB.
+התחל שיחת וידאו
+סיים שיחת וידאו
+(NICKNAME) רוצה להתחיל וידאו צ'אט איתך.
+בטל
+התעלם
+אל תתעלם
+לאמת
+אמתו את זהותו של משתמש זה על ידי שאלת שאלה סודית. תשובות חייבות להתאים במדויק!
+שאלה סודית
+תשובה סודית
+שאל
+שואל...
+נכשל
+זהות אומתה.
+(NICKNAME) רוצה לאמת את זהותכם. אנא ענו על השאלה הסודית למטה כדי לאמת את זהותכם:
+תשובתך חייבת להתאים בדיוק לזאת שניתנה על ידי (NICKNAME).
+תשובה
+אזהרה: התקבלה הודעה שאינה ניתנת לקריאה מ(NICKNAME). הדבר עלול להצביע על משתמש זדוני או הודעות שלא התקבלו. ייתכן גם כי מופעלת גרסה ישנה של Cryptocat. נא לבדוק אם קיימים עדכונים.
+אזהרה: הגרסה שברשותכם של קריפטוקאט אינה בתוקף. עדכון גרסה לגרסה הכי עדכנית מומלץ ביותר! אל תמשיכו בלי לעדכן לגרסה הכי עדכנית.
+אזהרה: לא ניתן לשלוח הודעה זו ל(NICKNAME)
+אומת בהצלחה
+המשתמש לא אומת
+לחץ כדי ללמוד עוד...
+למד עוד אודות תהליך ההזדהות
+בכל שיחת Cryptocat, נדרש לבצע אימות לאדם שאת/ה משוחח/ת איתו
+דרך אחת שניתן לבצע אימות היא ע"י שימוש בCryptocat כדי לשאול את חבר/ה שלך שאלה סודית שרק הם יודעים את התשובה
+ניתן גם לתקשר איתם באמצעי בטוח, כגון טלפון, ולבקש מהם להקריא את קוד האימות שלהם
+קוד האימות הינו מזהה שמאפשר לך לזהות אדם. הקוד יכול להשתנות בין שיחה לשיחה
+ללא אימות, ייתכן שמישהו יתחזה למשתמש או ייירט את השיחה
+קוד האימות של המשתמש השתנה. מצב זה לא אמור לקרות ויכול לרמוז על התנהגות חשודה. נא לבצע אימות למשתמש זה לפני השיחה.
+שיחת צ'אט קבוצתית
+פייסבוק
+התחבר לצ'אט בפייסבוק באמצעות Cryptocat. צור שיחות קצה לקצה מוצפנות עם חברים בפייסבוק שמשתמשים גם הם באפליקציית ה- Cryptocat.
+שוחח בצ'אט באמצעות פייסבוק
+מצב שיחה
+מוצפן.
+לא מוצפן!
+שיחה זו אינה מוצפנת מכיוון שאיש קשר זה אינו משתמש ב- Cryptocat. בקש מחברך להוריד את אפליקציית ה- Cryptocat, כדי להנות משיחה מוצפנת.
\ No newline at end of file
diff --git a/src/core/locale/in.txt b/src/core/locale/in.txt
new file mode 100644
index 0000000..c20395d
--- /dev/null
+++ b/src/core/locale/in.txt
@@ -0,0 +1,79 @@
+ltr
+Microsoft Himalaya, Jomolhari, "Helvetica Neue", Helvetica, Arial, Verdana
+প্রত্যেকের জন্য ব্যক্তিগত কথপোকথন।
+Cryptocat এ আপনাকে স্বাগতম। অাপনার জন্য কিছু উপযোগী টিপ্পনি: <ul> <li> Cryptocat কোন যাদুর ছরী নয়। আপনার কখনই কোনো সফ্টওয়্যারের টুকরোর ওপর আপনার প্রানের বিশ্বাস রাখা উচিত নয়। </li> <li class="key"> Cryptocat অাপনাকে অবিশ্বাসযোগ্য মানুষ ও কী-লগারদের হাথ থেকে রক্ষা করতে বা আপনার সংযোগ বেনামী করতে পারেনা । </li> </ul>
+ব্লগ
+কাস্টম সার্ভার
+রিসেট করুন
+কথোপকথনের নাম
+ডাক নাম
+সংযোগ
+আপনার কথোপকথনের জন্য একটি নাম লিখুন এবং আপনি যে ব্যক্তিদের সাথে কথা বলতে চান তাদের সাথে এটি শেয়ার করুন, অথবা নতুন লোকেদের সঙ্গে দেখা করতে <strong>লবিতে</strong> যোগ দিন!
+যোগ দিতে একটি কথোপকথনের নাম লিখুন।
+একটি কথোপকথনের নাম লিখুন।
+কথোপকথনের নামের আলফানিউমেরিক হওয়া আবশ্যক।
+একটি ডাক নাম লিখুন।
+ডাক নামের আলফানিউমেরিক হওয়া আবশ্যক।
+ডাকনাম ব্যবহার করা হচ্ছে।
+প্রমাণীকরণ করতে বিফল।
+সংযোগ ব্যর্থ হয়েছে।
+Cryptocat ব্যবহার করার জন্য ধন্যবাদ।
+নিবন্ধন করা হচ্ছে ...
+সংযুক্ত করা হচ্ছে ...
+সংযুক্ত।
+অনুগ্রহ করে কয়েক সেকেন্ডের জন্য আপনার কীবোর্ডের উপর যতটা সম্ভব ততটা এলোমেলোভাবে টাইপ করুন।
+এনক্রিপশন কী জেনারেট করা হচ্ছে ...
+গ্রুপ কথোপকথন। প্রাইভেট চ্যাটের জন্য একটি ব্যবহারকারীর ওপর ক্লিক করুন।
+OTR ফিঙ্গারপ্রিন্ট (ব্যক্তিগত কথোপকথন জন্য):
+গ্রুপ কথোপকথনের ফিঙ্গারপ্রিন্ট:
+আমার এনক্রিপশন কি রিসেট করুন
+আপনার এনক্রিপশন কি রিসেট করা হলে আপনার সংযোগ বিচ্ছিন্ন হবে। আপনার ফিঙ্গারপ্রিন্টেও পরিবর্তন হবে।
+অগ্রসর করুন
+স্থিতি: উপলব্ধ
+স্থিতি: অনুপলব্ধ
+আমার তথ্য
+​​ডেস্কটপ বিজ্ঞপ্তিগুলি চালু আছে
+ডেস্কটপ বিজ্ঞপ্তিগুলি বন্ধ আছে
+অডিও সূচনাবার্তা চালু আছে
+অডিও সূচনাবার্তা বন্ধ আছে
+আমার ডাক নাম মনে রাখুন
+আমার ডাক নাম মনে রেখোনা
+লগ আউট করুন
+তথ্য প্রদর্শন করুন
+এনক্রিপ্টেড ফাইলটি পাঠান
+ছবিটি দেখুন
+ফাইল ডাউনলোড করুন
+কথোপকথন
+শুধুমাত্র ZIP ফাইল এবং ইমেজ গ্রহনযোগ্য। সর্বোচ্চ ফাইল সাইজ: (SIZE) মেগাবাইট।
+ত্রুটি: দয়া করে এটা নিশ্চিত করুন যে আপনার ফাইলটি একটি ZIP বা একটি ইমেজ ফাইল।
+ত্রুটি: ফাইলটি (SIZE) কিলোবাইট এর চেয়ে বড় হতে পারে না।
+ভিডিও কথোপকথন শুরু করুন।
+ভিডিও কথোপকথন শেষ করুন।
+(NICKNAME) আপনার সাথে ভিডিও কথোপকথন শুরু করতে চাইছে।
+বাতিল করুন।
+প্রত্যাখ্যান করুন
+অপ্রত্যাখ্যান করুন
+প্রমাণীকরণ
+একটি গোপন প্রশ্ন জিজ্ঞাসা করে এই ব্যবহারকারীর পরিচয় যাচাই করুন. উত্তর হুবহু মেলা আবশ্যক!
+গোপন প্রশ্ন
+গোপন উত্তর
+জিজ্ঞাসা করুন
+জিজ্ঞাসা করা হচ্ছে...
+ব্যর্থ হয়েছে
+পরিচয় সফলভাবে যাচাই করা হয়েছে।
+(NICKNAME) আপনার পরিচয় যাচাই করতে চাইছে। নিজের পরিচয় প্রমাণ করতে নিচের গোপন প্রশ্নের উত্তরটি অনুগ্রহপূর্বক দিন:
+(NICKNAME) দ্বারা নির্দিষ্ট উত্তরটির সাথে আপনার উত্তরটির হুবহু মিল থাকা আবশ্যক।
+উত্তর
+সতর্কবার্তা: আপনি (NICKNAME) থেকে একটি পাঠযোগ্য বার্তা পেয়েছেন. এটি একটি অবিশ্বাসযোগ্য ব্যবহারকারী অথবা বার্তা হতে পারে. আপনি সম্ভবত Cryptocat-এর একটি পুরোনো সংস্করণ চালাচ্ছেন. আপডেটের জন্য চেক করুন.
+সাবধানবাণী: Cryptocat এর এই সংস্করণটি তারিখ সীমার বাইরে। সাম্প্রতিক সংস্করণে আপডেট করার বিশেষ পরামর্শ দেওয়া হচ্ছে! সাম্প্রতিক সংস্করণে আপডেট না করে এগোনো উপদিষ্ট নয়।
+সতর্কতা: এই বার্তা (NICKNAME)-কে পাঠানো যাবে না
+অনুমোদন যাচাই সফল
+ব্যবহারকারীর অনুমোদন যাচাই করা হয়নি.
+আরো জানতে ক্লিক করুন ...
+প্রমাণীকরণ সম্পর্কে আরও জানুন
+Cryptocat-এ কথোপকথন করার প্রতিটি সময়, অন্য ব্যক্তির প্রমাণীকরণ করা আপনার প্রয়োজন.
+আপনি প্রমাণীকরণ করতে পারেন আপনার বন্ধুকে একটি গোপন প্রশ্ন করে যার উত্তর শুধুমাত্র তার জানা আছে.
+আপনি একটি বিশ্বস্ত চ্যানেলের মাধ্যমে তাদের সঙ্গে যোগাযোগ, এবং তাদের ফিঙ্গারপ্রিন্ট পড়তে তাদের অনুরোধ করতে পারেন.
+ফিঙ্গারপ্রিন্ট দ্বারা ব্যক্তি প্রমাণীকরণ করতে পারেন কিন্তু সেগুলি প্রতি Cryptocat কথোপকথন মধ্যে পরিবর্তন হতে পারে.
+অনুমোদন এবং প্রমাণীকরণ ছাড়া, কেউ ভান করতে অথবা আপনার যোগাযোগ আটকাতে পারে.
+এই ব্যক্তির প্রমাণীকরণ ফিঙ্গারপ্রিন্ট পরিবর্তিত হয়েছে. এইটি সন্দেহজনক আচরণের নির্দেশ হতে পারে. এই ব্যক্তির সাথে চ্যাট করার আগে এই পরিচিতি অনুমোদন করুন.
\ No newline at end of file
diff --git a/src/core/locale/it.txt b/src/core/locale/it.txt
new file mode 100755
index 0000000..6ce03ad
--- /dev/null
+++ b/src/core/locale/it.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Conversazioni Private per Tutti.
+Benvenuto su Cryptocat. Ecco alcuni consigli: <ul><li>Cryptocat non è un proiettile magico. Non dovresti mai fidarti di un programma quando è in gioco la tua vita.</li><li class="key">Cryptocat non può proteggerti da persone inaffidabili o keylogger, e non ti rende anonima la tua connessione.</li></ul>
+Blog
+Server personalizzato
+Reset
+nome della conversazione
+nickname
+connetti
+Inserisci un nome per la tua conversazione e condividilo con le persone con cui vuoi comunicare o entra su <strong>lobby</strong> per scoprire nuove persone.
+Inserisci il nome di una conversazione a cui partecipare
+Inserisci un nome per la conversazione.
+Il nome della conversazione può contenere solo lettere o numeri.
+Inserisci un nickname.
+Il Nickname (soprannome) può contenere solo lettere o numeri.
+Nickname già utilizzato
+Autenticazione non riuscita.
+Connessione non riuscita.
+Grazie per aver scelto Cryptocat.
+Registrazione in corso...
+Connessione in corso...
+Connesso.
+Per favore digita dei caratteri sulla tua tastiera nel modo più casuale possibile per alcuni secondi.
+Generazione in corso delle chiavi di cifratura...
+Conversazione di gruppo. Clicca su un utente per iniziare una chat privata.
+Fingerprint OTR (per conversazioni private):
+Fingerprint della conversazione di gruppo:
+Resetta le mie chiavi di cifratura
+Il reset delle tue chiavi di cifratura comporterà la tua disconnessione. Inoltre anche le tue fingerprint cambieranno.
+Continua
+Stato: Disponibile
+Stato: Non al computer
+Mie Info
+Notifiche Desktop Attivate
+Notifiche Desktop Disattivate
+Notifiche Audio Attivate
+Notifiche Audio Disattivate
+Ricorda il mio nickname
+Non ricordare il mio nickname
+Disconnettiti
+Mostra Info
+Invia un file criptato
+vedi immagine
+scarica file
+Conversazione
+Solo i ZIP file e immagini sono accettati. Dimensione massima di un file: (SIZE) MB.
+Errore: per favore assicurati che sia un file ZIP o un'immagine.
+Errore: Il file non può essere più grande di (SIZE) MB.
+Inizia una videoconferenza
+Termina la videoconferenza
+(NICKNAME) vorrebbe avviare una chat video con te.
+Annulla
+Ignora
+Non ignorare
+Autenticati
+Verifica l'identità dell'utente con una domanda segreta. Le risposte devono corrispondere esattamente.
+Domanda segreta
+Risposta segreta
+Chiedi
+Richiesta in corso...
+Operazione non riuscita
+Identità verificata.
+(NICKNAME) vuole verificare la tua identità. Rispondi alla domanda segreta qui sotto per autenticarti:
+La tua risposta deve corrispondere esattamente a quella fornita da (NICKNAME).
+Rispondi
+Attenzione: Hai ricevuto un messaggio illeggibile da (NICKNAME). Questo potrebbe indicare un utente non fidato o messaggi che non sono stati ricevuti. É anche possibile che tu stia usando una vecchia versione di Cryptocat. Si prega di verificare la disponibilità di aggiornamenti.
+Attenzione: La tua versione di Cryptocat non è aggiornata. L'aggiornamento alla versione più recente è fortemente consigliato! Non procedere senza aggiornare alla versione più recente.
+Attenzione: non è stato possibile inviare il messaggio a (NICKNAME)
+Autentificato
+Utente non autenticato.
+Clicca per saperne di più
+Scopri di più sull'autenticazione
+Ogni volta che hai una conversazione con Cryptocat, devi autenticare la persona con cui stai conversando.
+Un modo per autenticare è usare Cryptocat per chiedere ad un tuo amico una domanda segreta al quale solo lui conosce la risposta.
+Puoi anche contattarli attraverso un canale fidato, per esempio via telefono, e chiedergli di leggereti la loro fingerprints.
+Le fingerprints sono identificatori che ti permettono di autenticare persone. Possono cambiare tra diverse conversazioni in Cryptocat.
+Senza autenticazione, qualcuno potrebbe impersonarsi o intercettare le tue conversazioni.
+Le fingerprints d'autenticazione per questa conversazione sono cambiate. Questo non dovrebbe accadere e potrebbe indicarne un comportamento sospetto. SI prega di autenticare questo contatto prima di chattare con lui.
+Chat di gruppo
+Facebook
+Collegati alla Chat di Facebook tramite Cryptocat. Imposta e configura chat criptate End-to-End con amici di Facebook che stanno anch'essi usando Cryptocat.
+Chat via Facebook
+Stato della conversazione
+criptato/crittografato.
+non criptato/crittografato!
+Questa conversazione non è criptata perché questo contatto non sta usando Cryptocat. Chiedi al tuo amico di scaricare Cryptocat per usufruire della chat criptata.
\ No newline at end of file
diff --git a/src/core/locale/ja.txt b/src/core/locale/ja.txt
new file mode 100644
index 0000000..8e87048
--- /dev/null
+++ b/src/core/locale/ja.txt
@@ -0,0 +1,87 @@
+ltr
+"Microsoft Yahei", "微软雅黑", Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif, STXihei, "华文细黑"
+プライベートな会話を万人に
+Cryptocatへようこそ。まずは以下の点についてご確認ください。<ul><li>Cryptocatは万能ではありません。このソフトウェアにあなたの生命を託すようなことは決して行わないでください。</li><li class="key">Cryptocatは信頼できない人々やキーロガーからあなたを守ることはできません。また、あなたの接続状況を匿名化することもできません。</li></ul>
+ブログ
+カスタムサーバーを生成
+リセット
+会話名
+ニックネーム
+接続する
+会話に名前をつけて、その名前を会話したい相手に伝えてください。または<strong>ロビー</strong>に参加して、新しい人たちと会話してみましょう。
+アクセスするために会話名を入力してください。
+会話名を入力してください。
+会話名にはアルファベットまたは数字のみ使用出来ます。
+ニックネームを入力してください。
+ニックネームにはアルファベットまたは数字のみ使用できます。
+ニックネームは利用されています。
+認証は失敗しました。
+接続出来ませんでした。
+Cryptocatを使用して頂き、ありがとうございます。
+登録しています…
+接続しています…
+接続完了
+キーボードで適当に数秒間に打ってください。
+暗号化キーを生成しています…
+グループ会話。ユーザーをクリックするとプライベートチャットができます
+オフレコ指紋(プライベートチャット用):
+グループ会話の指紋:
+暗号化キーをリセット
+暗号化キーをリセットすると、接続は切断され、指紋も変換されます。
+続ける
+在席中
+離席中
+私の情報
+デスクトップ通知:オン
+デスクトップ通知:オフ
+音声通知:オン
+音声通知:オフ
+ニックネームを記憶
+ニックネームを記憶しない
+ログアウト
+データを表示
+暗号化ファイルを送信
+画像を表示
+ファイルをダウンロード
+会話
+ZIP ファイルと画像のみ受け付けます。最大ファイルサイズ:  (SIZE) MB.
+エラー: ファイルがZIPファイルまたは画像であることを確認してください。
+エラー:ファイルは(SIZE) MB 以下にしてください。
+ビデオチャットを開始
+ビデオチャットを終了
+(NICKNAME) はあなたとのビデオチャットの開始を希望しています。
+キャンセル
+無視
+無視を解除
+認証
+秘密の質問をすることで、このユーザーのIDを認証します。回答は正確に一致していなければなりません。
+秘密の質問
+秘密の質問の回答
+質問
+質問しています...
+失敗しました
+IDは認証されました。
+(NICKNAME) はあなたのIDの認証を希望しています。認証のために以下の秘密の質問にお答えください。
+あなたの回答は (NICKNAME) に提示されたものと正確に一致していなければなりません。
+回答
+警告:(NICKNAME)から読み取り不可能なメッセージを受信しました。これは信頼できないユーザー、もしくは受信に失敗したメッセージを示唆しているかもしれません。もしくはCryptcatのバージョンが古くなっているかもしれません。アップデートをご確認ください。
+警告:現在お使いのCryptcatは古いバージョンです。最新版への更新を強く推奨します!最新版に更新しないまま、これより先には進まないでください。
+警告: このメッセージは (ニックネーム) に送ることができません
+認証されました
+ユーザーを認証出来ません
+詳細はこちらをクリック...
+本人確認についてもっと詳しく
+Cryptocatで会話をするときは、毎回、相手の本人確認をする必要があります。
+Cryptocatを使って、その人しにか分からない問題を問いかけるのが認証する方法の一つです。
+電話を使ったり相手に指紋の読み取りをお願いするなどして、信用出来る経路を確保できます。
+指紋は人の本人確認をするための鑑定ツールです。指紋はCryptocatの会話中でなければいつでも変更できます。
+本人確認をしないと、誰かが違う誰かになりきったり、あなたのコミュニケーションを妨害することが出来てしまいます。
+この連絡先の指紋認証は変更されています。これは起こるはずのないことであり、不審な行為である疑いがあります。この連絡先とチャットを始める前に相手の本人確認をしてください。
+グループチャット
+フェイスブック
+Cryptocatを使ってフェイスブック・チャットができます。Cryptocatを使っているフェイスブックの友達と、端から端まで暗号化されたチャットで話す設定ができます。
+フェイスブックからのチャット
+会話状態
+暗号化されました。
+暗号化されていません!
+この会話はこの連絡先がCryptocatを使用していないため暗号化されません。
\ No newline at end of file
diff --git a/src/core/locale/km.txt b/src/core/locale/km.txt
new file mode 100644
index 0000000..deb09e4
--- /dev/null
+++ b/src/core/locale/km.txt
@@ -0,0 +1,79 @@
+ltr
+Microsoft Himalaya, Jomolhari, "Helvetica Neue", Helvetica, Arial, Verdana
+ការ​សន្ទនា​ឯកជន​សម្រាប់​ទាំង​អស់​គ្នា។
+សូម​ស្វាគមន៍​មក​កាន់ Cryptocat។ នេះ​ជា​ជំនួយ​មាន​ប្រយោជន៍​មួយ​ចំនួន៖ <ul><li>Cryptocat មិនមែនជា​កម្មវិធី​មន្តអាគមន៍​ទេ។ អ្នក​មិន​គួរ​ទុក​ចិត្ត​ផ្នែក​ណាមួយ​នៃ​កម្មវិធី​ក្នុង​ជីវិត​របស់​អ្នក​ទេ។</li><li class="key">Cryptocat មិន​អាច​ការ​ពារ​អ្នក​ពី​មនុស្ស​ដែលមិន​ទុក​ចិត្ត​ ឬ​អ្នក​លួច​ពាក្យ​សម្ងាត់ និងមិន​ធ្វើ​ឲ្យ​ការ​តភ្ជាប់​របស់​អ្នក​អនាមិក​ទេ។</li></ul>
+ប្លុក
+ម៉ាស៊ី​ន​មេ​ផ្ទាល់ខ្លួន
+កំណត់​ឡើង​វិញ
+ឈ្មោះ​សន្ទនា
+សម្មតិនាម
+ភ្ជាប់
+បញ្ចូល​ឈ្មោះ​ការ​សន្ទនា និង​ចែករំលែក​ជា​មួយ​មនុស្ស​ដែល​អ្នក​ចង់​និយាយ ឬ​ចូលរួម <strong>បញ្ចុះបញ្ចូល</strong> ដើម្បី​ជួប​មនុស្ស​ថ្មី!
+បញ្ចូល​ឈ្មោះ​ការ​សន្ទនា​ត្រូវ​ចូលរួម។
+សូម​បញ្ចូល​ឈ្មោះ​ការ​សន្ទនា។
+ឈ្មោះ​ការ​សន្ទនា​ត្រូវ​តែ​ជា​អក្សរ​លាយ​ជា​មួយ​លេខ។
+សូម​​បញ្ចូល​សម្មតិនាម។
+សម្មតិនាម​ត្រូវ​តែ​ជា​អក្សរ​លាយលេខ។
+សម្មតិនាម​កំពុង​ប្រើ។
+ការ​ផ្ទៀងផ្ទាត់​បរាជ័យ។
+កា​រ​ភ្ជាប់​បាន​បរាជ័យ។
+អរគុណ​ដែល​បាន​ប្រើ Cryptocat ។
+កំពុង​ចុះឈ្មោះ...
+កំពុង​ភ្ជាប់...
+ភ្ជាប់​ហើយ។
+សូម​វាយ​បញ្ចូល​តាម​ក្ដារចុច​របស់​ដោយ​ចៃដន្យ​តាម​ដែល​អាច​ធ្វើ​បាន​រយៈ​ពេល​ពីរ ឬ​បី​វិនាទី។
+កំពុង​បង្កើត​សោ​ដាក់លេខ​កូដ...
+សន្ទនា​ជា​ក្រុម។ ចុច​អ្នកប្រើ​ដើម្បី​ជជែក​ជា​លក្ខណៈ​​ឯកជន។
+ស្នាម​ម្រាមដៃ OTR (សម្រាប់​ការ​សន្ទនា​លក្ខណៈ​ឯកជន)៖
+ស្នាម​ម្រាមដៃ​ការ​សន្ទនា​ជា​ក្រុម៖
+កំណត់​​សោ​ដាក់លេខ​កូដ​របស់ខ្ញុំ​ឡើងវិញ
+ការ​កំណត់​សោ​ដាក់លេខ​កូដ​របស់​អ្នក​ឡើង​វិញ​នឹង​ផ្ដាច់​អ្នក។ ស្នាម​ម្រាមដៃ​របស់​អ្នក​នឹង​ប្ដូរ​ផង​ដែរ។
+បន្ត
+ស្ថាន​ភាព៖ ទំនេរ
+ស្ថាន​ភាព៖ ទៅ​​ឆ្ងាយ
+ព័ត៌មាន​អំពី​ខ្ញុំ
+បើក​ការ​ជួន​ដំណឹង​លើ​​ផ្ទៃតុ។
+បិទ​ការ​ជួន​ដំណឹង​លើ​​ផ្ទៃតុ។
+បើក​សំឡេង​ជូន​ដំណឹង។
+បិទ​សំឡេង​ជូន​ដំណឹង
+ចងចាំ​សម្មតិនាម
+កុំ​ចងចាំ​សម្មតិនាម
+ចេញ
+បង្ហាញ​ព័ត៌មាន
+ផ្ញើ​ឯកសារ​បានដាក់លេខ​កូដ
+មើល​រូបភាព
+ទាញ​យក​ឯកសារ
+ការ​សន្ទនា
+មានតែឯកសារហ្ស៊ីបនិងរូបភាពប៉ុណ្ណោះត្រូវបានទទួល។ ទំហំឯកសារអតិបរមា: (SIZE) MB.
+កំហុស៖ សូម​ប្រាកដ​ថា​ឯកសារ​របស់​អ្នក​ជា​ឯកសារ ZIP ឬ​​ជា​រូបភាព។
+កំហុស:ឯកសារមិនអាចធំជាង (SIZE) MB.
+ចាប់ផ្ដើម​ការ​ជជែក​ជា​វីដេអូ
+បញ្ចប់​ការ​ជជែក​ជា​វីដេអូ
+(NICKNAME) ចង់​ចាប់ផ្ដើម​ជជែក​ជា​វីដេអូ​ជា​មួយ​អ្នក។
+បោះបង់
+មិនអើពើ
+អើពើ
+ផ្ទៀងផ្ទាត់
+ផ្ទៀងផ្ទាត់​អត្តសញ្ញាណ​របស់​អ្នក​ប្រើ​នេះ​ ដោយ​សួរ​សំណួរ​សម្ងាត់។ ចម្លើយ​ត្រូវ​តែ​ដូច​គ្នា​!
+សំណួរ​សម្ងាត់
+ចម្លើយ​សម្ងាត់
+សួរ
+កំពុង​សួរ...
+បរាជ័យ
+បាន​ផ្ទៀផ្ទាត់​អត្តសញ្ញាណ
+(NICKNAME) ចង់​ផ្ទៀងផ្ទាត់​អត្តសញ្ញាណ​របស់​អញនក។ សូម​ឆ្លើយ​សំណួរ​សម្ងាត់​ខាង​ក្រោម ដើម្បី​ផ្ទៀងផ្ទាត់​ខ្លួន​អ្នក​ផ្ទាល់៖
+ចម្លើយ​របស់​អ្នកត្រូវ​តែ​​ដូច​គ្នា​នឹង​ចម្លើយ​បាន​ផ្ដល់​ឲ្យ​ដោយ (NICKNAME) ។
+ឆ្លើយតប
+ការ​ព្រមាន​៖ អ្នកបានទទួល​សារ​មិន​ទាន់អាន​ពី (NICKNAME) ។ វា​អាច​បង្ហាញ​ថា​អ្នកប្រើ ឬ​សារ​ដែល​មិន​ទុក​ចិត្ត​​​បាន​បរាជ័យ​ក្នុងការ​ទទួល។ អ្នកក៏​អាច​ដំណើរការ​កំណែ Cryptocat ចាស់ៗ។ សូម​ពិនិត្យ​មើលបច្ចុប្បន្នភាព។
+ព្រមាន៖ កំណែ​ Cryptocat របស់​ហួស​សម័យ​ហើយ។ បាន​ផ្ដល់​អនុសាសន៍​ឲ្យ​ធ្វើ​បច្ចុប្បន្នភាព​ទៅ​កំណែ​ថ្មី​បំផុត​! កុំ​បន្ត​ដោយ​មិន​ធ្វើ​បច្ចុប្បន្នភាព​ទៅ​កំណែ​ថ្មី​បំផុត។
+ការ​ព្រមាន៖ សារ​នេះ​មិន​អាច​ត្រូវ​បាន​ផ្ញើ​ទៅ (NICKNAME)
+បាន​ផ្ទៀងផ្ទាត់
+អ្នកប្រើ​មិន​ត្រូវ​បាន​ផ្ទៀងផ្ទាត់។
+ចុច​ដើម្បី​ស្វែង​យល់​បន្ថែម...
+ស្វែងយល់​បន្ថែម​អំពី​ការ​ផ្ទៀងផ្ទាត់
+រាល់​ពេល​ដែល​អ្នក​មាន​ការ​សន្ទនា Cryptocat អ្នក​ត្រូវ​ផ្ទៀងផ្ទាត់​មនុស្ស​ដែលអ្នក​កំពុង​និយាយជា​មួួយ។
+នៅ​តាម​វិធីដែល​អ្នក​អាច​ផ្ទៀងផ្ទាត់​គឺ​ដោយ​ប្រើ​ Cryptocat ដើម្បី​​សួរ​សំណួរ​សុវត្ថិភាព​​មិត្តភក្ដិ​​ដែល​ពួកគេ​អាច​ដឹង​ចម្លើយ។
+អ្នក​ក៏​អាច​ទាក់ទង​ពួកគេ​តាមរយៈ​មធ្យោបាយ​ដែល​ទុចិត្ត ដូចជា​តាម​ទូរស័ព្ទ និងស្នើ​ឲ្យ​ពួកគេ​អាន​ឯកសារ​របស់​ពួកគេ។
+ឯកសារ​​គឺ​ជា​គ្រឿង​សម្គាល់​ដែល​អនុញ្ញាត​ឲ្យ​អ្នក​ផ្ទៀងផ្ទាត់​មនុស្ស។ ពួកគេ​អាច​ប្ដូរ​រវាង​រាល់​ការ​សន្ទនា​តាម Cryptocat ។
+ដោយ​គ្មាន​ការ​ផ្ទៀងផ្ទាត់ អ្នក​ណា​ម្នាក់​អាច​ក្លែង​ ឬ​​រំខាន​ការ​ប្រាស្រ័យ​ទាក់ទង​របស់​អ្នក។
+ឯកសារ​ផ្ទៀងផ្ទាត់​សម្រាប់​ទំនាក់ទំនង​នេះ​បាន​ប្ដូរ។ វា​មិន​ត្រូវ​បាន​ស្នើ​ឲ្យ​មាន​ និង​អាច​បង្ហាញ​ឥរិយាបថ​សង្ស័យ។ សូម​ផ្ទៀងផ្ទាត់​ទំនាក់ទំនង​នេះ​មុន​នឹង​ជជែកជា​មួយ​ពួកគេ។
\ No newline at end of file
diff --git a/src/core/locale/kn.txt b/src/core/locale/kn.txt
new file mode 100644
index 0000000..fbd3c89
--- /dev/null
+++ b/src/core/locale/kn.txt
@@ -0,0 +1,69 @@
+ltr
+Microsoft Himalaya, Jomolhari, "Helvetica Neue", Helvetica, Arial, Verdana
+ಸರ್ವರಿಗೂ ಖಾಸಗಿ ಸಂವಾದಗಳು.
+ಕ್ರಿಪ್ತೊಕ್ಯಾಟ್ ಗೆ  ಸುಸ್ವಾಗತ. ಕೆಲವೊಂದು ಉಪಯುಕ್ತ ಸಲಹೆಗಳು: <ul><li>ಕ್ರಿಪ್ತೊಕ್ಯಾಟ್ ಒಂದು ಮ್ಯಾಜಿಕ್ ಬುಲೆಟ್ ಅಲ್ಲ. ನೀವು ಜೀವನವನ್ನು ಯಾವುದೇ ಸಾಫ್ಟ್ವೇರ್ ಮೇಲೆ ನಂಬಿಕೆಯಿಟ್ಟು ನಡೆಸಬೇಡಿ.</li><li class="key">ಕ್ರಿಪ್ತೊಕ್ಯಾಟ್ ಗೆ ನಂಬಿಕೆಗೆ ಯೋಗ್ಯರಲ್ಲದ ಜನರಿಂದ ಅಥವಾ ಕೀ ಲಾಗರ್ಸ್ ವಿರುದ್ಧ ನಿಮ್ಮನ್ನು ರಕ್ಷಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ, ಮತ್ತು ನಿಮ್ಮ ಸಂಪರ್ಕ ಅನಾಮಧೇಯವಲ್ಲ.</li></ul>
+ಬ್ಲಾಗ್
+ಇಚ್ಛೆಯ ಸರ್ವರ್
+ಮರುಹೊಂದಿಸು
+ಸಂವಾದದ ಹೆಸರು
+ಉಪನಾಮ
+ಸಂಪರ್ಕಿಸು
+ನಿಮ್ಮ ಸಂಭಾಷಣೆಗೆ ಒಂದು ಹೆಸರನ್ನು ನಮೂದಿಸಿ ಮತ್ತು ನೀವು ಮಾತನಾಡಲು ಇಷ್ಟಪಡುವ ಜನರೊಂದಿಗೆ ಇದನ್ನು ಹಂಚಿಕೊಳ್ಳಿ, ಅಥವಾ <strong> ಮೊಗಸಾಲೆಯಲ್ಲಿ </strong> ಹೊಸ ಜನರನ್ನು ಭೇಟಿಯಾಗಿ!
+ಸೇರಲು ಒಂದು ಸಂಭಾಷಣೆಯ ಹೆಸರನ್ನು ನಮೂದಿಸಿ.
+ಒಂದು ಸಂವಾದದ  ಹೆಸರು ನಮೂದಿಸಿ.
+ಸಂವಾದದ ಹೆಸರು ಅಕ್ಷರಸಂಖ್ಯಾಯುಕ್ತವಾಗಿರಬೇಕು.
+ಒಂದು ಉಪನಾಮ ನಮೂದಿಸಿ.
+ಉಪನಾಮ ಅಕ್ಷರಸಂಖ್ಯಾಯುಕ್ತವಾಗಿರಬೇಕು.
+ಉಪನಾಮ ಬಳಕೆಯಲ್ಲಿದೆ.
+ದೃಢೀಕರಣವು ವಿಫಲಗೊಂಡಿದೆ.
+ಸಂಪರ್ಕ ವಿಫಲವಾಗಿದೆ
+ಕ್ರಿಪ್ಟೋಕ್ಯಾಟ್ ಬಳಸಿದ್ದಕ್ಕೆ ಧನ್ಯವಾದಗಳು.
+ನೋಂದಣಿ ಮಾಡಲಾಗುತ್ತಿದೆ ...
+ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ...
+ಸಂಪರ್ಕಿತಗೊಂಡಿದೆ.
+ಕೆಲವು ಸೆಕೆಂಡುಗಳ ಕಾಲ ಸಾಧ್ಯವಾದಷ್ಟು ಯಾದೃಚ್ಛಿಕವಾಗಿ ನಿಮ್ಮ ಕೀಲಿಮಣೆಯಲ್ಲಿ ಟೈಪ್ ಮಾಡಿ.
+ಗೂಢಲಿಪೀಕರಣ ಕೀಲಿಗಳನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ ...
+ಗುಂಪು ಸಂವಾದ. ಖಾಸಗಿ ಸಂವಾದಕ್ಕೆ ಒಬ್ಬ ಬಳಕೆದಾರನ ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ.
+ಓ. ಟಿ. ಅರ್ ಬೆರಳಗುರುತು (ಖಾಸಗಿ ಸಂವಾದಕ್ಕಾಗಿ):
+ಗುಂಪು ಸಂವಾದದ ಬೆರಳಗುರುತು:
+ನನ್ನ ಗೂಢಲಿಪೀಕರಣ ಕೀಲಿಗಳನ್ನು ಮರುಹೊಂದಿಸಿ
+ನಿಮ್ಮ ಗೂಢಲಿಪೀಕರಣ ಕೀಲಿಗಳನ್ನು ಮರುಹೊಂದಿಸುವುದರಿಂದ ನಿಮ್ಮ ಸಂಪರ್ಕ ಕಡಿದು ಹೋಗುತ್ತದೆ.  ನಿಮ್ಮ ಬೆರಳ ಗುರುತುಗಳು ಸಹ ಬದಲಾಗುತ್ತದೆ.
+ಮುಂದುವರಿಸು
+ಸ್ಥಿತಿ: ಲಭ್ಯ 
+ಸ್ಥಿತಿ: ಹೊರಗೆ
+ನನ್ನ ಮಾಹಿತಿ
+ಗಣಕತೆರೆ ಸೂಚನೆಯನ್ನು ಶಕ್ತಗೊಳಿಸು
+ಗಣಕತೆರೆ ಸೂಚನೆಯನ್ನು ಅಶಕ್ತಗೊಳಿಸು
+ಶ್ರಾವ್ಯ ಸೂಚನೆಯನ್ನು ಶಕ್ತಗೊಳಿಸು
+ಶ್ರಾವ್ಯ ಸೂಚನೆಯನ್ನು ಅಶಕ್ತಗೊಳಿಸು
+ನನ್ನ ಉಪನಾಮ ನೆನಪಿನಲ್ಲಿಡಿ
+ನನ್ನ ಉಪನಾಮವನ್ನು ನೆನಪಿಡಬೇಡ 
+ಲಾಗ್ಔಟ್
+ಮಾಹಿತಿಯನ್ನು ಪ್ರದರ್ಶಿಸಿ
+ಗೂಢಲಿಪೀಕರಣಗೊಂಡ ಕಡತವನ್ನು ಕಳುಹಿಸಿ
+ಚಿತ್ರವನ್ನು ವೀಕ್ಷಿಸಿ
+ಕಡತವನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ
+ಸಂವಾದ
+ಜಿಪ್ ಕಡತಗಳನ್ನು ಮತ್ತು ಚಿತ್ರಗಳನ್ನು ಮಾತ್ರ ಸ್ವೀಕರಿಸಲಾಗುತ್ತದೆ. ಗರಿಷ್ಠ ಫೈಲ್ ಗಾತ್ರ: (SIZE) ಎಂ. ಬಿ.
+ದೋಷ: ನಿಮ್ಮ ಕಡತ ಜಿಪ್ ಫೈಲ್ ಅಥವಾ ಒಂದು ಚಿತ್ರಿಕೆ ಎಂಬುದನ್ನು ದಯವಿಟ್ಟು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ.
+ದೋಷ: ಕಡತದ ಗಾತ್ರ (SIZE) ಎಂ. ಬಿ. ಗಿಂತ ಹೆಚ್ಚಿಗೆ ಇರುವಂತಿಲ್ಲ.
+ದೃಶ್ಯ ಸಂವಾದವನ್ನು ಪ್ರಾರಂಭಿಸಿ
+ದೃಶ್ಯ ಸಂವಾದವನ್ನು ಕೊನೆಗೊಳಿಸಿ
+(NICKNAME) ನಿಮ್ಮೊಂದಿಗೆ ದೃಶ್ಯ ಸಂವಾದವನ್ನು ಪ್ರಾರಂಭಿಸಲು ಬಯಸುತ್ತಾರೆ.
+ರದ್ದುಗೊಳಿಸಿ
+ನಿರ್ಲಕ್ಷಿಸು
+ನಿರ್ಲಕ್ಷಿಸಬೇಡಿ
+ದೃಢೀಕರಿಸಿ
+ಒಂದು ರಹಸ್ಯ ಪ್ರಶ್ನೆ ಕೇಳುವ ಮೂಲಕ ಈ ಬಳಕೆದಾರರ ಗುರುತನ್ನು ಪರಿಶೀಲಿಸಿ. ಉತ್ತರ ನಿಖರವಾಗಿ ಹೊಂದಿಕೆಯಾಗಬೇಕು!
+ರಹಸ್ಯ ಪ್ರಶ್ನೆ
+ರಹಸ್ಯವಾದ ಉತ್ತರ
+ಕೇಳಿ
+ಕೇಳುತ್ತಿದ್ದೇವೆ...
+ವಿಫಲವಾಗಿದೆ
+ಗುರುತನ್ನು ಪರಿಶೀಲಿಸಲಾಗಿದೆ.
+(NICKNAME) ನಿಮ್ಮ ಗುರುತನ್ನು ಪರಿಶೀಲಿಸಲು ಬಯಸುತ್ತಾರೆ. ನಿಮ್ಮನ್ನು ಪ್ರಮಾಣೀಕರಿಸಲು ಕೆಳಗಿನ ರಹಸ್ಯವಾದ ಪ್ರಶ್ನೆಗೆ ಉತ್ತರ ನಮೂದಿಸಿ:
+ನಿಮ್ಮ ಉತ್ತರ  (NICKNAME) ನೀಡಲ್ಪಟ್ಟ ಉತ್ತರಕ್ಕೆ ನಿಖರವಾಗಿ ಹೊಂದಿಕೆಯಾಗಬೇಕು.
+ಉತ್ತರ
+ಎಚ್ಚರಿಕೆ: ನೀವು ಒಂದು ಓದಲಾಗದಂತಹ ಸಂದೇಶವನ್ನು (NICKNAME)ನಿಂದ ಸ್ವೀಕರಿಸಿದ್ದಿರಿ. ಇದು ಬಳಕೆದಾರರು ನಂಬಿಕೆಗೆ ಯೋಗ್ಯವಲ್ಲದ್ದನ್ನು ಸೂಚಿಸಬಹುದು ಅಥವಾ ಈ ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲು ವಿಫಲಗೊಂಡಿದ್ದೇವೆ.   
+ಎಚ್ಚರಿಕೆ:  ನಿಮ್ಮ ಕ್ರಿಪ್ತೊಕ್ಯಾಟ್ ಆವೃತ್ತಿ ಹಳೆಯದಾಗಿದೆ. ಇತ್ತೀಚಿನ ಆವೃತ್ತಿಗೆ ನವೀಕರಿಸುವುದನ್ನು ದೃಢವಾಗಿ ಸೂಚಿಸಲಾಗಿದೆ!
+ ಇತ್ತೀಚಿನ ಆವೃತ್ತಿಗೆ ನವೀಕರಿಸುವ ಮುನ್ನ ಮುಂದುವರಿಯಬೇಡಿ.
\ No newline at end of file
diff --git a/src/core/locale/ko.txt b/src/core/locale/ko.txt
new file mode 100644
index 0000000..d0eb20f
--- /dev/null
+++ b/src/core/locale/ko.txt
@@ -0,0 +1,87 @@
+ltr
+"Microsoft Yahei", "微软雅黑", Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif, STXihei, "华文细黑"
+모든 사람을위한 개인 대화.
+Cryptocat에 오신 것을 환영합니다. 여기 유용한 팁들이 있습니다: <ul><li>Crytocat은 마법의 탄환이 아닙니다. 절대 어떤 소프트웨어도 전적으로 신뢰해서는 안됩니다. </li><li class="key">Cryptocat은 신뢰할 수 없는 사람 또는 키로거로부터 당신을 보호할 수 없으며, 당신의 연결상태를 익명화하지 않습니다.</li></ul>
+블로그
+사용자 지정 서버
+재설정
+대화 이름
+별명
+연결
+당신의 대화의 이름을 입력하고 얘기하고 싶어요 사람들과 공유 할 수 있습니다.
+가입 대화의 이름을 입력합니다.
+대화 이름을 입력하십시오.
+대화명은 문자 혹은 숫자에 한정됩니다.
+별명을 입력하십시오.
+별명은 문자 혹은 숫자에 한정됩니다.
+사용 닉네임.
+인증에 실패했습니다.
+연결에 실패했습니다.
+Cryptocat를 이용해 주셔서 감사합니다.
+등록 중 ...
+연결 중 ...
+연결.
+몇 초 동안 무작위로 가능한 키보드에서 입력하십시오.
+암호화 키를 생성하는 중 ...
+그룹 대화. 개인 채팅에 대한 사용자를 클릭합니다.
+OTR 지문 (개인 대화 용) :
+그룹 대화 지문 :
+내 암호화 키를 재설정
+귀하의 암호화 키를 재설정으로 연결을 끊을 것입니다. 지문도 변경됩니다.
+계속
+상태 : 사용 가능
+상태 : 진행중
+내 정보
+바탕 화면 알림 켜기
+바탕 화면 알림 끄기
+오디오 알림
+오디오 알림 끄기
+내 닉네임을 기억
+내 닉네임을 기억하지 마
+로그 아웃
+정보를 표시
+암호화 된 파일 보내기
+이미지를 보려면
+파일을 다운로드
+대화
+ZIP 파일과 이미지만 허용됩니다. 최대 파일 크기: (SIZE) MB.
+오류: ZIP 파일이나 이미지 파일인지 확인해주십시오.
+오류: 파일은 (SIZE) MB 를 초과할 수 없습니다.
+비디오 채팅 시작
+비디오 채팅 종료
+(NICKNAME)이 당신과 비디오 채팅을 시작하길 원합니다.
+취소
+무시
+무시 해제
+확인
+비밀 질문을 하여 사용자의 신원을 확인하세요. 답변은 정확히 일치해야 합니다!
+비밀 질문
+비밀 답변
+질문
+질문 중...
+실패
+신원 확인 완료
+(NICKNAME)이 귀하의 신원을 확인하고자 합니다. 신원을 입증하시려면 아래의 비밀질문에 답변해주세요:
+답변이 (NICKNAME)의 답과 정확히 일치하여야 합니다.
+답변
+경고: (NICKNAME)으로부터 읽을 수 없는 메세지를 수신하였습니다. 신뢰할 수 없는 사용자이거나 수신실패한 메세지를 의미할 수 있습니다. 또는 지난 버전의 크립토캣을 사용하고 있을 수 있습니다. 업데이트를 확인해주세요.
+경고: Cryptocat이 지난 버전입니다. 최신버전으로 업데이트 하세요! 최신버전으로 업데이트 하기 전에는 다음으로 진행하지 마십시오.
+경고: 이 메세지는 (별명)에게 전송될 수 없습니다.
+인증됨
+사용자가 인증되지 않았습니다.
+더 알아보려면 클릭...
+인증에 대해 더 알아보기
+크립토캣 대화를 할 때마다 당신이 대화하는 사람을 인증해야 합니다.
+인증하는 한 가지 방법은 크립토캣을 이용하여 당신의 친구만이 알 수 있는 비밀질문을 하는 것입니다.
+전화 등 신뢰성 있는 채널을 통해 그들과 연락하여 지문을 인식시키라고 요청할 수 있습니다.
+지문은 당신이 사람을 인증하는 것을 가능하게 하는 인식지표입니다. 각 크립토캣 대화마다 변경될 수 있습니다.
+인증이 없이는 누군가가 당신의 대화를 사칭하거나 도청할 수 있습니다.
+이 연락처에 대한 인증지문이 변경되었습니다. 이것은 일어나지 말아야 하는 일이며 의심스러운 행동을 의미할 수 있습니다. 이 연락처와 대화하기 전에 인증을 해주세요.
+그룹챗
+페이스북
+크립토캣을 통하여 페이스북 챗에 연결하세요. 크립토캣을 사용하는 페이스북 친구와 엔드-투-엔드 암호화된 챗을 설정하세요.
+페이스북을 통해 챗
+대화 상태
+암호화되었습니다.
+암호화되지 않았습니다!
+이 연락처가 크립토캣을 사용하고 있지 않으므로 이 대화는 암호화되지 않습니다. 당신의 친구에게 크립토캣을 다운로드하여 암호화된 대화를 즐기도록 요청하세요.
\ No newline at end of file
diff --git a/src/core/locale/lol.txt b/src/core/locale/lol.txt
new file mode 100755
index 0000000..2c6f690
--- /dev/null
+++ b/src/core/locale/lol.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+I Can Haz Private Conversashuns?
+O hai. Herez som helpful tips fur usin Cryptocat: <ul><li>Cryptocat iz not ceiling cat. Dunt trust it with ur lief!.</li><li class="key">Cryptocat can't protect u gainst evil pplz, keyloggarz and duz nut anonymize u (iz cat, not onion.)</li></ul>
+Blog
+custom servr...
+Reset
+conversashun naym
+nicknaem
+connect
+Entar a naem fur ur conversashun n share et wiz ppl u'd liek to talk to.
+Entr teh naym ov conversashun 2 join.
+Plz entr conversashun naym.'
+Conversashun naym must be alfanumeric.
+Plz entr a nicknaem.
+nicknaem must be alfanumeric.
+nicknaem in use.
+Authenticashun failure.
+Connecshun Faild.
+Thank mew 4 usin Cryptocat :3
+Registerin...
+Connectin...
+Haz Connecshun!
+Plz type on ur keybord as randomly as posible 4 few secondz.
+Generatin encrypshun keys...
+Group conversashun. Click on usr 4 private chat.
+OTR fingerprint (4 private conversashuns):
+Group conversashun fingerprint:
+Reset mah encrypshun keyz...
+Resettin yur encrypshun keyz will disconnect mew. Yur fingerprints will also chaeng!
+Continue
+Status: Available
+Status: Away
+My Info
+Desktop Notificashuns On
+Desktop Notificashuns Off
+Audio Notificashuns On
+Audio Notificashuns Off
+Remember mah nicknaem
+Don\'t remember mah nicknaem
+Disconnect
+Display info
+Send encryptd filez
+show meh picturz
+download teh file
+Conversashun
+ZIP fielz n cat pics only plz. Max file siez: (SIZE) MB.
+Error: Plz maek sure ur filez r an ZIP fiel or cat pic.
+Error: File haz to be smallr den (SIZE) MB plz.
+Start video chat
+End video chat
+(NICKNAME) wants 2 see u on der screen
+Cancel
+Ignore
+Unignore
+Authenticate
+Verify this user's identity by asking a secret question. Answers must match exactly!
+Secret question
+Secret answer
+Ask
+Asking...
+Failed
+Identity verified.
+(NICKNAME) wants to verify your identity. Please answer the below secret question to authenticate yourself:
+Your answer must exactly match the one given by (NICKNAME).
+Answer
+Warning: You have received an unreadable message from (NICKNAME). This may indicate an untrustworthy user or messages that failed to be received. You may also be running an outdated version of Cryptocat. Please check for updates.
+Warning: Your version of Cryptocat is outdated. Updating to the latest version is strongly recommended! Do not proceed without updating to the latest version.
+Warning: this message could not be sent to (NICKNAME)
+Authenticated
+User is not authenticated.
+Click to learn more...
+Learn more about authentication
+Every time you have a Cryptocat conversation, you need to authenticate the persons you are talking to.
+One way you can authenticate is by using Cryptocat to ask your friend a secret question that only they would know the answer to.
+You can also contact them via a trusted channel, such as by phone, and ask them to read their fingerprints.
+Fingerprints are identifiers that allow you to authenticate persons. They can change between every Cryptocat conversation.
+Without authentication, someone could be impersonating or intercepting your communications.
+The authentication fingerprints for this contact have changed. This is not supposed to happen and could indicate suspicious behaviour. Please authenticate this contact before chatting with them.
+Group Chat
+Facebook
+Connect to Facebook Chat via Cryptocat. Set up end-to-end encrypted chats with Facebook friends who are also using Cryptocat.
+Chat via Facebook
+Conversation status
+encrypted.
+not encrypted!
+This conversation is not encrypted because this contact is not using Cryptocat. Ask your friend to download Cryptocat to enjoy encrypted chat.
\ No newline at end of file
diff --git a/src/core/locale/lv.txt b/src/core/locale/lv.txt
new file mode 100644
index 0000000..38393c8
--- /dev/null
+++ b/src/core/locale/lv.txt
@@ -0,0 +1,68 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Privātas sarunas ikvienam.
+Esi sveicināts Cryptocat. Daži noderīgi padomi: <ul><li>Cryptocat nav maģiska lode. Tev nekad nevajadzētu uzticēt savu dzīvi nevienai programmatūrai.</li><li class="key">Cryptocat nevar tevi aizsargāt pret neuzticamiem cilvēkiem, taustiņspiedienu pierakstītājiem, kā arī tas nepadara tavu pieslēgumu anonīmu.</li></ul>
+Blogs
+Cits serveris
+Anulēt
+sarunas nosaukums
+pseidonīms
+savienoties
+Ievadi nosaukumu tavai sarunai un dalies ar to ar cilvēkiem, ar kuriem vēlies sarunāties, vai pievienojies <strong>forumam</strong>, lai iepazītos ar jauniem cilvēkiem!
+Ievadi sarunas nosaukumu, kuram pievienoties.
+Lūdzu, ievadi sarunas nosaukumu.
+Sarunas nosaukumā var būt tikai burti un cipari.
+Lūdzu, ievadi pseidonīmu.
+Pseidonīmā var būt tikai burti un cipari.
+Pseidonīms jau tiek lietots.
+Autentificēšanās kļūda.
+Savienojums neizdevās.
+Paldies, ka lieto Cryptocat.
+Rēģistrējas...
+Savienojas...
+Savienojums nodibināts.
+Lūdzu, raksti ar klaviatūru pēc iespējas neprognozējamāk turpmākās dažas sekundes.
+Ģenerē šifrēšanas atslēgas...
+Grupas saruna. Noklikšķini uz lietotāja, lai sāktu privātu sarunu.
+OTR kontrolsumma (privātai sarunai):
+Grupas sarunas kontrolsumma:
+Anulēt manas šifrēšanas atslēgas
+Šifrēšanas atslēgu anulēšana atslēgs tevi no sarunām. Mainīsies arī kontrolsumma.
+Turpināt
+Statuss: Pieejams
+Statuss: Prom
+Mana informācija
+Darbvirsmas brīdinājumi ieslēgti
+Darbvirsmas brīdinājumi izslēgti
+Skaņas brīdinājumi ieslēgti
+Skaņas brīdinājumi izslēgti
+Atcerēties manu pseidonīmu
+Neatcerēties manu pseidonīmu
+Atslēgties
+Parādīt informāciju
+Sūtīt šifrētu failu
+apskatīt attēlu
+lejupielādēt failu
+Saruna
+Pieņemti tiek vienīgi ZIP faili vai attēli. Maksimālais faila izmērs: (SIZE) MB. 
+Kļūda: Lūdzu pārliecinies, ka fails ir ZIP fails vai attēls.
+Kļūda: Fails nedrīkst būt lielāks par (SIZE) MB.
+Sākt video sarunu
+Beigt video sarunu
+(NICKNAME) vēlas uzsākt video sarunu ar tevi.
+Atcelt
+Ignorēt
+Neignorēt
+Autentificēt
+Pārbaudi šī lietotāja identitāti, uzdodot slepeno jautājumu. Atbildei jāsakrīt pilnībā!
+Slepenais jautājums
+Slepenā atbilde
+Jautāt
+Tiek jautāts...
+Neizdevās
+Identitāte pārbaudīta.
+(NICKNAME) vēlas pārbaudīt tavu identitāti. Lūdzu, atbildi uz slepeno jautājumu zemāk un autentificē sevi:
+Tavai atbildei jāsakrīt pilnībā ar atbildi, ko sniedza (NICKNAME).
+Atbilde
+Brīdinājums: Tu esi saņēmis neizlasāmu ziņu no (NICKNAME). Tas var liecināt par neuzticamu lietotāju vai kļūdu ziņas saņemšanā.
+Brīdinājums: Tava Cryptocat versija ir novecojusi. Atjaunināšana uz jaunāko versiju ir ļoti ieteicama! Neturpini pirms neesi veicis atjaunināšanu uz jaunāko versiju.
\ No newline at end of file
diff --git a/src/core/locale/nl.txt b/src/core/locale/nl.txt
new file mode 100755
index 0000000..d7719f9
--- /dev/null
+++ b/src/core/locale/nl.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Privégesprekken voor iedereen.
+Welkom bij Cryptocat. Hier volgen een paar handige tips: <ul><li>Cryptocat is geen wondermiddel. Vertrouw nooit je leven toe aan een stukje software.</li><li class="key">Cryptocat kan u niet beschermen tegen onbetrouwbare mensen of keyloggers en maakt niet uw verbinding anoniem.</li></ul>
+Blog
+Aangepaste server
+Reset
+gespreksnaam
+gebruikersnaam
+verbinden
+Geef je gesprek een naam en deel deze met mensen waarmee je wil praten, of bezoek de <strong>lobby</strong> om nieuwe mensen te ontmoeten!
+Geef de naam op van het gesprek waar je aan wil deelnemen.
+Voer alstublieft een gespreksnaam in.
+De gespreksnaam kan enkel cijfers of letters bevatten.
+Voer een gebruikersnaam in.
+Alias kan enkel cijfers of letters bevatten.
+Gebruikersnaam is reeds in gebruik.
+Authenticatie mislukt.
+Verbinding mislukt.
+Bedankt voor het gebruik van Cryptocat.
+Bezig met registratie...
+Initiatie verbinding...
+Verbonden.
+Sla gedurende enkele seconden lukraak toetsen op uw toetsenbord aan.
+Bezig met aanmaak van encryptiesleutels...
+Groepsgesprek. Klik op een gebruiker voor een privégesprek.
+OTR vingerafdruk (voor privégesprekken):
+Vingerafdruk groepsgesprek:
+Reset mijn encryptiesleutels
+Het resetten van uw encryptiesleutels verbreekt de verbinding. Uw digitale vingerafdruk verandert eveneens.
+Doorgaan
+Status: Beschikbaar
+Status: Afwezig
+Mijn gegevens
+Meldingen Bureaublad Aan
+Meldingen Bureaublad Uit.
+Geluidsmeldingen Aan
+Geluidsmeldingen Uit
+Mijn gebruikersnaam onthouden
+Mijn gebruikersnaam niet onthouden
+Afmelden
+Toon Info
+Verstuur versleuteld bestand
+afbeelding bekijken
+bestand downloaden
+Gesprek
+Enkel ZIP-bestanden en afbeeldingen worden aanvaard. Maximale bestandgrootte: (SIZE) MB.
+Fout: Controleer of je bestand een ZIP-bestand of afbeelding is.
+Fout: Bestand mag niet groter zijn dan (SIZE) MB.
+Begin videogesprek
+Beëindig videogesprek
+(NICKNAME) wil graag een videochat met u beginnen.
+Annuleren
+Negeer
+Stop met negeren
+Bevestigen
+Controleer de identiteit van deze gebruiker door een geheime vraag te stellen. Antwoorden moeten precies overeenkomen!
+Geheime vraag
+Geheime antwoord
+Vraag
+Aan het vragen...
+Mislukt
+Identiteit geverifieerd.
+(NICKNAME) wil uw identiteit verifiëren. Gelieve de geheime vraag hieronder te beantwoorden om uw identiteit te bevestigen:
+Uw antwoord moet precies overeenkomen met het antwoord welke door (NICKNAME) gegeven is.
+Antwoord
+Waarschuwing: u heeft een onleesbaar bericht van (NICKNAME) ontvangen. Dit kan een onbetrouwbare gebruiker zijn of berichten die niet kunnen worden ontvangen. Het kan ook zijn dat u een verouderde versie van Cryptocat gebruikt. Gelieve te controleren op updates.
+Waarschuwing: Uw versie van Cryptocat is verlopen. Bijwerken naar de meest recente versie wordt sterk aanbevolen! Ga niet verder zolang u niet huidige versie gebruikt.
+Waarschuwing: dit bericht kan niet verzonden worden naar (NICKNAME)
+Geauthenticeerd
+Gebruiker is niet geauthenticeerd.
+Klik hier om meer te weten te komen...
+Kom meer te weten over authenticatie
+Iedere keer als je een gesprek in Cryptocat hebt, moet je je de gene waar tegen je praat authenticeren.
+Een manier om je identiteit te verifiëren is door Cryptocat te gebruiken om een vriend een geheime vraag te stellen waar hij alleen het antwoord op kent.
+Je kunt hen ook contacteren via een betrouwbaar kanaal, zoals je telefoon, en hen vragen om hun vingerafdruk te lezen.
+Vingerafdrukken zijn identificatiemiddelen die het toelaten om iemands identiteit te verifiëren. Ze kunnen veranderen na elk Cryptocat gesprek.
+Zonder verifiëring zou iemand zich kunnen voordoen als een andere persoon of je gesprekken onderscheppen.
+De vingerafdrukken van deze contactpersoon zijn veranderd. Dit is niet de bedoeling en zou kunnen wijzen op verdacht gedrag. Controleer deze contactpersoon alstublieft voor je ermee begint te chatten.
+Groepsgesprek
+Facebook
+Verbind met Facebook Chat via Cryptocat.  Zet een volledig geëncrypteerd gesprek op met je Facebook vrienden die ook Cryptocat gebruiken.
+Chat via Facebook
+Gespreksstatus
+geëncrypteerd
+niet geëncrypteerd
+Dit gesprek is niet geëncrypteerd omdat het contact geen Cryptocat gebruikt.  Vraag je vriend(in) Cryptocat te downloaden om zo geëncrypteerde gesprekken te genieten.
\ No newline at end of file
diff --git a/src/core/locale/no.txt b/src/core/locale/no.txt
new file mode 100644
index 0000000..2b54a2d
--- /dev/null
+++ b/src/core/locale/no.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Private Samtaler For Alle.
+Velkommen til Cryptocat. Her er noen nyttige tips: <ul><li>Cryptocat er ingen mirakelkur. Du bør aldri stole på fullt og helt på noen programvare.</li><li class="key">Cryptocat kan ikke beskytte deg mot uærlige personer eller keyloggere og anonymiserer ikke tilkoblingen din.</li></ul>
+Blogg
+Egendefinert server
+Tilbakestill
+samtalenavn
+kallenavn
+koble til
+Gi samtalen din et navn og del navnet med de du ønsker å snakke med, eller gå til <strong>lobby</strong> for å treffe nye folk!
+Oppgi navnet på samtalen du ønsker å delta i.
+Vennligst angi et samtalenavn.
+Samtalenavn kan kun bestå av bokstaver eller tall.
+Vennligst angi et kallenavn.
+Kallenavn kan kun være bokstaver eller tall.
+Kallenavnet er i bruk.
+Autentisering feilet.
+Tilkobling feilet.
+Takk for at du bruker Cryptocat.
+Registrerer...
+Kobler til...
+Tilkoblet.
+Vennligst utfør tastetrykk på tastaturet så tilfeldig som mulig i noen sekunder.
+Genererer krypteringsnøkler...
+Gruppesamtale. Klikk på en bruker for å snakke privat.
+OTR fingeravtrykk (for private samtaler):
+Gruppesamtale fingeravtrykk:
+Tilbakestill krypteringsnøklene mine
+Tilbakestilling av krypteringsnøklene dine vil koble deg fra. Fingeravtrykkene dine vil også forandres.
+Fortsett
+Status: Tilgjengelig
+Status: Borte
+Min info
+Skrivebordsvarsler På
+Skrivebordsvarsler Av
+Lydvarslinger På
+Lydvarslinger Av
+Husk kallenavnet mitt
+Ikke husk kallenavnet mitt
+Logg ut
+Vis info
+Send kryptert fil
+vis bilde
+last ned fil
+Samtale
+Kun ZIP-filer og bilder aksepteres. Maksimum filstørrelse: (SIZE) MB.
+Feil: Sikker på at filen din er en ZIP-fil eller et bilde?
+Feil: Filen kan ikke være større enn (SIZE) MB.
+Start videosamtale
+Avslutt videosamtale
+(NICKNAME) vil starte en videosamtale med deg.
+Avbryt
+Ignorer
+Ikke ignorer
+Bekreft identitet
+Verifiser denne brukerens identitet ved å spørre et hemmelig spørsmål. Svaret må samsvare nøyaktig!
+Hemmelig spørsmål
+Hemmelig svar
+Spør
+Spør...
+Mislyktes
+Identitet bekreftet.
+(NICKNAME) ønsker å bekrefte din identitet. Besvar det hemmelige spørsmålet nedenfor for å autentisere deg selv.
+Ditt svar må være nøyaktig det samme som gitt av (NICKNAME)
+Svar
+Advarsel: Du har mottatt en uleselig melding fra (NICKNAME). Dette kan indikere en upålitelig bruker eller meldinger som ikke ble mottatt.  Det kan også være at du bruker en utdatert versjon av Cryptocat. Vennligst se etter oppdateringer.
+Advarsel: Din versjon av Cryptocat er utdatert. Vi anbefaler sterkt at du oppdaterer til den nyeste versjonen. Ikke fortsett uten å oppdatere til den nyeste versjonen.
+Advarsel: denne meldingen kunne ikke sendes til (NICKNAME)
+Identitet bekreftet
+Brukerens identitet er ikke bekreftet.
+Klikk her for å lære mer...
+Lær mer om identitetsbekreftelse
+Hver gang du har en Cryptocat-samtale, må du bekrefte identiteten til de du snakker med.
+En måte du kan bekrefte identitet på, er ved å bruke Cryptocat til å stille et spørsmål som du vet bare den forespurte kjenner svaret på.
+Du kan også kontakte dem gjennom en tiltrodd måte, for eksempel ved å få dem til å lese opp fingeravtrykket som er tilknyttet dem.
+Fingeravtrykk er identifikatorer som lar deg bekrefte identiteten til folk. De kan endre seg mellom hver Cryptocat-samtale.
+Uten autentikering kan det hende noen gir seg ut for å være dem du snakker med eller overhører samtalen.
+Autentiserings-fingeravtrykkene til denne kontakten har blitt endret. Dette skal ikke skje, og kan indikere at noe ikke er helt rett. Vennligst reautentiser denne kontakten før du snakker med vedkommende.
+Gruppechat
+Facebook
+Koble deg til Facebook Chat via Cryptocat. Sett opp fullstendig krypterte chatter med Facebook venner som også bruker Cryptocat.
+Chat via Facebook
+Samtalestatus
+Kryptert.
+Ikke kryptert!
+Denne samtalen er ikke kryptert fordi denne kontakten ikke bruker Cryptocat. Be vennen din laste ned Cryptocat for å nyte kryptert chatting. 
\ No newline at end of file
diff --git a/src/core/locale/old/ug.txt b/src/core/locale/old/ug.txt
new file mode 100644
index 0000000..eacc942
--- /dev/null
+++ b/src/core/locale/old/ug.txt
@@ -0,0 +1,53 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Hemmige Shexsi Parang
+Cryptocatke xush keldingiz. Töwendikiler sizge paydiliq uchurlardur: <ul><li>Cryptocat hemmige qadir emes. Siz héchqachan birer yumshaq détalgha  pütünley ishinip ketmeng. </li><li class="key">Cryptocat sizni ishenchsiz ademlerdin, konupka taxtisi közetküchiliridin saqliyalmaydu, tor ulinishingizni namsizlashturalmaydu. </li></ul>
+Blog
+Alahide mulazimétiri
+Yéngila
+Söhbet témisi
+leqem
+ulash
+Söhbet témisi üchün bir isim kirgüzüng we paranglashmaqchi bolghan kishige uqturung.
+Söhbet témisini kirgüzüp jör bolung.
+Söhbet témisini kirgüzüng.
+Söhbet témisi xet yaki reqem bolushi kérek
+Leqem kirgüzüng
+Leqimingiz xet yaki reqem bolushi kérek
+Bu leqem ishlitiliwatidu
+Delillesh xataliqi
+Ulinish xataliqi
+Cryptocatni ishletkenlikingizge rexmet.
+Tizimlitiliwatidu...
+Uliniwatidu...
+Ulandi.
+Konupka taxtisida birqanche sécond qalaymiqan xet bésing.
+Shifirlashturush achquchliri yasiliwatidu
+Gruppa söhbiti. Shexsi söhbet üchün shu kishining leqimini bésing.
+OTR barmaq izi (shexsi söhbet üchün)
+Gruppa söhbéti barmaq izi
+Shifirlash achquchumni yéngila
+Shifirlash achquchingizni yéngilisingiz ulinish késilidu. Barmaq izingizmu özgiridu.
+Dawamlashtur
+Haliti: Bikar
+Haliti: Nérida
+Uchurlirim
+Üstel yüzi üchurini échish
+Üstel yüzi üchurini taqash
+Aptomatik üchurni échish
+Aptomatik üchurni taqash
+Leqimimni xatirle
+Leqimimni xatirilime
+Chékinish
+Uchur körsitish
+Shifirlashturulghan höjjet ewetish
+resim körüsh
+höjjetni yüklesh
+Söhbet
+Peqet ZIP qisquchilar we suretler qobul qilindu. Maksimom razma: (SIZE) MB.
+Hataliq: Sizning qisquchingiz ZIP yaki suret ispatling.
+Hataliq: (SIZE) MBdin chongraq qisquchilar bolmaydu.
+Video paranglishish bashlimaq
+Video paranglishish ahirlashmaq
+(NICKNAME) siz bilen video paranglishishni bashlashi halaydu.
+Emeldin qaldurmaq
\ No newline at end of file
diff --git a/src/core/locale/old/ur.txt b/src/core/locale/old/ur.txt
new file mode 100644
index 0000000..4610a7c
--- /dev/null
+++ b/src/core/locale/old/ur.txt
@@ -0,0 +1,53 @@
+rtl
+Tahoma, DejaVu, "Helvetica Neue", Helvetica, Arial, Verdana
+سب کے لیئے نجی گفتگو
+کرپٹو کاٹ میں خوش آمدید۔ یہاں کچھ مفید تجاویز ہیں: <ul><li>کرپٹو کاٹ ایک جادوئی دوا نہیں ہے۔ ٓپ کو اپنی زندگی کے لیئے کبھی بھی کسی سافٹ ویئر پر اعتبار نہیں کرنا چاہیئے۔</li><li>کرپٹو کاٹ آپ کو ناقابل اعتماد لوگوں اور کی لوگروں سے نہیں بچا سکتا اور یہ آپ کے کنکشن کو گمنام نہیں کرتا۔</li class="key"></ul>
+بلاگ
+مخسسوس کردہ سرور
+دوبارہ ترتیب کیجیے
+گفتگو کا نام 
+عرف
+رابطہ کیجیے
+اپنی گفتگو کو نام دیجیے اور جن لوگوں کو اس کا حصّہ بنانا چاہتے ہیں، انھیں یہ ارسال کیجیے
+جس گفتگو میں آپ حصّہ لینا چاہتے ہیں اس کا نام بتائیے۔
+گفتگو کا نام تحریر کیجیے.
+گفتگو کے نام میں صرف حروف یا ہندسے ہو سکتے ہیں۔
+عرف تحریر کیجیے.
+عرف میں صرف حروف یا ہندسے ہو سکتے ہیں۔
+زیر استعمال عرف۔
+تصدیق ناکام۔
+رابطہ ناکام.
+کرپٹو کاٹ استعمال کرنے کا شکریہ۔
+زیر اندراج...
+رابطے کی کوشش میں...
+منسلک۔
+براۓ مہربانی اپنے کیبورڈ سے چند سیکنڈ کے لئے زیادہ سے زیادہ بے ترتیبی سے بٹن دبائیے.
+خفیہ کردہ "کی" تخلیق کی جارہے ہیں...
+اجتمائی گفتگو. کسی فرد سے نجی گفتگو کے لئے اس کے نام پر کلک کیجیے-
+او ٹی آر نشان انگشت (نجی گفتگو کے لیئے):
+نشان انگشت بری اجتمائی گفتگو۔
+میری خفیہ شدہ "کی" دوبارترتیب کیجیے
+آپ کی خفیہ شدہ "کی" کو دوبارہ ترتیب کرنے سے آپ کا سلسلہ ٹوٹ جائے گا. آپ کے نشان انگشت بھی تبدیل ہوں گے.
+جاری رکھیے
+حیثیت: دستیاب 
+حیثیت: دور
+میری معلومات
+ڈیسک ٹاپ اطلاعات: ہاں
+ڈیسک ٹاپ اطلاعات: نہیں
+آڈیو اطلاعات: ہاں
+آڈیو اطلاعات: نہیں
+میرا عرف یاد رکھیے
+میرا عرف مت یاد رکھیے
+لاگ اوٹ
+معلومات ظاہر کیجیے
+خفیہ شدہ فائل ارسال کیجیے
+تصویر دیکھیے
+فائل ڈآونلوڈ کیجیے
+گفتگو
+ صرف زپ فائل اور تصویروں کو قبول ہو گئے - سب سے بڑا. فائل سائز : (SIZE) MB-
+غلطی :جانچے اگر آپ کا فائل زپ فائل یا تصویر ہے
+غلطی: فائل (SIZE) MB سے زیادہ بڑا نہیں ہو سکتا ہے
+ویڈیو بات شرو کیجئے
+ویڈیو بات چیت ختم کیجئے
+(NICKNAME) آپ کے ساتھ  ویڈیو بات چیت شرو کرنا چاہتا ہے
+منسوخ کرنا
\ No newline at end of file
diff --git a/src/core/locale/pl.txt b/src/core/locale/pl.txt
new file mode 100755
index 0000000..c441798
--- /dev/null
+++ b/src/core/locale/pl.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Prywatne rozmowy dla każdego.
+Witamy w Cryptocat. Oto kilka przydatnych wskazówek: <ul><li>Cryptocat nie jest cudownym lekiem. Nigdy nie należy ufać żadnemu oprogramowaniu w stu procentach.</li><li class="key">Cryptocat nie oferuje ochrony przed oszustami i oprogramowaniu zbierającymi informacje o naciskanych klawiszach ani nie zapewnia anonimowości podczas korzystania z internetu.</li></ul>
+Blog
+Custom server
+Resetuj
+Nazwa rozmowy
+Pseudonim
+Połącz
+Wpisz tytuł konwersacji i udostępnij ją osobom, z którymi zamierzasz rozmawiać.
+Wpisz nazwę rozmowy, do której chcesz dołączyć.
+Proszę wpisać nazwę rozmowy.
+Nazwa rozmowy może zawierać jedynie litery i cyfry.
+Proszę o podanie pseudonimu.
+Pseudonim może zawierać jedynie litery i cyfry.
+Pseudonim jest w użyciu.
+Błąd uwierzytelniania.
+Połączenie nieudane.
+Dzięki, że  skorzystałeś z Cryptocata.
+Rejestruję...
+Łączę...
+Połączony
+Proszę losowo pisać na klawiaturze przez kilka sekund.
+Generuję klucze szyfrujące...
+Rozmowa grupowa. Kliknij na użytkownika, aby porozmawiać prywatnie.
+Klucz identyfikacyjny OTR (dla prywatnych rozmów):
+Klucz identyfikacyjny rozmowy grupowej:
+Skasuj moje klucze szyfrujące
+Skasowanie kluczy szyfrujących spowoduje utratę połączenia i zmianę kluczy identyfikacyjnych.
+Dalej
+Status: Dostępny
+Status: Nieobecny
+O mnie
+Powiadomienia ekranowe włączone
+Powiadomienia ekranowe wyłączone
+Powiadomienia dźwiękowe włączone
+Powiadomienia dźwiękowe wyłączone
+Pamiętaj mój pseudonim
+Nie pamiętaj mojego pseudonimu
+Wyloguj
+Wyświetl informacje
+Wyślij zaszyfrowany plik
+Pokaż obrazek
+Pobierz plik
+Rozmowa
+Dozwolone są tylko pliki ZIP i obrazy. Maksymalny rozmiar pliku: (SIZE) MB.
+Błąd: Upewnij się, że plik jest plikiem ZIP lub plikiem graficznym.
+Błąd: Plik nie może być większy niż (SIZE) MB.
+Rozpocznij czat wideo
+Zakończ czat wideo
+(NICKNAME) chce rozpocząć z Tobą rozmowę wideo.
+Anuluj
+Ignoruj
+Nie ignoruj
+Uwierzytelnij
+Zweryfikuj tożsamość tego użytkownika, zadając sekretne pytanie. Odpowiedzi muszą być zgodne!
+Sekretne pytanie
+Sekretna odpowiedź
+Zadaj pytanie
+Trwa zadawanie pytania...
+Nie powiodło się
+Tożsamość potwierdzona.
+(NICKNAME) chce zweryfikować Twoją tożsamość. Odpowiedz na poniższe sekretne pytanie, aby ją potwierdzić.
+Twoja odpowiedź musi być zgodna z odpowiedzią podaną przez (NICKNAME).
+Odpowiedz
+UWAGA: Otrzymałeś nieczytelną wiadomość od (NICKNAME). To może oznaczać, że ten użytkownik jest niezaufany lub wiadomość nie udało się otrzymać. Możesz także używać nieaktualną wersję Cyrptocat. Proszę sprawdź czy są dostępne aktualizacje.
+Ostrzeżenie: Twoja wersja Cryptocat jest nieaktualna. Zalecana jest aktualizacja do najnowszej wersji! Nie kontynuuj bez aktualizacji do najnowszej wersji.
+UWAGA: nie można wysłać tej wiadomości do (NICKNAME)
+Uwierzytelniony
+Użytkownik nie jest uwierzytelniony.
+Kliknij, aby dowiedzieć się więcej...
+Dowiedz się więcej o uwierzytelnianiu
+Za każdym razem, gdy prowadzisz rozmowę w Cryptocat, musisz uwierzytelnić osoby, z którymi rozmawiasz.
+Jednym ze sposobów uwierzytelnia, jest zapytanie twojego przyjaciela o tajne pytanie, na które tylko on zna odpowiedź.
+Możesz również skontaktować się z nimi za pośrednictwem zaufanego kanału, takiego jak przez telefon, i poprosić ich, aby przeczytali swoje klucze identyfikacyjne.
+Klucze identyfikacyjne są identyfikatorami, które pozwalają na uwierzytelnienie osób. Mogą się zmieniać pomiędzy każdą rozmową w Cryptocat.
+Bez uwierzytelniania, ktoś może podać się za ciebie lub podsłuchiwać twoje komunikacje.
+Klucze identyfikacyjne uwierzytelniania dla tego kontaktu zostały zmienione. To nie powinno się zdarzyć i może wskazywać na podejrzane zachowania. Proszę uwierzytelnić ten kontakt przed rozpoczęciem z nim rozmowy.
+Rozmowa grupowa
+Facebook
+Połącz się z czatem Facebooka przez Cryptocat. Rozmawiaj bezpiecznie ze swoimi znajomymi z Facebooka, którzy też używają Cryptocat.
+Rozmawiaj przez Facebooka
+Status rozmowy
+zaszyfrowane.
+niezaszyfrowane!
+Ta rozmowa nie jest szyfrowana, ponieważ ten kontakt nie używa Cryptocat. Poproś swojego znajomego o ściągnięcie Cryptocat aby cieszyć się kodowaną rozmową.
\ No newline at end of file
diff --git a/src/core/locale/pt.txt b/src/core/locale/pt.txt
new file mode 100755
index 0000000..52a031f
--- /dev/null
+++ b/src/core/locale/pt.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Conversas Privadas para Todos.
+Bem-vindo ao Cryptocat. Seguem algumas dicas úteis: <ul><li>O Cryptocat não é uma solução mágica. Você nunca deve confiar em nenhum software quando se trata da sua vida.</li><li class="key">O Cryptocat não pode protegê-lo contra pessoas não confiáveis ou keyloggers, e não torna sua conexão anônima.</li></ul>
+Blog
+Servidor personalizado
+Redefinir
+nome da conversa
+apelido
+conectar
+Introduza um nome para a sua conversa e compartilhe-o com as pessoas com quem gostaria de falar, ou entre na <strong>sala pública</strong> para conhecer novas pessoas!
+Digite o nome da conversa para participar.
+Por favor digite um nome de conversa.
+Nome pode conter apenas letras e números
+Por favor digite um apelido.
+Seu nome pode contar apenas letras e números
+Apelido em uso.
+Falha na autenticação.
+Conexão falhou.
+Obrigado por utilizar o Cryptocat.
+Registrando...
+Conectando...
+Conectado.
+Por favor digite em seu teclado o mais aleatoriamente possível por alguns segundos.
+Gerando chaves de criptografia...
+Conversa em grupo. Clique em um usuário para conversa privada.
+Impressão digital OTR (para conversas privadas):
+Impressão digital da conversa em grupo:
+Redefinir minhas chaves de criptografia
+Redefinir suas chaves de criptografia irá desconectá-lo. Suas impressões digitais também mudarão.
+Continuar
+Estado: Disponível
+Estado: Ausente
+Minhas Informações
+Notificações no Desktop Ligadas
+Notificações no Desktop Desligadas
+Notificações por Áudio Ligadas
+Notificações por Áudio Desligadas
+Lembrar meu apelido
+Não lembrar o meu apelido
+Sair
+Exibir Informações
+Enviar arquivo criptografado
+visualizar imagem
+baixar arquivo
+Conversa
+Apenas arquivos ZIP e imagens são aceitas. Tamanho máximo de arquivo: (SIZE) MB.
+Erro: Por favor tenha certeza de que seu arquivo é um arquivo ZIP ou uma imagem.
+Erro: O arquivo não pode ser maior do que (SIZE) MB.
+Iniciar vídeo-conferência
+Encerrar vídeo-conferência
+(NICKNAME) gostaria de iniciar uma conversa por vídeo com você.
+Cancelar
+Ignorar
+Não ignorar mais
+Autenticar
+Verificar a identidade desse usuário fazendo uma pergunta secreta. As respostas precisam coincidir de forma exata!
+Pergunta secreta
+Resposta secreta
+Perguntar
+Solicitando...
+Falha
+Identidade verificada.
+(NICKNAME) deseja verificar sua identidade. Responda a pergunta secreta abaixo para identificar-se:
+Sua resposta precisa coincidir de forma exata com a fornecida por (NICKNAME).
+Responder
+Aviso: Você recebeu uma mensagem ilegível do usuário (NICKNAME). Isto pode indicar um usuário não confiável ou mensagens que não foram recebidas corretamente. Você também pode estar executando uma versão desatualizada do Cryptocat. Por favor, verifique se há atualizações.
+Aviso: sua versão do Cryptocat está desatualizada. É altamente recomendado fazer a atualização para a última versão! Não prossiga sem atualizar para a última versão.
+Aviso: esta mensagem não pôde ser enviada para (NICKNAME)
+Autenticado
+O usuário não está autenticado.
+Clique para saber mais...
+Aprenda mais sobre autenticação
+Toda vez que você participa de uma conversa Cryptocat, é preciso que as pessoas com quem você conversa estejam autenticadas.
+Uma maneira de autenticação é usar o Cryptocat para perguntar aos seus amigos uma questão secreta que apenas eles saberão responder.
+Pode-se também usar um canal confiável para entrar em contato com eles, como via telefone, e pedir que eles leiam suas chaves de identidade.
+Chaves de identidade são identificadores que permitem autenticar pessoas. Elas podem mudar entre cada conversa Cryptocat.
+Sem autenticação, uma pessoa poderia estar se passando por outra ou interceptando suas comunicações.
+As chaves de identidade para este contato mudaram. Isto não deveria ocorrer e pode indicar comportamento suspeito. Favor autenticar este contato antes de conversar com ele.
+Conversa em grupo
+Facebook
+Conecte-se ao Facebook Chat via Cryptocat. Configure um chat criptografado de ponta a ponta com seus amigos do Facebook que também estão usando Cryptocat.
+Conversa pelo facebook
+Estado da conversa
+Criptografado
+Não criptografado
+Essa conversa não está criptografada porque o contato não está usando o Cryptocat. Fala para o seu amigo baixar o Cryptocat e curtir o chat criptografado
\ No newline at end of file
diff --git a/src/core/locale/ru.txt b/src/core/locale/ru.txt
new file mode 100755
index 0000000..f751b52
--- /dev/null
+++ b/src/core/locale/ru.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Конфиденциальные беседы для всех.
+Добро пожаловать в Cryptocat. Вот несколько полезных советов: <ul><li>Cryptocat - это не чудодейственное средство. Вам никогда не следует доверять свою жизнь какой-либо программе.</li><li class="key">Cryptocat не может защитить вас от неблагонадежных людей и программ перехвата введенной информации, а также не анонимизирует ваше подключение.</li></ul>
+Блог
+Выбрать сервер
+Сброс
+название разговора
+имя пользователя
+соединиться
+Введите название вашего разговора и поделитесь им с людьми, с которыми вы хотели бы поговорить, или посетите <strong>lobby</strong>, чтобы познакомиться с новыми людьми!
+Введите название разговора, к которому нужно присоединиться.
+Пожалуйста, введите название разговора.
+Название беседы может состоять только из букв и цифр.
+Пожалуйста, введите имя пользователя.
+Имя пользователя может состоять только из букв и цифр.
+Это имя пользователя уже используется.
+Ошибка аутентификации.
+Ошибка подключения.
+Спасибо за использование Cryptocat.
+Регистрация...
+Соединение...
+Соединен.
+Пожалуйста, нажимайте случайные клавиши на клавиатуре несколько секунд.
+Генерация ключей шифрования...
+Групповой разговор. Нажмите на имя пользователя для начала приватной беседы.
+Отпечаток OTR (для приватных бесед)
+Отпечаток для многопользовательского чата
+Сбросить мои ключи шифрования
+Сброс ключей шифрования отсоединит вас. Ваши отпечатки также изменятся.
+Продолжить
+Статус: Доступен
+Статус: Нет на месте
+Моя информация
+Браузерные уведомления включены
+Браузерные уведомления выключены
+Аудио уведомления включены
+Аудио уведомления выключены
+Запомнить имя пользователя
+Не запоминать имя пользователя
+Выйти
+Отобразить информацию
+Отправить зашифрованный файл
+просмотреть изображение
+скачать файл
+Разговор
+Принимаются только ZIP-файлы и изображения. Максимальный размер файла: (SIZE) MB.
+Ошибка: Пожалуйста, убедитесь в том, что ваш файл является ZIP-файлом или изображением.
+Ошибка: файл не может быть размером больше, чем (SIZE) MB.
+Начать видеочат
+Закончить видеочат
+(NICKNAME) хочет начать видеочат с вами.
+Отмена
+Игнорировать
+Перестать игнорировать
+Аутентифицировать
+Проверьте личность этого пользователя, задав ему секретный вопрос. Ответы должны точно совпадать!
+Секретный вопрос
+Секретный ответ
+Спросить
+Спрашиваем...
+Неудача
+Личность подтверждена.
+(NICKNAME) хочет проверить вашу личность. Пожалуйста, ответьте на секретный вопрос внизу для подтверждения своей личности:
+Ваш ответ должен в точности совпадать с ответом (NICKNAME).
+Ответ
+Внимание: Вы получили нечитаемое сообщение от (NICKNAME). Это могло произойти из-за неблагонадежности пользователя или из-за ошибки во время приема сообщения. Также возможно, что вы используете устаревшую версию Cryptocat. Пожалуйста, проверьте обновления.
+Внимание: Ваша версия Cryptocat устарела. Мы настоятельно рекомендуем обновление до последней версии! Не используйте приложение, не обновив его до последней версии.
+Внимание: это сообщение не было отправлено пользователю (NICKNAME)
+Аутентифицирован
+Пользователь не аутентифицирован
+Нажмите, чтобы узнать больше
+Узнать больше про аутентификацию
+Вы должны аутентифицировать собеседника каждый раз, когда вы общаетесь с кем-то с помощью Cryptocat.
+Для аутентификации вы можете задать собеседнику такой вопрос, на который только он знает правильный ответ.
+Также, вы можете связаться с ним другим способом, например по телефону, и попросить его прочитать свои отпечатки.
+Отпечатки - это идентификаторы, которые позволяют аутетифицировать пользователей. В разных беседах в Cryptocat они могут быть разными.
+Без аутентификации посторонние люди могут выдавать себя за вашего собеседника или перехватывать ваши сообщения.
+Отпечатки аутентификации для этого пользователя изменились. Этого не должно было произойти. Возможно, что-то здесь нечисто. Пожалуйста, аутентифицируйте этого пользователя перед разговором.
+Групповой чат
+Facebook
+Войдите в чат Facebook через Cryptocat. Вы можете зашифровать переписку со своими Facebook друзьями, если они тоже пользуются Cryptocat.
+Общаться через Fcebook
+Состояние беседы
+зашифрована.
+не зашифрована!
+Эта беседа не зашифрована, потому что ваш собеседник не пользуется Cryptocat. Попросите своего друга скачать Cryptocat, чтобы ваша переписка стала зашифрована.
\ No newline at end of file
diff --git a/src/core/locale/sk.txt b/src/core/locale/sk.txt
new file mode 100644
index 0000000..dab109c
--- /dev/null
+++ b/src/core/locale/sk.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Súkromné konverzácie pre každého.
+Vitajte v Cryptocat-e. Tu je pár nápomocných rád: <ul><li> Cryptocat nieje čarovný. Nikdy by ste nemali zveriť programu svoj život. </li><li class="key"> Cryptocat Vás neochráni pred nezodpovednými ľuďmi, keyloggermi a neanonymizuje Vaše pripojenie.
+Blog
+Vlastný server
+Obnoviť
+Názov konverzácie
+Prezývka
+Pripojiť
+Vložte názov pre Vašu konverzáciu a oznámte ho ľuďom, s ktorými chcete hovoriť.
+Zadajte názov konverzácie pre pripojenie.
+Zadajte názov konverzácie.
+Názov konverzácie môže pozostávať iba z písmen alebo číslic.
+Prosím zadajte prezývku.
+Prezývka môže pozostávať iba z písmen alebo číslic.
+Prezývka je už obsadená.
+Autentikácia neúspešná.
+Spojenie zlyhalo.
+Ďakujeme, že používate Cryptocat.
+Registrujem...
+Pripájam...
+Pripojený.
+Prosím píšte na klávesnici náhodné znaky počas pár sekúnd.
+Generujem kryptovacie kľúče...
+Skupinová konverzácia. Pre súkromnú konverzáciu kliknite na užívateľa.
+OTR odtlačok (pre súkromnú konverzáciu):
+Odtlačok skupinovej konverzácie:
+Vynuluj moje kryptovacie kľúče
+Vynulovanie Vašich kryptovacích kľúčov Vás odpojí. Zmenia sa aj Vaše odtlačky.
+Pokračovať
+Stav: Dostupný
+Stav: Preč
+O mne
+Notifikácie na ploche zapnuté
+Notifikácie na ploche vypnuté
+Zvukové notifikácie zapnuté
+Zvukové notifikácie vypnuté
+Zapamätaj si moju prezývku.
+Nepamätám si moju prezývku
+Odhlásiť
+Zobraz informácie
+Pošli zakryptovaný súbor
+Zobraz obrázok
+Stiahnuť súbor
+Konverzácia
+Sú povolené iba ZIP súbory a obrázky. Maximálna veľkosť súboru: (SIZE) MB.
+Chyba: Prosím, uistite sa, že súbor je ZIP alebo obrázok.
+Chyba: Súbor nemôže byť väčší než (SIZE) MB.
+Začať videohovor
+Ukončiť videohovor
+(NICKNAME) by s vami chcel začať videohovor.
+Zrušiť
+Ignorovať
+Odignorovať
+Overiť
+Overte identitu tohto používateľa opýtaním tajnej otázky. Odpovede musia byť rovnaké!
+Tajná otázka
+Tajná odpoveď
+Opýtať sa
+Spytujem sa...
+Neúspešné
+Identita overená.
+(NICKNAME) chce overiť vašu identitu. Prosím, odpovedzte na uvedenú tajnú otázku aby ste sa autentikovali:
+Vaša odpoveď musí presne zodpovedať odpovedi od (NICKNAME).
+Odpoveď
+Varovanie: Dostali ste nečitateľnú správu od užívateľa (NICKNAME). To môže značiť nedôveryhodného užívateľa alebo správy, ktorých doručenie zlyhalo. Možno tiež máte spustenú zastaralú verziu Cryptocat-u. Prosíme, skontrolujte dostupné aktualizácie.
+Varovanie: Vaša verzia Cryptocat-u je zastaralá. Prechod na novú verziu je silno odporúčaný! Bez aktualizácie nemožno pokračovať.
+Varovanie: táto správa nemohla byť zaslaná užívateľovi (NICKNAME)
+Overený
+Užívateľ nie je overený.
+Kliknite sem, ak sa chcete dozvedieť viac...
+Dozvedieť sa viac o overení
+Vždy, keď komunikujete prostredníctvom Cryptocatu, potrebujete overiť osoby, s ktorými sa rozprávate.
+Jedným spôsobom overenia pomocou Cryptocatu je položiť priateľovi nejakú tajnú otázku, na ktorú pozná odpoveď len on.
+Môžete ich tiež kontaktovať pomocou dôveryhodného kanála, napríklad telefonicky, a požiadať ich o prečítanie svojich odtlačkov.
+Odtlačky sú identifikátory, ktoré vám umožňujú overovať osoby. V každom rozhovore môžu byť iné.
+Bez overenia, hocikto by mohol zosobňovať alebo zachytávať vašu komunikáciu.
+Overovacie odtlačky pre tento kontakt sa zmenili. To by sa nemalo stávať a môže to označovať niečo podozrivé. Prosíme overte si tento kontakt predtým, než s ním začnete chatovať.
+Skupinový chat
+Facebook
+Pripojte sa k facebookovému chatu prostredníctvom Cryptocatu. Vytvorte si end-to-end konverzácie s facebookovými priateľmi tiež používajúcimi Cryptocat.
+Chatovať cez Facebook
+Stav konverzácie
+šifrované.
+nešifrované!
+Táto konverzácia nie je šifrovaná, pretože tento kontakt nepoužíva Cryptocat. Ak si chcete vychutnať šifrovaný chat, požiadajte vášho priateľa, nech si Cryptocat stiahne tiež.
\ No newline at end of file
diff --git a/src/core/locale/sv.txt b/src/core/locale/sv.txt
new file mode 100755
index 0000000..a744ae4
--- /dev/null
+++ b/src/core/locale/sv.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Privata konversationer för alla.
+Välkommen till Cryptocat. Här är några hjälpfulla tips: <ul><li>Cryptocat är ingen mirakelkur. Du ska aldrig lita helt på någon typ av mjukvara.</li><li class="key">Cryptocat kan inte skydda dig mot opålitliga människor eller keyloggers och anonymiserar inte din uppkoppling.</li></ul>
+Blogg
+Egen server
+Återställ
+Konversationsnamn
+namn
+ansluta
+Ange ett namn för konversationen och delge det till dem du önskar prata med.
+Skriv in namnet på det samtal du vill delta i.
+Vänligen skriv in ett konversationsnamn.
+Konversationsnamn kan endast bestå av bokstäver eller siffror.
+Vänligen skriv in ett smeknamn.
+Smeknamn kan endast bestå av bokstäver eller siffror.
+Smeknamnet användas redan.
+Autentiseringsfel.
+Anslutning misslyckades.
+Tack för att du använder Cryptocat!
+Registrerar...
+Ansluter...
+Uppkopplad
+Vänligen skriv in slumpmässiga tecken på ditt tangentbord under några sekunder.
+Genererar krypteringsnycklar...
+Gruppkonversation. Klicka på en användare för privat chatt.
+OTR fingeravtryck (för privata konversationer):
+Gruppkonversationens fingeravtryck:
+Återställ mina krypteringsnycklar
+Om du återställer dina krypteringsnycklar kommer du att kopplas ned. Ditt fingeravtryck kommer också att ändras.
+Fortsätt
+Status: Tillgänglig
+Status: Frånvarande
+Min information
+Skrivbordsnotifieringar På
+Skrivbordsnotifieringar Av
+Ljudnotifikationer På
+Ljudnotifikationer Av
+Kom ihåg mitt smeknamn
+Kom inte ihåg mitt smeknamn
+Logga ut
+Visa information
+Skicka krypterad fil
+visa bild
+ladda ner fil
+Konversation
+Endast zip-filer och bilder accepteras. Maximal filstorlek: (SIZE) MB.
+Fel: Kontrollera att din fil är en zip-fil eller en bild.
+Fel: Filen kan inte vara större än (SIZE) MB.
+Starta videochat
+Avsluta videochat
+(NICKNAME) vill starta en videochatt med dig.
+Avbryt
+Ignorera
+Sluta ignorera
+Autentisera
+Verifiera den här användarens identitet genom att ställa en hemlig fråga. Svaren måste matcha exakt!
+Hemlig fråga
+Hemligt svar
+Fråga
+Frågar...
+Misslyckades
+Identitet verifierad.
+(NICKNAME) vill verifiera din identitet. Var vänlig och svara på den hemliga frågan nedan för att autentisera dig själv:
+Ditt svar måste exakt matcha svaret angivet av (NICKNAME).
+Svar
+Varning: Du har fått ett oläsligt meddelande från (NICKNAME). Detta kan indikera att det är en opålitlig användare eller ett meddelande som misslyckades att tas emot. Du kan också köra en gammal version av Cryptocat. Var vänlig kolla efter uppdateringar.
+Varning: Din version av Cryptocat är gammal. Det är bestämt rekommenderat att uppdatera till den senaste versionen! Fortsätt inte utan att uppdatera till den senaste versionen.
+Varning: meddelandet kunde inte skickas till (NICKNAME)
+Autentiserad
+Användare ej autentiserad.
+Klicka för att lära dig mer...
+Lär dig mer om autentisering.
+Varje gång du har en Cryptocat konversation måste du autentisera personerna du pratar med.
+Ett sätt du kan autentisera är genom att använda Cryptocat till att fråga din vän en hemlig fråna som bara dom skulle veta svaret på.
+Du kan också kontakta dom via en pålitlig kanal, till exempel via telefon, och be dem läsa upp sina fingeravtryck.
+Fingeravtryck är identifierare som gör att du kan autentisera personer. Dom kan ändras mellan varje Cryptocat konversation.
+Utan autentisering skulle någon kunna utge sig för att vara du, eller avlyssna din kommunikation.
+Fingeravtrycken för autentisering av denna kontakt har ändrats. Det är inte meningen att det här ska hända och det kan indikera misstänkt beteende. Se till att autentisera denna kontakt innan du chattar med dem.
+Gruppchat
+Facebook
+Anslut till Facebook via Cryptocat. Ha helt krypterade chat med Facebook-vänner som också använder Cryptocat.
+Chatta via Facebook
+Konversationsstatus
+krypterat.
+inte krypterat!
+Denna konversation är inte krypterad för att denna kontakt använder inte  Cryptocat. Be din vän att ladda ner Cryptocat för att ha krypterat chat.
\ No newline at end of file
diff --git a/src/core/locale/tr.txt b/src/core/locale/tr.txt
new file mode 100644
index 0000000..ab8734f
--- /dev/null
+++ b/src/core/locale/tr.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Herkes İçin Güvenli İletişim
+Cryptocat'e hoşgeldiniz. İşte size birkaç faydalı ipucu: <ul><li>Cryptocat sihirli bir değnek değildir. Hayatınızda hiçbir yazılıma güvenmemelisiniz.</li><li class="key">Cryptocat sizi güvenilmez insanlar ya da tuş kaydedicilere karşı koruyamaz ve bağlantınızı anonimleştiremez.</li></ul>
+Blog
+Özel sunucu
+Yenile
+sohbet adı
+takma ad
+bağlan
+Konuşma için bir isim girin ve konuşmak istediğiniz insanlarla bu ismi paylaşın, ya da yeni insanlarla tanışmak için <strong>lobiye</stron> girin!
+Katılmak için bir sohbet adı girin
+Lütfen bir sohbet adı girin
+Konuşma ismi sadece harflerden ve numaralardan oluşabilir.
+Lütfen bir kullanıcı ismi girin.
+Takma ad sadece harflerden ve numaralardan oluşabilir.
+Kulanımda olan takma ad.
+Doğrulama hatası
+Bağlantı başarısız.
+Cryptocat kullandığınız için teşekkür ederiz.
+Kayıt ediliyor...
+Bağlanılıyor...
+Bağlandı.
+Lütfen bir kaç saniye mümkün olduğunca rastgele tuşlara basın.
+Şifreleme anahtarları oluşturuluyor...
+Grup sohbeti. Özel sohbet için bir kullanıcı üzerine tıklayınız.
+OTR parmak izi (özel konuşmalar için):
+Grup sohbeti parmak izi:
+Şifreleme anahtarımı yeniden oluştur.
+Şifreleme anahtarlarını yeniden oluşturmak bağlantınızı kopartacaktır. Parmak izleriniz de değişecektir.
+Devam et
+Durum: Uygun
+Durup: Uzakta
+Bilgilerim
+Masaüstü bildirimleri açık
+Masaüstü bildirimleri kapalı
+Ses bildirimleri açık
+Ses bildirimleri kapalı
+Benim takma adımı hatırla.
+Takma adımı hatırlama
+Çıkış
+Bilgileri Göster
+Şifrelenmiş dosya gönder
+resmi görüntüle
+dosya indir
+Sohbet
+Sadece ZIP dosyaları ve görüntü dosyaları kabul edilmektedir. Azami dosya boyutu: (SIZE) MB.
+Hata: Lütfen dosyanızın bir ZIP dosyası ya da bir görüntü dosyası olduğuna emin olun.
+Hata: Dosya (SIZE) MB'tan daha büyük olamaz.
+Görüntülü sohbeti başlatın
+Görüntülü sohbeti sonlandır
+(NICKNAME) sizinle görüntülü bir sohbet başlatmak istiyor.
+İptal et
+Yoksay
+Yoksaymaktan vazgeç
+Kimliği doğrulayın
+Gizli bir soru sorarak bu kullanıcının kimliğini onaylayın. Yanıtlar tam olarak eşleşmelidir!
+Gizli soru
+Gizli yanıt
+Sor
+Soruluyor...
+Başarısız
+Kimlik doğrulandı.
+(NICKNAME) kimliğinizi doğrulamak istiyor. Kendi kimliğinizi doğrulamak için lütfen aşağıdaki gizli soruya yanıt verin:
+Yanıtınız, (NICKNAME) tarafından verilen yanıtla birebir eşleşmelidir.
+Yanıt
+Uyarı: (NICKNAME) kullanıcısından okunamayan bir mesaj aldınız. Bunun sebebi kullanıcının güvenilmez olması ya da mesajın doğru şekilde alınamaması olabilir. Cryptocat'in eski bir sürümünü kullanıyor da olabilirsiniz. Lütfen güncelleştirmeleri kontrol edin.
+Uyarı: Cryptocat sürümünüz güncel değil. Son sürüme yükseltmeniz şiddetle önerilmektedir! Son sürüme yükseltmeden ilerlemeyin.
+Uyarı: bu mesaj (NICKNAME) kullanıcısına gönderilemiyor
+Kimlik doğrulandı
+Kullanıcının kimliği doğrulanmadı.
+Daha fazla bilgi için tıklayınız...
+Kimlik doğrulama hakkında daha fazla bilgi için
+Her Cryptocat görüşmenizde, konuştuğunuz kişinin kimliğini doğrulamanız gerekmektedir.
+Kimlik doğrulamanızın bir yolu Cryptocat'i kullanarak arkadaşınıza cevabını sadece onun bildiği bir gizli soru sormaktır.
+Kimliğini doğrulamak için o kişie güvenilir kanallardan da ulaşıp (telefon görüşmesi gibi) size parmak izini okumasını isteyebilirsiniz.
+Parmakizleri kişilerin kimliklerini onaylamanıza izi veren belirleyicilerdir. Her Cryptocat konuşması arasında değişebilirler.
+Kimlik doğrulaması olmadan, birisi başkasıymış gibi davranıyor ya da sizin iletişiminize müdahale ediyor olabilir.
+Bu kişinin kullanıcı onayı parmakizleri değişti. Bunun gerçekleşmemesi gerekiyor ve şüpheli bir davranışa işaret ediyor olabilir. Lütfen bu kişi ile konuşmadan önce kimlik onaylaması yapın.
+Grup Chat
+Facebook
+Cryptocat ile Facebook Chat'e bağlan. Yine Cryptocat kullanan Facebook arkadaşlarınızla yaptığınız şifrelenmiş sohbetleri sırayla yerleştirin.
+Facebook'tan sohbet
+Görüşme durumu
+şifrelendi.
+şifreli değil!
+Bu konuşma şifrelenmedi çünkü bu kişi Cryptocat kullanmamakta. Arkadaşınıza Cryptocat indirmesini söyleyin ve muhabbetin tadını çıkarın
\ No newline at end of file
diff --git a/src/core/locale/uk.txt b/src/core/locale/uk.txt
new file mode 100644
index 0000000..80d07fe
--- /dev/null
+++ b/src/core/locale/uk.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Приватні бесіди для кожного.
+Ласкаво просимо до Cryptocat. Ось декілька корисних порад: <ul><li>Cryptocat - це не панацея. Ви ніколи не повинні довіряти своє життя якій-небудь программі.</li><li class="key">Cryptocat не може захистити вас від ненадійних людей, кейлоґерів та не анонімізує ваше підключення.</li></ul>
+Блоґ
+Інший сервер
+Скинути
+назва бесіди
+нікнейм
+з’єднати
+Введіть назву для вашої бесіди та поділіться ним з людьми, з якими ви хотіли б поспілкуватись, або приєднайтеся до <strong>існуючої</strong>, щоб познайомитися з новими людьми!
+Уведіть назву бесіди, щоб приєднатися.
+Будь ласка, уведіть назву бесіди.
+Назва розмови може містити тільки букви або цифри.
+Будь ласка, уведіть нікнейм.
+Нікнейм може містити тільки букви або цифри.
+Нікнейм вже використовується.
+Помилка аутентифікації
+Помилка з’єднання.
+Дякуємо, що користуєтеся Cryptocat.
+Реєстрація...
+З’єднання...
+З’єднано.
+Будь ласка, наберіть на клавіатурі що-небудь випадкове протягом декількох секунд.
+Генерація ключів шифрування...
+Групова бесіда. Виберіть користувача для початку приватної бесіди.
+Відбиток OTR (для приватних бесід):
+Відбиток групових бесід:
+Скинути мої ключі шифрування
+Скидання ваших ключів шифрування від’єднає вас. Ваші відбитки також зміняться.
+Продовжити
+Статус: Доступний
+Статус: Відійшов
+Моя інформація
+Сповіщення на стільниці увімкнено.
+Сповіщення на стільниці вимкнено
+Аудіосповіщення увімкнено.
+Аудіосповіщення вимкнено.
+Запам’ятати мій нікнейм
+Не запам’ятовувати мій нікнейм
+Вийти
+Показати інформацію
+Відіслати зашифрований файл
+переглянути картинку
+завантажити файл
+Бесіда
+Дозволені тільки ZIP-файли та картинки. Максимальний розмір файлу: (SIZE) MB.
+Помилка: Будь ласка, переконайтеся, що ваш файл це ZIP-файл чи картинка.
+Помилка: Файл не може бути більше ніж (SIZE) MB.
+Розпочати відеочат
+Завершити відеочат
+(NICKNAME) хоче розпочати відеочат з вами.
+Відміна
+Ігнорувати
+Не ігнорувати
+Аутентифікувати
+Підтвердити особистість користувача за допомогою секретного запитання. Відповіді повинні співпадати!
+Секретне питання
+Секретна відповідь
+Запитати
+Запит...
+Помилка
+Особистість підтверджено.
+(NICKNAME) хоче підтвердити вашу особистість. Будь ласка, дайте відповідь на секретне запитання нижче щоб пройти аутентифікацію:
+Ваша відповідь повинна співпадати з відповіддю (NICKNAME).
+Відповісти
+ПОПЕРЕДЖЕННЯ: ви вже отримали нечитабельне повідомлення від  (NICKNAME). Це може означати, що ви ненадійний користувач або повідомлення не вдалося отримати. Ви також можете використовувати застарілу версію Cryptocat. Будь ласка, перевірте, чи доступні оновлення.
+Увага: Ваша версія Cryptocat застаріла. Вкрай рекомендуємо вам оновитися до останньої версії! Не продовжуйте використання без оновлення до останньої версії.
+Увага: це повідомлення не може бути надісланим до (NICKNAME)
+Аутентифіковано
+Користувача не аутентифіковано.
+Клікніть, щоб дізнатися більше...
+Дізнатися більше про аутентифікацію
+Ви повинні авторизувати співрозмовника щоразу при спілкуванні з кимось з допомогою Cryptocat.
+Для аутентифікації ви можете задати співрозмовнику таке питання, на який тільки він знає правильну відповідь.
+Також ви можете зв'язатися з ним іншим способом, наприклад, по телефону, і попросити його прочитати свої відбитки.
+Відбитки - це ідентифікатори, які дозволяють аунтетифікувати користувачів. У різних бесідах у Cryptocat вони можуть бути різними.
+Без аутентифікації сторонні люди можуть видавати себе за вашого співрозмовника або перехоплювати ваші повідомлення.
+Відбитки аутентифікації для цього користувача змінилися. Цього не повинно було статися. Можливо, щось тут нечисто. Будь ласка, перевірте автентичність цього користувача перед розмовою.
+Груповий чат
+Facebook
+Підключіть Facebook Chat до Cryptocat. Налаштуйте зашифровані чати з друзями у Facebook, які також використовують Cryptocat.
+Чат через Facebook
+Статус розмови
+зашифровано.
+незашифровано!
+Ця розмова незашифрована, тому що цей контакт не використовує Cryptocat. Попросіть вашого друга завантажити Cryptocat для того, щоб насолоджуватися зашифрованим чатом.
\ No newline at end of file
diff --git a/src/core/locale/vi.txt b/src/core/locale/vi.txt
new file mode 100755
index 0000000..8fcd27b
--- /dev/null
+++ b/src/core/locale/vi.txt
@@ -0,0 +1,87 @@
+ltr
+"Helvetica Neue", Helvetica, Arial, Verdana
+Đối thoại Riêng tư cho Mọi người.
+Chào mừng đến với Cryptocat. Sau đây là vài mẹo hữu ích: <ul><li> Cryptocat không phải là một viên đạn bạc. Bạn không bao giờ nên tin trọn đời vào một phần mềm nào.</li><li class="key">Cryptocat không thể bảo vệ bạn khỏi những kẻ không đáng tin cậy hay là trình theo dõi bàn phím, và không vô danh hoá kết nối của bạn.</li></ul>
+Blog
+Máy chủ tuỳ chỉnh
+Đặt lại
+tên cuộc đối thoại
+tên
+kết nối
+Nhập tên của cuộc đối thoại của bạn và chia sẽ nó cho những người mà bạn muốn nói chuyện, hay vào <strong>lobby</strong> để gặp người mới!
+Nhập vào tên của cuộc đối thoại để tham gia.
+Xin hãy nhập tên cuộc đối thoại.
+Tên cuộc trò chuyện chỉ có thể là chữ cái hoặc số đếm.
+Đánh vào tên.
+Tên biệt danh chỉ có thể là chữ cái hoặc số.
+Tên đang được sử dụng.
+Xác thực thất bại.
+Kết nối thất bại.
+Cám ơn bạn đã dùng Cryptocat.
+Đang ghi danh...
+Đang kết nối...
+Đã kết nối.
+Hãy nhấn các phím trên bàn phím một cách ngẫu nhiên trong vài giây.
+Đang tạo khoá mã hoá...
+Đối thoại nhóm. Nhấn vào một thành viên để trao đổi riêng.
+Vân tay OTR (dùng cho đối thoại riêng):
+Vân tay cho cuộc đối thoại nhóm:
+Khởi tạo lại chìa khóa mã hóa của tôi
+Khởi tạo lại chìa khóa mã hóa của bạn sẽ làm ngắt kết nối. Dấu vân tay của bạn cũng sẽ thay đổi.
+Tiếp tục
+Trạng Thái: Có mặt
+Trạng Thái: Không có mặt
+Thông tin Của Tôi
+Bật Thông báo
+Tắt Thông báo
+Mở Thông báo bằng Âm Thanh
+Tắt Thông báo bằng Âm thanh
+Ghi nhớ tên của tôi
+Đừng ghi nhớ tên của tôi
+Đăng xuất
+Thông tin Hiển thị
+Gửi tập tin mã hoá
+xem hình ảnh
+tải tập tin
+Cuộc đối thoại
+Chỉ chấp nhận định dạng ZIP và tập tin hình ảnh. Kích cỡ file lớn nhất: (SIZE) MB.
+Lỗi: Hãy đảm bảo rằng tập tin gửi đi có định dạng ZIP hay là tập tin hình ảnh.
+Lỗi: Tập tin không thể lớn hơn (SIZE) MB.
+Bắt đầu cuộc gọi video
+Kết thúc cuộc gọi video
+(NICKNAME) muốn bắt đầu tán gẫu hình với bạn.
+Hủy bỏ
+Bỏ qua
+Huỷ bỏ qua
+Xác minh
+Xác thực nhân dạng người dùng này bằng cách hỏi câu hỏi bí mật. Câu trả lời phải so khớp chính xác !
+Câu hỏi bí mật
+Câu trả lời bí mật
+Hỏi
+Đang hỏi...
+Hỏng
+Nhân dạng đã được xác thực
+(NICKNAME) muốn xác thực nhân dạng của bạn. Vui lòng trả lời câu hỏi bí mật bên dưới để xác minh bạn:
+Câu trả lời của bạn phải khớp chính xác với (NICKNAME).
+Trả lời
+Cảnh báo: Tin nhắn từ (NICKNAME) không thể đọc được. Đây có thể là tin nhắn từ một người dùng không đáng tin hoặc là tin nhắn không nhận được. Bạn cũng có thể đang dùng phiên bản hết hạn. Xin xem cập nhật mới.
+Cảnh báo: Phiên bản Cryptocat của bạn đã hết hạn. Chúng tôi nghiêm túc đề nghị bạn cấp nhận phiên bản mới nhất ! Đừng tiếp tục nếu chưa cập nhật mới nhất.
+Cảnh báo: tin nhắn này không thể gửi đến (NICKNAME)
+Đã xác minh
+Người dùng chưa được xác minh.
+Bấm vào để tìm hiểu thêm...
+Tìm hiểu thêm về quy trình xác minh
+Mỗi khi có cuộc chuyện trò qua Cryptocat, bạn cần xác minh người bạn muốn trò chuyện.
+Một cách để xác minh là dùng Cryptocat để hỏi người bạn một câu hỏi bí mật mà chỉ có họ biết câu trả lời.
+Bạn cũng có thể liên lạc với họ qua một kênh an toàn, thí dụ như điện thoại, và yêu cầu cung cấp dấu tay của họ.
+Dấu tay là ký hiệu nhận diện để giúp bạn xác minh người khác. Dấu tay có thể thay đổi sau mỗi lần trò chuyện qua Cryptocat.
+Nếu không có quy trình xác minh, ai đó có thể mạo nhận hoặc chận bắt các thông tin liên lạc của bạn.
+Dấu tay xác minh của người này đã thay đổi. Chuyện này đáng lẽ không xảy ra và vì vậy hành vi này rất khả nghi. Xin vui lòng xác minh người này trước khi trò chuyện.
+Trò chuyện nhóm
+Mạng xã hội Facebook
+Kết nối với đàm thoại Facebook qua Cryptocat. Thiết lập các cuộc hội thoại được mã hóa trực tiếp với những người bạn trên Facebook cũng đang sử dụng Cryptocat.
+Trò chuyện qua Facebook
+Trạng thái cuộc trò chuyện
+được mã hóa.
+Chưa được mã hóa!
+Cuộc trò chuyện này chưa được mã hóa vì người liên lạc này không sử dụng Cryptocat. Hãy yêu cầu bạn của bạn tải về Cryptocat để thưởng thức cuộc nói chuyện được mã hóa.
\ No newline at end of file
diff --git a/src/core/locale/zh-cn.txt b/src/core/locale/zh-cn.txt
new file mode 100755
index 0000000..1fca60e
--- /dev/null
+++ b/src/core/locale/zh-cn.txt
@@ -0,0 +1,87 @@
+ltr
+"Microsoft Yahei", "微软雅黑", Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif
+人人会用的保密聊天。
+欢迎来到 Cryptocat。 现给您提供一些有用的小贴士: <ul><li>Cryptocat 不是神器。您也不应该把生命托付给任何一款软件。</li><li class="key">对于靠不住的人或者是键盘记录器,Cryptocat 也无能为力保护您,并且也不能让您以匿名形式连接。</li></ul>
+博客
+自定义服务器
+重置
+会话名称
+昵称
+连接
+为您的群组会话输入一个名称,并将其分享给您的聊天对象,或者加入 <strong>lobby</strong> 来认识下其他人!
+请输入欲参加会话的名称
+请输入会话名称
+通话名称仅可使用字母或数字。
+请输入昵称
+昵称仅可使用字母或数字。
+此昵称已被人占用。
+身份验证失败。
+连接失败。
+谢谢使用 Cryptocat。
+正在注册...
+正在连接...
+已连接。
+请花几秒钟的时间,随意按动键盘上的数字、字母或符号键,尽量无规律。
+ 正在生成加密密钥...
+群组会话。点击单个用户可与其私下聊天。
+OTR 指纹 (用于私人会话):
+群组会话指纹:
+重置我的加密密钥
+重置加密密钥,将断开您的连接。您的指纹也会改变。
+继续
+状态:有空
+状态:离开
+我的信息
+桌面通知:已启用
+桌面通知:已关闭
+声音提醒:已启用
+声音提醒:已关闭
+记住我的昵称
+不要保存我的昵称
+退出
+显示信息
+加密发送文件
+查看图像
+下载文件
+会话
+仅支持 ZIP 或图像文件。文件最大体积:(SIZE) MB。
+错误:请确认文件为ZIP或图片文件
+错误:文件体积不得超过 (SIZE) MB。
+开始视频聊天
+结束视频聊天
+(NICKNAME) 想要和您视频聊天。
+取消
+忽略
+重新关注
+验证
+请提出一个私密问题,以验证该用户的身份。你们的答案必须完全一致。
+私密问题
+私密问题
+提问
+正在提问
+失败
+身份已验证
+(NICKNAME) 要验证您的身份。请回答下面的私密问题以自我验证。
+您的答案必须要和 (NICKNAME) 提供的答案完全一致。
+回答
+警告: 您收到了一条由 (NICKNAME) 发来的无效数据. 对方可能是一个不可信的用户, 或者对方并没有收到您的消息. 也可能是您的软件版本太低, 请检查更新.
+警告:您的 Cryptocat 版本已经过期。我们强烈建议您更新到最新版本!在更新到最新版本之前,请勿继续操作。
+警告:本条信息不能发送至(NICKNAME)
+已验证
+用户未被验证。
+点击了解更多……
+了解更多关于验证的信息
+每次您使用 Cryptocat 进行通话,您需要验证您的通话对象。
+一种验证方式是使用 Cryptocat 询问您的朋友一个只有他们知道答案的机密问题。
+您还可以通过可信渠道如手机联系他们,并要求他们读取指纹。
+指纹是验证他人身份的鉴别器。每次 Cryptocat 对话都可对其进行更改。
+如果没有验证,有人可能会冒充或者截取您的通讯信息。
+该联系人的指纹验证已更改。这种情况不应该发生,并表明存在可疑的行为。请在和他们聊天前验证该联系人。
+群组聊天
+Facebook
+通过 Cryptocat 连接到 Facebook 聊天。同使用 Cryptocat 的 Facebook 好友建立端对端的加密聊天。
+通过 Facebook 聊天
+通话状态
+已加密。
+未被加密!
+本次通话未被加密,原因是该联系人未使用 Cryptocat。让您的朋友下载 Cryptocat 来享受加密聊天。
\ No newline at end of file
diff --git a/src/core/locale/zh-hk.txt b/src/core/locale/zh-hk.txt
new file mode 100755
index 0000000..8fffd21
--- /dev/null
+++ b/src/core/locale/zh-hk.txt
@@ -0,0 +1,87 @@
+ltr
+"Microsoft Yahei", "微软雅黑", Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif, STXihei, "华文细黑"
+屬於每個人的私人對話。
+歡迎來到 Cryptocat。這裡是一些有用的提示:  <ul> <li> Cryptocat 不是靈丹妙藥,你從不應該。 </li> <li class="key"> Cryptocat 不能保障您被,鍵盤紀錄器,。 </li> </ul>
+博客
+自定義服務器
+重設
+通話名稱
+暱稱
+連接
+輸入你在會話中的名字, 並分享給你想要交談的人。
+輸入名稱加入對話。
+請輸入對話名稱。
+通話名稱必須是字母或數字。
+請輸入暱稱。
+暱稱必須是字母或數字。
+暱稱已在使用。
+驗證失敗。
+連接失敗。
+感謝您使用Cryptocat。
+註冊中...
+連接中...
+已連接
+請隨意在鍵盤上按鍵幾秒鐘。
+生產加密密鑰...
+群組對話。點擊一個用戶進行私人聊天。
+OTR特徵(私人對話):
+群組對話紀錄:
+重設我的加密鑰匙
+重設加密鑰匙你的連線將會中斷。你的特徵亦會改變。
+繼續
+狀態:有空
+狀態:離開
+我的信息
+桌面通知開啟
+桌面通知關閉
+音效通知開啟
+音效通知關閉
+記住我的暱稱
+不要記住我的暱稱
+登出
+顯示信息
+發送已加密的文件
+查看圖像
+下載文件
+對話
+隻接受ZIP文件和圖像文件。 最大文件大小: (SIZE) MB.
+出錯: 請確定您的文件是ZIP文件或圖像文件。
+出錯: 文件不能大於 (SIZE) MB。
+開始視頻聊天
+結束視頻聊天
+(NICKNAME) 希望和您開始視頻聊天。
+取消
+忽略
+重新关注
+验证
+请提出一个私密问题,以验证该用户的身份。你们的答案必须完全一致。
+私密问题
+私密问题
+提问
+正在提问
+失败
+身份已验证
+(NICKNAME) 要验证您的身份。请回答下面的私密问题以自我验证。
+您的答案必须要和 (NICKNAME) 提供的答案完全一致。
+回答
+警告:您从 (NICKNAME) 处收到了一条不可读消息。这可能表示一个不可信用户或者接收失败的消息。
+警告:您的 Cryptocat 版本已经过期。我们强烈建议您更新到最新版本!在更新到最新版本之前,请勿继续操作。
+警告:本条信息不能发送至(NICKNAME)
+已验证
+用户未被验证。
+点击了解更多……
+了解更多关于验证的信息
+每次您使用 Cryptocat 进行通话,您需要验证您的通话对象。
+一种验证方式是使用 Cryptocat 询问您的朋友一个只有他们知道答案的机密问题。
+您还可以通过可信渠道如手机联系他们,并要求他们读取指纹。
+指纹是验证他人身份的鉴别器。每次 Cryptocat 对话都可对其进行更改。
+如果没有验证,有人可能会冒充或者截取您的通讯信息。
+该联系人的指纹验证已更改。这种情况不应该发生,并表明存在可疑的行为。请在和他们聊天前验证该联系人。
+群组聊天
+Facebook
+通过 Cryptocat 连接到 Facebook 聊天。同使用 Cryptocat 的 Facebook 好友建立端对端的加密聊天。
+通过 Facebook 聊天
+通话状态
+已加密。
+未被加密!
+本次通话未被加密,原因是该联系人未使用 Cryptocat。让您的朋友下载 Cryptocat 来享受加密聊天。
\ No newline at end of file
diff --git a/src/core/manifest.json b/src/core/manifest.json
new file mode 100755
index 0000000..5f2270c
--- /dev/null
+++ b/src/core/manifest.json
@@ -0,0 +1,26 @@
+{
+	"manifest_version": 2,
+	"name": "Cryptocat",
+	"description": "Have encrypted, private conversations. Cryptocat is an open source encrypted instant messaging platform.",
+	"version": "2.2.2",
+	"offline_enabled": true,
+	"minimum_chrome_version": "23",
+	"icons": {
+		"16": "img/icon-16.png",
+		"48": "img/icon-48.png",
+		"128": "img/icon-128.png"
+	},
+	"app": {
+		"launch": {
+			"local_path": "index.html"
+		}
+	},
+	"background": {
+		"scripts": ["chrome.js"],
+		"persistent": false
+	},
+	"permissions": [
+		"notifications",
+		"storage"
+	]
+}
diff --git a/src/core/snd/balloon.mp3 b/src/core/snd/balloon.mp3
new file mode 100644
index 0000000..1dfec06
Binary files /dev/null and b/src/core/snd/balloon.mp3 differ
diff --git a/src/core/snd/balloon.ogg b/src/core/snd/balloon.ogg
new file mode 100644
index 0000000..4b71694
Binary files /dev/null and b/src/core/snd/balloon.ogg differ
diff --git a/src/core/snd/keygenEnd.mp3 b/src/core/snd/keygenEnd.mp3
new file mode 100644
index 0000000..1b769f5
Binary files /dev/null and b/src/core/snd/keygenEnd.mp3 differ
diff --git a/src/core/snd/keygenEnd.ogg b/src/core/snd/keygenEnd.ogg
new file mode 100644
index 0000000..ea6cf19
Binary files /dev/null and b/src/core/snd/keygenEnd.ogg differ
diff --git a/src/core/snd/keygenLoop.mp3 b/src/core/snd/keygenLoop.mp3
new file mode 100644
index 0000000..1e29e20
Binary files /dev/null and b/src/core/snd/keygenLoop.mp3 differ
diff --git a/src/core/snd/keygenLoop.ogg b/src/core/snd/keygenLoop.ogg
new file mode 100644
index 0000000..8d0bbbf
Binary files /dev/null and b/src/core/snd/keygenLoop.ogg differ
diff --git a/src/core/snd/keygenStart.mp3 b/src/core/snd/keygenStart.mp3
new file mode 100644
index 0000000..3036b69
Binary files /dev/null and b/src/core/snd/keygenStart.mp3 differ
diff --git a/src/core/snd/keygenStart.ogg b/src/core/snd/keygenStart.ogg
new file mode 100644
index 0000000..fd8339d
Binary files /dev/null and b/src/core/snd/keygenStart.ogg differ
diff --git a/src/core/snd/msgGet.mp3 b/src/core/snd/msgGet.mp3
new file mode 100644
index 0000000..35d9a27
Binary files /dev/null and b/src/core/snd/msgGet.mp3 differ
diff --git a/src/core/snd/msgGet.ogg b/src/core/snd/msgGet.ogg
new file mode 100644
index 0000000..860aee9
Binary files /dev/null and b/src/core/snd/msgGet.ogg differ
diff --git a/src/core/snd/userJoin.mp3 b/src/core/snd/userJoin.mp3
new file mode 100644
index 0000000..0f518ae
Binary files /dev/null and b/src/core/snd/userJoin.mp3 differ
diff --git a/src/core/snd/userJoin.ogg b/src/core/snd/userJoin.ogg
new file mode 100644
index 0000000..fef7213
Binary files /dev/null and b/src/core/snd/userJoin.ogg differ
diff --git a/src/core/snd/userLeave.mp3 b/src/core/snd/userLeave.mp3
new file mode 100644
index 0000000..273a3e1
Binary files /dev/null and b/src/core/snd/userLeave.mp3 differ
diff --git a/src/core/snd/userLeave.ogg b/src/core/snd/userLeave.ogg
new file mode 100644
index 0000000..eebe392
Binary files /dev/null and b/src/core/snd/userLeave.ogg differ
diff --git a/src/firefox/chrome.manifest b/src/firefox/chrome.manifest
new file mode 100755
index 0000000..9265875
--- /dev/null
+++ b/src/firefox/chrome.manifest
@@ -0,0 +1,5 @@
+content cryptocat chrome/content/
+overlay chrome://browser/content/browser.xul chrome://cryptocat/content/browser.xul
+locale cryptocat en-US locale/en-US/
+skin cryptocat classic/1.0 skin/
+style chrome://global/content/customizeToolbar.xul chrome://cryptocat/skin/skin.css
\ No newline at end of file
diff --git a/src/firefox/chrome/content/browser.xul b/src/firefox/chrome/content/browser.xul
new file mode 100755
index 0000000..08d9252
--- /dev/null
+++ b/src/firefox/chrome/content/browser.xul
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://cryptocat/skin/skin.css" type="text/css"?> 
+<!DOCTYPE cryptocat SYSTEM "chrome://cryptocat/locale/translations.dtd">
+<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+		<script type="text/javascript" src="cryptocat.js" />
+		
+		<menupopup id="menu_ToolsPopup"> 
+			<menuitem label="Cryptocat" key="cryptocatRunKey" oncommand="CryptocatFirefox.run()"/> 
+		</menupopup>
+		
+		<keyset>
+			<key id="cryptocatRunKey" modifiers="alt shift" key="C" oncommand="CryptocatFirefox.run()"/>
+		</keyset>
+		
+		<addonbar id="status-bar">
+			<statusbarpanel id="cryptocatStatusbarIcon" class="statusbarpanel-iconic" src="chrome://cryptocat/skin/bar.png" tooltiptext="&opencryptocat;" onclick="CryptocatFirefox.run()" />
+		</addonbar>
+		
+		<toolbarpalette id="BrowserToolbarPalette">
+			<toolbarbutton id="cryptocatToolbarButton" label="Cryptocat" tooltiptext="&opencryptocat;" oncommand="CryptocatFirefox.run()"/>
+		</toolbarpalette>
+		
+		<vbox id="appmenuSecondaryPane">
+			<menuitem id="cryptocatMenuItem"
+				label="Cryptocat"
+				accesskey="cryptocatRunKey"
+				oncommand="CryptocatFirefox.run()"
+				insertafter="appmenu_addons" 
+				class="menuitem-iconic"
+				image="chrome://cryptocat/skin/menu.png" />
+		</vbox>
+</overlay>
\ No newline at end of file
diff --git a/src/firefox/chrome/content/cryptocat.js b/src/firefox/chrome/content/cryptocat.js
new file mode 100755
index 0000000..966331b
--- /dev/null
+++ b/src/firefox/chrome/content/cryptocat.js
@@ -0,0 +1,50 @@
+/*jshint -W117*/
+
+Components.utils.import('resource://gre/modules/Services.jsm')
+Components.utils.import('resource://gre/modules/ctypes.jsm')
+var prefsService = Services.prefs
+
+var CryptocatFirefox = {}
+
+CryptocatFirefox.init = function() {
+	var firstRun = prefsService.getBoolPref('extensions.cryptocat.firstRun')
+	if (firstRun) {
+		Application.prefs.setValue('extensions.cryptocat.firstRun', false)
+		var navBar = document.getElementById('nav-bar')
+		var newSet = navBar.currentSet + ',cryptocatToolbarButton'
+		navBar.currentSet = newSet
+		navBar.setAttribute('currentset', newSet)
+		document.persist('nav-bar', 'currentset')
+		window.setTimeout(function() {
+			gBrowser.selectedTab = gBrowser.addTab('chrome://cryptocat/content/data/firstRun.html')
+		}, 1500)
+	}
+}
+
+CryptocatFirefox.run = function() {
+	gBrowser.selectedTab = gBrowser.addTab('chrome://cryptocat/content/data/index.html')
+	window.addEventListener('cryptocatFirefoxStorage', function(evt) {
+		var type = evt.target.getAttribute('type')
+		if (type === 'set') {
+			Application.prefs.setValue(
+				'extensions.cryptocat.' + evt.target.getAttribute('key'),
+				evt.target.getAttribute('val')
+			)
+		}
+		if (type === 'get') {
+			var get = prefsService.getCharPref(
+				'extensions.cryptocat.' + evt.target.getAttribute('key')
+			)
+			if (get.length) {
+				evt.target.setAttribute('firefoxStorageGet', get)
+			}
+		}
+		if (type === 'remove') {
+			Application.prefs.setValue(
+				'extensions.cryptocat.' + evt.target.getAttribute('key'), ''
+			)
+		}
+	}, false, true)
+}
+
+window.addEventListener('load', CryptocatFirefox.init(), false)
diff --git a/src/firefox/defaults/preferences/prefs.js b/src/firefox/defaults/preferences/prefs.js
new file mode 100755
index 0000000..ab64c55
--- /dev/null
+++ b/src/firefox/defaults/preferences/prefs.js
@@ -0,0 +1,2 @@
+pref("extensions.cryptocat.autoRun", false);
+pref("extensions.cryptocat.firstRun", true);
\ No newline at end of file
diff --git a/src/firefox/install.rdf b/src/firefox/install.rdf
new file mode 100755
index 0000000..38be6f9
--- /dev/null
+++ b/src/firefox/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+	<Description about="urn:mozilla:install-manifest">
+		<em:id>cryptocat at crypto.cat</em:id>
+		<em:name>Cryptocat</em:name>
+		<em:version>2.2.2</em:version>
+		<em:type>2</em:type>
+		<em:creator>Cryptocat Team</em:creator>
+		<em:description>Have encrypted, private conversations. Cryptocat is an open source encrypted instant messaging platform.</em:description>
+		<em:homepageURL>https://crypto.cat</em:homepageURL>
+		<em:iconURL>chrome://cryptocat/content/data/img/icon-48.png</em:iconURL>
+		<em:targetApplication>
+			<Description>
+				<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+				<em:minVersion>21.0</em:minVersion>
+				<em:maxVersion>31.*</em:maxVersion>
+			</Description>
+		</em:targetApplication>
+	</Description>
+</RDF>
diff --git a/src/firefox/locale/en-US/translations.dtd b/src/firefox/locale/en-US/translations.dtd
new file mode 100755
index 0000000..ce0c288
--- /dev/null
+++ b/src/firefox/locale/en-US/translations.dtd
@@ -0,0 +1 @@
+<!ENTITY opencryptocat "Open Cryptocat">
\ No newline at end of file
diff --git a/src/firefox/skin/bar.png b/src/firefox/skin/bar.png
new file mode 100644
index 0000000..1fb478c
Binary files /dev/null and b/src/firefox/skin/bar.png differ
diff --git a/src/firefox/skin/menu.png b/src/firefox/skin/menu.png
new file mode 100644
index 0000000..79cd72b
Binary files /dev/null and b/src/firefox/skin/menu.png differ
diff --git a/src/firefox/skin/skin.css b/src/firefox/skin/skin.css
new file mode 100755
index 0000000..d2d4f0b
--- /dev/null
+++ b/src/firefox/skin/skin.css
@@ -0,0 +1,3 @@
+#cryptocatToolbarButton {
+	list-style-image: url("chrome://cryptocat/skin/bar.png");
+}
\ No newline at end of file
diff --git a/src/mac/Cryptocat-Info.plist b/src/mac/Cryptocat-Info.plist
new file mode 100644
index 0000000..b8ae743
--- /dev/null
+++ b/src/mac/Cryptocat-Info.plist
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>${EXECUTABLE_NAME}</string>
+	<key>CFBundleIconFile</key>
+	<string>Cryptocat</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.cryptocat.cryptocat-mac</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>${PRODUCT_NAME}</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>2.2.2</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>2.2.2</string>
+	<key>LSApplicationCategoryType</key>
+	<string>public.app-category.social-networking</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>${MACOSX_DEPLOYMENT_TARGET}</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>Copyright © 2014 Cryptocat. All rights reserved.</string>
+	<key>NSMainNibFile</key>
+	<string>MainMenu</string>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+</dict>
+</plist>
diff --git a/src/mac/Cryptocat-Prefix.pch b/src/mac/Cryptocat-Prefix.pch
new file mode 100644
index 0000000..597cf5a
--- /dev/null
+++ b/src/mac/Cryptocat-Prefix.pch
@@ -0,0 +1,8 @@
+//
+// Prefix header for all source files of the 'Cryptocat' target in the 'Cryptocat' project
+//
+
+#ifdef __OBJC__
+	#import <Cocoa/Cocoa.h>
+	#import "kConstants.h"
+#endif
diff --git a/src/mac/Cryptocat.entitlements b/src/mac/Cryptocat.entitlements
new file mode 100644
index 0000000..85c03d7
--- /dev/null
+++ b/src/mac/Cryptocat.entitlements
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<true/>
+	<key>com.apple.security.files.user-selected.read-write</key>
+	<true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
+	<key>com.apple.security.network.server</key>
+	<true/>
+</dict>
+</plist>
diff --git a/src/mac/Cryptocat.icns b/src/mac/Cryptocat.icns
new file mode 100644
index 0000000..80045ce
Binary files /dev/null and b/src/mac/Cryptocat.icns differ
diff --git a/src/mac/Cryptocat.xcodeproj/project.pbxproj b/src/mac/Cryptocat.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..760d07f
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/project.pbxproj
@@ -0,0 +1,379 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		5F179561177395E600D286E0 /* CryptocatAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F7EA8E517738FF500D619BB /* CryptocatAppDelegate.m */; };
+		5F17956217739F3300D286E0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5F7EA8E61773903400D619BB /* MainMenu.xib */; };
+		5F1795631773A92400D286E0 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5F7EA8E91773904800D619BB /* Credits.rtf */; };
+		5F4ADDC017737FCF00309087 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F4ADDBF17737FCF00309087 /* Cocoa.framework */; };
+		5F7EA8E11773820400D619BB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F7EA8E01773820400D619BB /* WebKit.framework */; };
+		5F7EA8F21773905A00D619BB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F7EA8F11773905A00D619BB /* main.m */; };
+		5F7EA8F5177390FC00D619BB /* Cryptocat.icns in Resources */ = {isa = PBXBuildFile; fileRef = 5F7EA8F4177390FC00D619BB /* Cryptocat.icns */; };
+		5F7EA8F71773914D00D619BB /* htdocs in Resources */ = {isa = PBXBuildFile; fileRef = 5F7EA8F61773914D00D619BB /* htdocs */; };
+		B608451217756705004DFFD0 /* CryptocatWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = B608451117756705004DFFD0 /* CryptocatWindowController.m */; };
+		B608451417756B58004DFFD0 /* CryptocatWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B608451317756B58004DFFD0 /* CryptocatWindowController.xib */; };
+		B633A4D3177642F800BC20ED /* CryptocatWindowManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B633A4D2177642F800BC20ED /* CryptocatWindowManager.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		B6F5427A17E79D50008E3309 /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		5F179560177395A100D286E0 /* Cryptocat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Cryptocat.entitlements; sourceTree = SOURCE_ROOT; };
+		5F198EFC1778C7CC005CAFBF /* fileUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fileUtils.h; sourceTree = SOURCE_ROOT; };
+		5F4ADDBC17737FCF00309087 /* Cryptocat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cryptocat.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		5F4ADDBF17737FCF00309087 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+		5F4ADDC217737FCF00309087 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
+		5F4ADDC317737FCF00309087 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
+		5F4ADDC417737FCF00309087 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+		5F7EA8E01773820400D619BB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
+		5F7EA8E417738FF500D619BB /* CryptocatAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CryptocatAppDelegate.h; sourceTree = SOURCE_ROOT; };
+		5F7EA8E517738FF500D619BB /* CryptocatAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CryptocatAppDelegate.m; sourceTree = SOURCE_ROOT; };
+		5F7EA8E71773903400D619BB /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = SOURCE_ROOT; };
+		5F7EA8EA1773904800D619BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = SOURCE_ROOT; };
+		5F7EA8ED1773904E00D619BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = SOURCE_ROOT; };
+		5F7EA8EF1773905400D619BB /* Cryptocat-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Cryptocat-Info.plist"; sourceTree = SOURCE_ROOT; };
+		5F7EA8F11773905A00D619BB /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; };
+		5F7EA8F31773906100D619BB /* Cryptocat-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Cryptocat-Prefix.pch"; sourceTree = SOURCE_ROOT; };
+		5F7EA8F4177390FC00D619BB /* Cryptocat.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Cryptocat.icns; sourceTree = "<group>"; };
+		5F7EA8F61773914D00D619BB /* htdocs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = htdocs; sourceTree = SOURCE_ROOT; };
+		B608451017756705004DFFD0 /* CryptocatWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptocatWindowController.h; sourceTree = SOURCE_ROOT; };
+		B608451117756705004DFFD0 /* CryptocatWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptocatWindowController.m; sourceTree = SOURCE_ROOT; };
+		B608451317756B58004DFFD0 /* CryptocatWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CryptocatWindowController.xib; sourceTree = SOURCE_ROOT; };
+		B633A4D1177642F800BC20ED /* CryptocatWindowManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptocatWindowManager.h; sourceTree = SOURCE_ROOT; };
+		B633A4D2177642F800BC20ED /* CryptocatWindowManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptocatWindowManager.m; sourceTree = SOURCE_ROOT; };
+		B66BDA521775A038007BDB39 /* kConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = kConstants.h; sourceTree = SOURCE_ROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		5F4ADDB917737FCF00309087 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F7EA8E11773820400D619BB /* WebKit.framework in Frameworks */,
+				5F4ADDC017737FCF00309087 /* Cocoa.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		5F4ADDB317737FCF00309087 = {
+			isa = PBXGroup;
+			children = (
+				5F7EA8F4177390FC00D619BB /* Cryptocat.icns */,
+				5F4ADDC517737FCF00309087 /* Cryptocat */,
+				5F4ADDBE17737FCF00309087 /* Frameworks */,
+				5F4ADDBD17737FCF00309087 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		5F4ADDBD17737FCF00309087 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				5F4ADDBC17737FCF00309087 /* Cryptocat.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		5F4ADDBE17737FCF00309087 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				5F4ADDBF17737FCF00309087 /* Cocoa.framework */,
+				5F7EA8E01773820400D619BB /* WebKit.framework */,
+				5F4ADDC117737FCF00309087 /* Other Frameworks */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		5F4ADDC117737FCF00309087 /* Other Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				5F4ADDC217737FCF00309087 /* AppKit.framework */,
+				5F4ADDC317737FCF00309087 /* CoreData.framework */,
+				5F4ADDC417737FCF00309087 /* Foundation.framework */,
+			);
+			name = "Other Frameworks";
+			sourceTree = "<group>";
+		};
+		5F4ADDC517737FCF00309087 /* Cryptocat */ = {
+			isa = PBXGroup;
+			children = (
+				5F179560177395A100D286E0 /* Cryptocat.entitlements */,
+				5F7EA8E417738FF500D619BB /* CryptocatAppDelegate.h */,
+				5F7EA8E517738FF500D619BB /* CryptocatAppDelegate.m */,
+				B633A4D1177642F800BC20ED /* CryptocatWindowManager.h */,
+				B633A4D2177642F800BC20ED /* CryptocatWindowManager.m */,
+				B608451017756705004DFFD0 /* CryptocatWindowController.h */,
+				B608451117756705004DFFD0 /* CryptocatWindowController.m */,
+				B608451317756B58004DFFD0 /* CryptocatWindowController.xib */,
+				5F198EFC1778C7CC005CAFBF /* fileUtils.h */,
+				B66BDA521775A038007BDB39 /* kConstants.h */,
+				5F7EA8E61773903400D619BB /* MainMenu.xib */,
+				5F7EA8F61773914D00D619BB /* htdocs */,
+				5F4ADDC617737FCF00309087 /* Supporting Files */,
+			);
+			path = Cryptocat;
+			sourceTree = "<group>";
+		};
+		5F4ADDC617737FCF00309087 /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				5F7EA8EC1773904E00D619BB /* InfoPlist.strings */,
+				5F7EA8EF1773905400D619BB /* Cryptocat-Info.plist */,
+				5F7EA8F31773906100D619BB /* Cryptocat-Prefix.pch */,
+				5F7EA8F11773905A00D619BB /* main.m */,
+				5F7EA8E91773904800D619BB /* Credits.rtf */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		5F4ADDBB17737FCF00309087 /* Cryptocat */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5F4ADDD917737FCF00309087 /* Build configuration list for PBXNativeTarget "Cryptocat" */;
+			buildPhases = (
+				5F4ADDBA17737FCF00309087 /* Resources */,
+				5F4ADDB817737FCF00309087 /* Sources */,
+				B6F5427A17E79D50008E3309 /* CopyFiles */,
+				5F4ADDB917737FCF00309087 /* Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Cryptocat;
+			productName = Cryptocat;
+			productReference = 5F4ADDBC17737FCF00309087 /* Cryptocat.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		5F4ADDB417737FCF00309087 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				CLASSPREFIX = Cryptocat;
+				LastUpgradeCheck = 0500;
+				ORGANIZATIONNAME = Cryptocat;
+				TargetAttributes = {
+					5F4ADDBB17737FCF00309087 = {
+						DevelopmentTeam = HC689Z8JM4;
+						SystemCapabilities = {
+							com.apple.Sandbox = {
+								enabled = 1;
+							};
+						};
+					};
+				};
+			};
+			buildConfigurationList = 5F4ADDB717737FCF00309087 /* Build configuration list for PBXProject "Cryptocat" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+			);
+			mainGroup = 5F4ADDB317737FCF00309087;
+			productRefGroup = 5F4ADDBD17737FCF00309087 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				5F4ADDBB17737FCF00309087 /* Cryptocat */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		5F4ADDBA17737FCF00309087 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F7EA8F5177390FC00D619BB /* Cryptocat.icns in Resources */,
+				5F17956217739F3300D286E0 /* MainMenu.xib in Resources */,
+				5F1795631773A92400D286E0 /* Credits.rtf in Resources */,
+				5F7EA8F71773914D00D619BB /* htdocs in Resources */,
+				B608451417756B58004DFFD0 /* CryptocatWindowController.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		5F4ADDB817737FCF00309087 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F7EA8F21773905A00D619BB /* main.m in Sources */,
+				5F179561177395E600D286E0 /* CryptocatAppDelegate.m in Sources */,
+				B608451217756705004DFFD0 /* CryptocatWindowController.m in Sources */,
+				B633A4D3177642F800BC20ED /* CryptocatWindowManager.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		5F7EA8E61773903400D619BB /* MainMenu.xib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5F7EA8E71773903400D619BB /* en */,
+			);
+			name = MainMenu.xib;
+			sourceTree = "<group>";
+		};
+		5F7EA8E91773904800D619BB /* Credits.rtf */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5F7EA8EA1773904800D619BB /* en */,
+			);
+			name = Credits.rtf;
+			sourceTree = "<group>";
+		};
+		5F7EA8EC1773904E00D619BB /* InfoPlist.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5F7EA8ED1773904E00D619BB /* en */,
+			);
+			name = InfoPlist.strings;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		5F4ADDD717737FCF00309087 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.7;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+			};
+			name = Debug;
+		};
+		5F4ADDD817737FCF00309087 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = YES;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.7;
+				SDKROOT = macosx;
+			};
+			name = Release;
+		};
+		5F4ADDDA17737FCF00309087 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_ENTITLEMENTS = Cryptocat.entitlements;
+				CODE_SIGN_IDENTITY = "";
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "";
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "Cryptocat-Prefix.pch";
+				INFOPLIST_FILE = "Cryptocat-Info.plist";
+				MACOSX_DEPLOYMENT_TARGET = 10.7;
+				OTHER_CODE_SIGN_FLAGS = "--deep";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "EA48A8C0-7EAB-4807-AE96-BB7182B86D14";
+				SDKROOT = macosx;
+				WRAPPER_EXTENSION = app;
+			};
+			name = Debug;
+		};
+		5F4ADDDB17737FCF00309087 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_ENTITLEMENTS = Cryptocat.entitlements;
+				CODE_SIGN_IDENTITY = "";
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "";
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "Cryptocat-Prefix.pch";
+				INFOPLIST_FILE = "Cryptocat-Info.plist";
+				MACOSX_DEPLOYMENT_TARGET = 10.7;
+				OTHER_CODE_SIGN_FLAGS = "--deep";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "EA48A8C0-7EAB-4807-AE96-BB7182B86D14";
+				SDKROOT = macosx;
+				WRAPPER_EXTENSION = app;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		5F4ADDB717737FCF00309087 /* Build configuration list for PBXProject "Cryptocat" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5F4ADDD717737FCF00309087 /* Debug */,
+				5F4ADDD817737FCF00309087 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5F4ADDD917737FCF00309087 /* Build configuration list for PBXNativeTarget "Cryptocat" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5F4ADDDA17737FCF00309087 /* Debug */,
+				5F4ADDDB17737FCF00309087 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 5F4ADDB417737FCF00309087 /* Project object */;
+}
diff --git a/src/mac/Cryptocat.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/mac/Cryptocat.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..001e08d
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:Cryptocat.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/src/mac/Cryptocat.xcodeproj/project.xcworkspace/xcshareddata/Cryptocat.xccheckout b/src/mac/Cryptocat.xcodeproj/project.xcworkspace/xcshareddata/Cryptocat.xccheckout
new file mode 100644
index 0000000..53003d6
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/project.xcworkspace/xcshareddata/Cryptocat.xccheckout
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDESourceControlProjectFavoriteDictionaryKey</key>
+	<false/>
+	<key>IDESourceControlProjectIdentifier</key>
+	<string>DAE29419-27DD-4C9E-88EB-06EC4552F3E2</string>
+	<key>IDESourceControlProjectName</key>
+	<string>Cryptocat</string>
+	<key>IDESourceControlProjectOriginsDictionary</key>
+	<dict>
+		<key>06014FCE-5928-47C6-B292-5BEF829DCAEF</key>
+		<string>https://github.com/grabhive/Tor.framework.git</string>
+		<key>0A9A43B2-6286-45B2-8BDE-CBB11C124E04</key>
+		<string>https://git.torproject.org/git/tor</string>
+		<key>0ED76105-743B-4F70-96C9-EFA59278C725</key>
+		<string>https://github.com/cryptocat/cryptocat.git</string>
+		<key>89E82F23-2513-4046-9653-2C3E80E9CD84</key>
+		<string>https://github.com/pokeb/asi-http-request.git</string>
+	</dict>
+	<key>IDESourceControlProjectPath</key>
+	<string>src/mac/Cryptocat.xcodeproj/project.xcworkspace</string>
+	<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
+	<dict>
+		<key>06014FCE-5928-47C6-B292-5BEF829DCAEF</key>
+		<string>../../Libraries/Tor</string>
+		<key>0A9A43B2-6286-45B2-8BDE-CBB11C124E04</key>
+		<string>../../Libraries/Tor/libtor</string>
+		<key>0ED76105-743B-4F70-96C9-EFA59278C725</key>
+		<string>../../../..</string>
+		<key>89E82F23-2513-4046-9653-2C3E80E9CD84</key>
+		<string>../../Libraries/Tor/ASIHTTPRequest</string>
+	</dict>
+	<key>IDESourceControlProjectURL</key>
+	<string>https://github.com/cryptocat/cryptocat.git</string>
+	<key>IDESourceControlProjectVersion</key>
+	<integer>110</integer>
+	<key>IDESourceControlProjectWCCIdentifier</key>
+	<string>0ED76105-743B-4F70-96C9-EFA59278C725</string>
+	<key>IDESourceControlProjectWCConfigurations</key>
+	<array>
+		<dict>
+			<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
+			<string>public.vcs.git</string>
+			<key>IDESourceControlWCCIdentifierKey</key>
+			<string>89E82F23-2513-4046-9653-2C3E80E9CD84</string>
+			<key>IDESourceControlWCCName</key>
+			<string>ASIHTTPRequest</string>
+		</dict>
+		<dict>
+			<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
+			<string>public.vcs.git</string>
+			<key>IDESourceControlWCCIdentifierKey</key>
+			<string>0ED76105-743B-4F70-96C9-EFA59278C725</string>
+			<key>IDESourceControlWCCName</key>
+			<string>cryptocat</string>
+		</dict>
+		<dict>
+			<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
+			<string>public.vcs.git</string>
+			<key>IDESourceControlWCCIdentifierKey</key>
+			<string>0A9A43B2-6286-45B2-8BDE-CBB11C124E04</string>
+			<key>IDESourceControlWCCName</key>
+			<string>libtor</string>
+		</dict>
+		<dict>
+			<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
+			<string>public.vcs.git</string>
+			<key>IDESourceControlWCCIdentifierKey</key>
+			<string>06014FCE-5928-47C6-B292-5BEF829DCAEF</string>
+			<key>IDESourceControlWCCName</key>
+			<string>Tor</string>
+		</dict>
+	</array>
+</dict>
+</plist>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/Cryptocat.xcscheme b/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/Cryptocat.xcscheme
new file mode 100644
index 0000000..0c63457
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/Cryptocat.xcscheme
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0460"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+               BuildableName = "Cryptocat.app"
+               BlueprintName = "Cryptocat"
+               ReferencedContainer = "container:Cryptocat.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      buildConfiguration = "Debug">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </TestAction>
+   <LaunchAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Debug"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Release"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/xcschememanagement.plist b/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..63c78ec
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/dev.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>Cryptocat.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>5F4ADDBB17737FCF00309087</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/Cryptocat.xcscheme b/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/Cryptocat.xcscheme
new file mode 100644
index 0000000..16d833c
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/Cryptocat.xcscheme
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0500"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+               BuildableName = "Cryptocat.app"
+               BlueprintName = "Cryptocat"
+               ReferencedContainer = "container:Cryptocat.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      buildConfiguration = "Debug">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </TestAction>
+   <LaunchAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Debug"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Release"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/xcschememanagement.plist b/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..63c78ec
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/fred.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>Cryptocat.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>5F4ADDBB17737FCF00309087</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/Cryptocat.xcscheme b/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/Cryptocat.xcscheme
new file mode 100644
index 0000000..16d833c
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/Cryptocat.xcscheme
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0500"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+               BuildableName = "Cryptocat.app"
+               BlueprintName = "Cryptocat"
+               ReferencedContainer = "container:Cryptocat.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      buildConfiguration = "Debug">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </TestAction>
+   <LaunchAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Debug"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Release"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "5F4ADDBB17737FCF00309087"
+            BuildableName = "Cryptocat.app"
+            BlueprintName = "Cryptocat"
+            ReferencedContainer = "container:Cryptocat.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/xcschememanagement.plist b/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..63c78ec
--- /dev/null
+++ b/src/mac/Cryptocat.xcodeproj/xcuserdata/kaepora.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>Cryptocat.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>5F4ADDBB17737FCF00309087</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>
diff --git a/src/mac/CryptocatAppDelegate.h b/src/mac/CryptocatAppDelegate.h
new file mode 100644
index 0000000..7dc3a8c
--- /dev/null
+++ b/src/mac/CryptocatAppDelegate.h
@@ -0,0 +1,13 @@
+//
+//  CryptocatAppDelegate.h
+//  Cryptocat
+//
+//  Created by Nadim Kobeissi on 2013-06-20.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+ at interface CryptocatAppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate>
+
+ at end
diff --git a/src/mac/CryptocatAppDelegate.m b/src/mac/CryptocatAppDelegate.m
new file mode 100644
index 0000000..52d4d52
--- /dev/null
+++ b/src/mac/CryptocatAppDelegate.m
@@ -0,0 +1,43 @@
+//
+//  CryptocatAppDelegate.m
+//  Cryptocat
+//
+//  Created by Nadim Kobeissi on 2013-06-20.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import "CryptocatAppDelegate.h"
+#import "CryptocatWindowManager.h"
+
+ at implementation CryptocatAppDelegate;
+
+// To support multiple windows chats we implement the dock menu delegate method
+
+- (NSMenu *)applicationDockMenu:(NSApplication *)sender{
+	NSMenu *dockMenu = [[NSMenu alloc]initWithTitle:kApplicationName];
+	// We also want command + N to initiate a new chat window but addItem:initWithTitle:action:keyEquivalent doesn't support key modifiers so we just set it to an empty NSString.
+	[dockMenu addItem:[[NSMenuItem alloc] initWithTitle:@"New Window" action:@selector(openChatWindow) keyEquivalent:@""]];
+	return dockMenu;
+}
+
+- (void)applicationDidFinishLaunching:(NSNotification *)notification{
+	[self openChatWindow:nil];
+    
+	// Disable Debug
+    [[NSUserDefaults standardUserDefaults] setBool:FALSE forKey:@"WebKitDeveloperExtras"];
+	[[NSUserDefaults standardUserDefaults] synchronize];
+}
+
+- (IBAction)openChatWindow:(id)sender{
+    [[CryptocatWindowManager sharedManager] initiateNewConversation];
+}
+
+- (void)applicationDidBecomeActive:(NSNotification *)notification{
+	[[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications];
+}
+
+- (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) theApplication{
+    return YES;
+}
+
+ at end
\ No newline at end of file
diff --git a/src/mac/CryptocatWindowController.h b/src/mac/CryptocatWindowController.h
new file mode 100644
index 0000000..c1ec1fd
--- /dev/null
+++ b/src/mac/CryptocatWindowController.h
@@ -0,0 +1,15 @@
+//
+//  CryptocatWindow.h
+//  Cryptocat
+//
+//  Created by Frederic Jacobs on 22/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+#import <WebKit/WebKit.h>
+
+ at interface CryptocatWindowController : NSWindowController <NSWindowDelegate>
+ at property IBOutlet WebView *webView;
+
+ at end
diff --git a/src/mac/CryptocatWindowController.m b/src/mac/CryptocatWindowController.m
new file mode 100644
index 0000000..777c087
--- /dev/null
+++ b/src/mac/CryptocatWindowController.m
@@ -0,0 +1,119 @@
+//
+//  CryptocatWindow.m
+//  Cryptocat
+//
+//  Created by Frederic Jacobs on 22/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import "CryptocatWindowController.h"
+#import "CryptocatWindowManager.h"
+#import "fileUtils.h"
+
+ at interface WebPreferences (WebPreferencesPrivate)
+- (void)_setLocalStorageDatabasePath:(NSString *)path;
+- (void) setLocalStorageEnabled:(BOOL)localStorageEnabled;
+ at end
+
+ at implementation CryptocatWindowController
+
+#pragma mark Initialization
+
+- (id)init {
+	self = [self initWithWindowNibName:@"CryptocatWindowController" owner:self];
+	if (self) {
+		[self showWindow:nil];
+	}
+	return self;
+}
+
+- (void)awakeFromNib {
+	[self.window setDelegate:[CryptocatWindowManager sharedManager]];
+	NSString *appSupportPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
+	NSString *htmlPath = htmlPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"/htdocs/index.html"];
+	
+	// Set user agent to Chrome in order to load some Cryptocat features not available to Safari.
+	[_webView setCustomUserAgent:@"Chrome (Mac app)"];
+    [_webView setUIDelegate:self];
+    [_webView setGroupName:@"Cryptocat"];
+	
+	// Initialize localStorage.
+	WebPreferences* prefs = [WebPreferences standardPreferences];
+	[prefs _setLocalStorageDatabasePath:appSupportPath];
+	[prefs setLocalStorageEnabled:YES];
+	[_webView setPreferences:prefs];
+	
+	// Initialize Cryptocat.
+	[[_webView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlPath]]];
+}
+
+// Show Cryptocat window once everything is ready.
+- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
+	[self.window makeKeyAndOrderFront:nil];
+}
+
+// Bind file dialog for file transfers.
+- (void)webView:(WebView *)sender runOpenPanelForFileButtonWithResultListener:(id <WebOpenPanelResultListener>)resultListener {
+    NSOpenPanel* openDlg = [NSOpenPanel openPanel];
+    [openDlg setCanChooseFiles:YES];
+    [openDlg setCanChooseDirectories:NO];
+    if ([openDlg runModal] == NSOKButton) {
+        NSArray* files = [[openDlg URLs]valueForKey:@"relativePath"];
+        [resultListener chooseFilenames:files];
+    }
+}
+
+// Remove useless items from webView context menu.
+- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems {
+	NSMutableArray *defaultMenuItemsFixed = [(NSArray*)defaultMenuItems mutableCopy];
+	[defaultMenuItemsFixed removeObjectAtIndex:0];
+	return defaultMenuItemsFixed;
+}
+
+// Handle popups.
+- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
+    [[sender mainFrame] loadRequest:request];
+    return sender;
+}
+
+// Handle links.
+- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation
+	request:(NSURLRequest *)request frame:(WebFrame *)frame
+	decisionListener:(id < WebPolicyDecisionListener >)listener {
+	// We use an iFrame location's property as a bridge to Objective-C from JavaScript.
+    if ([[[request URL] absoluteString] hasPrefix:@"js-call:"]) {
+		NSArray *components = [[[request URL] absoluteString] componentsSeparatedByString:@":"];
+		NSUserNotification *userNotification = [[NSUserNotification alloc]init];
+		userNotification.title = [components[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+		userNotification.subtitle = [components[2] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+		userNotification.hasActionButton = FALSE;
+		[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:userNotification];
+		
+		// If the window is not in the foreground we want to bounce the dock icon. Nothing happens if the window is in focus.
+		
+		[NSApp requestUserAttention:NSCriticalRequest];
+		
+    }
+	// Open links in default browser.
+	else if ([[[request URL] absoluteString] hasPrefix:@"http"]) {
+             [[NSWorkspace sharedWorkspace] openURL:[request URL]];
+    }
+	// Save files.
+	else if ([[[request URL] absoluteString] hasPrefix:@"data:"]) {
+		NSSavePanel *savePanel = [NSSavePanel savePanel];
+		NSString *base64 = [[[request URL] absoluteString] substringFromIndex:37];
+		NSData *data = [fileUtils base64DataFromString:base64];
+		NSString *savePath = [NSString stringWithFormat:@"%@%@", @"file.", [fileUtils getFileExtension:data]];
+		[savePanel setNameFieldStringValue:savePath];
+		[savePanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
+			if (result == NSFileHandlingPanelOKButton) {
+				[data writeToURL:[savePanel URL] atomically:FALSE];
+			}
+		}];
+    }
+	else {
+		[listener use];
+	}
+}
+
+ at end
diff --git a/src/mac/CryptocatWindowController.xib b/src/mac/CryptocatWindowController.xib
new file mode 100644
index 0000000..b111324
--- /dev/null
+++ b/src/mac/CryptocatWindowController.xib
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6154.17" systemVersion="13D65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6154.17"/>
+        <plugIn identifier="com.apple.WebKitIBPlugin" version="6154.17"/>
+    </dependencies>
+    <objects>
+        <customObject id="-2" userLabel="File's Owner" customClass="CryptocatWindowController">
+            <connections>
+                <outlet property="webView" destination="5" id="18"/>
+                <outlet property="window" destination="3" id="17"/>
+            </connections>
+        </customObject>
+        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+        <customObject id="-3" userLabel="Application"/>
+        <window title="Cryptocat" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="MainWindow" animationBehavior="default" id="3">
+            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
+            <rect key="contentRect" x="335" y="121" width="735" height="610"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1028"/>
+            <view key="contentView" id="4">
+                <rect key="frame" x="0.0" y="0.0" width="735" height="610"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <webView maintainsBackForwardList="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5">
+                        <rect key="frame" x="0.0" y="0.0" width="735" height="610"/>
+                        <webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12" plugInsEnabled="NO" javaEnabled="NO" allowsAnimatedImages="NO" allowsAnimatedImageLooping="NO">
+                            <nil key="identifier"/>
+                        </webPreferences>
+                        <connections>
+                            <outlet property="UIDelegate" destination="-2" id="21"/>
+                            <outlet property="downloadDelegate" destination="-2" id="19"/>
+                            <outlet property="frameLoadDelegate" destination="-2" id="20"/>
+                            <outlet property="policyDelegate" destination="-2" id="23"/>
+                            <outlet property="resourceLoadDelegate" destination="-2" id="22"/>
+                        </connections>
+                    </webView>
+                </subviews>
+                <constraints>
+                    <constraint firstItem="5" firstAttribute="leading" secondItem="4" secondAttribute="leading" id="DqG-eP-h59"/>
+                    <constraint firstItem="5" firstAttribute="top" secondItem="4" secondAttribute="top" id="ihm-cq-ldM"/>
+                    <constraint firstAttribute="trailing" secondItem="5" secondAttribute="trailing" id="phW-iD-WZI"/>
+                    <constraint firstAttribute="bottom" secondItem="5" secondAttribute="bottom" id="qFl-RR-QTd"/>
+                </constraints>
+            </view>
+            <connections>
+                <outlet property="delegate" destination="-2" id="24"/>
+            </connections>
+            <point key="canvasLocation" x="503.5" y="386"/>
+        </window>
+    </objects>
+</document>
diff --git a/src/mac/CryptocatWindowManager.h b/src/mac/CryptocatWindowManager.h
new file mode 100644
index 0000000..9b31725
--- /dev/null
+++ b/src/mac/CryptocatWindowManager.h
@@ -0,0 +1,25 @@
+//
+//  CryptocatWindowManager.h
+//  Cryptocat
+//
+//  Created by Frederic Jacobs on 22/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+ at interface CryptocatWindowManager : NSObject<NSWindowDelegate>
+
+ at property (nonatomic,retain) NSMutableArray *conversationWindows;
+
+#pragma mark Singleton instance
+
++ (id)sharedManager;
+
+#pragma mark Instance methods
+
+- (void)initiateNewConversation;
+- (void)removeConversationWindow:(NSWindow*) conversation;
+- (void)removeAllConversationWindows;
+
+ at end
diff --git a/src/mac/CryptocatWindowManager.m b/src/mac/CryptocatWindowManager.m
new file mode 100644
index 0000000..89af64f
--- /dev/null
+++ b/src/mac/CryptocatWindowManager.m
@@ -0,0 +1,78 @@
+//
+//  CryptocatWindowManager.m
+//	The Window Manager handles the making of discussions and also tells the NetworkManager if they have to be routed by Tor!
+//Cryptocat
+//
+//  Created by Frederic Jacobs on 22/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import "CryptocatWindowManager.h"
+#import "CryptocatWindowController.h"
+
+ at implementation CryptocatWindowManager
+
+- (id)init {
+	if (self = [super init]) {
+		self.conversationWindows = [NSMutableArray array];
+	}
+	return self;
+}
+
++ (id)sharedManager {
+    static CryptocatWindowManager *sharedMyManager = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedMyManager = [[self alloc] init];
+    });
+    return sharedMyManager;
+}
+
+- (void)initiateNewConversation{
+	// If there is already an open discussion let's place that new window a few pixels beneath it so that there is no confusion between windows.
+	
+	CryptocatWindowController *controller = [[CryptocatWindowController alloc]init];
+	
+	if ([self.conversationWindows count]>0) {
+		CryptocatWindowController *wC = [self.conversationWindows lastObject];
+		[controller.window setFrame:wC.window.frame display:YES animate:NO];
+		[controller.window setFrame:CGRectOffset(wC.window.frame, 40, 0) display:NO animate:YES];
+	}else{
+		[controller.window center];
+		[controller showWindow:nil];
+	}
+	
+	[self.conversationWindows addObject:controller];
+}
+
+- (void)removeConversationWindow:(NSWindow*) conversation{
+	CryptocatWindowController *conversationController;
+	for (CryptocatWindowController* controller in self.conversationWindows) {
+		if ([controller.window isEqualTo:conversation]) {
+			conversationController = controller;
+		}
+	}
+	if (conversationController) {
+		[self removeWindowController:conversationController];
+	}
+}
+
+- (void)removeAllConversationWindows{
+	for (CryptocatWindowController* controller in self.conversationWindows) {
+		[self removeWindowController:controller];
+	}
+}
+
+- (void)removeWindowController:(CryptocatWindowController*)CWC{
+	[CWC.webView stopLoading:nil];
+	[CWC.webView close];
+	[self.conversationWindows removeObject:CWC];
+}
+
+#pragma mark NSWindowDelegate protocol methods
+
+- (void)windowWillClose:(NSNotification *)notification{
+	[self removeConversationWindow:[notification object]];
+}
+
+ at end
diff --git a/src/mac/en.lproj/Credits.rtf b/src/mac/en.lproj/Credits.rtf
new file mode 100644
index 0000000..ff7c554
--- /dev/null
+++ b/src/mac/en.lproj/Credits.rtf
@@ -0,0 +1,10 @@
+{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\vieww9600\viewh8400\viewkind0
+\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
+
+\f0\fs24 \cf0 Cryptocat is released as Free Software under \
+the Affero GNU General Public License 3.\
+\
+{\field{\*\fldinst{HYPERLINK "https://crypto.cat"}}{\fldrslt Visit Cryptocat's website!}}}
\ No newline at end of file
diff --git a/src/mac/en.lproj/InfoPlist.strings b/src/mac/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/src/mac/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/src/mac/en.lproj/MainMenu.xib b/src/mac/en.lproj/MainMenu.xib
new file mode 100644
index 0000000..cf539ae
--- /dev/null
+++ b/src/mac/en.lproj/MainMenu.xib
@@ -0,0 +1,329 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6154.17" systemVersion="13D65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6154.17"/>
+    </dependencies>
+    <objects>
+        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+            <connections>
+                <outlet property="delegate" destination="494" id="574"/>
+            </connections>
+        </customObject>
+        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+        <customObject id="-3" userLabel="Application"/>
+        <menu title="AMainMenu" systemMenu="main" id="29">
+            <items>
+                <menuItem title="Cryptocat" id="56">
+                    <menu key="submenu" title="Cryptocat" systemMenu="apple" id="57">
+                        <items>
+                            <menuItem title="About Cryptocat" id="58">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="236">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Preferences…" keyEquivalent="," id="129"/>
+                            <menuItem isSeparatorItem="YES" id="143">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Services" id="131">
+                                <menu key="submenu" title="Services" systemMenu="services" id="130"/>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="144">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Hide Cryptocat" keyEquivalent="h" id="134">
+                                <connections>
+                                    <action selector="hide:" target="-1" id="367"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Hide Others" keyEquivalent="h" id="145">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="hideOtherApplications:" target="-1" id="368"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Show All" id="150">
+                                <connections>
+                                    <action selector="unhideAllApplications:" target="-1" id="370"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="149">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Quit Cryptocat" keyEquivalent="q" id="136">
+                                <connections>
+                                    <action selector="terminate:" target="-3" id="449"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="File" id="83">
+                    <menu key="submenu" title="File" id="81">
+                        <items>
+                            <menuItem title="New Chat Window" keyEquivalent="n" id="82">
+                                <connections>
+                                    <action selector="openChatWindow:" target="494" id="567"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="79">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Close" keyEquivalent="w" id="73">
+                                <connections>
+                                    <action selector="performClose:" target="-1" id="193"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Edit" id="217">
+                    <menu key="submenu" title="Edit" id="205">
+                        <items>
+                            <menuItem title="Undo" keyEquivalent="z" id="207">
+                                <connections>
+                                    <action selector="undo:" target="-1" id="223"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Redo" keyEquivalent="Z" id="215">
+                                <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+                                <connections>
+                                    <action selector="redo:" target="-1" id="231"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="206">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Cut" keyEquivalent="x" id="199">
+                                <connections>
+                                    <action selector="cut:" target="-1" id="228"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Copy" keyEquivalent="c" id="197">
+                                <connections>
+                                    <action selector="copy:" target="-1" id="224"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Paste" keyEquivalent="v" id="203">
+                                <connections>
+                                    <action selector="paste:" target="-1" id="226"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Delete" id="202">
+                                <connections>
+                                    <action selector="delete:" target="-1" id="235"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Select All" keyEquivalent="a" id="198">
+                                <connections>
+                                    <action selector="selectAll:" target="-1" id="232"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="214">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Find" id="218">
+                                <menu key="submenu" title="Find" id="220">
+                                    <items>
+                                        <menuItem title="Find…" tag="1" keyEquivalent="f" id="209">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="241"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="534">
+                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="535"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Next" tag="2" keyEquivalent="g" id="208">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="487"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213">
+                                            <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="488"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="489"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Jump to Selection" keyEquivalent="j" id="210">
+                                            <connections>
+                                                <action selector="centerSelectionInVisibleArea:" target="-1" id="245"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Spelling and Grammar" id="216">
+                                <menu key="submenu" title="Spelling and Grammar" id="200">
+                                    <items>
+                                        <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="204">
+                                            <connections>
+                                                <action selector="showGuessPanel:" target="-1" id="230"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Document Now" keyEquivalent=";" id="201">
+                                            <connections>
+                                                <action selector="checkSpelling:" target="-1" id="225"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="453"/>
+                                        <menuItem title="Check Spelling While Typing" id="219">
+                                            <connections>
+                                                <action selector="toggleContinuousSpellChecking:" target="-1" id="222"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Grammar With Spelling" id="346">
+                                            <connections>
+                                                <action selector="toggleGrammarChecking:" target="-1" id="347"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Correct Spelling Automatically" id="454">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="456"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Substitutions" id="348">
+                                <menu key="submenu" title="Substitutions" id="349">
+                                    <items>
+                                        <menuItem title="Show Substitutions" id="457">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="orderFrontSubstitutionsPanel:" target="-1" id="458"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="459"/>
+                                        <menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350">
+                                            <connections>
+                                                <action selector="toggleSmartInsertDelete:" target="-1" id="355"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351">
+                                            <connections>
+                                                <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="356"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Dashes" id="460">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticDashSubstitution:" target="-1" id="461"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354">
+                                            <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticLinkDetection:" target="-1" id="357"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Text Replacement" id="462">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticTextReplacement:" target="-1" id="463"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Transformations" id="450">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Transformations" id="451">
+                                    <items>
+                                        <menuItem title="Make Upper Case" id="452">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="uppercaseWord:" target="-1" id="464"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Make Lower Case" id="465">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="lowercaseWord:" target="-1" id="468"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Capitalize" id="466">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="capitalizeWord:" target="-1" id="467"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Speech" id="211">
+                                <menu key="submenu" title="Speech" id="212">
+                                    <items>
+                                        <menuItem title="Start Speaking" id="196">
+                                            <connections>
+                                                <action selector="startSpeaking:" target="-1" id="233"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Stop Speaking" id="195">
+                                            <connections>
+                                                <action selector="stopSpeaking:" target="-1" id="227"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Window" id="19">
+                    <menu key="submenu" title="Window" systemMenu="window" id="24">
+                        <items>
+                            <menuItem title="Minimize" keyEquivalent="m" id="23">
+                                <connections>
+                                    <action selector="performMiniaturize:" target="-1" id="37"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Zoom" id="239">
+                                <connections>
+                                    <action selector="performZoom:" target="-1" id="240"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="92">
+                                <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+                            </menuItem>
+                            <menuItem title="Bring All to Front" id="5">
+                                <connections>
+                                    <action selector="arrangeInFront:" target="-1" id="39"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Help" id="490">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Help" systemMenu="help" id="491">
+                        <items>
+                            <menuItem title="Cryptocat Help" keyEquivalent="?" id="492">
+                                <connections>
+                                    <action selector="showHelp:" target="-1" id="493"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+            </items>
+            <connections>
+                <outlet property="delegate" destination="494" id="575"/>
+            </connections>
+        </menu>
+        <customObject id="494" customClass="CryptocatAppDelegate"/>
+        <customObject id="420" customClass="NSFontManager"/>
+    </objects>
+</document>
diff --git a/src/mac/fileUtils.h b/src/mac/fileUtils.h
new file mode 100644
index 0000000..b271824
--- /dev/null
+++ b/src/mac/fileUtils.h
@@ -0,0 +1,83 @@
+//
+//  fileUtils.h
+//  Cryptocat
+//
+//  Created by Nadim Kobeissi on 24/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+ at interface fileUtils : NSObject
+
++ (NSString *)getFileExtension:(NSData *)data;
++ (NSData *)base64DataFromString: (NSString *)string;
+
+ at end
+
+ at implementation fileUtils
+
+// Detect file extension using first byte of NSData.
++ (NSString *)getFileExtension:(NSData *)data {
+    uint8_t c;
+    [data getBytes:&c length:1];
+    switch (c) {
+		case 0xFF:
+			return @"jpg";
+		case 0x89:
+			return @"png";
+		case 0x47:
+			return @"gif";
+    }
+    return @"zip";
+}
+
+// Convert base64 NSString to NSData object.
++ (NSData *)base64DataFromString: (NSString *)string { 	unsigned long ixtext, lentext;
+	unsigned char ch, inbuf[4], outbuf[3];
+	short i, ixinbuf;
+	Boolean flignore, flendtext = false;
+	const unsigned char *tempcstring;
+	NSMutableData *theData;
+	if (string == nil) { return [NSData data]; }
+	ixtext = 0;
+	tempcstring = (const unsigned char *)[string UTF8String];
+	lentext = [string length];
+	theData = [NSMutableData dataWithCapacity: lentext];
+	ixinbuf = 0;
+	while (true) {
+		if (ixtext >= lentext) { break; }
+		ch = tempcstring [ixtext++];
+		flignore = false;
+		if ((ch >= 'A') && (ch <= 'Z')) { ch = ch - 'A'; }
+		else if ((ch >= 'a') && (ch <= 'z')) { ch = ch - 'a' + 26; }
+		else if ((ch >= '0') && (ch <= '9')) { ch = ch - '0' + 52; }
+		else if (ch == '+') { ch = 62; }
+		else if (ch == '=') { flendtext = true; }
+		else if (ch == '/') { ch = 63; }
+		else { flignore = true; }
+		if (!flignore) {
+			short ctcharsinbuf = 3;
+			Boolean flbreak = false;
+			if (flendtext) {
+				if (ixinbuf == 0) { break; }
+				if ((ixinbuf == 1) || (ixinbuf == 2)) { ctcharsinbuf = 1; }
+				else { ctcharsinbuf = 2; }
+				ixinbuf = 3;
+				flbreak = true;
+			}
+			inbuf [ixinbuf++] = ch;
+			if (ixinbuf == 4) {
+				ixinbuf = 0;
+				outbuf[0] = (inbuf[0] << 2) | ((inbuf[1] & 0x30) >> 4);
+				outbuf[1] = ((inbuf[1] & 0x0F) << 4) | ((inbuf[2] & 0x3C) >> 2);
+				outbuf[2] = ((inbuf[2] & 0x03) << 6) | (inbuf[3] & 0x3F);
+				for (i = 0; i < ctcharsinbuf; i++) { [theData appendBytes: &outbuf[i] length: 1]; }
+			}
+			if (flbreak) {
+				break;
+			}
+		}
+	}
+	return theData;
+}
+
+ at end
\ No newline at end of file
diff --git a/src/mac/htdocs/placeholder.txt b/src/mac/htdocs/placeholder.txt
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/src/mac/htdocs/placeholder.txt
@@ -0,0 +1 @@
+.
diff --git a/src/mac/kConstants.h b/src/mac/kConstants.h
new file mode 100644
index 0000000..ded80b6
--- /dev/null
+++ b/src/mac/kConstants.h
@@ -0,0 +1,15 @@
+//
+//  kConstants.h
+//  Cryptocat
+//
+//  Created by Frederic Jacobs on 22/6/13.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#ifndef Cryptocat_kConstants_h
+#define Cryptocat_kConstants_h
+
+#define kApplicationName @"Cryptocat"
+#define kConversationMenuItemName @"Conversations"
+
+#endif
diff --git a/src/mac/main.m b/src/mac/main.m
new file mode 100644
index 0000000..bd0e252
--- /dev/null
+++ b/src/mac/main.m
@@ -0,0 +1,13 @@
+//
+//  main.m
+//  Cryptocat
+//
+//  Created by Nadim Kobeissi on 2013-06-20.
+//  Copyright (c) 2013 Cryptocat. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+int main(int argc, char *argv[]) {
+	return NSApplicationMain(argc, (const char **)argv);
+}
diff --git a/src/opera/manifest.json b/src/opera/manifest.json
new file mode 100755
index 0000000..aace975
--- /dev/null
+++ b/src/opera/manifest.json
@@ -0,0 +1,27 @@
+{
+	"manifest_version": 2,
+	"name": "Cryptocat",
+	"description": "Have encrypted, private conversations. Cryptocat is an open source encrypted instant messaging platform.",
+	"developer": {
+		"name": "Nadim Kobeissi & the Cryptocat team",
+		"url": "https://crypto.cat"
+	},
+	"version": "2.2.2",
+	"homepage_url": "https://crypto.cat",
+	"icons": {
+		"16": "img/icon-16.png",
+		"48": "img/icon-48.png",
+		"128": "img/icon-128.png"
+	},
+	"background": {
+		"scripts": ["opera.js"],
+		"persistent": false
+	},
+	"browser_action": {
+		"default_icon": "img/icon-48.png",
+		"default_title": "Open Cryptocat"
+	},
+	"permissions": [
+		"storage"
+	]
+}
diff --git a/src/opera/opera.js b/src/opera/opera.js
new file mode 100644
index 0000000..f7436ee
--- /dev/null
+++ b/src/opera/opera.js
@@ -0,0 +1,9 @@
+chrome.runtime.onInstalled.addListener(function(details) {
+	if (details.reason === 'install') {
+		chrome.tabs.create({'url': chrome.extension.getURL('firstRun.html')})
+	}
+})
+
+chrome.browserAction.onClicked.addListener(function(){
+	chrome.tabs.create({'url': chrome.extension.getURL('index.html')})
+})
\ No newline at end of file
diff --git a/src/safari/Info.plist b/src/safari/Info.plist
new file mode 100755
index 0000000..7c07d8b
--- /dev/null
+++ b/src/safari/Info.plist
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>Author</key>
+	<string>Cryptocat</string>
+	<key>Builder Version</key>
+	<string>8536.29.13</string>
+	<key>CFBundleDisplayName</key>
+	<string>Cryptocat</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.cryptocat.cryptocat</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleShortVersionString</key>
+	<string>2.2.2</string>
+	<key>CFBundleVersion</key>
+	<string>2.2.2</string>
+	<key>Chrome</key>
+	<dict>
+		<key>Global Page</key>
+		<string>safari.html</string>
+		<key>Toolbar Items</key>
+		<array>
+			<dict>
+				<key>Command</key>
+				<string>toolbarClick</string>
+				<key>Identifier</key>
+				<string>Cryptocat</string>
+				<key>Image</key>
+				<string>img/newMessage.png</string>
+				<key>Label</key>
+				<string>Cryptocat</string>
+				<key>Tool Tip</key>
+				<string>Open Cryptocat</string>
+			</dict>
+		</array>
+	</dict>
+	<key>Content</key>
+	<dict>
+		<key>Scripts</key>
+		<dict/>
+	</dict>
+	<key>Description</key>
+	<string>Cryptocat is an open source encrypted instant messaging platform.</string>
+	<key>ExtensionInfoDictionaryVersion</key>
+	<string>1.0</string>
+	<key>Permissions</key>
+	<dict>
+		<key>Website Access</key>
+		<dict>
+			<key>Include Secure Pages</key>
+			<true/>
+			<key>Level</key>
+			<string>All</string>
+		</dict>
+	</dict>
+	<key>Update Manifest URL</key>
+	<string>https://crypto.cat/get/safari.update.plist</string>
+	<key>Website</key>
+	<string>https://crypto.cat</string>
+</dict>
+</plist>
diff --git a/src/safari/Settings.plist b/src/safari/Settings.plist
new file mode 100755
index 0000000..5dd5da8
--- /dev/null
+++ b/src/safari/Settings.plist
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<array/>
+</plist>
diff --git a/src/safari/icon-16.png b/src/safari/icon-16.png
new file mode 100755
index 0000000..79cd72b
Binary files /dev/null and b/src/safari/icon-16.png differ
diff --git a/src/safari/icon-32.png b/src/safari/icon-32.png
new file mode 100644
index 0000000..caf7986
Binary files /dev/null and b/src/safari/icon-32.png differ
diff --git a/src/safari/icon-64.png b/src/safari/icon-64.png
new file mode 100644
index 0000000..e30a08e
Binary files /dev/null and b/src/safari/icon-64.png differ
diff --git a/src/safari/safari.html b/src/safari/safari.html
new file mode 100755
index 0000000..5cc99eb
--- /dev/null
+++ b/src/safari/safari.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+	<script type="application/javascript">
+		var firstRun = (typeof localStorage['firstRun'] === 'undefined') || (!JSON.parse(localStorage['firstRun']))
+		
+		if (firstRun) {
+			localStorage['firstRun'] = JSON.stringify(true)
+			safari.application.activeBrowserWindow.openTab().url = safari.extension.baseURI + 'firstRun.html'
+		}
+
+		safari.application.addEventListener('command', function(event) {
+			safari.application.activeBrowserWindow.openTab().url = safari.extension.baseURI + 'index.html'
+		}, false)
+	</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/src/standaloneServer.js b/src/standaloneServer.js
new file mode 100755
index 0000000..432ca42
--- /dev/null
+++ b/src/standaloneServer.js
@@ -0,0 +1,11 @@
+#!/usr/bin/env node
+
+var path = require('path')
+var express = require('express')
+var app = express()
+var port = 1337
+
+app.use(express.static(path.join(__dirname, 'core')))
+
+app.listen(port)
+console.log('[Cryptocat] listening on port: ' + port)
\ No newline at end of file
diff --git a/test/core/js/aes.test.js b/test/core/js/aes.test.js
new file mode 100644
index 0000000..026e495
--- /dev/null
+++ b/test/core/js/aes.test.js
@@ -0,0 +1,103 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var CryptoJS = require('../../../src/core/js/lib/crypto-js')
+
+var opts = {
+	mode: CryptoJS.mode.CTR,
+	iv: CryptoJS.enc.Hex.parse('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'),
+	padding: CryptoJS.pad.NoPadding
+}
+
+test['AES'] = {
+
+	'CTR-128': {
+		'Encrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee'
+			var aesctr = CryptoJS.AES.encrypt(
+				CryptoJS.enc.Hex.parse(p),
+				CryptoJS.enc.Hex.parse('2b7e151628aed2a6abf7158809cf4f3c'),
+				opts
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse(aesctr.toString()).toString(CryptoJS.enc.Hex),
+				c
+			)
+		},
+		'Decrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee'
+			var aesctr = CryptoJS.AES.decrypt(
+				CryptoJS.enc.Hex.parse(c).toString(CryptoJS.enc.Base64),
+				CryptoJS.enc.Hex.parse('2b7e151628aed2a6abf7158809cf4f3c'),
+				opts
+			)
+			test.assert.same(
+				aesctr.toString(),
+				p
+			)
+		}
+	},
+	
+	'CTR-192': {
+		'Encrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '1abc932417521ca24f2b0459fe7e6e0b090339ec0aa6faefd5ccc2c6f4ce8e941e36b26bd1ebc670d1bd1d665620abf74f78a7f6d29809585a97daec58c6b050'
+			var aesctr = CryptoJS.AES.encrypt(
+				CryptoJS.enc.Hex.parse(p),
+				CryptoJS.enc.Hex.parse('8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'),
+				opts
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse(aesctr.toString()).toString(CryptoJS.enc.Hex),
+				c
+			)
+		},
+		'Decrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '1abc932417521ca24f2b0459fe7e6e0b090339ec0aa6faefd5ccc2c6f4ce8e941e36b26bd1ebc670d1bd1d665620abf74f78a7f6d29809585a97daec58c6b050'
+			var aesctr = CryptoJS.AES.decrypt(
+				CryptoJS.enc.Hex.parse(c).toString(CryptoJS.enc.Base64),
+				CryptoJS.enc.Hex.parse('8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'),
+				opts
+			)
+			test.assert.same(
+				aesctr.toString(),
+				p
+			)
+		}
+	},
+	
+	'CTR-256': {
+		'Encrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '601ec313775789a5b7a7f504bbf3d228f443e3ca4d62b59aca84e990cacaf5c52b0930daa23de94ce87017ba2d84988ddfc9c58db67aada613c2dd08457941a6'
+			var aesctr = CryptoJS.AES.encrypt(
+				CryptoJS.enc.Hex.parse(p),
+				CryptoJS.enc.Hex.parse('603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'),
+				opts
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse(aesctr.toString()).toString(CryptoJS.enc.Hex),
+				c
+			)
+		},
+		'Decrypt': function() {
+			var p = '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'
+			var c = '601ec313775789a5b7a7f504bbf3d228f443e3ca4d62b59aca84e990cacaf5c52b0930daa23de94ce87017ba2d84988ddfc9c58db67aada613c2dd08457941a6'
+			var aesctr = CryptoJS.AES.decrypt(
+				CryptoJS.enc.Hex.parse(c).toString(CryptoJS.enc.Base64),
+				CryptoJS.enc.Hex.parse('603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'),
+				opts
+			)
+			test.assert.same(
+				aesctr.toString(),
+				p
+			)
+		}
+	},
+	
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/bigint.test.js b/test/core/js/bigint.test.js
new file mode 100644
index 0000000..f59bc4b
--- /dev/null
+++ b/test/core/js/bigint.test.js
@@ -0,0 +1,25 @@
+var assert = require('assert'),
+	BigInt = require('../../../src/core/js/lib/bigint')
+
+describe('BigInt', function() {
+	
+	it('should exponentiate a BigInt with base two', function () {
+		assert.equal((Math.pow(2, 513)).toString(16), BigInt.bigInt2str(BigInt.twoToThe(513), 16))
+	})
+	
+	it('should return a bit string of the proper length', function () {
+		// 2^(8*3) < 2^(15*2) < 2^(8*4) === 4 bytes
+		var test = BigInt.str2bigInt((Math.pow(2, 30)).toString(), 10)
+		assert.equal(4, BigInt.bigInt2bits(test).length)
+	})
+
+	it('should handle shift distances greater than the bit length of x', function () {
+		var bi = BigInt.str2bigInt('10000000000', 2)
+		BigInt.rightShift_(bi, 12)
+		assert.ok(BigInt.equalsInt(bi, 0))
+		bi = BigInt.str2bigInt('10', 2)
+		BigInt.rightShift_(bi, 26*3)
+		assert.ok(BigInt.equalsInt(bi, 0))
+	})
+
+})
\ No newline at end of file
diff --git a/test/core/js/catFacts.test.js b/test/core/js/catFacts.test.js
new file mode 100644
index 0000000..9895b55
--- /dev/null
+++ b/test/core/js/catFacts.test.js
@@ -0,0 +1,31 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var CatFacts = require('../../../src/core/js/etc/catFacts')
+
+test['Cat Facts'] = {
+
+	'Get Cat Fact': function() {
+		function factLength(fact) {
+			if (fact.length >= 8) {
+				return true
+			}
+			return false
+		}
+		test.assert.same(
+			typeof(CatFacts.getFact()),
+			'string'
+		)
+		test.assert.same(
+			factLength(CatFacts.getFact()),
+			true
+		)
+		test.assert.same(
+			CatFacts.getFact() === CatFacts.getFact(),
+			false
+		)
+	},
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/elliptic.test.js b/test/core/js/elliptic.test.js
new file mode 100644
index 0000000..8589591
--- /dev/null
+++ b/test/core/js/elliptic.test.js
@@ -0,0 +1,105 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var Curve25519 = require('../../../src/core/js/lib/elliptic'),
+	BigInt = require('../../../src/core/js/lib/bigint')
+
+// basePoint is the generator of the elliptic curve group
+var basePoint = BigInt.str2bigInt('9', 10)
+
+test['Curve25519'] = {
+
+	'Key generation': {
+		'This should print the same thing twice': function() {
+			var priv1 = BigInt.randBigInt(256, 0)
+			var priv2 = BigInt.randBigInt(256, 0)
+			var pub1 = Curve25519.scalarMult(priv1, basePoint)
+			var pub2 = Curve25519.scalarMult(priv2, basePoint)
+			test.assert.ok(pub1)
+			test.assert.ok(pub2)
+			test.assert.same(
+				Curve25519.scalarMult(priv1, pub2),
+				Curve25519.scalarMult(priv2, pub1)
+			)
+		}
+	},
+
+	'Test vectors': {
+		'Test vectors must match generated output': function() {
+			var testVectors = [
+				['0000000000000000000000000000000000000000000000000000000000000003',
+				'0000000000000000000000000000000000000000000000000000000000000009',
+				'743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52f'],
+				['0000000000000000000000000000000000000000000000000000000000000005',
+				'743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52f',
+				'68bb5dd8064094890c91dc9671bdfcdf8baead5bff5264fd2cb6aec1a7a2fe93'],
+				['0000000000000000000000000000000000000000000000000000000000000005',
+				'0000000000000000000000000000000000000000000000000000000000000009',
+				'743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52f'],
+				['0000000000000000000000000000000000000000000000000000000000000003',
+				'743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52f',
+				'68bb5dd8064094890c91dc9671bdfcdf8baead5bff5264fd2cb6aec1a7a2fe93'],
+				['743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52c',
+				'68bb5dd8064094890c91dc9671bdfcdf8baead5bff5264fd2cb6aec1a7a2fe9a',
+				'0aa146f21b8af23a40f2cd35c1d92335149d6e60a32f57277867bba22a6b162f'],
+				['743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52a',
+				'0aa146f21b8af23a40f2cd35c1d92335149d6e60a32f57277867bba22a6b162f',
+				'05f01364fe26edde85716acb31c759125cfc4e0b812758a77872990da0bb2ffd'],
+				['743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52a',
+				'68bb5dd8064094890c91dc9671bdfcdf8baead5bff5264fd2cb6aec1a7a2fe9a',
+				'0aa146f21b8af23a40f2cd35c1d92335149d6e60a32f57277867bba22a6b162f'],
+				['743bcb585f9990edc2cfc4af84f6ff300729bb5facda28154362cd47a37de52c',
+				'0aa146f21b8af23a40f2cd35c1d92335149d6e60a32f57277867bba22a6b162f',
+				'05f01364fe26edde85716acb31c759125cfc4e0b812758a77872990da0bb2ffd'],
+				['7e9a8daa441362d7823d099a452fdc0513b4d53f0ff57f323b0576e58916f303',
+				'6d4b4ebcf866795789e0b65d407aa5cdd752e3507e753c5a54c437cc0719d167',
+				'57a81cf7cf65972e7203804c24a2a3203b3fd424b616e94bb2a6a741e7513259'],
+				['7e9a8daa441362d7823d099a452fdc0513b4d53f0ff57f323b0576e58916f305',
+				'57a81cf7cf65972e7203804c24a2a3203b3fd424b616e94bb2a6a741e7513259',
+				'77f87686f8c6446a8095bc12746e52fbb3facb75e10e680751e68fe44fd052fd'],
+				['7e9a8daa441362d7823d099a452fdc0513b4d53f0ff57f323b0576e58916f305',
+				'6d4b4ebcf866795789e0b65d407aa5cdd752e3507e753c5a54c437cc0719d167',
+				'57a81cf7cf65972e7203804c24a2a3203b3fd424b616e94bb2a6a741e7513259'],
+				['7e9a8daa441362d7823d099a452fdc0513b4d53f0ff57f323b0576e58916f303',
+				'57a81cf7cf65972e7203804c24a2a3203b3fd424b616e94bb2a6a741e7513259',
+				'77f87686f8c6446a8095bc12746e52fbb3facb75e10e680751e68fe44fd052fd'],
+				['2932915d8b76f5f9f03e89d6618d7f25288b011bb9e3967989a3d1a46e47c15a',
+				'1ab3383a00a03d3d09750a4f3414f73664a828259f7b545d0522b82848c9839a',
+				'080a6d908125f4c87ede9aa7685f779ac1e4aecc62351ed3ca40658f9694bbaa'],
+				['2932915d8b76f5f9f03e89d6618d7f25288b011bb9e3967989a3d1a46e47c15c',
+				'080a6d908125f4c87ede9aa7685f779ac1e4aecc62351ed3ca40658f9694bbaa',
+				'1e7392876764c9c9d5d4c9d15b88eb031cd40910a39427d78a400a9dff277afb'],
+				['2932915d8b76f5f9f03e89d6618d7f25288b011bb9e3967989a3d1a46e47c15c',
+				'1ab3383a00a03d3d09750a4f3414f73664a828259f7b545d0522b82848c9839a',
+				'080a6d908125f4c87ede9aa7685f779ac1e4aecc62351ed3ca40658f9694bbaa'],
+				['2932915d8b76f5f9f03e89d6618d7f25288b011bb9e3967989a3d1a46e47c15a',
+				'080a6d908125f4c87ede9aa7685f779ac1e4aecc62351ed3ca40658f9694bbaa',
+				'1e7392876764c9c9d5d4c9d15b88eb031cd40910a39427d78a400a9dff277afb'],
+				['2138fccd0a5301318ee0137109d208bfe96fafd7dbd688aa43e3b42bf8d37af0',
+				'04c0aabd67c4f4f4dca1c39e6f9c1c35787c21353cef738a8f62b2b5b7eef961',
+				'1e2af4aa269b80d797f3bb592b1efa4be25c085a67180322a0022f9f9dcc8697'],
+				['2138fccd0a5301318ee0137109d208bfe96fafd7dbd688aa43e3b42bf8d37af6',
+				'1e2af4aa269b80d797f3bb592b1efa4be25c085a67180322a0022f9f9dcc8697',
+				'4ab2cbf418aff79f2d791e536099e135ad81dbee879ec8a5281c94f2c498ba6f'],
+				['2138fccd0a5301318ee0137109d208bfe96fafd7dbd688aa43e3b42bf8d37af6',
+				'04c0aabd67c4f4f4dca1c39e6f9c1c35787c21353cef738a8f62b2b5b7eef961',
+				'1e2af4aa269b80d797f3bb592b1efa4be25c085a67180322a0022f9f9dcc8697'],
+				['2138fccd0a5301318ee0137109d208bfe96fafd7dbd688aa43e3b42bf8d37af0',
+				'1e2af4aa269b80d797f3bb592b1efa4be25c085a67180322a0022f9f9dcc8697',
+				'4ab2cbf418aff79f2d791e536099e135ad81dbee879ec8a5281c94f2c498ba6f']]
+
+			for (var i = 0; i < testVectors.length; i++) {
+				var privateKey = BigInt.str2bigInt(testVectors[i][0], 16)
+				var basePoint  = BigInt.str2bigInt(testVectors[i][1], 16)
+				var sharedKey  = (
+					'000000000000000000000000000000000000000000000000000000000000000'
+					+ BigInt.bigInt2str(Curve25519.ecDH(privateKey, basePoint), 16).toLowerCase()
+				).substr(-64)
+				test.assert.same(sharedKey, testVectors[i][2])
+			}
+		}
+	}
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/encoding.test.js b/test/core/js/encoding.test.js
new file mode 100644
index 0000000..4ad923d
--- /dev/null
+++ b/test/core/js/encoding.test.js
@@ -0,0 +1,93 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var CryptoJS = require('../../../src/core/js/lib/crypto-js')
+
+test['Encoding'] = {
+
+	'CryptoJS encoding types': {
+		'Hex': function() {
+			test.assert.same(
+				CryptoJS.enc.Hex.parse('6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839').toString(CryptoJS.enc.Hex),
+				'6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839'
+			)
+			test.assert.same(
+				CryptoJS.enc.Hex.parse('6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839').toString(CryptoJS.enc.Base64),
+				'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5'
+			)
+			test.assert.same(
+				CryptoJS.enc.Hex.parse('6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839').toString(CryptoJS.enc.Latin1),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+			test.assert.same(
+				CryptoJS.enc.Hex.parse('6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839').toString(CryptoJS.enc.Utf8),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+		},
+		'Base64': function() {
+			test.assert.same(
+				CryptoJS.enc.Base64.parse('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5').toString(CryptoJS.enc.Base64),
+				'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5'
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5').toString(CryptoJS.enc.Hex),
+				'6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839'
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5').toString(CryptoJS.enc.Latin1),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+			test.assert.same(
+				CryptoJS.enc.Base64.parse('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5').toString(CryptoJS.enc.Utf8),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+		},
+		'Latin1': function() {
+			test.assert.same(
+				CryptoJS.enc.Latin1.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Latin1),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+			test.assert.same(
+				CryptoJS.enc.Latin1.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Hex),
+				'6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839'
+			)
+			test.assert.same(
+				CryptoJS.enc.Latin1.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Base64),
+				'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5'
+			)
+			test.assert.same(
+				CryptoJS.enc.Latin1.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Utf8),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+		},
+		'Utf8': function() {
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Utf8),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Latin1),
+				'abcdefghijklmnopqrstuvwxyz0123456789'
+			)
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Hex),
+				'6162636465666768696a6b6c6d6e6f707172737475767778797a30313233343536373839'
+			)
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('abcdefghijklmnopqrstuvwxyz0123456789').toString(CryptoJS.enc.Base64),
+				'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5'
+			)
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('التشفير القط').toString(CryptoJS.enc.Utf8),
+				'التشفير القط'
+			)
+			test.assert.same(
+				CryptoJS.enc.Utf8.parse('加密貓').toString(CryptoJS.enc.Utf8),
+				'加密貓'
+			)
+		},
+	},
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/hmac.test.js b/test/core/js/hmac.test.js
new file mode 100644
index 0000000..b8c4da6
--- /dev/null
+++ b/test/core/js/hmac.test.js
@@ -0,0 +1,200 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var CryptoJS = require('../../../src/core/js/lib/crypto-js')
+
+test['HMAC'] = {
+
+	'HMAC-SHA1 test vectors': {
+		'Test Case 1': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA1(
+					CryptoJS.enc.Hex.parse('4869205468657265'),
+					CryptoJS.enc.Hex.parse('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b')
+				).toString(),
+				'b617318655057264e28bc0b6fb378c8ef146be00'
+			)
+		},
+		'Test Case 2': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA1(
+					CryptoJS.enc.Hex.parse('7768617420646f2079612077616e7420666f72206e6f7468696e673f'),
+					CryptoJS.enc.Hex.parse('4a656665')
+				).toString(),
+				'effcdf6ae5eb2fa2d27416d5f184df9c259a7c79'
+			)
+		},
+		'Test Case 3': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA1(
+					CryptoJS.enc.Hex.parse(
+						  'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddd'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaa')
+				).toString(),
+				'125d7342b9ac11cd91a39af48aa17b4f63f175d3'
+			)
+		},
+		'Test Case 7': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA1(
+					CryptoJS.enc.Latin1.parse('Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+				).toString(),
+				'e8e99d0f45237d786d6bbaa7965c7808bbff1a91'
+			)
+		},
+	},
+
+	'HMAC-SHA256 test vectors': {
+		'Test Case 1': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA256(
+					CryptoJS.enc.Hex.parse('4869205468657265'),
+					CryptoJS.enc.Hex.parse('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b')
+				).toString(),
+				'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
+			)
+		},
+		'Test Case 2': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA256(
+					CryptoJS.enc.Hex.parse('7768617420646f2079612077616e7420666f72206e6f7468696e673f'),
+					CryptoJS.enc.Hex.parse('4a656665')
+				).toString(),
+				'5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843'
+			)
+		},
+		'Test Case 3': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA256(
+					CryptoJS.enc.Hex.parse(
+						  'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddd'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaa')
+				).toString(),
+				'773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe'
+			)
+		},
+		'Test Case 7': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA256(
+					CryptoJS.enc.Hex.parse(
+						  '54686973206973206120746573742075'
+						+ '73696e672061206c6172676572207468'
+						+ '616e20626c6f636b2d73697a65206b65'
+						+ '7920616e642061206c61726765722074'
+						+ '68616e20626c6f636b2d73697a652064'
+						+ '6174612e20546865206b6579206e6565'
+						+ '647320746f2062652068617368656420'
+						+ '6265666f7265206265696e6720757365'
+						+ '642062792074686520484d414320616c'
+						+ '676f726974686d2e'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaa')
+				).toString(),
+				'9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2'
+			)
+		},
+	},
+	
+	'HMAC-SHA512 test vectors': {
+		'Test Case 1': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA512(
+					CryptoJS.enc.Hex.parse('4869205468657265'),
+					CryptoJS.enc.Hex.parse('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b')
+				).toString(),
+				  '87aa7cdea5ef619d4ff0b4241a1d6cb0'
+				+ '2379f4e2ce4ec2787ad0b30545e17cde'
+				+ 'daa833b7d6b8a702038b274eaea3f4e4'
+				+ 'be9d914eeb61f1702e696c203a126854'
+			)
+		},
+		'Test Case 2': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA512(
+					CryptoJS.enc.Hex.parse('7768617420646f2079612077616e7420666f72206e6f7468696e673f'),
+					CryptoJS.enc.Hex.parse('4a656665')
+				).toString(),
+				  '164b7a7bfcf819e2e395fbe73b56e0a3'
+				+ '87bd64222e831fd610270cd7ea250554'
+				+ '9758bf75c05a994a6d034f65f8f0e6fd'
+				+ 'caeab1a34d4a6b4b636e070a38bce737'
+			)
+		},
+		'Test Case 3': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA512(
+					CryptoJS.enc.Hex.parse(
+						  'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddddddddddddddddddddddddddddddd'
+						+ 'dddd'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaa')
+				).toString(),
+				  'fa73b0089d56a284efb0f0756c890be9'
+				+ 'b1b5dbdd8ee81a3655f83e33b2279d39'
+				+ 'bf3e848279a722c806b485a47e67c807'
+				+ 'b946a337bee8942674278859e13292fb'
+			)
+		},
+		'Test Case 7': function() {
+			test.assert.same(
+				CryptoJS.HmacSHA512(
+					CryptoJS.enc.Hex.parse(
+						  '54686973206973206120746573742075'
+						+ '73696e672061206c6172676572207468'
+						+ '616e20626c6f636b2d73697a65206b65'
+						+ '7920616e642061206c61726765722074'
+						+ '68616e20626c6f636b2d73697a652064'
+						+ '6174612e20546865206b6579206e6565'
+						+ '647320746f2062652068617368656420'
+						+ '6265666f7265206265696e6720757365'
+						+ '642062792074686520484d414320616c'
+						+ '676f726974686d2e'),
+					CryptoJS.enc.Hex.parse(
+						  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+						+ 'aaaaaa')
+				).toString(),
+				  'e37b6a775dc87dbaa4dfa9f96e5e3ffd'
+				+ 'debd71f8867289865df5a32d20cdc944'
+				+ 'b6022cac3c4982b10d5eeb55c3e4de15'
+				+ '134676fb6de0446065c97440fa8c6a58'
+			)
+		},
+	}
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/locale.test.js b/test/core/js/locale.test.js
new file mode 100755
index 0000000..6411697
--- /dev/null
+++ b/test/core/js/locale.test.js
@@ -0,0 +1,94 @@
+Cryptocat = function() {}
+
+require('../../../src/core/js/etc/locale')
+
+var assert = require('assert')
+Cryptocat.otr = {}
+Cryptocat.otr.maximumFileSize = 10
+describe('Localization module', function() {
+		it('Should create language object from language tokens', function() {
+			var testStr = 'SPB'
+			var lang = new Array(101).join('ss').split('') // 100 elements array
+			lang[2] = testStr
+
+			var langObj = Cryptocat.locale.buildObject('ru', lang)
+
+			assert.equal(langObj.loginWindow.introHeader, testStr)
+		})
+
+	var validServerResponse = 'ltr                                                                                             \n' +
+		'"Helvetica Neue", Helvetica, Arial, Verdana                                                                           \n' +
+		'Private Conversations for Everyone.                                                                                   \n' +
+		'Welcome to Cryptocat. Here are some helpful tips:                                                                     \n' +
+		'Blog                                                                                                                  \n' +
+		'Custom server                                                                                                         \n' +
+		'Reset                                                                                                                 \n' +
+		'conversation name                                                                                                     \n' +
+		'nickname                                                                                                              \n' +
+		'connect                                                                                                               \n' +
+		'Enter a name for your conversation and share it with people you\'d like to talk to, or join lobby to meet new people! \n' +
+		'Enter the name of a conversation to join.                                                                             \n' +
+		'Please enter a conversation name.                                                                                     \n' +
+		'Conversation name must be alphanumeric.                                                                               \n' +
+		'Please enter a nickname.                                                                                              \n' +
+		'Nickname must be alphanumeric.                                                                                        \n' +
+		'Nickname in use.                                                                                                      \n' +
+		'Authentication failure.                                                                                               \n' +
+		'Connection failed.                                                                                                    \n' +
+		'Thank you for using Cryptocat.                                                                                        \n' +
+		'Registering...                                                                                                        \n' +
+		'Connecting...                                                                                                         \n' +
+		'Connected.                                                                                                            \n' +
+		'Please type on your keyboard as randomly as possible for a few seconds.                                               \n' +
+		'Generating encryption keys...                                                                                         \n' +
+		'Group conversation. Click on a user for private chat.                                                                 \n' +
+		'OTR fingerprint (for private conversations):                                                                          \n' +
+		'Group conversation fingerprint:                                                                                       \n' +
+		'Reset my encryption keys                                                                                              \n' +
+		'Resetting your encryption keys will disconnect you. Your fingerprints will also change.                               \n' +
+		'Continue                                                                                                              \n' +
+		'Status: Available                                                                                                     \n' +
+		'Status: Away                                                                                                          \n' +
+		'My Info                                                                                                               \n' +
+		'Desktop Notifications On                                                                                              \n' +
+		'Desktop Notifications Off                                                                                             \n' +
+		'Audio Notifications On                                                                                                \n' +
+		'Audio Notifications Off                                                                                               \n' +
+		'Remember my nickname                                                                                                  \n' +
+		'Don\'t remember my nickname                                                                                           \n' +
+		'Logout                                                                                                                \n' +
+		'Display Info                                                                                                          \n' +
+		'Send encrypted file                                                                                                   \n' +
+		'view image                                                                                                            \n' +
+		'download file                                                                                                         \n' +
+		'Conversation                                                                                                          \n' +
+		'Only ZIP files and images are accepted. Maximum file size: (SIZE) MB.                                                 \n' +
+		'Error: Please make sure your file is a ZIP file or an image.                                                          \n' +
+		'Error: File cannot be larger than (SIZE) MB.                                                                          \n' +
+		'Start video chat                                                                                                      \n' +
+		'End video chat                                                                                                        \n' +
+		'(NICKNAME) would like to start a video chat with you.                                                                 \n' +
+		'Cancel                                                                                                                \n' +
+		'Block                                                                                                                 \n' +
+		'Unblock'
+
+	var parsedResponse = (function() {
+		var parsedTokens = []
+		var tokens = validServerResponse.split('\n')
+		for(var i = 0; i < tokens.length; ++i) {
+			parsedTokens.push(tokens[i].trim())
+		}
+		return parsedTokens
+	})()
+
+	it('Should fill all keys on valid server response', function() {
+		var langObj = Cryptocat.locale.buildObject('en', parsedResponse)
+		assert.equal(langObj.chatWindow.cancel, 'Cancel')
+	})
+
+	it('Should decode cryptocat filesize units', function() {
+		var langObj = Cryptocat.locale.buildObject('en', parsedResponse)
+		assert.ok(langObj.chatWindow.fileTransferInfo.indexOf('0.009765625') > 0)
+	})
+
+})
diff --git a/test/core/js/mustache.test.js b/test/core/js/mustache.test.js
new file mode 100644
index 0000000..ed07493
--- /dev/null
+++ b/test/core/js/mustache.test.js
@@ -0,0 +1,27 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var Mustache = require('../../../src/core/js/lib/mustache')
+
+test['mustache.js'] = {
+
+	'Rendering': {
+		'Escaped': function() {
+			var template = '<h1>{{message}}</h1>'
+			test.assert.same(
+				Mustache.render(template, { message: '<b>abc</b>' }),
+				'<h1><b>abc<&#x2F;b></h1>'
+			)
+		},
+		'Unescaped': function() {
+			var template = '<h1>{{&message}}</h1>'
+			test.assert.same(
+				Mustache.render(template, { message: '<b>abc</b>' }),
+				'<h1><b>abc</b></h1>'
+			)
+		}
+	},
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/salsa20.test.js b/test/core/js/salsa20.test.js
new file mode 100644
index 0000000..5f5eaf6
--- /dev/null
+++ b/test/core/js/salsa20.test.js
@@ -0,0 +1,131 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var Salsa20 = require('../../../src/core/js/lib/salsa20')
+
+test['Salsa20'] = {
+
+  'key=8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0': {
+	beforeEach: function() {
+	  this.key = [8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
+	},
+
+	'nonce=0,0,0,0,0,0,0,0': {
+	  beforeEach: function() {
+		this.salsa20 = new Salsa20(this.key, [0,0,0,0,0,0,0,0])
+	  },
+	  'Check first 512 bytes': function() {
+		test.assert.same(
+			this.salsa20.getHexString(512),
+			'5b393241fd4138028805e664fdd1589f45379ccb57f884234329013e5f0ac632' +
+			'0ed2c012bff2b4cbec4f102abad7380828c3a4872995ff062626039958757f91' +
+			'ffadf73799ffc0662d41b9e2a646c9fce9486ba8e7ab433623452151fdd06a23' +
+			'04c64a5ebcf7e7f1a03df95855f3cf1e8f3a95f33b330325ba0e87eba988e9d1' +
+			'7eeff4eb21ba6a13c3774dc0a4d2fcacafbc9dc4b5b6d72d4b8d44d86b4f5fe7' +
+			'dcb4cd4f7b9b2bdcbf2e53c50ff5ebc9f62ab923e2204221bc019afdb32f7493' +
+			'bab99f2e7bb4708c20480884e77ad038b260c56ec89317847f3548bf12c50f73' +
+			'06e44f7453fa4565658f02d9b839c6c3e1c255c813d8fb2bf621ac0e32da99a3' +
+			'62fe684d8a57078601bf1809a2ed96df5da5b8d248b87118cd5b20a2945cafb6' +
+			'f578933bdfd4ae25955088a30d57810f031b6582bf75cb7dba3dde96d07be006' +
+			'6a6a56031fb1a40ac095a6978a7f68a2f34d854bef168cc5c8669290976b6ff4' +
+			'cebd421b6dea6242926715c8161c330756ebf0217d84ec942a04664d56a1c8ba' +
+			'0408c91f2242dc1dff5052bce9f73f7f29d9887bbe6370f1899af0285d22f28b' +
+			'78e42cd079d391993a2e1c11d4b9e307ca7377828d1a48a569b47553e98f9ec4' +
+			'6ecc14402da20972df13a86a7707b782c50673a220583d8bcbc5408780f95697' +
+			'7edb177c27da09644efa06e008b7fc659e06a532223e88675820ba78e6647877'
+		)
+	  }
+	},
+
+	'nonce=255,255,255,255,255,255,255,255': {
+	  beforeEach: function() {
+		this.salsa20 = new Salsa20(this.key, [255,255,255,255,255,255,255,255])
+	  },
+
+	  'Check first 512 bytes': function() {
+		test.assert.same(
+			this.salsa20.getHexString(512),
+			'61b832391a3584a26de4a61f9100132c8c182b83b26c8305b55b64b9dfb420b2fc' +
+			'348d934b092d5cbe0b967283bdef5945309d768227e29d018555571aa20e3a6634' +
+			'92275eec13315aca0353314a314d67ba117f7e6ebb9ac9d0fa4e0e87f34c73b7e9' +
+			'e089d92ff9a6b775d40e5e00c10935f5fc52c0def08fba6d87ed7b4de694a751fc' +
+			'b6c4472b8da0ef2b2e605d467cd9a397e6b337827ab2e6f3eb9c5f98e60f256592' +
+			'c39bd029ed2c86de3f4f70baffc66aca63f043dafd348dd75aac37636e4f04ebaf' +
+			'727d66ca2a0005ee2ff8519ccb95a3979d20539e1ef9d736251acdc9da54e4132c' +
+			'9c112f16d4d42ad68a48b3ad35d67db5f49a023376f9c0d5340ac317e7b24749e3' +
+			'db1c8fcbb045566eb2ba1658dad2dd737914103df8232f59dca466038663940332' +
+			'6449b6cc5ede58fe5bf5500d5c170f2057402467b7d35f88870beb083811320abf' +
+			'dcc581b11468474be50fd5f66cbcbe5f08016e582710537ca2c63157896c19c409' +
+			'8591ac7109478e9b00b35cdcd575b64488af2151bd7f1c87acc186ad7b6d79698f' +
+			'8a8133646ab8044fef54dc3c2dd9b6d8bc2f93f9dc22bf4cf63607b94a7fd1d0bf' +
+			'e450e0a1ed90618ac6f716f740946e0ebb1957749b374f47520a0bdd11d74f55c3' +
+			'2f27ce85bae4238eb121fd8a2c08f00d8723a850bca31d195271202e98cdd13e18' +
+			'1abaf478ab754b27d975247fbd1dbca489'
+		)
+	  }
+	}
+  },
+
+  'key=1,255,32,78,90,3,200,5,0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,133,211,1': {
+	beforeEach: function() {
+	  this.key = [1,255,32,78,90,3,200,5,0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,133,211,1]
+	},
+
+	'nonce=0,0,0,0,0,0,0,0': {
+	  beforeEach: function() {
+		this.salsa20 = new Salsa20(this.key, [0,0,0,0,0,0,0,0])
+	  },
+	  'Check first 512 bytes': function() {
+		test.assert.same(
+			this.salsa20.getHexString(512),
+			'3e8311417455a6abf6d1548d1b0866006343bb7bcef27f44879b88710897785f' +
+			'96e65abd965788574f84f41a44b4b6bd5bb06eae65793128ede75a0bafb9b164' +
+			'523ca836f233558b78b83d5751a93cb5131def0d787e0b5044bc0416213cef70' +
+			'd1c835414f30b467af33436942f1ecebf7f80b60e2afd3078334de660395ae19' +
+			'b242a41a2900ad02e7d5522b13a35691ae16055fd1777e7514fdfafd59e70626' +
+			'57072102c2be634d1d27241ddc555f7ca5933aef7b16fc70d9c33b769e7f23d1' +
+			'67e8fea371140f45b26d31fcc8bf2d298bbf7c03a7b7555717e79204459772d7' +
+			'fefa3af253e3b6d1af57811047b032b0e6b038f1df02dbb14e79117e21b866c4' +
+			'24a14847641f9da6789aab06a66f159c9cfc2b96192751d0c23ca7893adfcc6f' +
+			'82671368ba4ae794e7f9378cbe6090b2c132e154fe4f7decca8613b771be93d3' +
+			'03f62f960df01ca9c8d63b63f267d797d7613d5f4fbd5abc9ccdf435d3e8a2be' +
+			'138861fa812465e7ada1f4259a29bec7ba7e9ffa78b3c4add61b228a1701a9aa' +
+			'58195befb7f57051cf2fd01e6f785fcda5a83a1877038517fe59d6c57448e15a' +
+			'62711c40b523153d9e3f2cfeae9166f129f3db0c73be97eb5b7a12b08f53f3c0' +
+			'5013fe1241a874cb625dbf34380bfe6fe132959d3b528e102b11ad0faf2c0143' +
+			'a680950006a99d3617a92273c6d5198aa71bd4aa2ab98623057b3a679cf544a7'
+		)
+	  }
+	},
+
+	'nonce=255,255,255,255,255,255,255,255': {
+	  beforeEach: function() {
+		this.salsa20 = new Salsa20(this.key, [255,255,255,255,255,255,255,255])
+	  },
+
+	  'Check first 512 bytes': function() {
+		test.assert.same(
+			this.salsa20.getHexString(512),
+			'b342fa48e76835d16a17b20cade51c3337f93c9819dd9866475196010d258f94' +
+			'95eadc0d3dde542cceba358bb99a744e2fe699f6e6f98e30abdd007c24c2aa72' +
+			'3b6719f0bcabaeeb7d420c3d4f48c40864401ca929081bf7e888d3ef7f99e247' +
+			'0ac7a80605345aa824499672393ae8896a2ef55e3e0275c6c21d49bb18482a29' +
+			'773c34e31e597eb01ff9ca0db33a288c00c2615758fe6f1832f3afdefc7c195a' +
+			'749581d04ecbf86903af3e5767fed1f43fd6232c5798f75136ce366cf7737fa1' +
+			'0a6c066fd77be5b1bf62e5b13c357010cce314f6e5aa0a36cef10eb57e301745' +
+			'1a98c0e50cf0cadfd2d26134b4c35a08013019a0157eae3798f72b567e2edfc6' +
+			'3415101bd2e7cf60d303a8bd926cd0a7184a8a4053838100249e2c4fd79ba120' +
+			'bd24202a1f7db642871170a4a2d03997f76339d25e7ab5c8a18f9e12a372a9f9' +
+			'5ef561b275dca094d78174fab8aefb5ac3cf82e873e2a8373b144f9bf9448b06' +
+			'16fdaa0a3b367fb677546412c0a038d8c5677feb334dd52ad14f555c596de436' +
+			'3e445dd70da65d732de4432b5e2cca05e654cf640f12ec6bad59c26cc9e906ea' +
+			'48e2161a4f186d5da764b12cd9480e4686db9391f2d29cd1696b5e584bb04b6f' +
+			'feb815e1709f39c1e24aa344014b3616b1c34bb034644233857dd33a6ee428d1' +
+			'90b0d1438f8e6730221226c452c6ff1f0122ac47ac3a477f2f8bf4659d4c5ad9'
+		)
+	  }
+	}
+  }
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/core/js/sha.test.js b/test/core/js/sha.test.js
new file mode 100644
index 0000000..3f57c42
--- /dev/null
+++ b/test/core/js/sha.test.js
@@ -0,0 +1,99 @@
+var path = require('path'),
+	test = require('../../testBase')()
+
+var CryptoJS = require('../../../src/core/js/lib/crypto-js')
+
+test['SHA1'] = {
+
+	'SHA1 test vectors': {
+		'abc': function() {
+			var a = 'abc'
+			test.assert.same(
+				CryptoJS.SHA1(a).toString(),
+				'a9993e364706816aba3e25717850c26c9cd0d89d'
+			)
+		},
+		'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq': function() {
+			var a = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
+			test.assert.same(
+				CryptoJS.SHA1(a).toString(),
+				'84983e441c3bd26ebaae4aa1f95129e5e54670f1'
+			)
+		},
+		'a*1000000': function() {
+			var a = ''
+			for (var i = 0; i !== 100000; i++) {
+				a += 'aaaaaaaaaa'
+			}
+			test.assert.same(
+				CryptoJS.SHA1(a).toString(),
+				'34aa973cd4c4daa4f61eeb2bdbad27316534016f'
+			)
+		}
+	}
+
+}
+
+test['SHA256'] = {
+
+	'SHA256 test vectors': {
+		'abc': function() {
+			var a = 'abc'
+			test.assert.same(
+				CryptoJS.SHA256(a).toString(),
+				'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
+			)
+		},
+		'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq': function() {
+			var a = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
+			test.assert.same(
+				CryptoJS.SHA256(a).toString(),
+				'248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1'
+			)
+		},
+		'a*1000000': function() {
+			var a = ''
+			for (var i = 0; i !== 100000; i++) {
+				a += 'aaaaaaaaaa'
+			}
+			test.assert.same(
+				CryptoJS.SHA256(a).toString(),
+				'cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0'
+			)
+		}
+	}
+
+}
+
+test['SHA512'] = {
+
+	'SHA512 test vectors': {
+		'abc': function() {
+			var a = 'abc'
+			test.assert.same(
+				CryptoJS.SHA512(a).toString(),
+				'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'
+			)
+		},
+		'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq': function() {
+			var a = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
+			test.assert.same(
+				CryptoJS.SHA512(a).toString(),
+				'204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445'
+			)
+		},
+		'a*1000000': function() {
+			var a = ''
+			for (var i = 0; i !== 100000; i++) {
+				a += 'aaaaaaaaaa'
+			}
+			test.assert.same(
+				CryptoJS.SHA512(a).toString(),
+				'e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b'
+			)
+		}
+	}
+
+}
+
+module.exports[path.basename(__filename)] = test
\ No newline at end of file
diff --git a/test/testBase.js b/test/testBase.js
new file mode 100644
index 0000000..60183eb
--- /dev/null
+++ b/test/testBase.js
@@ -0,0 +1,43 @@
+/** Test utilities */
+
+var sinon = require('sinon'),
+	chai = require('chai')
+
+var assert = chai.assert,
+	expect = chai.expect,
+	should = chai.should()
+
+/**
+ * Short-hand version of assert.deepEqual
+ *
+ * @param actual Any the actual value
+ * @param expected Any the expected value
+ * @param message String the message show if assertion fails
+ *
+ * @type {Boolean}
+ */
+assert.same = function(actual, expected, message) {
+	assert.deepEqual(actual, expected, message)
+}
+
+/**
+ * Create a test object.
+ *
+ * This will setup a mocking sandbox for the test.
+ */
+var createTest = function() {
+	var test = {
+		beforeEach: function() {
+			test.mocker = sinon.sandbox.create()
+			test.assert = assert
+			test.expect = expect
+			test.should = should
+		},
+		afterEach: function() {
+			test.mocker.restore()
+		}
+	}
+	return test
+}
+
+module.exports = createTest
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/cryptocat.git



More information about the Pkg-mozext-commits mailing list