├── .eslintrc ├── .gitignore ├── .jshintrc ├── CHANGELOG.md ├── DataFile.js ├── LICENSE.md ├── NoxiousClient.js ├── NoxiousCrypto.js ├── README.md ├── cryptoWorker.js ├── css └── style.css ├── docs ├── protocol-overview.md └── protocol-specification.md ├── index.html ├── nox-main.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "rules": { 6 | "comma-dangle": ["error", "never"], 7 | "eol-last": "error", 8 | "keyword-spacing": ["error", {"overrides": { 9 | "catch": {"after": false}, 10 | "do": {"after": false}, 11 | "for": {"after": false}, 12 | "if": {"after": false}, 13 | "switch": {"after": false}, 14 | "while": {"after": false} 15 | }}], 16 | "indent": ["error", 2, {"SwitchCase": 1}], 17 | "max-len": ["error", 80], 18 | "no-irregular-whitespace": "error", 19 | "no-mixed-spaces-and-tabs": "error", 20 | "no-spaced-func": "error", 21 | "no-trailing-spaces": "error", 22 | "object-curly-spacing": ["error", "never"], 23 | "semi": ["error", "always"], 24 | "space-before-blocks": "error", 25 | "space-before-function-paren": ["error", "never"], 26 | "space-infix-ops": "error", 27 | "space-in-parens": ["error", "never"], 28 | "spaced-comment": ["error", "always"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | npm-debug.log 4 | noxious-data/ 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # noxious ChangeLog 2 | 3 | ## 1.0.0 - 2016-08-13 4 | 5 | - See git history for changes. 6 | -------------------------------------------------------------------------------- /DataFile.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict"; 3 | var 4 | fs = require('fs'); 5 | 6 | class DataFile { 7 | constructor(path, init) { 8 | this.path = path; 9 | this.init = init ? init : {}; 10 | try { 11 | fs.accessSync(this.path, fs.R_OK); 12 | this.collection = JSON.parse(fs.readFileSync(this.path)); 13 | } catch(error) { 14 | this.collection = this.init; 15 | this.write(); 16 | } 17 | } 18 | 19 | write() { 20 | fs.writeFile(this.path, JSON.stringify(this.collection)); 21 | } 22 | getAll() { return this.collection; } 23 | get(key) { return this.collection[key]; } 24 | has(key) { 25 | let hasKey = true; 26 | if(this.collection[key] === undefined) { 27 | hasKey = false; 28 | } 29 | return hasKey; 30 | } 31 | set(key,value) { this.collection[key] = value; this.write(); } 32 | delete(key) { delete this.collection[key]; this.write(); } 33 | size() { return Object.keys(this.collection).length; } 34 | } 35 | 36 | module.exports = DataFile; 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ##The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /NoxiousClient.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | socks = require('socksv5'), 5 | http = require('http'); 6 | 7 | class NoxiousClient { 8 | constructor() { 9 | this.socksConfig = { 10 | proxyHost: '127.0.0.1', 11 | proxyPort: 9999, 12 | localDNS: false, 13 | auths: [ 14 | socks.auth.None() 15 | ] 16 | }; 17 | } 18 | transmitObject(destAddress, msg, cb) { 19 | let postData = JSON.stringify(msg); 20 | // localDNS: false is a critical parameter here, this allows lookup of 21 | // hidden (*.onion) addresses. proxy here refers to the tor instance 22 | 23 | let postOptions = { 24 | host: destAddress, 25 | port: 1111, 26 | path: '/', 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | 'Content-Length': Buffer.byteLength(postData) 31 | }, 32 | agent: new socks.HttpAgent(this.socksConfig) 33 | }; 34 | let postReq = http.request(postOptions, function(res) { 35 | res.setEncoding('utf8'); 36 | let body = ''; 37 | res.on('data', function(d) { 38 | body += d; 39 | }); 40 | res.on('end', function() { 41 | console.log('[noxclient transmitObject] HTTP Status: ', res.statusCode); 42 | if(cb && typeof(cb) == 'function') { 43 | let response = {}; 44 | response.status = res.statusCode; 45 | response.body = JSON.parse(body); 46 | cb(response); 47 | } 48 | }); 49 | }); 50 | postReq.end(postData,'utf8'); 51 | } 52 | } 53 | 54 | module.exports = NoxiousClient; 55 | -------------------------------------------------------------------------------- /NoxiousCrypto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | DataFile = require('./DataFile'), 5 | forge = require('node-forge'), 6 | rsa = require('node-forge').pki.rsa, 7 | pki = require('node-forge').pki; 8 | 9 | class NoxiousCrypto { 10 | constructor(obj) { 11 | this.myPrivKey = null; 12 | this.pubPem = null; 13 | this.myPubKey = null; 14 | this.keySize = 0; 15 | // default size for new keys 16 | this.newKeySize = 3072; 17 | 18 | // accepts either dir, filename or public key 19 | if(obj.pubPem) { 20 | // object has public Key 21 | this.pubPem = obj.pubPem; 22 | this.myPubKey = pki.publicKeyFromPem(this.pubPem); 23 | this.keySize = this.myPubKey.n.bitLength(); 24 | } else { 25 | // assume it's a dataDir and filename 26 | let keyData = new DataFile(obj.path); 27 | if(keyData.has('privPem')) { 28 | // key already exists 29 | this.myPrivKey = pki.privateKeyFromPem(keyData.get('privPem')); 30 | this.pubPem = keyData.get('pubPem'); 31 | this.myPubKey = pki.publicKeyFromPem(this.pubPem); 32 | this.keySize = this.myPubKey.n.bitLength(); 33 | console.log('[NoxiousCrypto] Existing Key Bits: ', this.keySize); 34 | } else { 35 | // key was not on disk, create a new one 36 | // generate an RSA key pair in steps that attempt to run for a 37 | // specified period of time on the main JS thread 38 | var state = rsa.createKeyPairGenerationState(this.newKeySize, 0x10001); 39 | var step = (function() { 40 | if(!rsa.stepKeyPairGenerationState(state, 1000)) { 41 | console.log('[NoxiousCrypto] Generating Key...'); 42 | process.nextTick(step); 43 | } 44 | else { 45 | console.log('[NoxiousCrypto] Key Generation Complete.'); 46 | this.pubPem = pki.publicKeyToPem(state.keys.publicKey); 47 | keyData.set('privPem', pki.privateKeyToPem(state.keys.privateKey)); 48 | keyData.set('pubPem', this.pubPem); 49 | this.myPrivKey = state.keys.privateKey; 50 | this.myPubKey = state.keys.publicKey; 51 | this.keySize = this.newKeySize; 52 | } 53 | }).bind(this); 54 | process.nextTick(step); 55 | } 56 | } 57 | } 58 | encrypt(plainText) { 59 | var keySizeBytes = Math.ceil(this.keySize / 8); 60 | var buffer = new Buffer(plainText, 'utf8'); 61 | var maxBufferSize = keySizeBytes - 42; // according to ursa documentation 62 | var bytesEncrypted = 0; 63 | var encryptedBuffersList = []; 64 | // loops through all data buffer encrypting piece by piece 65 | while(bytesEncrypted < buffer.length) { 66 | // calculates next maximun length for temporary buffer and creates it 67 | var amountToCopy = 68 | Math.min(maxBufferSize, buffer.length - bytesEncrypted); 69 | var tempBuffer = new Buffer(amountToCopy); 70 | // copies next chunk of data to the temporary buffer 71 | buffer.copy(tempBuffer, 0, bytesEncrypted, bytesEncrypted + amountToCopy); 72 | // encrypts and stores current chunk 73 | var encryptedBuffer = 74 | new Buffer(this.myPubKey.encrypt(tempBuffer, 'RSA-OAEP'), 'binary'); 75 | encryptedBuffersList.push(encryptedBuffer); 76 | bytesEncrypted += amountToCopy; 77 | } 78 | return Buffer.concat(encryptedBuffersList).toString('base64'); 79 | } 80 | decrypt(cipherText) { 81 | var keySizeBytes = Math.ceil(this.keySize / 8); 82 | var encryptedBuffer = new Buffer(cipherText, 'base64'); 83 | var decryptedBuffers = []; 84 | // if the plain text was encrypted with a key of size N, the encrypted 85 | // result is a string formed by the concatenation of strings of N bytes 86 | // long, so we can find out how many substrings there are by diving the 87 | // final result size per N 88 | var totalBuffers = encryptedBuffer.length / keySizeBytes; 89 | // decrypts each buffer and stores result buffer in an array 90 | for(var i = 0 ; i < totalBuffers; i++) { 91 | // copies next buffer chunk to be decrypted in a temp buffer 92 | var tempBuffer = new Buffer(keySizeBytes); 93 | encryptedBuffer.copy( 94 | tempBuffer, 0, i * keySizeBytes, (i + 1) * keySizeBytes); 95 | // decrypts and stores current chunk 96 | var decryptedBuffer = 97 | this.myPrivKey.decrypt(tempBuffer.toString('binary'), 'RSA-OAEP'); 98 | decryptedBuffers.push(new Buffer(decryptedBuffer, 'utf8')); 99 | } 100 | // concatenates all decrypted buffers and returns the corresponding String 101 | return Buffer.concat(decryptedBuffers).toString(); 102 | } 103 | signString(data) { 104 | var md = forge.md.sha256.create(); 105 | md.update(data, 'utf8'); 106 | var pss = forge.pss.create({ 107 | md: forge.md.sha256.create(), 108 | mgf: forge.mgf.mgf1.create(forge.md.sha256.create()), 109 | saltLength: 20 110 | }); 111 | return new Buffer(this.myPrivKey.sign(md, pss), 'binary') 112 | .toString('base64'); 113 | } 114 | signatureVerified(data, signature) { 115 | let pss = forge.pss.create({ 116 | md: forge.md.sha256.create(), 117 | mgf: forge.mgf.mgf1.create(forge.md.sha256.create()), 118 | saltLength: 20 119 | }); 120 | var md = forge.md.sha256.create(); 121 | md.update(data, 'utf8'); 122 | return this.myPubKey.verify( 123 | md.digest().getBytes(), 124 | new Buffer(signature, 'base64').toString('binary'), pss); 125 | } 126 | } 127 | 128 | module.exports = NoxiousCrypto; 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Noxious 2 | Noxious is designed to be a secure, decentralized, and anonymous instant messaging platform. 3 | ## Anonymous 4 | All communications are conducted between [tor hidden services](https://www.torproject.org/docs/hidden-services.html.en) and never leave the tor network. 5 | ## Secure 6 | In addition to the encryption offered by the tor hidden service protocol, all chat messages are 7 | RSA public-key encrypted using a 3072 bit key. All crytography is handled by the [forge module](https://github.com/digitalbazaar/forge). Although forge is 100% 8 | JavaScript, it does access the CSPRNG (Cryptographically Secure Random Number 9 | Generator) provided by the native openssl library via a call to [node's crypto.randomBytes 10 | function](https://iojs.org/api/crypto.html#crypto_crypto_randombytes_size_callback). 11 | ## Platform 12 | Noxious is built on the [Electron Application Shell](http://electron.atom.io/). 13 | ## Screenshot 14 | ![noxious screenshot](https://github.com/mattcollier/noxious/blob/screenshots/screenshot1.png) 15 | ### Operating System Support 16 | The current version has been tested on 32bit and 64bit version of Debian Linux, 17 | OSX 64bit 18 | ##### Installation Instructions 19 | ###### Node.js 20 | [Get Node.js here.](https://nodejs.org). npm, node package manager will be included with the other Node.js binaries. 21 | 22 | ###### Clone and Build 23 | Next, as a **regular user**, clone this repository into the folder of your choice: 24 | ``` 25 | git clone https://github.com/mattcollier/noxious.git 26 | cd noxious 27 | npm install 28 | ``` 29 | The 'npm install' command will download all the required dependencies. 30 | ##### Run Noxious 31 | From inside the noxious folder do: 32 | ``` 33 | npm start 34 | ``` 35 | You should see the GUI appear. Within 30 seconds or so, you should see your 'Chat ID' 36 | appear next to the asterisk (*) in the upper left hand corner of the window. 37 | You may now provide your Chat ID to another Noxious user who can add you as a 38 | contact which initiates a 'contact request' process which facilitates the 39 | exchange of public keys. 40 | ### Support 41 | Please [submit an issue](https://github.com/mattcollier/noxious/issues). We can 42 | also be reached via irc at #noxious on freenode. 43 | ## Noxious Chat Bot 44 | The [Noxious Chat Bot](https://github.com/mattcollier/noxiousChatBot) is 45 | available for testing. The bot is console based and utilizes native openssl 46 | libraries for crypto. Successful communication between the Noxious Client and 47 | the Noxious Chat Bot demonstrates that the JavaScript forge module utilized 48 | in the Noxious client is openssl compatible. 49 | -------------------------------------------------------------------------------- /cryptoWorker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | myCrypto = null, 5 | initialized = false, 6 | NoxiousCrypto = require('./NoxiousCrypto'); 7 | 8 | process.on('message', function(msgObj) { 9 | switch(msgObj.type) { 10 | case 'init': 11 | myCrypto = new NoxiousCrypto(msgObj.pathToKey); 12 | initialized = true; 13 | break; 14 | case 'decrypt': 15 | if(initialized) { 16 | var parentMsg = {}; 17 | parentMsg.type = 'decryptedData'; 18 | parentMsg.data = JSON.parse(myCrypto.decrypt(msgObj.data)); 19 | process.send(parentMsg); 20 | } 21 | break; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body,html { 2 | height: 100%; 3 | overflow: hidden; 4 | } 5 | 6 | #startupOverlay { 7 | opacity: 0.85; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | background-color: white; 12 | width: 100%; 13 | z-index: 5000; 14 | } 15 | 16 | #progressBar { 17 | position: absolute; 18 | top: 10px; 19 | left: 10px; 20 | width: 300px; 21 | z-index: 6000; 22 | } 23 | 24 | 25 | #mainContainer { 26 | } 27 | 28 | #messageBox { 29 | padding-top: 1em; 30 | } 31 | 32 | #sideBar{ 33 | border-right: 1px solid #B6B6B6; 34 | padding-right: 0px; 35 | padding-bottom: 0px; 36 | } 37 | 38 | #textInput { 39 | margin-top: 5px; 40 | padding: 5px; 41 | width: 100%; 42 | } 43 | 44 | #userInformationContainer { 45 | border-bottom: 1px solid #B6B6B6; 46 | background-color: #303f9f; 47 | padding: 5px; 48 | color: white; 49 | } 50 | 51 | #userInformationHeader h5{ 52 | padding-left: 5px; 53 | font-weight:bold; 54 | } 55 | 56 | #userInformationData { 57 | height: 30px; 58 | padding: 5px; 59 | } 60 | 61 | #contactsContainer { 62 | background-color: #c5cae9; 63 | padding: 5px; 64 | } 65 | 66 | #contactsHeader { 67 | padding-left: 5px; 68 | } 69 | 70 | #contactsHeader h6 { 71 | display:inline-block; 72 | font-weight:bold; 73 | color: #212121 74 | } 75 | 76 | #addContactButton { 77 | display:inline-block; 78 | float:right; 79 | padding-top: 5px; 80 | padding-right: 10px; 81 | cursor: pointer; 82 | } 83 | 84 | #addContactButton:hover { 85 | color: #FF0000; 86 | } 87 | 88 | #newContactFormContainer { 89 | display: none; 90 | padding: 5px; 91 | border-bottom: solid 1px #B6B6B6; 92 | } 93 | 94 | #newContactFormButtons { 95 | padding-top: 5px; 96 | } 97 | 98 | #contactRequestContainer { 99 | display: none; 100 | border-bottom: solid 1px #B6B6B6; 101 | background-color: #ff9800; 102 | color: #212121; 103 | } 104 | #contactRequestHeader { 105 | padding: 5px; 106 | } 107 | 108 | .pendingContactRequest { 109 | padding: 8px; 110 | } 111 | 112 | .pendingContactRequest:hover { 113 | background-color: #448aff; 114 | cursor: pointer; 115 | color: white; 116 | } 117 | 118 | .contact { 119 | padding: 8px; 120 | color: #212121; 121 | } 122 | 123 | .contact:hover { 124 | background-color: #448aff; 125 | color: white; 126 | cursor: pointer; 127 | } 128 | 129 | .contact:hover .editContactIcon, .contact:hover .deleteContactIcon { 130 | visibility: visible; 131 | } 132 | 133 | .deleteContactIcon { 134 | float: right; 135 | padding-right: 3px; 136 | visibility: hidden; 137 | } 138 | 139 | .editContactIcon { 140 | float: right; 141 | padding-right: 5px; 142 | visibility: hidden; 143 | } 144 | 145 | .contactAddress { 146 | padding-left: 3px; 147 | } 148 | 149 | .contactRequestAddress { 150 | padding-left: 5px; 151 | } 152 | 153 | .requestIcon { 154 | padding-right: 5px; 155 | } 156 | 157 | .deleteRequestIcon { 158 | float: right; 159 | } 160 | 161 | .requestStatusIcon { 162 | float: right; 163 | padding-right: 10px; 164 | } 165 | 166 | .nickTextInput { 167 | width: 65%; 168 | padding: 0px; 169 | color: #212121; 170 | } 171 | 172 | .chatContainer { 173 | } 174 | 175 | .chatHeader { 176 | font-weight: bold; 177 | font-size: 1.25em; 178 | } 179 | 180 | .chatInputDiv { 181 | position: absolute; 182 | bottom: -35px; 183 | width: 90%; 184 | } 185 | 186 | .chatInput { 187 | width: 100%; 188 | } 189 | 190 | .chatContent { 191 | overflow-x: hidden; 192 | overflow-y: scroll; 193 | height: 100%; 194 | } 195 | 196 | .specificChatContainer { 197 | height: 100%; 198 | padding-right: 10px; 199 | } 200 | 201 | .hiddenChat { 202 | display: none; 203 | } 204 | 205 | .chatSending { 206 | background-color: #FFFF66; 207 | } 208 | 209 | .chatFailed { 210 | background-color: #FF8080; 211 | } 212 | 213 | .chatName { 214 | font-weight: bold; 215 | } 216 | 217 | .chatTextDiv { 218 | transition: background 0.5s linear; 219 | padding-top: 1px; 220 | padding-bottom: 1px; 221 | padding-left: 10px; 222 | } 223 | 224 | .chatTextSpan { 225 | padding-left: 10px; 226 | } 227 | 228 | .height100 { 229 | height: 100%; 230 | } 231 | 232 | .container-fluid { 233 | padding:0px; 234 | margin: 0px; 235 | } 236 | 237 | .messageNotification { 238 | color: #FF3300; 239 | } 240 | 241 | .btn { 242 | padding: 7px 24px; 243 | border: 0 none; 244 | font-weight: 700; 245 | letter-spacing: 1px; 246 | border-radius: 14px; 247 | } 248 | 249 | .buttonRow { 250 | margin-top: 10px; 251 | } 252 | 253 | .systemMessage { 254 | margin-top: 10px; 255 | border: solid 1px #B6B6B6; 256 | padding: 7px 24px; 257 | border-radius: 14px; 258 | } 259 | -------------------------------------------------------------------------------- /docs/protocol-overview.md: -------------------------------------------------------------------------------- 1 | Noxious Messaging Protocol Overview 2 | ================================== 3 | 4 | Noxious applies a **REST**ful approach for information exchange. Messages are 5 | transmitted as HTTP POST requests. Message delivery confirmation is accomplished 6 | through the use of HTTP status codes. 7 | 8 | ### Tor and Hidden Services 9 | 10 | The Noxious 'Client ID' is a [Tor hidden service name][THSN] 11 | with '.onion' removed from the end. Noxious sets up a rudimentary HTTP server 12 | to accept messages arriving at the hidden service name. The Tor network is responsible 13 | for delivering messages to their destination. Messages sent from a Tor client 14 | to a Tor hidden service never leave the Tor network which would make it difficult 15 | for anyone, including other chat participants, to determine a user's physical 16 | location. 17 | 18 | ### Message Encryption 19 | 20 | All traffic on the Tor network is encrypted using a symmetric key algorithm. The 21 | key used by the symmetric algorithm is changed frequently. This means that it would 22 | be very difficult to decrypt stored network traffic at some future date. 23 | 24 | Additionally, all Noxious chat messages are digitally signed, then encrypted using the RSA 25 | algorithm using a 3072 bit key before they are passed to the Tor network for 26 | delivery. The public key is used to encrypt the message and only the person 27 | possessing the matching private key will be able to decrypt the message. Before 28 | encryption, the message message, including the senders Tor hidden service name 29 | is digitally signed using the SHA-256 hashing algorithm. 30 | 31 | The Noxious 'Client ID' is a Tor hidden service name with '.onion' removed from the end. Noxious sets up a rudimentary HTTP server to accept messages arriving at the hidden service name. The Tor network is responsible for delivering messages to their destination. Messages sent from a Tor client to a Tor hidden service never leave the Tor network which would make it difficult for anyone, including the other chat participant, to determine a user's physical location. 32 | 33 | ### Message Encryption 34 | 35 | All traffic on the Tor network is encrypted using a symmetric key algorithm. The key used by the symmetric algorithm is changed frequently. This means that it would be very difficult to decrypt stored network traffic at some future date. 36 | 37 | Additionally, all Noxious chat messages are digitally signed, then encrypted using the RSA algorithm using a 3072 bit key before they are passed to the Tor network for delivery. The public key is used to encrypt the message and only the person possessing the matching private key will be able to decrypt the message. Before encryption, the message message, including the senders Tor hidden service name is digitally signed using the SHA-256 hashing algorithm. 38 | 39 | Upon receipt, message are decrypted using the recipients private key. This reveals the contents of the message including the digital signature. The digital signature is verified using the senders public key. This insures that the message has not been altered during transit and in fact originated from the proper sender. 40 | 41 | [THSN]:https://trac.torproject.org/projects/tor/wiki/doc/HiddenServiceNames 42 | -------------------------------------------------------------------------------- /docs/protocol-specification.md: -------------------------------------------------------------------------------- 1 | Noxious Messaging Protocol Specification 2 | ================================== 3 | ###Encryption and Digital Signatures 4 | All encryption and digital signatures are based on 3072 bit RSA public/private keys. 5 | 6 | ###Message Types 7 | There are two types of messages Noxious uses to communicate: introduction, and 8 | encryptedData. 9 | 10 | ####Introduction Messages 11 | Introduction messages are exchanged once per contact during the contact request 12 | process. The introduction message contains a user's public encryption key. The 13 | public key is stored as part of the contact record for future use. After 14 | introduction messages have been exchanged between two parties, the parties may 15 | begin sending and receiving encrypted messages. The contents of the introduction 16 | are digitally signed with the sending parties private key. The recipient is able 17 | to verify the signature utilizing the provided public key, which insures that the 18 | contents of the introduction message have not been altered in transit. 19 | ``` 20 | { 21 | content: { 22 | from: 'f5jya7neu64cmhuz.onion', 23 | pubPEM: '', 24 | to: 'cniymubgqjzckk3s.onion', 25 | type: 'introduction' 26 | }, 27 | protocol: '1.0', 28 | signature: '' 29 | } 30 | ``` 31 | Name | Type | Required | Encoding | Description 32 | ---- | ---- | -------- | -------- | ----------- 33 | content | object | true | | 34 | from | property | true | UTF-8 | Sender's Tor hidden service name 35 | pubPEM | property | true | BASE64 | Sender's public key in PEM format 36 | to | property | true | UTF-8 | Recipient's Tor hidden service name 37 | type | property | true | UTF-8 | must equal 'introduction' 38 | protocol | property | true | UTF-8 | must equal '1.0' 39 | signature | property | true | BASE64 | Digital signature based on a SHA256 hash of a stringified version of the content object. 40 | 41 | #####Signing the 'content' object 42 | The digital signature is based on a SHA256 hash of a stringified version of the 43 | 'content' object. JavaScript's built-in JSON.stringify() method does not 44 | guarantee that objects will be stringified in any particular order. In io.js 45 | the [canonical-json module][CJ] is used to stringify the properties of the 'content' 46 | object in **alphabetical order** as shown in the example above. 47 | 48 | ####encryptedData Messages 49 | Parties may begin sending 'encryptedData' messages after they have exchanged 50 | public keys by way of sending and receiving 'introduction' messages. 51 | ``` 52 | { 53 | content: { 54 | clearFrom: 'f5jya7neu64cmhuz.onion', 55 | data: '', 56 | type: 'encryptedData' 57 | }, 58 | protocol: '1.0' 59 | } 60 | ``` 61 | Name | Type | Required | Encoding | Description 62 | ---- | ---- | -------- | -------- | ----------- 63 | content | object | true | | 64 | clearFrom | property | true | UTF-8 | Sender's Tor hidden service name 65 | data | property | true | BASE64 | RSA encrypted [message 'content' object](#message-content-object) 66 | type | property | true | UTF-8 | must equal 'encryptedData' 67 | protocol | property | true | UTF-8 | must equal '1.0' 68 | 69 | #####The 'clearFrom' Property 70 | The 'clearFrom' property is used to quickly determine if the message is from a party 71 | listed in the recipient's contact list. This ensures that the sender's public 72 | key is on file and therefore the digital signature contained in the message can 73 | be verified. A message from an unknown party is dropped which prevents the 74 | recipient from being spammed by an unknown sender. 75 | #####Message 'content' Object 76 | ``` 77 | { 78 | content: { 79 | from: 'f5jya7neu64cmhuz.onion', 80 | msgText: '', 81 | to: 'cniymubgqjzckk3s.onion', 82 | }, 83 | signature: '<digitalSignature>' 84 | } 85 | ``` 86 | Name | Type | Required | Encoding | Description 87 | ---- | ---- | -------- | -------- | ----------- 88 | content | object | true | | 89 | from | property | true | UTF-8 | Sender's Tor hidden service name 90 | msgText | property | true | UTF-8 | Plain text message 91 | to | property | true | UTF-8 | Recipient's Tor hidden service name 92 | type | property | true | UTF-8 | must equal 'introduction' 93 | signature | property | true | BASE64 | Digital signature based on a SHA256 hash of a stringified version of the content object. 94 | #####Signing the 'content' object 95 | The digital signature is based on a SHA256 hash of a stringified version of the 96 | 'content' object. JavaScript's built-in JSON.stringify() method does not 97 | guarantee that objects will be stringified in any particular order. In io.js 98 | the [canonical-json module][CJ] is used to stringify the properties of the 'content' 99 | object in **alphabetical order** as shown in the example above. 100 | ###Transmitting Messages 101 | Messages are transmitted to the recipient's Tor hidden service name with an HTTP 102 | POST request on port 1111 via Tor's socksv5 proxy. See the 103 | [NoxClient transmitObject function][TOF] for an io.js implementation. 104 | 105 | [CJ]:https://www.npmjs.com/package/canonical-json 106 | [TOF]:https://github.com/mattcollier/noxious/blob/master/NoxiousClient.js 107 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>noxious</title> 5 | <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.min.css"> 6 | <link rel="stylesheet" type="text/css" href="css/style.css"> 7 | <!-- Optional theme 8 | <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap-theme.min.css"> 9 | --> 10 | <!-- Latest compiled and minified JavaScript --> 11 | <!-- <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script> --> 12 | <script> 13 | window.$ = window.jQuery = require('./node_modules/jquery/dist/jquery.min.js'); 14 | require('./node_modules/bootstrap/dist/js/bootstrap.min.js'); 15 | var activeChatAddress = ''; 16 | var messageCounter = 0; 17 | var myAddress = ''; 18 | var systemMessagesAddress = 'systemMessages'; 19 | var systemMessagesDisplay = 'System Messages'; 20 | var notifications = {}; 21 | var contactDisplayNames = {}; 22 | 23 | const {ipcRenderer} = require('electron'); 24 | 25 | function isValidTorHiddenServiceName (name) { 26 | var toReturn = false; 27 | if (name.search(/^[a-zA-Z2-7]{16}\.onion$/) != -1) { 28 | // it matched 29 | toReturn = true; 30 | } 31 | return toReturn; 32 | } 33 | 34 | function notifyAtom(msg) { 35 | ipcRenderer.send(msg.method, msg.content); 36 | } 37 | 38 | // tor Hidden Service Address 39 | // .onion is removed from the address to avoid css issues 40 | var torAddress = {fullAddress: '', get address() {return this.fullAddress.split('.')[0];}, nickName: ''}; 41 | 42 | ipcRenderer.on('error', function(e, msg) { 43 | alert(msg.message); 44 | }); 45 | 46 | ipcRenderer.on('status', function(e, msg) { 47 | switch (msg.type) { 48 | case 'onionAddress': 49 | var chatId = Object.create(torAddress); 50 | chatId.fullAddress = msg.content; 51 | // global 52 | myAddress = chatId.address; 53 | $('#chatID').html(myAddress); 54 | // once the chat ID is availabe, noxious is ready for action 55 | // remove the overlay and progress bar 56 | $('#progressBar').remove(); 57 | $('#startupOverlay').fadeOut('slow', function() { 58 | $(this).remove(); 59 | }) 60 | break; 61 | case 'bootstrap': 62 | $('.progress-bar').css('width', msg.content+'%').attr('aria-valuenow', msg.content); 63 | break; 64 | } 65 | }); 66 | 67 | function genContactRequestHTML(contactRequest) { 68 | // contactRequest is a torAddress object plus .direction and .status 69 | var reqCode = '<div id="request_' + contactRequest.address + '" \ 70 | class="pendingContactRequest" \ 71 | data-realaddress="' + contactRequest.address + '" \ 72 | data-direction="' + contactRequest.direction + '">' 73 | if(contactRequest.direction == 'incoming') { 74 | reqCode = reqCode + '<span data-tip="incoming request" \ 75 | class="requestIcon glyphicon glyphicon-log-in" aria-hidden="true"></span>'; 76 | } else { 77 | reqCode = reqCode + '<span data-tip="outgoing request" \ 78 | class="requestIcon glyphicon glyphicon-log-out" aria-hidden="true"></span>'; 79 | } 80 | reqCode = reqCode + '<span class="contactAddress">' + contactRequest.address + '</span> \ 81 | <span data-tip="delete request" \ 82 | class="deleteRequestIcon glyphicon glyphicon-remove" aria-hidden="true"></span>'; 83 | switch (contactRequest.status) { 84 | case 'delivered': 85 | reqCode = reqCode + '<span id="requestStatus_' + contactRequest.address + '" \ 86 | data-status="delivered" data-tip="delivered" \ 87 | class="requestStatusIcon glyphicon glyphicon glyphicon-ok" \ 88 | aria-hidden="true"></span>'; 89 | break; 90 | case 'sending': 91 | reqCode = reqCode + '<span id="requestStatus_' + contactRequest.address + '" \ 92 | data-status="sending" data-tip="sending" \ 93 | class="requestStatusIcon glyphicon glyphicon-transfer" \ 94 | aria-hidden="true"></span>'; 95 | break; 96 | case 'failed': 97 | reqCode = reqCode + '<span id="requestStatus_' + contactRequest.address + '" \ 98 | data-status="failed" data-tip="failed" \ 99 | class="requestStatusIcon glyphicon glyphicon-exclamation-sign" \ 100 | aria-hidden="true"></span>'; 101 | break; 102 | default: 103 | reqCode = reqCode + '<span id="requestStatus_' + contactRequest.address + '" \ 104 | class="requestStatusIcon glyphicon" \ 105 | aria-hidden="true"></span>'; 106 | break; 107 | } 108 | reqCode = reqCode + '</div>'; 109 | return reqCode; 110 | } 111 | 112 | ipcRenderer.on('message', function(e, msg){ 113 | switch (msg.type) { 114 | case 'message': 115 | var contactAddress = Object.create(torAddress); 116 | contactAddress.fullAddress = msg.from; 117 | var chatText = msg.msgText; 118 | if($('#chatContainer_' + contactAddress.address).length == 0) { 119 | genChatContainer(contactAddress.address); 120 | } 121 | // updates the chat content, but does not force display 122 | postChat(contactAddress.address, contactAddress.address, chatText); 123 | // message notification on contact list 124 | if (activeChatAddress !== contactAddress.address) { 125 | // set notifications 126 | $('#notification_' + contactAddress.address).addClass('messageNotification'); 127 | function myFlasher() { 128 | $('#notification_' + contactAddress.address).fadeTo(850, 0.1).fadeTo(850, 1.0); 129 | } 130 | if (notifications[contactAddress.address] === undefined) { 131 | // there's not already a notification for this contact 132 | notifications[contactAddress.address] = setInterval(myFlasher, 2000); 133 | } 134 | } 135 | break; 136 | case 'status': 137 | switch (msg.status) { 138 | case 'delivered': 139 | $('#' + msg.msgId).removeClass('chatSending'); 140 | break; 141 | case 'failed': 142 | $('#' + msg.msgId).removeClass('chatSending'); 143 | $('#' + msg.msgId).addClass('chatFailed'); 144 | break; 145 | } 146 | break; 147 | } 148 | }); 149 | 150 | ipcRenderer.on('contact', function(e, msg) { 151 | switch (msg.type) { 152 | case 'contactRequest': 153 | var contactRequest = Object.create(torAddress); 154 | contactRequest.fullAddress = msg.from; 155 | contactRequest.direction = msg.direction; 156 | contactRequest.status = msg.status; 157 | displayRequestInfo(contactRequest.address, contactRequest.direction); 158 | $('#contactRequestList').append(genContactRequestHTML(contactRequest)); 159 | $('#contactRequestContainer').slideDown(400); 160 | break; 161 | case 'initContactList': 162 | var contactList = msg.contactList; 163 | $('#contactList').html(''); 164 | var contactArray = Object.keys(contactList).map(function (key) { return contactList[key]; }); 165 | for (contactIndex in contactArray) { 166 | // TODO store contact objects into array?? 167 | var contactAddress = Object.create(torAddress); 168 | contactAddress.fullAddress = contactArray[contactIndex].contactAddress; 169 | contactAddress.nickName = contactArray[contactIndex].nickName; 170 | var displayName = (contactAddress.nickName) ? contactAddress.nickName : contactAddress.address; 171 | var isNickName = (contactAddress.nickName) ? true : false; 172 | var contactCode = '<div id="contact_' + contactAddress.address +'" \ 173 | data-realaddress="' + contactAddress.address + '" \ 174 | data-displayname="' + displayName + '" \ 175 | data-isnickname="' + isNickName + '" \ 176 | class="contact"> \ 177 | <span id="notification_' + contactAddress.address + '" \ 178 | class="glyphicon glyphicon-user" aria-hidden="true"></span> \ 179 | <span id="addressSpan_' + contactAddress.address + '" class="contactAddress" \ 180 | >' + displayName + '</span> \ 181 | <span class="deleteContactIcon glyphicon glyphicon-remove" \ 182 | aria-hidden="true"></span> \ 183 | <span class="editContactIcon glyphicon glyphicon-edit" \ 184 | aria-hidden="true"></span> \ 185 | </div>'; 186 | $('#contactList').append(contactCode); 187 | // Remove any messages relating to contact requests for contacts 188 | $('#reqInfo_' + contactAddress.address).remove(); 189 | // display name are tracked to avoid duplicates 190 | contactDisplayNames[displayName] = true; 191 | } 192 | break; 193 | case 'initContactRequestList': 194 | $('#contactRequestList').html(''); 195 | var contactRequestList = msg.contactRequestList; 196 | var reqArray = Object.keys(contactRequestList).map(function(key){ 197 | return contactRequestList[key]; 198 | }); 199 | for (reqIndex in reqArray) { 200 | var contactRequest = Object.create(torAddress); 201 | contactRequest.fullAddress = reqArray[reqIndex].contactAddress; 202 | contactRequest.direction = reqArray[reqIndex].direction; 203 | contactRequest.status = reqArray[reqIndex].status; 204 | $('#contactRequestList').append(genContactRequestHTML(contactRequest)); 205 | displayRequestInfo(contactRequest.address, contactRequest.direction); 206 | } 207 | $('#contactRequestContainer').slideDown(400); 208 | break; 209 | case 'clearContactRequestList': 210 | $('#contactRequestContainer').slideUp(400, function() { 211 | $('#contactRequestList').html(''); 212 | }); 213 | break; 214 | } 215 | }); 216 | 217 | function genChatContainer(contactAddress) { 218 | var chatContainerId = 'chatContainer_' + contactAddress; 219 | var containerHTML = '<div id="' + chatContainerId + '" class="hiddenChat \ 220 | specificChatContainer"> \ 221 | <div id="chatContent_' + contactAddress + '" class="chatContent"> \ 222 | </div> \ 223 | <div id="chatInputDiv_' + contactAddress + '" class="chatInputDiv"> \ 224 | <input id="chatInput_' + contactAddress + '" type="text" class="chatInput" \ 225 | data-realaddress="' + contactAddress + '"> \ 226 | </div> \ 227 | </div>'; 228 | $('#chatContainer').append(containerHTML); 229 | } 230 | 231 | $(document).on('click','.contact',function() { 232 | var contactAddress = $(this).data('realaddress'); 233 | var displayName = $(this).data('displayname'); 234 | if($('#chatContainer_' + contactAddress).length == 0) { 235 | // container needs to be created 236 | genChatContainer(contactAddress); 237 | } 238 | setActiveChat(contactAddress, displayName); 239 | }); 240 | 241 | function scrollToBottom (element) { 242 | // TODO scrolling should be enhanced if user has scrolled up 243 | $(element).scrollTop($(element).prop("scrollHeight")); 244 | } 245 | 246 | function sendMessage(destAddress, msgText, msgId) { 247 | destAddress = destAddress + '.onion'; 248 | var msgObj = {}; 249 | msgObj.method = 'message'; 250 | msgObj.content = {type: 'sendEncrypted', destAddress: destAddress, msgText: msgText, msgId: msgId}; 251 | notifyAtom(msgObj); 252 | } 253 | 254 | function setActiveChat(contactAddress, displayName) { 255 | if(activeChatAddress !== contactAddress) { 256 | var chatContainerId = 'chatContainer_' + contactAddress; 257 | var activeChatContainerId = 'chatContainer_' + activeChatAddress; 258 | activeChatAddress = contactAddress; 259 | $('#' + activeChatContainerId).addClass('hiddenChat'); 260 | $('#' + chatContainerId).removeClass('hiddenChat'); 261 | $('#chatHeader').html(displayName); 262 | if(contactAddress=='systemMessage') { 263 | // do something? 264 | } else { 265 | $('#chatInput_' + contactAddress).focus(); 266 | $('#notification_' + contactAddress).removeClass('messageNotification'); 267 | if(notifications[contactAddress] !== undefined) { 268 | clearInterval(notifications[contactAddress]); 269 | $('#notification_' + contactAddress).fadeTo('fast', 1.0); 270 | delete notifications[contactAddress]; 271 | } 272 | } 273 | } else if (activeChatAddress == contactAddress) { 274 | // just focus the chat text 275 | $('#chatInput_' + contactAddress).focus(); 276 | } 277 | } 278 | 279 | function postChat(contactAddress, authorAddress, chatText) { 280 | var messageId = 'message_' + messageCounter++; 281 | var chatContentId = 'chatContent_' + contactAddress; 282 | var displayName = ''; 283 | var statusClass = ''; 284 | if (authorAddress == myAddress) { 285 | displayName = 'me'; 286 | statusClass = 'chatSending'; 287 | } else { 288 | displayName = $('#contact_' + contactAddress).data('displayname'); 289 | } 290 | displayName = displayName + ':'; 291 | var chatHTML = '<div id="' + messageId + '" class="chatTextDiv ' + statusClass + '"> \ 292 | <span class="chatName">' + displayName + '</span><span \ 293 | class="chatTextSpan">' + chatText + '</span></div>'; 294 | $('#' + chatContentId).append(chatHTML); 295 | scrollToBottom($('#' + chatContentId)); 296 | return messageId; 297 | } 298 | 299 | function genRequestInfoHTML (contactAddress, direction) { 300 | var requestInfoHTML = ''; 301 | if(direction == 'incoming') { 302 | requestInfoHTML = '<div id="reqInfo_' + contactAddress + '" \ 303 | class="col-sm-6 systemMessage text-center"> \ 304 | <strong>' + contactAddress + '</strong> \ 305 | would like to add you on Noxious \ 306 | <div id="requestButtons_' + contactAddress + '" \ 307 | class="text-center buttonRow"> \ 308 | <button data-contactaddress="' + contactAddress + '" \ 309 | class="btn btn-success requestChoiceButton" data-choice="acceptContactRequest" \ 310 | data-description="Accepted">Accept</button> or \ 311 | <button data-contactaddress="' + contactAddress + '" \ 312 | class="btn btn-danger requestChoiceButton" \ 313 | data-choice="declineContactRequest" data-description="Declined"> \ 314 | Decline</button></div></div>'; 315 | } else { 316 | switch($('#requestStatus_'+contactAddress).data('status')) { 317 | case 'delivered': 318 | requestInfoHTML = '<div id="reqInfo_' + contactAddress + '" \ 319 | class="col-sm-6 systemMessage text-center"> \ 320 | You successfully sent a contact request \ 321 | to <strong>' + contactAddress + '</strong> but there has been no reply. \ 322 | </div>'; 323 | break; 324 | case 'failed': 325 | requestInfoHTML = '<div id="reqInfo_' + contactAddress + '" \ 326 | class="col-sm-6 systemMessage text-center">You attempted to \ 327 | send a contact request \ 328 | to <strong>' + contactAddress + '</strong> but the recipient did \ 329 | not receive your request. \ 330 | <div class="text-center buttonRow"><button \ 331 | class="btn btn-success requestRetryButton text-uppercase" \ 332 | data-choice="retryContactRequest" \ 333 | data-contactaddress="' + contactAddress +'">Retry \ 334 | </button></div> \ 335 | </div>'; 336 | break; 337 | } 338 | } 339 | return requestInfoHTML; 340 | } 341 | 342 | $(document).on('keydown', '.chatInput', function(e) { 343 | if(e.keyCode == 13) { 344 | var contactAddress = $(this).data('realaddress'); 345 | var chatText = $(this).val(); 346 | if (chatText == '') { 347 | // blank lines should not be transmitted 348 | alert('Blank lines will not be transmitted.'); 349 | } else { 350 | $(this).val(''); 351 | var messageId = postChat(contactAddress, myAddress, chatText); 352 | sendMessage(contactAddress, chatText, messageId); 353 | } 354 | } 355 | }); 356 | 357 | function displayRequestInfo(contactAddress, direction) { 358 | // display if it's not already displayed 359 | if($('#reqInfo_' + contactAddress).length == 0) { 360 | $('#chatContent_systemMessages').append(genRequestInfoHTML(contactAddress, direction)); 361 | } else { 362 | // replace it 363 | $('#reqInfo_' + contactAddress).replaceWith(genRequestInfoHTML(contactAddress, direction)); 364 | } 365 | } 366 | 367 | $(document).on('click','.pendingContactRequest', function() { 368 | displayRequestInfo($(this).data('realaddress'), $(this).data('direction')); 369 | setActiveChat(systemMessagesAddress, systemMessagesDisplay); 370 | }); 371 | 372 | function removeContactRequest(contactAddress) { 373 | // remove the contact request as well as related informational messages 374 | $('#request_' + contactAddress).remove(); 375 | $('#reqInfo_' + contactAddress).remove(); 376 | // if there are no other requests, hide container 377 | if($('#contactRequestList').children().length == 0) { 378 | $('#contactRequestContainer').slideUp(400); 379 | } 380 | } 381 | 382 | $(document).on('click','.requestChoiceButton', function() { 383 | var contactAddress = $(this).data('contactaddress'); 384 | var choice = $(this).data('choice'); 385 | var description = $(this).data('description'); 386 | $('#requestButtons_' + contactAddress).html(description); 387 | if(choice == "acceptContactRequest") { 388 | $('#requestStatus_' + contactAddress).addClass('glyphicon-transfer'); 389 | } 390 | var msgObj = {}; 391 | msgObj.method = 'contact'; 392 | msgObj.content = { type: choice, contactAddress: contactAddress+'.onion' }; 393 | notifyAtom(msgObj); 394 | function removeReqInfo(contactAddress) { 395 | $('#reqInfo_' + contactAddress).fadeOut(2000, function() { 396 | $(this).remove(); 397 | }); 398 | } 399 | setTimeout(function() { 400 | removeReqInfo(contactAddress); 401 | }, 2000); 402 | }); 403 | 404 | $(document).on('click','.deleteRequestIcon', function(e) { 405 | // TODO confirm request? 406 | e.stopPropagation(); 407 | var contactAddress = $(this).closest('div.pendingContactRequest').data('realaddress'); 408 | var msgObj = {}; 409 | msgObj.method = 'contact'; 410 | msgObj.content = {type: 'delContactRequest', contactAddress: contactAddress+'.onion'}; 411 | notifyAtom(msgObj); 412 | removeContactRequest(contactAddress); 413 | }); 414 | 415 | $(document).on('click','.requestRetryButton', function(e) { 416 | var contactAddress = $(this).data('contactaddress'); 417 | var msgObj = {}; 418 | msgObj.method = 'contact'; 419 | msgObj.content = { type: 'retryContactRequest', contactAddress: contactAddress + '.onion' }; 420 | notifyAtom(msgObj); 421 | $('#requestStatus_' + contactAddress).removeClass('glyphicon-exclamation-sign'); 422 | $('#requestStatus_' + contactAddress).addClass('glyphicon-transfer'); 423 | }); 424 | 425 | $(document).on('click','.editContactIcon', function(e) { 426 | e.stopPropagation(); 427 | var contactAddress = $(this).closest('div.contact').data('realaddress'); 428 | var currentValue = $('#addressSpan_'+contactAddress).html(); 429 | var textInputID = 'nickTextInput_' + contactAddress; 430 | var textInputHTML = '<input id="' + textInputID + '" type="text" class="nickTextInput" \ 431 | data-currentvalue="' + currentValue + '" placeholder="nickname">'; 432 | $('#addressSpan_'+contactAddress).html(textInputHTML); 433 | $('#' + textInputID).focus(); 434 | }); 435 | 436 | $(document).on('click','.deleteContactIcon', function(e) { 437 | e.stopPropagation(); 438 | var contactAddress = $(this).closest('div.contact').data('realaddress'); 439 | var msgObj = {}; 440 | msgObj.method = 'contact'; 441 | msgObj.content = {type: 'delContact', contactAddress: contactAddress+'.onion'}; 442 | notifyAtom(msgObj); 443 | if($('#chatContainer_' + contactAddress).length !== 0) { 444 | if(activeChatAddress == contactAddress) { 445 | setActiveChat(systemMessagesAddress, systemMessagesDisplay); 446 | $('#chatContainer_' + contactAddress).remove(); 447 | } 448 | } 449 | }); 450 | 451 | function updateDisplayName(contactAddress, displayName) { 452 | $('#contact_' + contactAddress).data('displayname', displayName); 453 | if (activeChatAddress == contactAddress) { 454 | $('#chatHeader').html(displayName); 455 | } 456 | } 457 | 458 | function processNickName(textInput) { 459 | var realAddress = $(textInput).closest('div.contact').data('realaddress'); 460 | var currentValue = $(textInput).data('currentvalue'); 461 | var isNickName = $(textInput).closest('div.contact').data('isnickname'); 462 | var nickName = $(textInput).val(); 463 | // TODO validate nickname: length, allowed chars etc. 464 | if (nickName === '~' && isNickName) { 465 | // remove nickname 466 | $(textInput).closest('span.contactAddress').html(realAddress); 467 | updateDisplayName(realAddress, realAddress); 468 | delete contactDisplayNames[currentValue]; 469 | var msgObj = {}; 470 | msgObj.method = 'contact'; 471 | msgObj.content = {type: 'delNickName', contactAddress: realAddress+'.onion'}; 472 | notifyAtom(msgObj); 473 | } else if (nickName === '') { 474 | $(textInput).closest('span.contactAddress').html(currentValue); 475 | } else { 476 | if (contactDisplayNames[nickName] === undefined) { 477 | $(textInput).closest('span.contactAddress').html(nickName); 478 | updateDisplayName(realAddress, nickName); 479 | contactDisplayNames[nickName] = true; 480 | if (currentValue !== realAddress) { 481 | delete contactDisplayNames[currentValue]; 482 | } 483 | var msgObj = {}; 484 | msgObj.method = 'contact'; 485 | msgObj.content = {type: 'setNickName', contactAddress: realAddress+'.onion', nickName: nickName}; 486 | notifyAtom(msgObj); 487 | } else { 488 | $(textInput).closest('span.contactAddress').html(currentValue); 489 | alert('The nickname you provided is already in use, please enter a unique nickname.'); 490 | } 491 | } 492 | } 493 | 494 | $(document).on('keydown', '.nickTextInput', function(e) { 495 | if(e.keyCode == 27) { 496 | $(this).val(''); 497 | $(this).blur(); 498 | } else if (e.keyCode == 13) { 499 | // enter=13, tab is caught by focusout 500 | //validateNickName(this, realAddress, currentValue, isNickName); 501 | $(this).blur(); 502 | } else if (e.keyCode == 46) { 503 | // del=46, remove nick and revert back to address 504 | $(this).val('~'); 505 | $(this).blur(); 506 | } 507 | }); 508 | 509 | $(document).on('focusout', '.nickTextInput', function() { 510 | processNickName(this); 511 | }); 512 | 513 | $(window).resize(function(){ 514 | $('#chatContainer').css('height', $(window).height() - 80); 515 | }).resize(); 516 | 517 | $(document).ready(function() { 518 | // set the startup overlay height to 100% of window size 519 | $('#startupOverlay').css('height', $(window).height()); 520 | // center the progress bar 521 | $('#progressBar').css('top', ($(window).height() / 2) - 20); 522 | $('#progressBar').css('left', ($(window).width() / 2) - 150); 523 | // set chat window height based on the window size 524 | $('#chatContainer').css('height', $(window).height() - 80); 525 | // Create container for system messages 526 | genChatContainer(systemMessagesAddress); 527 | //remove the text input for system messages 528 | $('#chatInputDiv_' + systemMessagesAddress).remove(); 529 | setActiveChat(systemMessagesAddress, systemMessagesDisplay); 530 | // Add Contact 531 | $('#addContactButton').click(function () { 532 | $('#newContactFormContainer').slideDown('fast'); 533 | $('#newContactAddressText').focus(); 534 | }); 535 | 536 | // Send Contact Request 537 | $('#sendContactRequestButton').click(function () { 538 | var contactAddress = $('#newContactAddressText').val() + '.onion'; 539 | if (isValidTorHiddenServiceName(contactAddress)) { 540 | var msgObj = {}; 541 | msgObj.method = 'contact'; 542 | msgObj.content = {type: 'sendContactRequest', contactAddress: contactAddress}; 543 | notifyAtom(msgObj); 544 | $('#newContactAddressText').val(''); 545 | $('#newContactFormContainer').slideUp('fast'); 546 | } else { 547 | alert('The Chat ID you have entered is invalid. A valid ID is 16 characters long and contains the letters a-z and the numbers 2-7, inclusive.'); 548 | } 549 | }); 550 | 551 | // Cancel Contact Request 552 | $('#cancelContactRequestButton').click(function () { 553 | $('#newContactAddressText').val(''); 554 | $('#newContactFormContainer').slideUp('fast'); 555 | }); 556 | }); 557 | 558 | </script> 559 | </head> 560 | <body> 561 | <!-- We are using node.js <script>document.write(process.version)</script> 562 | and atom-shell <script>document.write(process.versions['atom-shell'])</script>. 563 | --> 564 | <div id='startupOverlay'></div> 565 | <div id='progressBar' class="progress"> 566 | <div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> 567 | <span class="sr-only">0% Complete</span> 568 | </div> 569 | </div> 570 | <div id='mainContainer' class='container-fluid height100'> 571 | <div class='row height100'> 572 | <div id='sideBar' class='col-sm-3 height100'> 573 | <div id='userInformationContainer'> 574 | <div id='userInformationHeader'> 575 | <h5>CHAT ID</h5> 576 | </div> 577 | <div id='userInformationData'> 578 | <span class="glyphicon glyphicon-asterisk" aria-hidden="true"></span> 579 | <span id="chatID" class='text-center'></span> 580 | </div> 581 | </div> 582 | <div id='contactsContainer'> 583 | <div id='contactsHeader'> 584 | <h6>CONTACTS</h6> 585 | <span id="addContactButton" class="glyphicon glyphicon-plus" aria-hidden="true"></span> 586 | </div> 587 | <div id="newContactFormContainer"> 588 | <div><input id='newContactAddressText' type="text" size="17"></div> 589 | <div id="newContactFormButtons"> 590 | <button id="sendContactRequestButton" class="btn-success">Send Invite</button> 591 | <button id="cancelContactRequestButton" class="btn-danger">Cancel</button> 592 | </div> 593 | </div> 594 | <div id='contactRequestContainer'> 595 | <div id='contactRequestHeader'> 596 | Pending contact requests 597 | </div> 598 | <div id='contactRequestList'> 599 | </div> 600 | </div> 601 | <div id='contactList'> 602 | </div> 603 | </div> 604 | </div> 605 | <div id='middleColumn' class='col-sm-9'> 606 | <div id="chatHeader" class="chatHeader"> 607 | </div> 608 | <div id="chatContainer" class="chatContainer"> 609 | </div> 610 | </div> 611 | </div> 612 | </div> 613 | </body> 614 | </html> 615 | -------------------------------------------------------------------------------- /nox-main.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, esversion: 6 */ 2 | 3 | "use strict"; 4 | 5 | // Module to control application life. 6 | const {app} = require('electron'); 7 | // Module to create native browser window. 8 | const {BrowserWindow} = require('electron'); 9 | 10 | var 11 | Path = require('path'), 12 | http = require('http'), 13 | DataFile = require('./DataFile'), 14 | // communications functions 15 | NoxiousClient = require('./NoxiousClient'), 16 | myNoxClient = new NoxiousClient(), 17 | // cononical json.stringify 18 | // Stringify objects in a consistent way prior to hashing/signing 19 | jsStringify = require('canonical-json'), 20 | contactList = 21 | new DataFile(Path.join(app.getPath('userData'), 'Contacts.json')), 22 | contactRequestList = 23 | new DataFile(Path.join(app.getPath('userData'), 'ContactRequests.json')), 24 | thsBuilder = require('ths'), 25 | ths = new thsBuilder(app.getPath('userData')), 26 | fork = require('child_process').fork, 27 | cryptoWorker = fork('./cryptoWorker.js'), 28 | NoxiousCrypto = require('./NoxiousCrypto'), 29 | myCrypto = new NoxiousCrypto({ 30 | path: Path.join(app.getPath('userData'), 'PrivateKey.json') 31 | }), 32 | dataTransmitDomain = require('domain').create(), 33 | contactRequestDomain = require('domain').create(), 34 | myAddress; 35 | 36 | function notifyCommError(error) { 37 | let msgObj = {}; 38 | msgObj.method = 'error'; 39 | switch(error) { 40 | case 'EHOSTUNREACH': 41 | msgObj.content = { 42 | type: 'communication', 43 | message: 'The recipient does not appear to be online at this time. ' + 44 | 'Try again later.'}; 45 | break; 46 | case 'ETTLEXPIRED': 47 | msgObj.content = { 48 | type: 'communication', 49 | message: 'There seems to be trouble with your Internet connection. ' + 50 | 'Try again later.'}; 51 | break; 52 | default: 53 | msgObj.content = { 54 | type: 'communication', 55 | message: 'A communication error occurred, see the console log for ' + 56 | 'more information.'}; 57 | break; 58 | } 59 | notifyGUI(msgObj); 60 | } 61 | 62 | dataTransmitDomain.on('error', function(err) { 63 | console.log(err); 64 | notifyCommError(err.code); 65 | }); 66 | 67 | contactRequestDomain.on('error', function(err) { 68 | console.log(err); 69 | notifyCommError(err.code); 70 | updateRequestStatus(err.domainEmitter._dstaddr, 'failed'); 71 | }); 72 | 73 | function notifyGUI(msg) { 74 | if(!mainWindow.webContents.isLoading()) { 75 | mainWindow.webContents.send(msg.method, msg.content); 76 | } 77 | } 78 | 79 | function isValidTorHiddenServiceName(name) { 80 | let toReturn = false; 81 | if(name.search(/^[a-zA-Z2-7]{16}\.onion$/) != -1) { 82 | // it matched 83 | toReturn = true; 84 | } 85 | return toReturn; 86 | } 87 | 88 | function getContacts(forceReload) { 89 | if(contactList.size() > 0 || forceReload) { 90 | var msgObj = {}; 91 | msgObj.method = 'contact'; 92 | msgObj.content = { 93 | type: 'initContactList', contactList: contactList.getAll() 94 | }; 95 | notifyGUI(msgObj); 96 | } 97 | } 98 | 99 | function getContactRequests() { 100 | let msgObj = {}; 101 | msgObj.method = 'contact'; 102 | if(contactRequestList.size() > 0) { 103 | msgObj.content = { 104 | type: 'initContactRequestList', 105 | contactRequestList: contactRequestList.getAll() 106 | }; 107 | } else { 108 | msgObj.content = { 109 | type: 'clearContactRequestList', contactRequestList: {} 110 | }; 111 | } 112 | notifyGUI(msgObj); 113 | } 114 | 115 | function updateRequestStatus(contactAddress, status, updateGui) { 116 | if(updateGui === undefined) { 117 | updateGui = true; 118 | } 119 | let tmpContact = contactRequestList.get(contactAddress); 120 | tmpContact.status = status; 121 | contactRequestList.set(contactAddress, tmpContact); 122 | if(updateGui) { 123 | getContactRequests(); 124 | } 125 | } 126 | 127 | function buildEncryptedMessage(destAddress, msgText) { 128 | let tmpCrypto = 129 | new NoxiousCrypto({'pubPem': contactList.get(destAddress).pubPem}); 130 | let msgContent = {}; 131 | msgContent.type = 'message'; 132 | msgContent.from = myAddress; 133 | msgContent.to = destAddress; 134 | msgContent.msgText = msgText; 135 | let msgObj = {}; 136 | msgObj.content = msgContent; 137 | // sign using my private key 138 | msgObj.signature = myCrypto.signString(jsStringify(msgContent)); 139 | // encrypt using recipients public key 140 | let encryptedData = tmpCrypto.encrypt(JSON.stringify(msgObj)); 141 | let encObj = {}; 142 | encObj.content = { 143 | type: 'encryptedData', clearFrom: myAddress, data: encryptedData 144 | }; 145 | encObj.protocol = '1.0'; 146 | return encObj; 147 | } 148 | 149 | function buildContactRequest(destAddress) { 150 | let introObj = {}; 151 | introObj.type = 'introduction'; 152 | introObj.from = myAddress; 153 | introObj.to = destAddress; 154 | introObj.pubPem = myCrypto.pubPem; 155 | let msgObj = {}; 156 | msgObj.content = introObj; 157 | msgObj.signature = myCrypto.signString(jsStringify(introObj)); 158 | msgObj.protocol = '1.0'; 159 | return msgObj; 160 | } 161 | 162 | function transmitContactRequest(destAddress) { 163 | contactRequestDomain.run(function() { 164 | myNoxClient.transmitObject( 165 | destAddress, buildContactRequest(destAddress), function(res) { 166 | switch(res.status) { 167 | case 200: 168 | updateRequestStatus(destAddress, 'delivered'); 169 | break; 170 | case 409: 171 | updateRequestStatus(destAddress, 'failed'); 172 | var msgObj = {}; 173 | msgObj.method = 'error'; 174 | var failedReason = res.body.reason; 175 | switch(failedReason) { 176 | case 'EKEYSIZE': 177 | msgObj.content = { 178 | type: 'contact', 179 | message: 'The contact request was rejected because your ' + 180 | 'public encryption key is not proper. Please upgrade your ' + 181 | 'Noxious software.'}; 182 | break; 183 | case 'EPROTOCOLVERSION': 184 | msgObj.content = { 185 | type: 'contact', 186 | message: 'The contact request was rejected because the ' + 187 | 'message format is not proper. Please upgrade your ' + 188 | 'Noxious software.'}; 189 | break; 190 | default: 191 | msgObj.content = { 192 | type: 'contact', 193 | message: 'The recipient already has your contact ' + 194 | 'information. Ask them to delete your contact ' + 195 | 'information and try again.'}; 196 | break; 197 | } 198 | notifyGUI(msgObj); 199 | break; 200 | } 201 | }); 202 | }); 203 | } 204 | 205 | // Keep a global reference of the window object, if you don't, the window will 206 | // be closed automatically when the javascript object is GCed. 207 | var mainWindow = null; 208 | var pageLoaded = false; 209 | 210 | var server = http.createServer(function(req, res) { 211 | if(req.method === 'GET') { 212 | res.writeHead(200, {'Content-Type': 'text/plain'}); 213 | res.end('Hello world!'); 214 | } else if(req.method === 'POST') { 215 | if(req.url === '/') { 216 | var reqBody = ''; 217 | req.on('data', function(d) { 218 | reqBody += d; 219 | if(reqBody.length > 1e7) { 220 | res.writeHead( 221 | 413, 'Request Entity Too Large', {'Content-Type': 'text/html'}); 222 | res.end('<!doctype html><html><head><title>413</title></head>' + 223 | '<body>413: Request Entity Too Large</body></html>'); 224 | } 225 | }); 226 | req.on('end', function() { 227 | console.log('[HTTP Server] ', reqBody); 228 | let status = preProcessMessage(reqBody); 229 | if(status.code == 200) { 230 | res.writeHead(200, {'Content-Type': 'application/json'}); 231 | res.end(JSON.stringify({status: 'OK'})); 232 | processMessage(reqBody); 233 | } else { 234 | res.writeHead(status.code, {'Content-Type': 'application/json'}); 235 | res.end(JSON.stringify({reason: status.reason})); 236 | } 237 | }); 238 | } 239 | } 240 | }); 241 | server.listen(1111, '127.0.0.1'); 242 | console.log('Server running at http://127.0.0.1:1111'); 243 | 244 | function relayMessage(msg) { 245 | mainWindow.webContents 246 | .send('msg', 'from: ' + msg.name + ' message: ' + msg.message); 247 | } 248 | 249 | function registerContactRequest(req) { 250 | // add to list of other contact requests for saving to disk 251 | // build object 252 | // TODO is a date/time wanted or needed here? Other Data? 253 | // this same data structure is copied to the contact list upon acceptance. 254 | var tmpObj = {}; 255 | tmpObj.pubPem = req.pubPem ; 256 | tmpObj.contactAddress = req.from; 257 | // check for dups in requests list and contact list 258 | if(!contactRequestList.has(req.from) && !contactList.has(req.from)) { 259 | // this is a new incoming contact Request 260 | console.log('[contact] New Contact Request Received'); 261 | tmpObj.direction = 'incoming'; 262 | contactRequestList.set(req.from, tmpObj); 263 | let msgObj = {}; 264 | msgObj.method = 'contact'; 265 | msgObj.content = { 266 | type: 'contactRequest', from: req.from, direction: 'incoming' 267 | }; 268 | notifyGUI(msgObj); 269 | } else if(contactRequestList.has(req.from) && 270 | contactRequestList.get(req.from).direction == 'outgoing') { 271 | // this person accepted a contact request 272 | contactList.set( 273 | req.from, { 274 | pubPem: tmpObj.pubPem, contactAddress: tmpObj.contactAddress 275 | }); 276 | contactRequestList.delete(req.from); 277 | getContacts(); 278 | getContactRequests(); 279 | } else if(contactRequestList.get(req.from)) { 280 | console.log('[contact] Contact request is from an existing contact.'); 281 | } 282 | } 283 | 284 | function preProcessMessage(msg) { 285 | // default statusCode = forbidden; 286 | var status = {}; 287 | status.code = 403; 288 | status.reason = ''; 289 | var msgObj = JSON.parse(msg); 290 | console.log('[preProcessMessage] Start'); 291 | // TODO this function should verify message integrity 292 | if(msgObj.protocol === '1.0') { 293 | if(msgObj.content !== undefined) { 294 | var content = msgObj.content; 295 | if(content.type !== undefined) { 296 | switch(content.type) { 297 | case 'introduction': 298 | if(content.from !== undefined && content.from && 299 | isValidTorHiddenServiceName(content.from)) { 300 | if(!contactList.has(content.from) && 301 | !contactRequestList.has(content.from)) { 302 | // we don't know this person already, intro is OK 303 | status.code = 200; 304 | } else if(contactRequestList.has(content.from) && 305 | contactRequestList.get(content.from).direction == 'outgoing' && 306 | contactRequestList.get(content.from).status == 'delivered') { 307 | // we're expecting to hear back from this person, intro is OK 308 | status.code = 200; 309 | } else { 310 | // contact request (key exchange) process needs to be repeated. 311 | status.code = 409; 312 | } 313 | } 314 | if(status.code === 200) { 315 | // so far so good, but now check the pubkey, reset status code 316 | status.code = 403; 317 | var minKeySize = 3072; 318 | var tmpCrypto = new NoxiousCrypto({'pubPem': content.pubPem}); 319 | var keySize = tmpCrypto.keySize; 320 | console.log( 321 | '[preprocessing message] The key size is ', keySize, 'bits.'); 322 | if(keySize < minKeySize) { 323 | console.log( 324 | '[preprocessing message] The key must be at least ', 325 | minKeySize, ' bits'); 326 | status.code = 409; 327 | status.reason = 'EKEYSIZE'; 328 | } else { 329 | console.log( 330 | '[preprocessing message] The key size meets the ', 331 | minKeySize, 'bit requirement'); 332 | status.code = 200; 333 | } 334 | } 335 | break; 336 | case 'encryptedData': 337 | if(content.clearFrom !== undefined && content.clearFrom && 338 | isValidTorHiddenServiceName(content.clearFrom)) { 339 | if(contactList.has(content.clearFrom)) { 340 | // this is from an existing contact, it's OK 341 | status.code = 200; 342 | } else { 343 | // there is no public key for this contact 344 | status.code = 410; 345 | } 346 | } 347 | break; 348 | } 349 | } 350 | } else { 351 | // protocol version mismatch 352 | console.log('[preprocessing message] Protocol version mismatch.'); 353 | status.code = 409; 354 | status.reason = 'EPROTOCOLVERSION'; 355 | } 356 | } 357 | return status; 358 | } 359 | 360 | cryptoWorker.on('message', function(msgObj) { 361 | switch(msgObj.type) { 362 | case 'decryptedData': 363 | var decObj = msgObj.data; 364 | // console.log('Decrypted Data: ', decObj); 365 | var content = decObj.content; 366 | var signature = decObj.signature; 367 | // TODO additional integrity checks? 368 | if(content.to && content.to == myAddress && content.from && 369 | isValidTorHiddenServiceName(content.from) && content.type && 370 | content.msgText) { 371 | if(contactList.has(content.from)) { 372 | switch(content.type) { 373 | case 'message': 374 | var tmpCrypto = new NoxiousCrypto({ 375 | 'pubPem': contactList.get(content.from).pubPem 376 | }); 377 | if(tmpCrypto.signatureVerified(jsStringify(content), signature)) { 378 | console.log('[process message] Message is properly signed.'); 379 | if(content.to == myAddress && content.from !== undefined && 380 | content.from && content.from !== myAddress) { 381 | console.log( 382 | '[process message] Message is properly addressed.'); 383 | var incomingMessage = { 384 | method: 'message', 385 | content: { 386 | type: 'message', 387 | from: content.from, 388 | msgText: content.msgText 389 | } 390 | }; 391 | notifyGUI(incomingMessage); 392 | } 393 | } else { 394 | console.log('[process message] Message is NOT properly ' + 395 | 'signed. Disregarding.'); 396 | } 397 | break; 398 | } 399 | } 400 | } 401 | break; 402 | } 403 | }); 404 | 405 | function processMessage(msg) { 406 | var msgObj = JSON.parse(msg); 407 | var content = msgObj.content; 408 | switch(content.type) { 409 | case 'introduction': 410 | var signature = msgObj.signature; 411 | var tmpCrypto = new NoxiousCrypto({'pubPem': content.pubPem}); 412 | if(tmpCrypto.signatureVerified(jsStringify(content), signature)) { 413 | console.log('[process message] Introduction is properly signed.'); 414 | // TODO enhance from address checking, for now, not null or undefined, 415 | // and not myAddress 416 | if(content.to == myAddress && content.from !== undefined && 417 | content.from && content.from !== myAddress) { 418 | // content.to and content.from are part of the signed content. 419 | console.log('[process message] Introduction is properly addressed.'); 420 | registerContactRequest(content); 421 | } 422 | } else { 423 | console.log('[process message] Introduction is NOT properly signed. ' + 424 | 'Disregarding.'); 425 | } 426 | break; 427 | case 'encryptedData': 428 | var workerMsg = {}; 429 | workerMsg.type = 'decrypt'; 430 | workerMsg.data = content.data; 431 | cryptoWorker.send(workerMsg); 432 | break; 433 | } 434 | } 435 | 436 | function startHiddenService() { 437 | // we know that tor is loaded and web page is loaded. 438 | var serviceList = ths.getServices(); 439 | console.log('Service List: %j',serviceList); 440 | 441 | function noxiousExists(element) { 442 | return element.name === 'noxious'; 443 | } 444 | 445 | var noxiousProperties = serviceList.filter(noxiousExists); 446 | if(noxiousProperties === 0) { 447 | // does not exist, create it 448 | console.log('Creating new noxious service'); 449 | ths.createHiddenService('noxious','1111'); 450 | ths.saveConfig(); 451 | // FIXME: https://github.com/Mowje/node-ths/issues/5 452 | var myDelegate = function() { 453 | ths.signalReload(); 454 | }; 455 | var myVar = setTimeout(myDelegate, 250); 456 | } 457 | // FIXME: does not work properly on initial startup: 458 | // https://github.com/Mowje/node-ths/issues/3 459 | ths.getOnionAddress('noxious', function(err, onionAddress) { 460 | if(err) { 461 | console.error( 462 | '[getOnionAddress] Error while reading hostname file: ' + err); 463 | } else { 464 | console.log('[getOnionAddress] Onion Address is: ', onionAddress); 465 | myAddress = onionAddress; 466 | var msgObj = {}; 467 | msgObj.method = 'status'; 468 | msgObj.content = {type: 'onionAddress', content: myAddress}; 469 | notifyGUI(msgObj); 470 | } 471 | }); 472 | } 473 | 474 | // track ths / tor bootstrapping 475 | ths.on('bootstrap', function(state) { 476 | var msgObj = {}; 477 | msgObj.method = 'status'; 478 | msgObj.content = {type: 'bootstrap', content: state}; 479 | notifyGUI(msgObj); 480 | }); 481 | 482 | // This method will be called when atom-shell has done everything 483 | // initialization and ready for creating browser windows. 484 | app.on('ready', function() { 485 | // handles communication between browser and io.js (webpage) 486 | const {ipcMain} = require('electron'); 487 | // var ipc = require('ipc'); 488 | var workerMsg = {}; 489 | workerMsg.type = 'init'; 490 | workerMsg.pathToKey = { 491 | path: Path.join(app.getPath('userData'), 'PrivateKey.json') 492 | }; 493 | cryptoWorker.send(workerMsg); 494 | 495 | // Create the browser window. 496 | mainWindow = new BrowserWindow({width: 900, height: 600}); 497 | 498 | ths.start(false, function() { 499 | console.log("tor Started!"); 500 | if(!mainWindow.webContents.isLoading()) { 501 | startHiddenService(); 502 | } 503 | }); 504 | 505 | // and load the index.html of the app. 506 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 507 | mainWindow.webContents.on('did-finish-load', function() { 508 | console.log('[webContents] Finished loading.'); 509 | getContacts(); 510 | getContactRequests(); 511 | }); 512 | 513 | ipcMain.on('message', function(event, content) { 514 | console.log('[message event] ', content); 515 | switch(content.type) { 516 | case 'sendEncrypted': 517 | var encObj = 518 | buildEncryptedMessage(content.destAddress, content.msgText); 519 | dataTransmitDomain.run(function() { 520 | myNoxClient.transmitObject( 521 | content.destAddress, encObj, function(res) { 522 | var msgObj = {}; 523 | switch(res.status) { 524 | case 200: 525 | // OK 526 | msgObj.method = 'message'; 527 | msgObj.content = { 528 | type: 'status', status: 'delivered', msgId: content.msgId 529 | }; 530 | notifyGUI(msgObj); 531 | break; 532 | case 410: 533 | // recipient does not have the public key (anymore) 534 | msgObj.method = 'error'; 535 | msgObj.content = { 536 | type: 'message', 537 | message: 'The recipient no longer has you in their ' + 538 | 'contact list. Delete the contact, then send a contact ' + 539 | 'request.'}; 540 | notifyGUI(msgObj); 541 | msgObj.method = 'message'; 542 | msgObj.content = { 543 | type: 'status', status: 'failed', msgId: content.msgId 544 | }; 545 | notifyGUI(msgObj); 546 | break; 547 | case 409: 548 | var failedReason = res.body.reason; 549 | switch(failedReason) { 550 | case 'EPROTOCOLVERSION': 551 | msgObj.method = 'error'; 552 | msgObj.content = { 553 | type: 'message', 554 | message: 'The message was rejected because the ' + 555 | 'message format is not proper. Please upgrade your ' + 556 | 'Noxious software.'}; 557 | notifyGUI(msgObj); 558 | msgObj.method = 'message'; 559 | msgObj.content = { 560 | type: 'status', status: 'failed', msgId: content.msgId 561 | }; 562 | notifyGUI(msgObj); 563 | break; 564 | } 565 | break; 566 | } 567 | }); 568 | }); 569 | break; 570 | } 571 | }); 572 | 573 | ipcMain.on('contact', function(event, content) { 574 | console.log('[contact event] ', content); 575 | 576 | switch(content.type) { 577 | case 'acceptContactRequest': 578 | // user has chosen to accept the contact request 579 | // send a contact request to sender to provide pubKey 580 | updateRequestStatus(content.contactAddress, 'sending', false); 581 | contactRequestDomain.run(function() { 582 | myNoxClient.transmitObject( 583 | content.contactAddress, buildContactRequest(content.contactAddress), 584 | function(res) { 585 | if(res.status == 200) { 586 | contactList.set( 587 | content.contactAddress, { 588 | pubPem: contactRequestList 589 | .get(content.contactAddress).pubPem, 590 | contactAddress: content.contactAddress 591 | }); 592 | contactRequestList.delete(content.contactAddress); 593 | getContacts(); 594 | getContactRequests(); 595 | } else if(res.status == 409) { 596 | // this can occur in a case where a successfully transmitted 597 | // contact request is deleted before a reply is sent. 598 | updateRequestStatus(content.contactAddress, 'failed'); 599 | var msgObj = {}; 600 | msgObj.method = 'error'; 601 | var failedReason = res.body.reason; 602 | switch(failedReason) { 603 | case 'EKEYSIZE': 604 | msgObj.content = { 605 | type: 'contact', 606 | message: 'The contact request was rejected because ' + 607 | 'your public encryption key is not proper. Please ' + 608 | 'upgrade your Noxious software.'}; 609 | break; 610 | default: 611 | msgObj.content = { 612 | type: 'contact', 613 | message: 'The recipient already has your contact ' + 614 | 'information. Ask them to delete your contact ' + 615 | 'information and try again.'}; 616 | break; 617 | } 618 | notifyGUI(msgObj); 619 | } 620 | }); 621 | }); 622 | break; 623 | case 'delContactRequest': 624 | contactRequestList.delete(content.contactAddress); 625 | break; 626 | case 'declineContactRequest': 627 | contactRequestList.delete(content.contactAddress); 628 | getContactRequests(); 629 | break; 630 | case 'sendContactRequest': 631 | var msgObj = {}; 632 | // do not send request to myAddress or to existing contacts 633 | if(content.contactAddress === myAddress) { 634 | msgObj.method = 'error'; 635 | msgObj.content = { 636 | type: 'contact', 637 | message: 'You may not send a contact request to your own ' + 638 | 'Client ID.'}; 639 | notifyGUI(msgObj); 640 | } else if(contactList.has(content.contactAddress)) { 641 | msgObj.method = 'error'; 642 | msgObj.content = { 643 | type: 'contact', 644 | message: 'You may not send a contact request to an existing ' + 645 | 'contact. Delete the contact and try again.'}; 646 | notifyGUI(msgObj); 647 | } else if(contactRequestList.get(content.contactAddress)) { 648 | msgObj.method = 'error'; 649 | msgObj.content = { 650 | type: 'contact', 651 | message: 'There is already a pending contact request for this ' + 652 | 'Client ID. Delete the contact request and try again.'}; 653 | notifyGUI(msgObj); 654 | } else { 655 | transmitContactRequest(content.contactAddress); 656 | var contactRequest = { 657 | contactAddress: content.contactAddress, 658 | direction: 'outgoing', status: 'sending' 659 | }; 660 | contactRequestList.set(content.contactAddress, contactRequest); 661 | // reinit the request list 662 | getContactRequests(); 663 | } 664 | break; 665 | case 'retryContactRequest': 666 | // user wants to resend a failed failed contact requests 667 | // the address has already passed inspection and the GUI has been 668 | // set to show sending status 669 | transmitContactRequest(content.contactAddress); 670 | break; 671 | case 'setNickName': 672 | var contactInfo = contactList.get(content.contactAddress); 673 | contactInfo.nickName = content.nickName; 674 | contactList.set(content.contactAddress, contactInfo); 675 | break; 676 | case 'delNickName': 677 | var contactInfoDel = contactList.get(content.contactAddress); 678 | contactInfoDel.nickName = ''; 679 | contactList.set(content.contactAddress, contactInfoDel); 680 | break; 681 | case 'delContact': 682 | contactList.delete(content.contactAddress); 683 | getContacts(true); 684 | break; 685 | } 686 | }); 687 | 688 | // Emitted when the window is closed. 689 | mainWindow.on('closed', function() { 690 | // Dereference the window object, usually you would store windows 691 | // in an array if your app supports multi windows, this is the time 692 | // when you should delete the corresponding element. 693 | mainWindow = null; 694 | }); 695 | }); 696 | 697 | app.on('before-quit', function(e) { 698 | if(ths.isTorRunning()) { 699 | e.preventDefault(); 700 | ths.stop(function() { 701 | console.log("tor has been stopped"); 702 | app.quit(); 703 | }); 704 | } 705 | cryptoWorker.kill(); 706 | console.log('I\'m quitting.'); 707 | }); 708 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noxious", 3 | "version": "1.0.1-0", 4 | "description": "secure anonymous instant messaging client built on atom-shell", 5 | "main": "nox-main.js", 6 | "scripts": { 7 | "start": "electron ." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mattcollier/noxious.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/mattcollier/noxious/issues" 17 | }, 18 | "homepage": "https://github.com/mattcollier/noxious", 19 | "dependencies": { 20 | "bootstrap": "^3.3.2", 21 | "canonical-json": "0.0.4", 22 | "electron": "^1.3.3", 23 | "jquery": "^3.1.0", 24 | "node-forge": "^0.6.25", 25 | "socksv5": "0.0.6", 26 | "ths": "git+https://github.com/Mowje/node-ths.git#dev" 27 | } 28 | } 29 | --------------------------------------------------------------------------------