├── .gitignore ├── .travis.yml ├── COPYING ├── COPYING.LESSER ├── README.md ├── cryptully.spec ├── docs ├── Makefile ├── building.rst ├── conf.py ├── downloads.rst ├── images │ ├── accept_dialog.png │ ├── chatting.png │ ├── fingerprint_dialog.png │ ├── login.png │ ├── new_chat.png │ ├── options_menu.png │ ├── smp_request_dialog.png │ ├── smp_response_dialog.png │ └── smp_success.png ├── index.rst ├── protocol.rst └── usage.rst ├── make.py ├── setup.py └── src ├── __init__.py ├── __main__.py ├── crypto ├── __init__.py ├── crypto.py └── smp.py ├── cryptully.py ├── images ├── dark │ ├── delete.png │ ├── exit.png │ ├── fingerprint.png │ ├── help.png │ ├── icon.png │ ├── menu.png │ ├── new_chat.png │ ├── save.png │ ├── splash_icon.png │ └── waiting.gif ├── icon.ico ├── light │ ├── delete.png │ ├── exit.png │ ├── fingerprint.png │ ├── help.png │ ├── icon.png │ ├── menu.png │ ├── new_chat.png │ ├── save.png │ ├── splash_icon.png │ └── waiting.gif ├── placeholder.png └── splash_logo.psd ├── ncurses ├── __init__.py ├── cursesAcceptDialog.py ├── cursesDialog.py ├── cursesInputDialog.py ├── cursesModeDialog.py ├── cursesPassphraseDialog.py ├── cursesSendThread.py ├── cursesStatusWindow.py └── ncurses.py ├── network ├── __init__.py ├── client.py ├── connectionManager.py ├── message.py ├── qtThreads.py └── sock.py ├── qt ├── __init__.py ├── qAcceptDialog.py ├── qChatTab.py ├── qChatWidget.py ├── qChatWindow.py ├── qConnectingWidget.py ├── qHelpDialog.py ├── qLine.py ├── qLinkLabel.py ├── qLoginWindow.py ├── qNickInputWidget.py ├── qPassphraseDialog.py ├── qSMPInitiateDialog.py ├── qSMPRespondDialog.py ├── qWaitingDialog.py ├── qt.py └── qtUtils.py ├── server ├── __init__.py ├── console.py └── turnServer.py ├── test.py ├── tests ├── __init__.py ├── mockClient.py ├── mockServer.py └── waitingMock.py └── utils ├── __init__.py ├── constants.py ├── errors.py ├── exceptions.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | cryptully.log 2 | logdict* 3 | 4 | *.py[cod] 5 | _build 6 | builds 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | dist 15 | build 16 | eggs 17 | parts 18 | bin 19 | var 20 | sdist 21 | develop-eggs 22 | .installed.cfg 23 | lib 24 | lib64 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | before_install: 7 | - sudo apt-get update -qq 8 | - sudo apt-get install -qq python-qt4-dev python-m2crypto python-mock 9 | 10 | # Skip the install stage 11 | install: true 12 | 13 | script: "./make.py test" 14 | 15 | branches: 16 | only: 17 | - master 18 | 19 | # Allow M2Crypto to be found 20 | virtualenv: 21 | system_site_packages: true 22 | -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cryptully 2 | ========= 3 | 4 | ## Encrypted chat for those that don't know crypto. 5 | #### Shane Tully (shanetully.com) 6 | 7 | [![Build Status](https://travis-ci.org/shanet/Cryptully.png)](https://travis-ci.org/shanet/Cryptully) 8 | 9 | ## Quick Start 10 | 11 | 1. Download the executable for your platform on the releases page. 12 | 2. Launch the executable (no need to install anything). 13 | 3. Select a nickname and connect to the server. 14 | 4. Enter the nickname of the person you want to chat with. 15 | 5. You should now be chatting! 16 | 17 | Need more info? See the [documentation](https://cryptully.readthedocs.io/en/latest/) for much more detailed instructions. 18 | 19 | ## Running From Source 20 | 21 | Install the following: 22 | 23 | * Python 2.7 24 | * PyQt4 25 | * M2Crypto 26 | * Python Curses 27 | * Clone the repo and run `python make.py run`. 28 | 29 | Detailed instructions are on the [building page](https://cryptully.readthedocs.io/en/latest/building.html) of the documentation. 30 | 31 | ## Building 32 | 33 | Cryptully builds and runs on Linux, Windows, and OS X. See the [building page](https://cryptully.readthedocs.io/en/latest/building.html) for detailed 34 | instructions. 35 | 36 | ## Documentation 37 | 38 | Documentation is available at https://cryptully.readthedocs.io/en/latest/. 39 | 40 | ## License 41 | 42 | Copyright (C) 2013-2018 Shane Tully 43 | 44 | This program is free software: you can redistribute it and/or modify 45 | it under the terms of the GNU Lesser General Public License as published by 46 | the Free Software Foundation, either version 3 of the License, or 47 | (at your option) any later version. 48 | 49 | This program is distributed in the hope that it will be useful, 50 | but WITHOUT ANY WARRANTY; without even the implied warranty of 51 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 52 | GNU Lesser General Public License for more details. 53 | 54 | You should have received a copy of the GNU Lesser General Public License 55 | along with this program. If not, see . 56 | -------------------------------------------------------------------------------- /cryptully.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | import sys 3 | 4 | a = Analysis(['src/cryptully.py'], 5 | hiddenimports=[], 6 | hookspath=None) 7 | pyz = PYZ(a.pure) 8 | exe = EXE(pyz, 9 | a.scripts, 10 | # Static link the Visual C++ Redistributable DLLs if on Windows 11 | a.binaries + [('msvcp100.dll', 'C:\\Windows\\System32\\msvcp100.dll', 'BINARY'), 12 | ('msvcr100.dll', 'C:\\Windows\\System32\\msvcr100.dll', 'BINARY')] 13 | if sys.platform == 'win32' else a.binaries, 14 | a.zipfiles, 15 | a.datas + [('images/light/delete.png', 'src/images/light/delete.png', 'DATA'), 16 | ('images/light/exit.png', 'src/images/light/exit.png', 'DATA'), 17 | ('images/light/fingerprint.png', 'src/images/light/fingerprint.png', 'DATA'), 18 | ('images/light/help.png', 'src/images/light/help.png', 'DATA'), 19 | ('images/light/icon.png', 'src/images/light/icon.png', 'DATA'), 20 | ('images/light/menu.png', 'src/images/light/menu.png', 'DATA'), 21 | ('images/light/new_chat.png', 'src/images/light/new_chat.png', 'DATA'), 22 | ('images/light/save.png', 'src/images/light/save.png', 'DATA'), 23 | ('images/light/splash_icon.png', 'src/images/light/splash_icon.png', 'DATA'), 24 | ('images/light/waiting.gif', 'src/images/light/waiting.gif', 'DATA'), 25 | 26 | ('images/dark/delete.png', 'src/images/dark/delete.png', 'DATA'), 27 | ('images/dark/exit.png', 'src/images/dark/exit.png', 'DATA'), 28 | ('images/dark/fingerprint.png', 'src/images/dark/fingerprint.png', 'DATA'), 29 | ('images/dark/help.png', 'src/images/dark/help.png', 'DATA'), 30 | ('images/dark/icon.png', 'src/images/dark/icon.png', 'DATA'), 31 | ('images/dark/menu.png', 'src/images/dark/menu.png', 'DATA'), 32 | ('images/dark/new_chat.png', 'src/images/dark/new_chat.png', 'DATA'), 33 | ('images/dark/save.png', 'src/images/dark/save.png', 'DATA'), 34 | ('images/dark/splash_icon.png', 'src/images/dark/splash_icon.png', 'DATA'), 35 | ('images/dark/waiting.gif', 'src/images/dark/waiting.gif', 'DATA')], 36 | name=os.path.join('dist', 'cryptully' + ('.exe' if sys.platform == 'win32' else '')), 37 | debug=False, 38 | strip=None, 39 | upx=True, 40 | console=False, 41 | icon='src/images/icon.ico') 42 | 43 | # Build a .app if on OS X 44 | if sys.platform == 'darwin': 45 | app = BUNDLE(exe, 46 | name='cryptully.app', 47 | icon=None) 48 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Cryptully.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Cryptully.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Cryptully" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Cryptully" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/building.rst: -------------------------------------------------------------------------------- 1 | .. _building: 2 | 3 | Building |project| 4 | ================== 5 | 6 | -------------- 7 | Get the source 8 | -------------- 9 | 10 | You can get the source from GitHub at https://github.com/shanet/Cryptully. 11 | 12 | ------------- 13 | Dependencies 14 | ------------- 15 | 16 | So you want to build Crpytully from source? Sweet! There's a few dependencies you'll need to install first 17 | though. The core dependencies are: 18 | 19 | * Python 2.7 20 | * PyQt4 21 | * M2Crypto 22 | * PyInstaller 2 (only if you want to build a binary, otherwise it is not needed) 23 | 24 | Depending what platform you're building on, you'll need a few more things, but those are documented 25 | in the relevant sections below. 26 | 27 | ------------ 28 | Sanity Check 29 | ------------ 30 | 31 | You can do a basic sanity check by doing the following:: 32 | 33 | $ python 34 | >>> import PyQt4.QtGui 35 | >>> import M2Crypto 36 | >>> import curses 37 | 38 | If you got any errors, something isn't right. 39 | 40 | ---------------- 41 | Running Directly 42 | ---------------- 43 | 44 | |project| is, after all, a Python script so you can run it without packaging it into anything fancy. 45 | Once the sanity check passes, just do:: 46 | 47 | $ python cryptully/cryptully.py 48 | 49 | You can also check out all the command line options with:: 50 | 51 | $ python cryptully/cryptully.py --help 52 | 53 | ----- 54 | Linux 55 | ----- 56 | 57 | These instructions are for Debian/Ubuntu. They should work on other distros, but the package names 58 | will most likely be different. 59 | 60 | 1. ``$ apt-get install python-dev python-qt4-dev python-m2crypto python-stdeb`` 61 | 2. Download and extract PyInstaller from http://www.pyinstaller.org. 62 | 3. ``$ cd /path/to/cryptully/`` 63 | 4. ``$ python make.py dist /path/to/pyinstaller/`` 64 | 65 | If everything went as intended, the packaged application should be in ``dist/``. 66 | 67 | ------- 68 | Windows 69 | ------- 70 | 71 | 1. Download and install Python 2.7 from http://www.python.org/download/releases/2.7.5. You'll 72 | probably want to add it to your PATH as well. 73 | 2. Download and install of PyWin32 from http://sourceforge.net/projects/pywin32/files/pywin32 (these 74 | instructions were tested with build 218) 75 | 3. Download and install M2Crypto http://chandlerproject.org/Projects/MeTooCrypto#Downloads 76 | 4. Download and install PyQt4 from http://www.riverbankcomputing.com/software/pyqt/download 77 | 5. Download and install the Visual C++ 2010 Redistributable Package from 78 | https://www.microsoft.com/en-us/download/details.aspx?id=14632 79 | 6. Download and install Curses from http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses 80 | 7. Download and extract PyInstaller from http://www.pyinstaller.org 81 | 82 | It's probably a good idea to do the sanity check as described in the sanity check above at this point. 83 | 84 | 8. ``> cd \path\to\cryptully\`` 85 | 9. ``> python make.py dist \path\to\pyinstaller\`` 86 | 87 | If everything went as intended, the packaged application should be in ``dist\``. 88 | 89 | ---- 90 | OS X 91 | ---- 92 | 93 | 1. Install Homebrew from http://mxcl.github.io/homebrew (it's the easiest way to get the dependencies) 94 | 2. Run ``$ brew doctor`` to make sure everything is okay. You'll probably need to install the 95 | OS X Command Line Tools. 96 | 3. ``$ brew install python`` (OS X comes with a version of Python, but it's best to use the homebrew version) 97 | 4. If not done already, set your Python path with: ``export PYTHONPATH=/usr/local/lib/python2.7/site-packages:$PYTHONPATH`` 98 | 5. ``$ brew install pyqt`` 99 | 6. Download the relevant M2Crpyto .egg for your version of OS X from http://chandlerproject.org/Projects/MeTooCrypto#Downloads 100 | 7. Copy the M2Crypto .egg to ``/usr/local/lib/python2.7/site-packages`` 101 | 8. Download and extract PyInstaller from http://www.pyinstaller.org **At the time of this writing 102 | the development version of PyInstaller must be used!** Stable version 2.0 will not work. Future stable 103 | versions may or may not. 104 | 105 | It's probably a good idea to do the sanity check as described in the sanity check above at this point. 106 | 107 | 9. ``$ cd /path/to/cryptully/`` 108 | 10. ``$ python make.py dist /path/to/pyinstaller/`` 109 | 110 | If everything went as intended, the packaged application should be in ``dist\``. 111 | 112 | ---------- 113 | Unit Tests 114 | ---------- 115 | 116 | Install the Python Mock package first. 117 | 118 | Units tests are located in the ``src/tests`` directory. Running them is as simple as ``python make.py test``. 119 | 120 | ------------- 121 | Documentation 122 | ------------- 123 | 124 | The documentation you are reading right now was generated by Sphinx and its source is located in 125 | the ``docs`` folder. To build it into a pretty HTML page just run the following from the 126 | ``docs`` folder 127 | 128 | Linux/OS X:: 129 | 130 | make html 131 | 132 | Windows:: 133 | 134 | .\make.bat html 135 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Cryptully documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jul 1 00:15:49 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Cryptully' 44 | copyright = u'2013-2018, Shane Tully' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '5.0.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '5.0.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'sphinxdoc' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Cryptullydoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'Cryptully.tex', u'Cryptully Documentation', 187 | u'Shane Tully', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'cryptully', u'Cryptully Documentation', 217 | [u'Shane Tully'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'Cryptully', u'Cryptully Documentation', 231 | u'Shane Tully', 'Cryptully', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | 244 | rst_epilog = '.. |project| replace:: %s' % project 245 | -------------------------------------------------------------------------------- /docs/downloads.rst: -------------------------------------------------------------------------------- 1 | .. _downloads: 2 | 3 | Downloads 4 | ========= 5 | 6 | |project| is available for Linux, Windows, and OS X. See the releases page on GitHub for download 7 | links. 8 | 9 | https://github.com/shanet/Cryptully/releases 10 | -------------------------------------------------------------------------------- /docs/images/accept_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/accept_dialog.png -------------------------------------------------------------------------------- /docs/images/chatting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/chatting.png -------------------------------------------------------------------------------- /docs/images/fingerprint_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/fingerprint_dialog.png -------------------------------------------------------------------------------- /docs/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/login.png -------------------------------------------------------------------------------- /docs/images/new_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/new_chat.png -------------------------------------------------------------------------------- /docs/images/options_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/options_menu.png -------------------------------------------------------------------------------- /docs/images/smp_request_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/smp_request_dialog.png -------------------------------------------------------------------------------- /docs/images/smp_response_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/smp_response_dialog.png -------------------------------------------------------------------------------- /docs/images/smp_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/docs/images/smp_success.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Cryptully documentation master file, created by 2 | sphinx-quickstart on Mon Jul 1 00:15:49 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to |project|'s documentation! 7 | ===================================== 8 | 9 | ------------ 10 | Introduction 11 | ------------ 12 | 13 | |project| is an encrypted chat program meant for secure conversations between two people 14 | with no knowledge of cryptography needed. 15 | 16 | .. image:: images/chatting.png 17 | 18 | -------- 19 | Features 20 | -------- 21 | 22 | * Provides basic, encrypted chat with no prerequisite knowledge of cryptography 23 | * Runs on Linux, Windows, and Mac OS X 24 | * No registration or software installation required 25 | * Chat with multiple people simultaneously 26 | * Ability to host your own server (for the technically inclined) 27 | * Graphical UI and command line (Curses) UI 28 | * Open source (LGPL license) 29 | 30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 31 | How does it work and how is it secure? 32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | 34 | |project| works by relaying messages from one person to another through a relay server. It generates 35 | per-session encryption keys (256bit AES) that all communications are encrypted with before leaving 36 | your computer and then decrypted on the destination computer. 37 | 38 | ----------- 39 | Quick Start 40 | ----------- 41 | 42 | 1. Download the executable for your platform on the :ref:`downloads` page. 43 | 2. Launch the executable (no need to install anything). 44 | 3. Select a nickname and connect to the server. 45 | 4. Enter the nickname of the person you want to chat with. 46 | 5. You should now be chatting! 47 | 48 | Need more info? See the :ref:`using-|project|` page for much more detailed instructions. 49 | 50 | ------------------------------------- 51 | Doesn't encrypted chat already exist? 52 | ------------------------------------- 53 | 54 | Yup, it does. There are plenty of other encrypted chat programs so what's the point of |project|? 55 | The problem is just that, there's plenty of other chat programs. There's too many options 56 | for chating with another person. Other solutions require downloading and installing software, creating 57 | accounts, etc. With |project|, you just download and run the software. No need to install anything or 58 | create an account. Just enter the nickname of the person you want to chat with and you're off. 59 | 60 | Another advantage is that |project| is a relatively simple program and is open source. For the paranoid, 61 | you can inspect the source code to ensure that |project| is not doing anything nefarious or host your 62 | own relay server. 63 | 64 | -------- 65 | Contents 66 | -------- 67 | 68 | .. toctree:: 69 | :maxdepth: 2 70 | 71 | downloads 72 | usage 73 | building 74 | protocol 75 | 76 | ------------ 77 | Contributing 78 | ------------ 79 | 80 | For non-programmers: 81 | 82 | Even if you're not writing code, you can still help! Submitting any issues or problems you run into 83 | at https://github.com/shanet/Cryptully/issues or by emailing shane@shanetully.com. Even reporting something like 84 | a section in the documentation not being as clear as it could be or just a typo is helpful. 85 | 86 | For programmers: 87 | 88 | If you would like contribute to |project|, see the :ref:`downloads` page on how to set up a build environment 89 | and get the source code. Please any issues encountered at https://github.com/shanet/Cryptully/issues. 90 | -------------------------------------------------------------------------------- /docs/protocol.rst: -------------------------------------------------------------------------------- 1 | .. _protocol: 2 | 3 | Protocol 4 | ======== 5 | 6 | This is an overview of |project|'s protocol for easier understanding the code, or so someone 7 | could implement another type of client. 8 | 9 | **Note: This protocol is highly likely to change in the future.** 10 | 11 | ---------------- 12 | Basic Properties 13 | ---------------- 14 | 15 | * All traffic over the network is formatted as JSON messages with the following properties: 16 | 17 | * ``serverCommand``: The command given to the server 18 | * ``clientCommand``: The command given to the destination client 19 | * ``sourceNick``: The nickname of the sender 20 | * ``destNick``: The nickname of the receiver 21 | * ``payload``: The content of the message. If an encrypted message, it is base64 encoded 22 | * ``hmac``: The HMAC as calculated by the sender to be verified against by the receiver 23 | * ``error``: The error code, if applicable 24 | * ``num``: The message number, starting from 0 and monotonically increasing with sequential numbers. 25 | 26 | * All commands *are* case sensitive 27 | * After the initial handshake is complete, the connection is kept alive indefinitely in a message loop until 28 | either the client or server sends the ``END`` command. 29 | * The client or server may send the ``END`` command at any time. 30 | 31 | -------------------- 32 | List of All Commands 33 | -------------------- 34 | 35 | ^^^^^^^^^^^^^^^ 36 | Server Commands 37 | ^^^^^^^^^^^^^^^ 38 | 39 | * ``VERSION``: Tell the server what protocol version the client is using 40 | * ``REG``: Register a nickname with the server 41 | * ``REL``: Relay a message to the client as specified in the ``destClient`` field 42 | 43 | ^^^^^^^^^^^^^^^ 44 | Client Commands 45 | ^^^^^^^^^^^^^^^ 46 | 47 | * ``HELO``: The first command denotes the initation of a new connection with a client 48 | * ``REDY``: The client is ready to initiate a handshake 49 | * ``REJ``: If the client rejected a connection from another client 50 | * ``PUB_KEY [arg]`` 51 | * ``SMP0 [arg]``: The question the SMP initiator is asking 52 | * ``SMP1 [arg]`` 53 | * ``SMP2 [arg]`` 54 | * ``SMP3 [arg]`` 55 | * ``SMP4 [arg]`` 56 | * ``MSG [arg]``: The user has sent a chat message 57 | * ``TYPING [arg]``: The user is currently typing or has stopped typing 58 | * ``END`` 59 | * ``ERR`` 60 | 61 | ^^^^^^^^^^^^^ 62 | Typing Status 63 | ^^^^^^^^^^^^^ 64 | 65 | A client may optional give the typing status of the user to the remote client by issuing the ``TYPING`` 66 | command. The ``TYPING`` command takes one of three possible arguments: 67 | 68 | * ``0``: The user is currently typing 69 | * ``1``: The user has stopped typing and deleted all text from the buffer 70 | * ``2``: The user has stopped typing, but left some text in the buffer 71 | 72 | ------------------ 73 | Encryption Details 74 | ------------------ 75 | 76 | * 4096-bit prime is used to generate and exchange a shared secret via Diffie-Hellman. 77 | * An AES key is the first 32 bytes of the SHA512 digest of the Diffie-Hellman secret. The IV last 32 bytes of this hash. 78 | * All AES operations are with a 256-bit key in CBC mode. 79 | * HMAC's are the SHA256 digest of the AES key and the encrypted message payload. The receiver calculates 80 | and verifies the HMAC before attempting to decrypt the message payload. 81 | 82 | 83 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 84 | Socialist Millionaire Protocol 85 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | The Socialist Millionaire Protocol (SMP) is a method for determining whether two clients share the same secret, 88 | but without exchanging the secret itself. In |project|'s case, it is used to determine whether a MITM 89 | attack has occurred or is occurring and compromised the Diffie-Hellman key exchange protocol. 90 | 91 | The innards of the SMP is relatively complex so it is best to defer to the documentation of it's implementation 92 | as defined in the Off-The-Record (OTR) protocol version 3. 93 | 94 | |project|'s implementation uses the following commands: 95 | 96 | +--------+---------+--------+ 97 | |Client A|direction|Client B| 98 | +========+=========+========+ 99 | | | <- |SMP0 | 100 | +--------+---------+--------+ 101 | | | <- |SMP1 | 102 | +--------+---------+--------+ 103 | |SMP2 | -> | | 104 | +--------+---------+--------+ 105 | | | <- |SMP3 | 106 | +--------+---------+--------+ 107 | |SMP4 | -> | | 108 | +--------+---------+--------+ 109 | 110 | ``SMP0`` contains the question the initiator is asking. The remaining commands may be sent at any time, as long as they are 111 | completed in order and another SMP request is not started before the previous one is completed. 112 | 113 | ----------------- 114 | Handshake Details 115 | ----------------- 116 | 117 | The commands in the handshake must be performed in the following order: 118 | 119 | +--------+---------+--------+ 120 | |Client A|direction|Client B| 121 | +========+=========+========+ 122 | | | <- |HELO | 123 | +--------+---------+--------+ 124 | |REDY | -> | | 125 | +--------+---------+--------+ 126 | | | <- |PUB_KEY | 127 | +--------+---------+--------+ 128 | |PUB_KEY | -> | | 129 | +--------+---------+--------+ 130 | |(switch to AES encryption) | 131 | +--------+---------+--------+ 132 | 133 | 134 | The client may reject a connection with the ``REJ`` command instead of sending the ``REDY`` command. 135 | 136 | -------------------- 137 | Message Loop Details 138 | -------------------- 139 | 140 | Clients may send messages any order including multiple messages in a row. 141 | 142 | +--------+---------+--------+ 143 | |Client A|direction|Client B| 144 | +========+=========+========+ 145 | |MSG | <-> |MSG | 146 | +--------+---------+--------+ 147 | |TYPING | <-> |TYPING | 148 | +--------+---------+--------+ 149 | |END | <-> |END | 150 | +--------+---------+--------+ 151 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _using-|project|: 2 | 3 | Using |project| 4 | =============== 5 | 6 | ----------------- 7 | Getting |project| 8 | ----------------- 9 | 10 | The first step is downloading |project|. To do that, head over to the :ref:`downloads` page. |project| is 11 | available for Linux, Windows, and OS X. Just download the file and run it. No need to install anything 12 | or create any accounts. 13 | 14 | ---------------------- 15 | Connecting to a friend 16 | ---------------------- 17 | 18 | |project| uses a central server to relay messages from one person to another. 19 | 20 | Let's run through the process of connecting with a friend. 21 | 22 | 1. When you first open |project|, you'll see the following screen: 23 | 24 | .. image:: images/login.png 25 | 26 | 2. Pick a nickname that will identify you to other people you'll chat with. 27 | 28 | 3. Once connected to the server, you may enter the nickname of someone you wish to chat with. 29 | 30 | .. image:: images/new_chat.png 31 | 32 | 4. If the connection was successful, the person being connected to will see a dialog asking to accept 33 | or reject the connection. 34 | 35 | .. image:: images/accept_dialog.png 36 | 37 | 5. Upon accepting the connection, both people are chatting securely! 38 | 39 | .. image:: images/chatting.png 40 | 41 | ------------------- 42 | Chat Authentication 43 | ------------------- 44 | 45 | In order to verify the person you're chatting with is not an impersonator and that no one is 46 | eavesdropping on your conversation, |project| uses a secret question and answer method. 47 | 48 | To authenticate a chat session: 49 | 50 | 1. When chatting, select "Authenticate Chat" from the options menu. 51 | 52 | .. image:: images/options_menu.png 53 | 54 | 2. Enter a question and answer that only you are your buddy knows the answer to. Note that the answer is case sensitive. 55 | 56 | .. image:: images/smp_request_dialog.png 57 | 58 | 3. Your buddy will then have a window open that allows the answer to the given question to be entered. 59 | 60 | .. image:: images/smp_response_dialog.png 61 | 62 | 4. If your buddy entered the same answer as you, the chat will be successfully authenticated. A failure to 63 | authenticate means either the wrong answer was entered or someone is listening in on your conversation. 64 | 65 | .. image:: images/smp_success.png 66 | 67 | ------------------------------- 68 | Command Line Options (advanced) 69 | ------------------------------- 70 | 71 | Advanced users may utilize command line options of |project|: 72 | 73 | usage: cryptully.py [-h] [-k [NICK]] [-r [TURN]] [-p [PORT]] [-s] [-n] 74 | 75 | optional arguments: 76 | -h, --help show this help message and exit 77 | -k [NICK], --nick [NICK] 78 | Nickname to use. 79 | -r [TURN], --relay [TURN] 80 | The relay server to use. 81 | -p [PORT], --port [PORT] 82 | Port to connect listen on (server) or connect to 83 | (client). 84 | -s, --server Run as TURN server for other clients. 85 | -n, --ncurses Use the NCurses UI. 86 | 87 | 88 | ---------------------------------- 89 | Running Your Own Server (advanced) 90 | ---------------------------------- 91 | 92 | If you don't want to use the default relay server, you can host your own. 93 | 94 | This is as easy as downloading a pre-built binary, or getting the source and running |project| with 95 | the ``--server`` command line argument. 96 | -------------------------------------------------------------------------------- /make.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python2.7 2 | 3 | import os 4 | import shutil 5 | import subprocess 6 | import sys 7 | 8 | def clean(): 9 | deleteDirectory('build') 10 | deleteDirectory('dist') 11 | deleteDirectory('deb_dist') 12 | deleteDirectory('Cryptully.egg-info') 13 | deleteFile('logdict2.7.4.final.0-1.log') 14 | 15 | 16 | def deleteDirectory(path): 17 | try: 18 | shutil.rmtree(path) 19 | except OSError as ose: 20 | # Ignore 'no such file or directory' errors 21 | if ose.errno != 2: 22 | print ose 23 | 24 | 25 | def deleteFile(path): 26 | try: 27 | os.unlink(path) 28 | except OSError as ose: 29 | if ose.errno != 2: 30 | print ose 31 | 32 | 33 | arg = sys.argv[1] if len(sys.argv) >= 2 else None 34 | 35 | if arg == 'dist': 36 | if len(sys.argv) == 3: 37 | pyinstallerPath = sys.argv[2] 38 | else: 39 | pyinstallerPath = raw_input("Path to pyinstaller: ") 40 | 41 | clean() 42 | subprocess.call(['python2.7', os.path.join(pyinstallerPath, 'pyinstaller.py'), 'cryptully.spec']) 43 | 44 | elif arg == 'deb': 45 | print "Ensure you have the python-stdeb package installed!" 46 | subprocess.call(['python2.7', 'setup.py', '--command-packages=stdeb.command', 'bdist_deb']) 47 | 48 | elif arg == 'rpm': 49 | subprocss.call(['python2.7', 'setup.py', 'bdist_rpm', '--post-install=rpm/postinstall', '--pre-uninstall=rpm/preuninstall']) 50 | 51 | elif arg == 'install': 52 | subprocess.call(['python2.7', 'setup.py', 'install']) 53 | 54 | elif arg == 'source': 55 | subprocess.call(['python2.7', 'setup.py', 'sdist']) 56 | 57 | elif arg == 'run': 58 | subprocess.call(['python2.7', os.path.join('src', 'cryptully.py')]) 59 | 60 | elif arg == 'test': 61 | # Carry the exit code from the tests 62 | exitCode = subprocess.call(['python2.7', os.path.join('src', 'test.py')]) 63 | sys.exit(exitCode) 64 | 65 | elif arg == 'clean': 66 | clean() 67 | 68 | else: 69 | print "Invalid option\nPossible options: dist, deb, rpm, install, source, run, test, clean" 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | 6 | setup( 7 | name='Cryptully', 8 | version='5.0.0', 9 | author='Shane Tully', 10 | author_email='shane@shanetully.com', 11 | url='https://github.com/shanet/Cryptully', 12 | license='LGPL3', 13 | description='An encrypted chat program for those that don\'t know crypto', 14 | packages=find_packages(), 15 | package_data={ 16 | 'cryptully': ['images/*.png', 'images/light/*.png', 'images/dark/*.png'] 17 | }, 18 | install_requires=[ 19 | 'M2Crypto' 20 | # PyQt4 is also required, but it doesn't play nicely with setup.py 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/__init__.py -------------------------------------------------------------------------------- /src/__main__.py: -------------------------------------------------------------------------------- 1 | import cryptully 2 | 3 | cryptully.main() 4 | -------------------------------------------------------------------------------- /src/crypto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/crypto/__init__.py -------------------------------------------------------------------------------- /src/crypto/crypto.py: -------------------------------------------------------------------------------- 1 | import os 2 | import M2Crypto 3 | 4 | from utils import constants 5 | from utils import exceptions 6 | 7 | class Crypto(object): 8 | ENCRYPT = 1; 9 | DECRYPT = 0; 10 | 11 | dhPrime = 0x00a53d56c30fe79d43e3c9a0b678e87c0fcd2e78b15c676838d2a2bd6c299b1e7fdb286d991f62e8f366b0067ae71d3d91dac4738fd744ee180b16c97a54215236d4d393a4c85d8b390783566c1b0d55421a89fca20b85e0faecded7983d038821778b6504105f455d8655953d0b62841e9cc1248fa21834bc9f3e3cc1c080cfcb0b230fd9a2059f5f637395dfa701981fad0dbeb545e2e29cd20f7b6baee9314039e16ef19f604746fe596d50bb3967da51b948184d8d4511f2c0b8e4b4e3abc44144ce1f5968aadd053600a40430ba97ad9e0ad26fe4c444be3f48434a68aa132b1677d8442454fe4c6ae9d3b7164e6603f1c8a8f5b5235ba0b9f5b5f86278e4f69eb4d5388838ef15678535589516a1d85d127da8f46f150613c8a49258be2ed53c3e161d0049cabb40d15f9042a00c494746753b9794a9f66a93b67498c7c59b8253a910457c10353fa8e2edcafdf6c9354a3dc58b5a825c353302d686596c11e4855e86f3c6810f9a4abf917f69a6083330492aedb5621ebc3fd59778a40e0a7fa8450c8b2c6fe3923775419b2ea35cd19abe62c50020df991d9fc772d16dd5208468dc7a9b51c6723495fe0e72e818ee2b2a8581fab2caf6bd914e4876573b023862286ec88a698be2dd34c03925ab5ca0f50f0b2a246ab852e3779f0cf9d3e36f9ab9a50602d5e9216c3a29994e81e151accd88ea346d1be6588068e873 12 | dhGenerator = 2 13 | 14 | def __init__(self): 15 | self.localKeypair = None 16 | self.remoteKeypair = None 17 | self.aesKey = None 18 | self.aesIv = None 19 | self.aesSalt = None 20 | self.dh = None 21 | self.aesMode = constants.DEFAULT_AES_MODE 22 | 23 | 24 | def generateKeys(self, rsaBits=2048, aesMode=constants.DEFAULT_AES_MODE): 25 | self.generateRSAKeypair(rsaBits) 26 | self.generateAESKey(aesMode) 27 | 28 | 29 | def generateRSAKeypair(self, bits=2048): 30 | # Generate the keypair (65537 as the public exponent) 31 | self.localKeypair = M2Crypto.RSA.gen_key(bits, 65537, self.__generateKeypairCallback) 32 | 33 | 34 | def generateAESKey(self, aesMode=constants.DEFAULT_AES_MODE): 35 | self.aesMode = aesMode 36 | 37 | # Generate the AES key and IV 38 | bitsString = aesMode[4:7] 39 | if bitsString == '128': 40 | self.aesBytes = 16 41 | elif bitsString == '192': 42 | self.aesBytes = 24 43 | elif bitsString == '256': 44 | self.aesBytes = 32 45 | else: 46 | raise exceptions.CryptoError("Invalid AES mode") 47 | 48 | self.aesKey = M2Crypto.Rand.rand_bytes(self.aesBytes) 49 | self.aesIv = M2Crypto.Rand.rand_bytes(self.aesBytes) 50 | self.aesSalt = M2Crypto.Rand.rand_bytes(8) 51 | 52 | 53 | def generateDHKey(self): 54 | self.dh = M2Crypto.DH.set_params(decToMpi(self.dhPrime), decToMpi(self.dhGenerator)) 55 | self.dh.gen_key() 56 | 57 | 58 | def computeDHSecret(self, publicKey): 59 | self.dhSecret = binToDec(self.dh.compute_key(decToMpi(publicKey))) 60 | hash = self.hash(str(self.dhSecret), 'sha512') 61 | self.aesKey = hash[0:32] 62 | self.aesIv = hash[32:64] 63 | self.aesSalt = hash[56:64] 64 | 65 | 66 | def setRemotePubKey(self, pubKey): 67 | if type(pubKey) is str: 68 | bio = M2Crypto.BIO.MemoryBuffer(pubKey) 69 | self.remoteKeypair = M2Crypto.RSA.load_pub_key_bio(bio) 70 | elif type(pubKey) is M2Crypto.RSA: 71 | self.remoteKeypair = pubKey 72 | else: 73 | raise exceptions.CryptoError("Public key is not a string or RSA key object.") 74 | 75 | 76 | def rsaEncrypt(self, message): 77 | self.__checkRemoteKeypair() 78 | try: 79 | return self.remoteKeypair.public_encrypt(message, M2Crypto.RSA.pkcs1_oaep_padding) 80 | except M2Crypto.RSA.RSAError as rsae: 81 | raise exceptions.CryptoError(str(rsae)) 82 | 83 | 84 | def rsaDecrypt(self, message): 85 | self.__checkLocalKeypair() 86 | try: 87 | return self.localKeypair.private_decrypt(message, M2Crypto.RSA.pkcs1_oaep_padding) 88 | except M2Crypto.RSA.RSAError as rsae: 89 | raise exceptions.CryptoError(str(rsae)) 90 | 91 | 92 | def aesEncrypt(self, message): 93 | try: 94 | cipher = self.__aesGetCipher(self.ENCRYPT) 95 | encMessage = cipher.update(message) 96 | return encMessage + cipher.final() 97 | except M2Crypto.EVP.EVPError as evpe: 98 | raise exceptions.CryptoError(str(evpe)) 99 | 100 | 101 | def aesDecrypt(self, message): 102 | try: 103 | cipher = self.__aesGetCipher(self.DECRYPT) 104 | decMessage = cipher.update(message) 105 | return decMessage + cipher.final() 106 | except M2Crypto.EVP.EVPError as evpe: 107 | raise exceptions.CryptoError(str(evpe)) 108 | 109 | 110 | def __aesGetCipher(self, op): 111 | return M2Crypto.EVP.Cipher(alg=self.aesMode, key=self.aesKey, iv=self.aesIv, salt=self.aesSalt, d='sha256', op=op) 112 | 113 | 114 | def generateHmac(self, message): 115 | hmac = M2Crypto.EVP.HMAC(self.aesKey, 'sha256') 116 | hmac.update(message) 117 | return hmac.digest() 118 | 119 | 120 | def hash(self, message, type='sha256'): 121 | hash = M2Crypto.EVP.MessageDigest(type) 122 | hash.update(message) 123 | return hash.final() 124 | 125 | 126 | def stringHash(self, message): 127 | digest = self.hash(message) 128 | return hex(self.__octx_to_num(digest))[2:-1].upper() 129 | 130 | 131 | def mapStringToInt(self, string): 132 | num = 0 133 | shift = 0 134 | 135 | for char in reversed(string): 136 | num |= ord(char) << shift 137 | shift += 8 138 | 139 | return num 140 | 141 | 142 | def readLocalKeypairFromFile(self, file, passphrase): 143 | self._keypairPassphrase = passphrase 144 | try: 145 | self.localKeypair = M2Crypto.RSA.load_key(file, self.__passphraseCallback) 146 | except M2Crypto.RSA.RSAError as rsae: 147 | raise exceptions.CryptoError(str(rsae)) 148 | 149 | 150 | def readRemotePubKeyFromFile(self, file): 151 | self.remoteKeypair = M2Crypto.RSA.load_pub_key(file) 152 | 153 | 154 | def writeLocalKeypairToFile(self, file, passphrase): 155 | self.__checkLocalKeypair() 156 | self._keypairPassphrase = passphrase 157 | self.localKeypair.save_key(file, self.aesMode, self.__passphraseCallback) 158 | 159 | 160 | def writeLocalPubKeyToFile(self, file): 161 | self.__checkLocalKeypair() 162 | self.localKeypair.save_pub_key(file) 163 | 164 | 165 | def writeRemotePubKeyToFile(self, file): 166 | self.__checkRemoteKeypair() 167 | self.remoteKeypair.save_pub_key(file) 168 | 169 | 170 | def getLocalPubKeyAsString(self): 171 | self.__checkLocalKeypair() 172 | bio = M2Crypto.BIO.MemoryBuffer() 173 | self.localKeypair.save_pub_key_bio(bio) 174 | return bio.read() 175 | 176 | 177 | def getRemotePubKeyAsString(self): 178 | self.__checkRemoteKeypair() 179 | bio = M2Crypto.BIO.MemoryBuffer() 180 | self.remoteKeypair.save_pub_key_bio(bio) 181 | return bio.read() 182 | 183 | 184 | def getKeypairAsString(self, passphrase): 185 | self._keypairPassphrase = passphrase 186 | return self.localKeypair.as_pem(self.aesMode, self.__passphraseCallback) 187 | 188 | 189 | def getLocalFingerprint(self): 190 | return self.__generateFingerprint(self.getLocalPubKeyAsString()) 191 | 192 | 193 | def getRemoteFingerprint(self): 194 | return self.__generateFingerprint(self.getRemotePubKeyAsString()) 195 | 196 | 197 | def __generateFingerprint(self, key): 198 | digest = self.stringHash(key) 199 | 200 | # Add colons between every 2 characters of the fingerprint 201 | fingerprint = '' 202 | digestLength = len(digest) 203 | for i in range(0, digestLength): 204 | fingerprint += digest[i] 205 | if i&1 and i != 0 and i != digestLength-1: 206 | fingerprint += ':' 207 | return fingerprint 208 | 209 | 210 | def __octx_to_num(self, data): 211 | converted = 0L 212 | length = len(data) 213 | for i in range(length): 214 | converted = converted + ord(data[i]) * (256L ** (length - i - 1)) 215 | return converted 216 | 217 | 218 | def getDHPubKey(self): 219 | return mpiToDec(self.dh.pub) 220 | 221 | 222 | def __checkLocalKeypair(self): 223 | if self.localKeypair is None: 224 | raise exceptions.CryptoError("Local keypair not set.") 225 | 226 | 227 | def __checkRemoteKeypair(self): 228 | if self.remoteKeypair is None: 229 | raise exceptions.CryptoError("Remote public key not set.") 230 | 231 | 232 | def __generateKeypairCallback(self): 233 | pass 234 | 235 | 236 | def __passphraseCallback(self, ignore, prompt1=None, prompt2=None): 237 | return self._keypairPassphrase 238 | 239 | 240 | def mpiToDec(mpi): 241 | bn = M2Crypto.m2.mpi_to_bn(mpi) 242 | hex = M2Crypto.m2.bn_to_hex(bn) 243 | return int(hex, 16) 244 | 245 | 246 | def binToDec(binval): 247 | bn = M2Crypto.m2.bin_to_bn(binval) 248 | hex = M2Crypto.m2.bn_to_hex(bn) 249 | return int(hex, 16) 250 | 251 | 252 | def decToMpi(dec): 253 | bn = M2Crypto.m2.dec_to_bn('%s' % dec) 254 | return M2Crypto.m2.bn_to_mpi(bn) 255 | -------------------------------------------------------------------------------- /src/crypto/smp.py: -------------------------------------------------------------------------------- 1 | import crypto 2 | import M2Crypto 3 | import struct 4 | 5 | from utils import errors 6 | from utils import exceptions 7 | 8 | 9 | class SMP(object): 10 | def __init__(self, secret=None): 11 | self.mod = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF 12 | self.modOrder = (self.mod-1) / 2 13 | self.gen = 2 14 | self.match = False 15 | self.crypto = crypto.Crypto() 16 | self.secret = self.crypto.mapStringToInt(secret) 17 | 18 | 19 | def step1(self): 20 | self.x2 = createRandomExponent() 21 | self.x3 = createRandomExponent() 22 | 23 | self.g2 = pow(self.gen, self.x2, self.mod) 24 | self.g3 = pow(self.gen, self.x3, self.mod) 25 | 26 | (c1, d1) = self.createLogProof('1', self.x2) 27 | (c2, d2) = self.createLogProof('2', self.x3) 28 | 29 | # Send g2a, g3a, c1, d1, c2, d2 30 | return packList(self.g2, self.g3, c1, d1, c2, d2) 31 | 32 | 33 | def step2(self, buffer): 34 | (g2a, g3a, c1, d1, c2, d2) = unpackList(buffer) 35 | 36 | if not self.isValidArgument(g2a) or not self.isValidArgument(g3a): 37 | raise exceptions.CryptoError("Invalid g2a/g3a values", errors.ERR_SMP_CHECK_FAILED) 38 | 39 | if not self.checkLogProof('1', g2a, c1, d1): 40 | raise exceptions.CryptoError("Proof 1 check failed", errors.ERR_SMP_CHECK_FAILED) 41 | 42 | if not self.checkLogProof('2', g3a, c2, d2): 43 | raise exceptions.CryptoError("Proof 2 check failed", errors.ERR_SMP_CHECK_FAILED) 44 | 45 | self.g2a = g2a 46 | self.g3a = g3a 47 | 48 | self.x2 = createRandomExponent() 49 | self.x3 = createRandomExponent() 50 | 51 | r = createRandomExponent() 52 | 53 | self.g2 = pow(self.gen, self.x2, self.mod) 54 | self.g3 = pow(self.gen, self.x3, self.mod) 55 | 56 | (c3, d3) = self.createLogProof('3', self.x2) 57 | (c4, d4) = self.createLogProof('4', self.x3) 58 | 59 | self.gb2 = pow(self.g2a, self.x2, self.mod) 60 | self.gb3 = pow(self.g3a, self.x3, self.mod) 61 | 62 | self.pb = pow(self.gb3, r, self.mod) 63 | self.qb = mulm(pow(self.gen, r, self.mod), pow(self.gb2, self.secret, self.mod), self.mod) 64 | 65 | (c5, d5, d6) = self.createCoordsProof('5', self.gb2, self.gb3, r) 66 | 67 | # Sends g2b, g3b, pb, qb, all the c's and d's 68 | return packList(self.g2, self.g3, self.pb, self.qb, c3, d3, c4, d4, c5, d5, d6) 69 | 70 | 71 | def step3(self, buffer): 72 | (g2b, g3b, pb, qb, c3, d3, c4, d4, c5, d5, d6) = unpackList(buffer) 73 | 74 | if not self.isValidArgument(g2b) or not self.isValidArgument(g3b) or \ 75 | not self.isValidArgument(pb) or not self.isValidArgument(qb): 76 | raise exceptions.CryptoError("Invalid g2b/g3b/pb/qb values", errors.ERR_SMP_CHECK_FAILED) 77 | 78 | if not self.checkLogProof('3', g2b, c3, d3): 79 | raise exceptions.CryptoError("Proof 3 check failed", errors.ERR_SMP_CHECK_FAILED) 80 | 81 | if not self.checkLogProof('4', g3b, c4, d4): 82 | raise exceptions.CryptoError("Proof 4 check failed", errors.ERR_SMP_CHECK_FAILED) 83 | 84 | self.g2b = g2b 85 | self.g3b = g3b 86 | 87 | self.ga2 = pow(self.g2b, self.x2, self.mod) 88 | self.ga3 = pow(self.g3b, self.x3, self.mod) 89 | 90 | if not self.checkCoordsProof('5', c5, d5, d6, self.ga2, self.ga3, pb, qb): 91 | raise exceptions.CryptoError("Proof 5 check failed", errors.ERR_SMP_CHECK_FAILED) 92 | 93 | s = createRandomExponent() 94 | 95 | self.qb = qb 96 | self.pb = pb 97 | self.pa = pow(self.ga3, s, self.mod) 98 | self.qa = mulm(pow(self.gen, s, self.mod), pow(self.ga2, self.secret, self.mod), self.mod) 99 | 100 | (c6, d7, d8) = self.createCoordsProof('6', self.ga2, self.ga3, s) 101 | 102 | inv = self.invm(qb) 103 | self.ra = pow(mulm(self.qa, inv, self.mod), self.x3, self.mod) 104 | 105 | (c7, d9) = self.createEqualLogsProof('7', self.qa, inv, self.x3) 106 | 107 | # Sends pa, qa, ra, c6, d7, d8, c7, d9 108 | return packList(self.pa, self.qa, self.ra, c6, d7, d8, c7, d9) 109 | 110 | 111 | 112 | def step4(self, buffer): 113 | (pa, qa, ra, c6, d7, d8, c7, d9) = unpackList(buffer) 114 | 115 | if not self.isValidArgument(pa) or not self.isValidArgument(qa) or not self.isValidArgument(ra): 116 | raise exceptions.CryptoError("Invalid pa/qa/ra values", errors.ERR_SMP_CHECK_FAILED) 117 | 118 | if not self.checkCoordsProof('6', c6, d7, d8, self.gb2, self.gb3, pa, qa): 119 | raise exceptions.CryptoError("Proof 6 check failed", errors.ERR_SMP_CHECK_FAILED) 120 | 121 | if not self.checkEqualLogs('7', c7, d9, self.g3a, mulm(qa, self.invm(self.qb), self.mod), ra): 122 | raise exceptions.CryptoError("Proof 7 check failed", errors.ERR_SMP_CHECK_FAILED) 123 | 124 | inv = self.invm(self.qb) 125 | rb = pow(mulm(qa, inv, self.mod), self.x3, self.mod) 126 | 127 | (c8, d10) = self.createEqualLogsProof('8', qa, inv, self.x3) 128 | 129 | rab = pow(ra, self.x3, self.mod) 130 | 131 | inv = self.invm(self.pb) 132 | if rab == mulm(pa, inv, self.mod): 133 | self.match = True 134 | 135 | # Send rb, c8, d10 136 | return packList(rb, c8, d10) 137 | 138 | 139 | def step5(self, buffer): 140 | (rb, c8, d10) = unpackList(buffer) 141 | 142 | if not self.isValidArgument(rb): 143 | raise exceptions.CryptoError("Invalid rb values", errors.ERR_SMP_CHECK_FAILED) 144 | 145 | if not self.checkEqualLogs('8', c8, d10, self.g3b, mulm(self.qa, self.invm(self.qb), self.mod), rb): 146 | raise exceptions.CryptoError("Proof 8 check failed", errors.ERR_SMP_CHECK_FAILED) 147 | 148 | rab = pow(rb, self.x3, self.mod) 149 | 150 | inv = self.invm(self.pb) 151 | if rab == mulm(self.pa, inv, self.mod): 152 | self.match = True 153 | 154 | 155 | def createLogProof(self, version, x): 156 | randExponent = createRandomExponent() 157 | c = self.hash(version + str(pow(self.gen, randExponent, self.mod))) 158 | d = subm(randExponent, mulm(x, c, self.modOrder), self.modOrder) 159 | return (c, d) 160 | 161 | 162 | def checkLogProof(self, version, g, c, d): 163 | gd = pow(self.gen, d, self.mod) 164 | gc = pow(g, c, self.mod) 165 | gdgc = gd * gc % self.mod 166 | return (self.hash(version + str(gdgc)) == c) 167 | 168 | 169 | def createCoordsProof(self, version, g2, g3, r): 170 | r1 = createRandomExponent() 171 | r2 = createRandomExponent() 172 | 173 | tmp1 = pow(g3, r1, self.mod) 174 | tmp2 = mulm(pow(self.gen, r1, self.mod), pow(g2, r2, self.mod), self.mod) 175 | 176 | c = self.hash(version + str(tmp1) + str(tmp2)) 177 | 178 | d1 = subm(r1, mulm(r, c, self.modOrder), self.modOrder) 179 | d2 = subm(r2, mulm(self.secret, c, self.modOrder), self.modOrder) 180 | 181 | return (c, d1, d2) 182 | 183 | 184 | def checkCoordsProof(self, version, c, d1, d2, g2, g3, p, q): 185 | tmp1 = mulm(pow(g3, d1, self.mod), pow(p, c, self.mod), self.mod) 186 | tmp2 = mulm(mulm(pow(self.gen, d1, self.mod), pow(g2, d2, self.mod), self.mod), pow(q, c, self.mod), self.mod) 187 | 188 | cprime = self.hash(version + str(tmp1) + str(tmp2)) 189 | 190 | return (c == cprime) 191 | 192 | 193 | def createEqualLogsProof(self, version, qa, qb, x): 194 | r = createRandomExponent() 195 | tmp1 = pow(self.gen, r, self.mod) 196 | qab = mulm(qa, qb, self.mod) 197 | tmp2 = pow(qab, r, self.mod) 198 | 199 | c = self.hash(version + str(tmp1) + str(tmp2)) 200 | tmp1 = mulm(x, c, self.modOrder) 201 | d = subm(r, tmp1, self.modOrder) 202 | 203 | return (c, d) 204 | 205 | 206 | def checkEqualLogs(self, version, c, d, g3, qab, r): 207 | tmp1 = mulm(pow(self.gen, d, self.mod), pow(g3, c, self.mod), self.mod) 208 | tmp2 = mulm(pow(qab, d, self.mod), pow(r, c, self.mod), self.mod) 209 | 210 | cprime = self.hash(version + str(tmp1) + str(tmp2)) 211 | 212 | return (c == cprime) 213 | 214 | 215 | def invm(self, x): 216 | return pow(x, self.mod-2, self.mod) 217 | 218 | 219 | def isValidArgument(self, val): 220 | return (val >= 2 and val <= self.mod-2) 221 | 222 | 223 | def hash(self, message): 224 | return long(self.crypto.stringHash(message), 16) 225 | 226 | 227 | 228 | def packList(*items): 229 | buffer = '' 230 | 231 | # For each item in the list, convert it to a byte string and add its length as a prefix 232 | for item in items: 233 | bytes = longToBytes(item) 234 | buffer += struct.pack('!I', len(bytes)) + bytes 235 | 236 | return buffer 237 | 238 | 239 | def unpackList(buffer): 240 | items = [] 241 | 242 | index = 0 243 | while index < len(buffer): 244 | # Get the length of the long (4 byte int before the actual long) 245 | length = struct.unpack('!I', buffer[index:index+4])[0] 246 | index += 4 247 | 248 | # Convert the data back to a long and add it to the list 249 | item = bytesToLong(buffer[index:index+length]) 250 | items.append(item) 251 | index += length 252 | 253 | return items 254 | 255 | 256 | def bytesToLong(bytes): 257 | length = len(bytes) 258 | string = 0 259 | for i in range(length): 260 | string += byteToLong(bytes[i:i+1]) << 8*(length-i-1) 261 | return string 262 | 263 | 264 | def longToBytes(long): 265 | bytes = '' 266 | while long != 0: 267 | bytes = longToByte(long & 0xff) + bytes 268 | long >>= 8 269 | return bytes 270 | 271 | 272 | def byteToLong(byte): 273 | return struct.unpack('B', byte)[0] 274 | 275 | 276 | def longToByte(long): 277 | return struct.pack('B', long) 278 | 279 | 280 | def mulm(x, y, mod): 281 | return x * y % mod 282 | 283 | 284 | def subm(x, y, mod): 285 | return (x - y) % mod 286 | 287 | 288 | def createRandomExponent(): 289 | return crypto.binToDec(M2Crypto.Rand.rand_bytes(192)) 290 | -------------------------------------------------------------------------------- /src/cryptully.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python2.7 2 | 3 | import sys 4 | import signal 5 | import argparse 6 | 7 | from utils import constants 8 | 9 | 10 | turnServer = None 11 | ncursesUI = None 12 | qtUI = None 13 | 14 | 15 | def main(): 16 | args = parse_cmdline_args() 17 | 18 | signal.signal(signal.SIGINT, signalHandler) 19 | 20 | if args.server: 21 | from server.turnServer import TURNServer 22 | global turnServer 23 | 24 | turnServer = TURNServer(args.port) 25 | turnServer.start() 26 | elif args.ncurses: 27 | from ncurses.ncurses import NcursesUI 28 | global ncursesUI 29 | 30 | ncursesUI = NcursesUI(args.nick, args.turn, args.port) 31 | ncursesUI.start() 32 | else: 33 | from qt.qt import QtUI 34 | global qtUI 35 | 36 | qtUI = QtUI(sys.argv, args.nick, args.turn, args.port) 37 | qtUI.start() 38 | 39 | sys.exit(0) 40 | 41 | 42 | def parse_cmdline_args(): 43 | argvParser = argparse.ArgumentParser() 44 | argvParser.add_argument('-k', '--nick', dest='nick', nargs='?', type=str, help="Nickname to use.") 45 | argvParser.add_argument('-r', '--relay', dest='turn', nargs='?', type=str, default=str(constants.DEFAULT_TURN_SERVER), help="The relay server to use.") 46 | argvParser.add_argument('-p', '--port', dest='port', nargs='?', type=int, default=str(constants.DEFAULT_PORT), help="Port to connect listen on (server) or connect to (client).") 47 | argvParser.add_argument('-s', '--server', dest='server', default=False, action='store_true', help="Run as TURN server for other clients.") 48 | argvParser.add_argument('-n', '--ncurses', dest='ncurses', default=False, action='store_true', help="Use the NCurses UI.") 49 | 50 | args = argvParser.parse_args() 51 | 52 | # Check the port range 53 | if args.port <= 0 or args.port > 65536: 54 | print "The port must be between 1 and 65536 inclusive." 55 | sys.exit(1) 56 | 57 | return args 58 | 59 | 60 | def signalHandler(signal, frame): 61 | if turnServer is not None: 62 | turnServer.stop() 63 | elif ncursesUI is not None: 64 | ncursesUI.stop() 65 | elif qtUI is not None: 66 | qtUI.stop() 67 | 68 | sys.exit(0) 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /src/images/dark/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/delete.png -------------------------------------------------------------------------------- /src/images/dark/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/exit.png -------------------------------------------------------------------------------- /src/images/dark/fingerprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/fingerprint.png -------------------------------------------------------------------------------- /src/images/dark/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/help.png -------------------------------------------------------------------------------- /src/images/dark/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/icon.png -------------------------------------------------------------------------------- /src/images/dark/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/menu.png -------------------------------------------------------------------------------- /src/images/dark/new_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/new_chat.png -------------------------------------------------------------------------------- /src/images/dark/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/save.png -------------------------------------------------------------------------------- /src/images/dark/splash_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/splash_icon.png -------------------------------------------------------------------------------- /src/images/dark/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/dark/waiting.gif -------------------------------------------------------------------------------- /src/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/icon.ico -------------------------------------------------------------------------------- /src/images/light/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/delete.png -------------------------------------------------------------------------------- /src/images/light/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/exit.png -------------------------------------------------------------------------------- /src/images/light/fingerprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/fingerprint.png -------------------------------------------------------------------------------- /src/images/light/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/help.png -------------------------------------------------------------------------------- /src/images/light/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/icon.png -------------------------------------------------------------------------------- /src/images/light/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/menu.png -------------------------------------------------------------------------------- /src/images/light/new_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/new_chat.png -------------------------------------------------------------------------------- /src/images/light/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/save.png -------------------------------------------------------------------------------- /src/images/light/splash_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/splash_icon.png -------------------------------------------------------------------------------- /src/images/light/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/light/waiting.gif -------------------------------------------------------------------------------- /src/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/placeholder.png -------------------------------------------------------------------------------- /src/images/splash_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/images/splash_logo.psd -------------------------------------------------------------------------------- /src/ncurses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/ncurses/__init__.py -------------------------------------------------------------------------------- /src/ncurses/cursesAcceptDialog.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | from utils import constants 4 | 5 | 6 | class CursesAcceptDialog(object): 7 | def __init__(self, screen, nick): 8 | self.screen = screen 9 | self.nick = nick 10 | 11 | 12 | def show(self): 13 | (height, width) = self.screen.getmaxyx() 14 | 15 | dialogWidth = 28 + len(self.nick); 16 | acceptWindow = self.screen.subwin(6, dialogWidth, height/2 - 3, width/2 - int(dialogWidth/2)) 17 | acceptWindow.border(0) 18 | 19 | # Enable arrow key detection for this window 20 | acceptWindow.keypad(True) 21 | 22 | # Disable the cursor 23 | curses.curs_set(0) 24 | 25 | acceptWindow.addstr(1, 1, "Recveived connection from %s" % self.nick) 26 | 27 | position = constants.ACCEPT 28 | 29 | while True: 30 | if position == constants.ACCEPT: 31 | acceptWindow.addstr(3, 2, "Accept", curses.color_pair(4)) 32 | acceptWindow.addstr(4, 2, "Reject") 33 | else: 34 | acceptWindow.addstr(3, 2, "Accept") 35 | acceptWindow.addstr(4, 2, "Reject", curses.color_pair(4)) 36 | 37 | self.screen.refresh() 38 | key = acceptWindow.getch() 39 | # Enter key 40 | if key == ord('\n'): 41 | break 42 | elif position == constants.ACCEPT: 43 | position = constants.REJECT 44 | elif position == constants.REJECT: 45 | position = constants.ACCEPT 46 | 47 | # Re-enable the cursor 48 | curses.curs_set(2) 49 | 50 | acceptWindow.clear() 51 | acceptWindow.refresh() 52 | 53 | return position 54 | -------------------------------------------------------------------------------- /src/ncurses/cursesDialog.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | class CursesDialog: 4 | def __init__(self, screen, message, title="", isError=False, isFatal=False, isBlocking=False): 5 | self.screen = screen 6 | self.title = title 7 | self.message = message 8 | 9 | self.isError = isError 10 | self.isFatal = isFatal 11 | self.isBlocking = isBlocking 12 | 13 | if curses.has_colors(): 14 | curses.init_pair(6, curses.COLOR_GREEN, curses.COLOR_BLACK) 15 | curses.init_pair(7, curses.COLOR_RED, curses.COLOR_BLACK) 16 | 17 | 18 | def show(self): 19 | (height, width) = self.screen.getmaxyx() 20 | 21 | if self.isFatal: 22 | exitMessage = "Press enter to exit" 23 | elif self.isError: 24 | exitMessage = "Press enter to continue" 25 | elif self.isBlocking: 26 | exitMessage = "Press any key to continue" 27 | else: 28 | exitMessage = "" 29 | 30 | # Determine the max width of the dialog window 31 | dialogWidth = max(len(self.title), len(self.message), len(exitMessage)) + 2 32 | 33 | if self.title: 34 | dialogHeight = 7 35 | elif self.isError or self.isBlocking: 36 | dialogHeight = 5 37 | else: 38 | dialogHeight = 3 39 | 40 | self.dialogWindow = self.screen.subwin(dialogHeight, dialogWidth, height/2 - int(dialogHeight/2), width/2 - int(dialogWidth/2)) 41 | self.dialogWindow.clear() 42 | self.dialogWindow.border(0) 43 | 44 | # Add the title if provided 45 | if self.title: 46 | self.dialogWindow.addstr(1, 1, self.title, curses.color_pair(7) if self.isError else curses.color_pair(6)) 47 | self.dialogWindow.hline(2, 1, 0, dialogWidth-2) 48 | 49 | # Add the message 50 | if self.message: 51 | verticalPos = 3 if self.title else 1 52 | self.dialogWindow.addstr(verticalPos, 1, self.message) 53 | 54 | # Add the exit message if the dialog is an error dialog or is blocking 55 | if self.isError or self.isBlocking: 56 | if self.title: 57 | verticalPos = 5 58 | else: 59 | verticalPos = 3 60 | self.dialogWindow.addstr(verticalPos, 1, exitMessage) 61 | 62 | # Disable the cursor 63 | curses.curs_set(0) 64 | 65 | self.dialogWindow.refresh() 66 | 67 | if self.isBlocking: 68 | self.dialogWindow.getch() 69 | self.hide() 70 | 71 | 72 | def hide(self): 73 | curses.curs_set(2) 74 | self.dialogWindow.clear() 75 | self.dialogWindow.refresh() 76 | -------------------------------------------------------------------------------- /src/ncurses/cursesInputDialog.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | from utils import constants 4 | 5 | 6 | class CursesInputDialog(object): 7 | def __init__(self, screen, prompt): 8 | self.screen = screen 9 | self.prompt = prompt 10 | 11 | 12 | def show(self): 13 | (height, width) = self.screen.getmaxyx() 14 | 15 | dialogWidth = len(self.prompt) + 32 16 | inputWindow = self.screen.subwin(3, dialogWidth, height/2 - 1, width/2 - dialogWidth/2) 17 | inputWindow.border(0) 18 | inputWindow.addstr(1, 1, self.prompt) 19 | inputWindow.refresh() 20 | 21 | # Turn on echo and wait for enter key to read buffer 22 | curses.echo() 23 | curses.nocbreak() 24 | 25 | input = inputWindow.getstr(1, len(self.prompt) + 1) 26 | 27 | # Turn off echo and disable buffering 28 | curses.cbreak() 29 | curses.noecho() 30 | 31 | # Clear the window 32 | inputWindow.clear() 33 | inputWindow.refresh() 34 | 35 | return input 36 | -------------------------------------------------------------------------------- /src/ncurses/cursesModeDialog.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | from utils import constants 4 | 5 | 6 | class CursesModeDialog(object): 7 | def __init__(self, screen): 8 | self.screen = screen 9 | 10 | 11 | def show(self): 12 | (height, width) = self.screen.getmaxyx() 13 | 14 | modeDialog = self.screen.subwin(4, 23, height/2 - 3, width/2 - 11) 15 | modeDialog.border(0) 16 | 17 | # Enable arrow key detection for this window 18 | modeDialog.keypad(True) 19 | 20 | # Disable the cursor 21 | curses.curs_set(0) 22 | 23 | position = constants.CONNECT 24 | 25 | while True: 26 | if position == constants.CONNECT: 27 | modeDialog.addstr(1, 2, "Initiate connection", curses.color_pair(4)) 28 | modeDialog.addstr(2, 2, "Wait for connection") 29 | else: 30 | modeDialog.addstr(1, 2, "Initiate connection") 31 | modeDialog.addstr(2, 2, "Wait for connection", curses.color_pair(4)) 32 | 33 | self.screen.refresh() 34 | key = modeDialog.getch() 35 | # Enter key 36 | if key == ord('\n'): 37 | break 38 | elif position == constants.CONNECT: 39 | position = constants.WAIT 40 | elif position == constants.WAIT: 41 | position = constants.CONNECT 42 | 43 | # Re-enable the cursor 44 | curses.curs_set(2) 45 | 46 | modeDialog.clear() 47 | modeDialog.refresh() 48 | 49 | return position 50 | -------------------------------------------------------------------------------- /src/ncurses/cursesPassphraseDialog.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | from getpass import getpass 4 | 5 | 6 | class CursesPassphraseDialog(object): 7 | def __init__(self, screen, verify=False): 8 | self.screen = screen 9 | self.verify = verify 10 | 11 | 12 | def show(self): 13 | (height, width) = self.screen.getmaxyx() 14 | 15 | passphraseWindow = self.screen.subwin(3, 36, height/2 - 1, width/2 - 18) 16 | 17 | # Turn on echo and wait for enter key to read buffer 18 | curses.echo() 19 | curses.nocbreak() 20 | 21 | while True: 22 | passphraseWindow.border(0) 23 | passphraseWindow.addstr(1, 1, "Passphrase: ") 24 | passphraseWindow.refresh() 25 | passphrase = getpass('') 26 | 27 | if not self.verify: 28 | break 29 | 30 | passphraseWindow.clear() 31 | passphraseWindow.border(0) 32 | passphraseWindow.addstr(1, 1, "Verify: ") 33 | passphraseWindow.refresh() 34 | verifyPassphrase = getpass('') 35 | 36 | if passphrase == verifyPassphrase: 37 | break 38 | else: 39 | curses.cbreak() 40 | CursesDialog(self.screen, errors.VERIFY_PASSPHRASE_FAILED, '', isBlocking=True).show() 41 | curses.nocbreak() 42 | 43 | # Turn off echo and disable buffering 44 | curses.cbreak() 45 | curses.noecho() 46 | 47 | # Get rid of the passphrase window 48 | passphraseWindow.clear() 49 | passphraseWindow.refresh() 50 | 51 | return passphrase 52 | -------------------------------------------------------------------------------- /src/ncurses/cursesSendThread.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import threading 3 | 4 | from utils import utils 5 | 6 | 7 | class CursesSendThread(threading.Thread): 8 | def __init__(self, ncurses): 9 | threading.Thread.__init__(self) 10 | self.daemon = True 11 | 12 | self.ncurses = ncurses 13 | self.stop = threading.Event() 14 | self.smpRequested = threading.Event() 15 | 16 | 17 | def run(self): 18 | (height, width) = self.ncurses.chatWindow.getmaxyx() 19 | self.ncurses.textboxWindow.move(0, 0) 20 | 21 | while True: 22 | message = self.ncurses.textbox.edit(self.inputValidator)[:-1] 23 | self.ncurses.screen.refresh() 24 | 25 | # Don't send anything if we're not connected to a nick 26 | if self.ncurses.connectedNick is None: 27 | self.ncurses.appendMessage('', "Not connected to client", curses.color_pair(0)) 28 | 29 | # Stop the thread if the stop flag is set 30 | if self.stop.is_set(): 31 | self.ncurses.dialogDismissed.acquire() 32 | self.ncurses.dialogDismissed.notify() 33 | self.ncurses.dialogDismissed.release() 34 | return 35 | 36 | # If requesting an SMP answer, set the given message as the answer 37 | if self.smpRequested.is_set(): 38 | self.ncurses.setSmpAnswer(message) 39 | self.smpRequested.clear() 40 | else: 41 | self.__addMessageToChat(message) 42 | self.__sendMessageToClient(message) 43 | 44 | self.__clearChatInput() 45 | 46 | 47 | def inputValidator(self, char): 48 | if char == 21: # Ctrl+U 49 | self.ncurses.showOptionsMenuWindow() 50 | return 0 51 | elif char == curses.KEY_HOME: 52 | return curses.ascii.SOH 53 | elif char == curses.KEY_END: 54 | return curses.ascii.ENQ 55 | elif char == curses.KEY_ENTER or char == ord('\n'): 56 | return curses.ascii.BEL 57 | else: 58 | return char 59 | 60 | 61 | def __clearChatInput(self): 62 | self.ncurses.textboxWindow.deleteln() 63 | self.ncurses.textboxWindow.move(0, 0) 64 | self.ncurses.textboxWindow.deleteln() 65 | 66 | 67 | def __addMessageToChat(self, message): 68 | prefix = "(%s) %s: " % (utils.getTimestamp(), self.ncurses.nick) 69 | self.ncurses.appendMessage(prefix, message, curses.color_pair(3)) 70 | 71 | 72 | def __sendMessageToClient(self, message): 73 | self.ncurses.connectionManager.getClient(self.ncurses.connectedNick).sendChatMessage(message) 74 | -------------------------------------------------------------------------------- /src/ncurses/cursesStatusWindow.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | from utils import constants 4 | 5 | 6 | class CursesStatusWindow(object): 7 | def __init__(self, screen, text): 8 | self.screen = screen 9 | self.text = text 10 | 11 | 12 | def show(self): 13 | (height, width) = self.screen.getmaxyx() 14 | 15 | self.statusWindow = self.screen.subwin(height-3, width-34) 16 | 17 | self.setText(self.text) 18 | 19 | 20 | def setText(self, text): 21 | (height, width) = self.statusWindow.getmaxyx() 22 | 23 | self.text = text 24 | 25 | self.statusWindow.clear() 26 | self.statusWindow.border(0) 27 | self.statusWindow.addstr(1, width-len(text)-1, text) 28 | self.statusWindow.refresh() 29 | -------------------------------------------------------------------------------- /src/ncurses/ncurses.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import curses.ascii 3 | import curses.textpad 4 | import os 5 | import Queue 6 | import signal 7 | import sys 8 | import threading 9 | import time 10 | 11 | from cursesAcceptDialog import CursesAcceptDialog 12 | from cursesDialog import CursesDialog 13 | from cursesInputDialog import CursesInputDialog 14 | from cursesModeDialog import CursesModeDialog 15 | from cursesPassphraseDialog import CursesPassphraseDialog 16 | from cursesSendThread import CursesSendThread 17 | from cursesStatusWindow import CursesStatusWindow 18 | 19 | from network.client import Client 20 | from network.connectionManager import ConnectionManager 21 | 22 | from utils import constants 23 | from utils import errors 24 | from utils import exceptions 25 | from utils import utils 26 | 27 | 28 | class NcursesUI(object): 29 | def __init__(self, nick, turn, port): 30 | self.nick = nick 31 | self.turn = turn 32 | self.port = port 33 | 34 | self.connectedNick = None 35 | self.inRecveiveLoop = False 36 | self.clientConnectError = False 37 | self.messageQueue = Queue.Queue() 38 | self.connectionManager = None 39 | self.sendThread = None 40 | 41 | self.errorRaised = threading.Event() 42 | self.dialogDismissed = threading.Condition() 43 | self.clientConnected = threading.Condition() 44 | 45 | 46 | def start(self): 47 | curses.wrapper(self.run) 48 | 49 | 50 | def stop(self): 51 | if self.connectionManager is not None: 52 | self.connectionManager.disconnectFromServer() 53 | 54 | # Give the send thread time to get the disconnect messages out before exiting 55 | # and killing the thread 56 | time.sleep(.25) 57 | 58 | curses.endwin() 59 | 60 | 61 | def __restart(self): 62 | self.__drawUI() 63 | 64 | self.connectedNick = None 65 | self.inRecveiveLoop = False 66 | self.clientConnectError = False 67 | 68 | self.errorRaised.clear() 69 | self.postConnectToServer() 70 | 71 | 72 | def run(self, screen): 73 | self.screen = screen 74 | (self.height, self.width) = self.screen.getmaxyx() 75 | 76 | self.__drawUI() 77 | 78 | # Get the nick if not given 79 | if self.nick == None: 80 | self.nick = CursesInputDialog(self.screen, "Nickname: ").show() 81 | 82 | self.__connectToServer() 83 | self.postConnectToServer() 84 | 85 | 86 | def __drawUI(self): 87 | # Change the colors, clear the screen and set the overall border 88 | self.__setColors() 89 | self.screen.clear() 90 | self.screen.border(0) 91 | 92 | # Create the chat log and chat input windows 93 | self.makeChatWindow() 94 | self.makeChatInputWindow() 95 | 96 | self.statusWindow = CursesStatusWindow(self.screen, "Disconnected") 97 | self.statusWindow.show() 98 | self.screen.refresh() 99 | 100 | 101 | def postConnectToServer(self): 102 | # Ask if to wait for a connection or connect to someone 103 | self.mode = CursesModeDialog(self.screen).show() 104 | 105 | # If waiting for a connection, enter the recv loop and start the send thread 106 | if self.mode == constants.WAIT: 107 | self.waitingDialog = CursesDialog(self.screen, "Waiting for connection...", '') 108 | self.waitingDialog.show() 109 | else: 110 | # Get the nickname of who to connect to 111 | while True: 112 | nick = CursesInputDialog(self.screen, "Nickname: ").show() 113 | 114 | # Don't allow connections to self 115 | if nick == self.nick: 116 | CursesDialog(self.screen, errors.SELF_CONNECT, errors.TITLE_SELF_CONNECT, isError=True, isBlocking=True).show() 117 | continue 118 | if nick == '': 119 | CursesDialog(self.screen, errors.EMPTY_NICK, errors.TITLE_EMPTY_NICK, isError=True, isBlocking=True).show() 120 | continue 121 | 122 | self.__connectToNick(nick) 123 | break 124 | 125 | self.__receiveMessageLoop() 126 | 127 | 128 | def __connectToServer(self): 129 | # Create the connection manager to manage all communication to the server 130 | self.connectionManager = ConnectionManager(self.nick, (self.turn, self.port), self.postMessage, self.newClient, self.clientReady, self.smpRequest, self.handleError) 131 | 132 | dialogWindow = CursesDialog(self.screen, "Connecting to server...", "", False) 133 | dialogWindow.show() 134 | try: 135 | # TODO: push this to it's own thread 136 | self.connectionManager.connectToServer() 137 | except exceptions.GenericError as ge: 138 | dialogWindow.hide() 139 | CursesDialog(self.screen, str(ge), "Error connecting to server", isError=True, isFatal=True, isBlocking=True).show() 140 | self.__quitApp() 141 | 142 | dialogWindow.hide() 143 | 144 | 145 | def __connectToNick(self, nick): 146 | connectingDialog = CursesDialog(self.screen, "Connecting to %s..." % nick, "", False) 147 | connectingDialog.show() 148 | self.connectionManager.openChat(nick) 149 | 150 | self.clientConnected.acquire() 151 | self.clientConnected.wait() 152 | self.clientConnected.release() 153 | 154 | connectingDialog.hide() 155 | 156 | # If there was an error while connecting to the client, restart 157 | if self.clientConnectError: 158 | self.__restart() 159 | return 160 | 161 | self.__startSendThread() 162 | 163 | 164 | def postMessage(self, command, sourceNick, payload): 165 | self.messageQueue.put((command, sourceNick, payload)) 166 | 167 | 168 | def __receiveMessageLoop(self): 169 | self.inRecveiveLoop = True 170 | 171 | while True: 172 | # Keyboard interrupts are ignored unless a timeout is specified 173 | # See http://bugs.python.org/issue1360 174 | message = self.messageQueue.get(True, 31536000) 175 | 176 | if self.errorRaised.is_set(): 177 | self.__restart() 178 | return 179 | 180 | prefix = "(%s) %s: " % (utils.getTimestamp(), message[1]) 181 | self.appendMessage(prefix, message[2], curses.color_pair(2)) 182 | 183 | self.messageQueue.task_done() 184 | 185 | 186 | def appendMessage(self, prefix, message, color): 187 | (height, width) = self.chatWindow.getmaxyx() 188 | 189 | # Put the received data in the chat window 190 | self.chatWindow.scroll(1) 191 | self.chatWindow.addstr(height-1, 0, prefix, color) 192 | self.chatWindow.addstr(height-1, len(prefix), message) 193 | 194 | # Move the cursor back to the chat input window 195 | self.textboxWindow.move(0, 0) 196 | 197 | self.chatWindow.refresh() 198 | self.textboxWindow.refresh() 199 | 200 | 201 | def newClient(self, nick): 202 | # Only allow one client (TODO: support multiple clients) 203 | if self.connectedNick is not None or self.mode != constants.WAIT: 204 | self.connectionManager.newClientRejected(nick) 205 | return 206 | 207 | self.waitingDialog.hide() 208 | 209 | # Show the accept dialog 210 | accept = CursesAcceptDialog(self.screen, nick).show() 211 | 212 | if accept == constants.REJECT: 213 | self.waitingDialog.show() 214 | self.connectionManager.newClientRejected(nick) 215 | return 216 | 217 | # Set who we're connected to in the status window 218 | self.statusWindow.setText(nick) 219 | self.connectedNick = nick 220 | 221 | self.__startSendThread() 222 | 223 | self.connectionManager.newClientAccepted(nick) 224 | 225 | 226 | def __startSendThread(self): 227 | # Add a hint on how to display the options menu 228 | self.screen.addstr(0, 5, "Ctrl+U for options") 229 | self.screen.refresh() 230 | 231 | # Show the now chatting message 232 | self.appendMessage('', "Now chatting with %s" % self.connectedNick, curses.color_pair(0)) 233 | 234 | self.sendThread = CursesSendThread(self) 235 | self.sendThread.start() 236 | 237 | 238 | def clientReady(self, nick): 239 | self.connectedNick = nick 240 | self.statusWindow.setText(nick) 241 | 242 | self.clientConnected.acquire() 243 | self.clientConnected.notify() 244 | self.clientConnected.release() 245 | 246 | 247 | def smpRequest(self, type, nick, question='', errno=0): 248 | if type == constants.SMP_CALLBACK_REQUEST: 249 | # Set the SMP request event and pump the message queue to fire it in the UI thread 250 | self.appendMessage("Chat authentication request (case senstitive):", ' ' + question, curses.color_pair(4)) 251 | self.sendThread.smpRequested.set() 252 | elif type == constants.SMP_CALLBACK_COMPLETE: 253 | CursesDialog(self.screen, "Chat with %s authenticated successfully." % nick, isBlocking=True).show() 254 | elif type == constants.SMP_CALLBACK_ERROR: 255 | self.handleError(nick, errno) 256 | 257 | 258 | def setSmpAnswer(self, answer): 259 | self.connectionManager.respondSMP(self.connectedNick, answer) 260 | 261 | 262 | def handleError(self, nick, errorCode): 263 | # Stop the send thread after the user presses enter if it is running 264 | clientConnectError = False 265 | waiting = False 266 | if self.sendThread is not None: 267 | waiting = True 268 | self.sendThread.stop.set() 269 | 270 | if errorCode == errors.ERR_CONNECTION_ENDED: 271 | dialog = CursesDialog(self.screen, errors.CONNECTION_ENDED % (nick), errors.TITLE_CONNECTION_ENDED, isError=True) 272 | elif errorCode == errors.ERR_NICK_NOT_FOUND: 273 | dialog = CursesDialog(self.screen, errors.NICK_NOT_FOUND % (nick), errors.TITLE_NICK_NOT_FOUND, isError=True) 274 | clientConnectError = True 275 | elif errorCode == errors.ERR_CONNECTION_REJECTED: 276 | dialog = CursesDialog(self.screen, errors.CONNECTION_REJECTED % (nick), errors.TITLE_CONNECTION_REJECTED, isError=True) 277 | clientConnectError = True 278 | elif errorCode == errors.ERR_BAD_HANDSHAKE: 279 | dialog = CursesDialog(self.screen, errors.PROTOCOL_ERROR % (nick), errors.TITLE_PROTOCOL_ERROR, isError=True) 280 | elif errorCode == errors.ERR_CLIENT_EXISTS: 281 | dialog = CursesDialog(self.screen, errors.CLIENT_EXISTS % (nick), errors.TITLE_CLIENT_EXISTS, isError=True) 282 | elif errorCode == errors.ERR_SELF_CONNECT: 283 | dialog = CursesDialog(self.screen, errors.SELF_CONNECT, errors.TITLE_SELF_CONNECT, isError=True) 284 | elif errorCode == errors.ERR_SERVER_SHUTDOWN: 285 | dialog = CursesDialog(self.screen, errors.SERVER_SHUTDOWN, errors.TITLE_SERVER_SHUTDOWN, isError=True, isFatal=True) 286 | elif errorCode == errors.ERR_ALREADY_CONNECTED: 287 | dialog = CursesDialog(self.screen, errors.ALREADY_CONNECTED % (nick), errors.TITLE_ALREADY_CONNECTED, isError=True) 288 | elif errorCode == errors.ERR_INVALID_COMMAND: 289 | dialog = CursesDialog(self.screen, errors.INVALID_COMMAND % (nick), errors.TITLE_INVALID_COMMAND, isError=True) 290 | elif errorCode == errors.ERR_NETWORK_ERROR: 291 | dialog = CursesDialog(self.screen, errors.NETWORK_ERROR, errors.TITLE_NETWORK_ERROR, isError=True, isFatal=True) 292 | elif errorCode == errors.ERR_BAD_HMAC: 293 | dialog = CursesDialog(self.screen, errors.BAD_HMAC, errors.TITLE_BAD_HMAC, isError=True) 294 | elif errorCode == errors.ERR_BAD_DECRYPT: 295 | dialog = CursesDialog(self.screen, errors.BAD_DECRYPT, errors.TITLE_BAD_DECRYPT, isError=True) 296 | elif errorCode == errors.ERR_KICKED: 297 | dialog = CursesDialog(self.screen, errors.KICKED, errors.TITLE_KICKED, isError=True) 298 | elif errorCode == errors.ERR_NICK_IN_USE: 299 | dialog = CursesDialog(self.screen, errors.NICK_IN_USE, errors.TITLE_NICK_IN_USE, isError=True, isFatal=True) 300 | elif errorCode == errors.ERR_SMP_CHECK_FAILED: 301 | dialog = CursesDialog(self.screen, errors.PROTOCOL_ERROR, errors.TITLE_PROTOCOL_ERROR, isError=True) 302 | elif errorCode == errors.ERR_SMP_MATCH_FAILED: 303 | dialog = CursesDialog(self.screen, errors.SMP_MATCH_FAILED_SHORT, errors.TITLE_SMP_MATCH_FAILED, isError=True) 304 | elif errorCode == errors.ERR_MESSAGE_REPLAY: 305 | dialog = CursesDialog(self.screen, errors.MESSAGE_REPLAY, errors.TITLE_MESSAGE_REPLAY, isError=True) 306 | elif errorCode == errors.ERR_MESSAGE_DELETION: 307 | dialog = CursesDialog(self.screen, errors.MESSAGE_DELETION, errors.TITLE_MESSAGE_DELETION, isError=True) 308 | elif errorCode == errors.ERR_PROTOCOL_VERSION_MISMATCH: 309 | dialog = CursesDialog(self.screen, errors.PROTOCOL_VERSION_MISMATCH, errors.TITLE_PROTOCOL_VERSION_MISMATCH, isError=True, isFatal=True) 310 | else: 311 | dialog = CursesDialog(self.screen, errors.UNKNOWN_ERROR % (nick), errors.TITLE_UNKNOWN_ERROR, isError=True) 312 | 313 | dialog.show() 314 | 315 | # Wait for the send thread to report that the dialog has been dismissed (enter was pressed) 316 | # or, if the send thread was not started yet, wait for a key press here 317 | if waiting: 318 | self.dialogDismissed.acquire() 319 | self.dialogDismissed.wait() 320 | self.dialogDismissed.release() 321 | else: 322 | self.screen.getch() 323 | 324 | dialog.hide() 325 | 326 | if dialog.isFatal: 327 | self.__quitApp() 328 | elif self.inRecveiveLoop: 329 | # If not fatal, the UI thread needs to restart, but it's blocked the message queue 330 | # Set a flag and send an empty message to pump the message queue 331 | self.errorRaised.set() 332 | self.postMessage('', '', '') 333 | elif clientConnectError: 334 | self.__handleClientConnectingError() 335 | else: 336 | self.__restart() 337 | 338 | 339 | def __handleClientConnectingError(self): 340 | self.clientConnectError = True 341 | self.clientConnected.acquire() 342 | self.clientConnected.notify() 343 | self.clientConnected.release() 344 | 345 | 346 | def __setColors(self): 347 | if curses.has_colors(): 348 | curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) 349 | curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) 350 | curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK) 351 | curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_GREEN) 352 | self.screen.bkgd(curses.color_pair(1)) 353 | 354 | 355 | def makeChatWindow(self): 356 | self.chatWindow = self.screen.subwin(self.height-4, self.width-2, 1, 1) 357 | self.chatWindow.scrollok(True) 358 | 359 | 360 | def makeChatInputWindow(self): 361 | self.textboxWindow = self.screen.subwin(1, self.width-36, self.height-2, 1) 362 | 363 | self.textbox = curses.textpad.Textbox(self.textboxWindow, insert_mode=True) 364 | curses.textpad.rectangle(self.screen, self.height-3, 0, self.height-1, self.width-35) 365 | self.textboxWindow.move(0, 0) 366 | 367 | 368 | def showOptionsMenuWindow(self): 369 | numMenuEntires = 5 370 | menuWindow = self.screen.subwin(numMenuEntires+2, 34, 3, self.width/2 - 14) 371 | 372 | # Enable arrow key detection for this window 373 | menuWindow.keypad(True) 374 | 375 | pos = 1 376 | 377 | while True: 378 | # Redraw the border on each loop in case something is shown on top of this window 379 | menuWindow.border(0) 380 | 381 | # Disable the cursor 382 | curses.curs_set(0) 383 | 384 | while True: 385 | item = 1 386 | menuWindow.addstr(item, 1, str(item) + ".| End current chat ", curses.color_pair(4) if pos == item else curses.color_pair(1)) 387 | item += 1 388 | menuWindow.addstr(item, 1, str(item) + ".| Authenticate chat", curses.color_pair(4) if pos == item else curses.color_pair(1)) 389 | item += 1 390 | menuWindow.addstr(item, 1, str(item) + ".| Show help ", curses.color_pair(4) if pos == item else curses.color_pair(1)) 391 | item += 1 392 | menuWindow.addstr(item, 1, str(item) + ".| Close menu ", curses.color_pair(4) if pos == item else curses.color_pair(1)) 393 | item += 1 394 | menuWindow.addstr(item, 1, str(item) + ".| Quit application ", curses.color_pair(4) if pos == item else curses.color_pair(1)) 395 | 396 | menuWindow.refresh() 397 | key = menuWindow.getch() 398 | if key == curses.KEY_DOWN and pos < numMenuEntires: 399 | pos += 1 400 | elif key == curses.KEY_UP and pos > 1: 401 | pos -= 1 402 | # Wrap around from top of menu 403 | elif key == curses.KEY_UP and pos == 1: 404 | pos = numMenuEntires 405 | # Wrap around from bottom of menu 406 | elif key == curses.KEY_DOWN and pos == numMenuEntires: 407 | pos = 1 408 | # Enter key 409 | elif key == ord('\n'): 410 | break 411 | 412 | # Process the selected option 413 | if pos == 1: 414 | self.connectionManager.closeChat(self.connectedNick) 415 | menuWindow.clear() 416 | menuWindow.refresh() 417 | self.__restart() 418 | elif pos == 2: 419 | if self.connectionManager is None: 420 | CursesDialog(self.screen, "Chat authentication is not available until you are chatting with someone.", isBlocking=True).show() 421 | return 422 | 423 | question = CursesInputDialog(self.screen, "Question: ").show() 424 | answer = CursesInputDialog(self.screen, "Answer (case senstitive): ").show() 425 | 426 | self.connectionManager.getClient(self.connectedNick).initiateSMP(question, answer) 427 | elif pos == 3: 428 | CursesDialog(self.screen, "Read the docs at https://cryptully.readthedocs.org/en/latest/", isBlocking=True).show() 429 | elif pos == 4: 430 | # Move the cursor back to the chat input textbox 431 | self.textboxWindow.move(0, 0) 432 | break 433 | elif pos == 5: 434 | os.kill(os.getpid(), signal.SIGINT) 435 | 436 | # Re-enable the cursor 437 | curses.curs_set(2) 438 | 439 | # Get rid of the accept window 440 | menuWindow.clear() 441 | menuWindow.refresh() 442 | 443 | 444 | def __quitApp(self): 445 | os.kill(os.getpid(), signal.SIGINT) 446 | -------------------------------------------------------------------------------- /src/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/network/__init__.py -------------------------------------------------------------------------------- /src/network/client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import Queue 3 | 4 | from crypto.crypto import Crypto 5 | from crypto.smp import SMP 6 | 7 | from message import Message 8 | 9 | from threading import Thread 10 | 11 | from utils import constants 12 | from utils import errors 13 | from utils import exceptions 14 | from utils import utils 15 | 16 | 17 | class Client(Thread): 18 | def __init__(self, connectionManager, remoteNick, sendMessageCallback, recvMessageCallback, handshakeDoneCallback, smpRequestCallback, errorCallback, initiateHandkshakeOnStart=False): 19 | Thread.__init__(self) 20 | self.daemon = True 21 | 22 | self.connectionManager = connectionManager 23 | self.remoteNick = remoteNick 24 | self.sendMessageCallback = sendMessageCallback 25 | self.recvMessageCallback = recvMessageCallback 26 | self.handshakeDoneCallback = handshakeDoneCallback 27 | self.smpRequestCallback = smpRequestCallback 28 | self.errorCallback = errorCallback 29 | self.initiateHandkshakeOnStart = initiateHandkshakeOnStart 30 | 31 | self.incomingMessageNum = 0 32 | self.outgoingMessageNum = 0 33 | self.isEncrypted = False 34 | self.wasHandshakeDone = False 35 | self.messageQueue = Queue.Queue() 36 | 37 | self.crypto = Crypto() 38 | self.crypto.generateDHKey() 39 | self.smp = None 40 | 41 | 42 | def sendChatMessage(self, text): 43 | self.sendMessage(constants.COMMAND_MSG, text) 44 | 45 | 46 | def sendTypingMessage(self, status): 47 | self.sendMessage(constants.COMMAND_TYPING, str(status)) 48 | 49 | 50 | def sendMessage(self, command, payload=None): 51 | message = Message(clientCommand=command, destNick=self.remoteNick) 52 | 53 | # Encrypt all outgoing data 54 | if payload is not None and self.isEncrypted: 55 | payload = self.crypto.aesEncrypt(payload) 56 | message.setEncryptedPayload(payload) 57 | 58 | # Generate and set the HMAC for the message 59 | message.setBinaryHmac(self.crypto.generateHmac(payload)) 60 | 61 | # Encrypt the message number of the message 62 | message.setBinaryMessageNum(self.crypto.aesEncrypt(str(self.outgoingMessageNum))) 63 | self.outgoingMessageNum += 1 64 | else: 65 | message.payload = payload 66 | 67 | self.sendMessageCallback(message) 68 | 69 | 70 | def postMessage(self, message): 71 | self.messageQueue.put(message) 72 | 73 | 74 | def initiateSMP(self, question, answer): 75 | self.sendMessage(constants.COMMAND_SMP_0, question) 76 | 77 | self.smp = SMP(answer) 78 | buffer = self.smp.step1() 79 | self.sendMessage(constants.COMMAND_SMP_1, buffer) 80 | 81 | 82 | def respondSMP(self, answer): 83 | self.smp = SMP(answer) 84 | self.__doSMPStep1(self.smpStep1) 85 | 86 | 87 | def run(self): 88 | if self.initiateHandkshakeOnStart: 89 | self.__initiateHandshake() 90 | else: 91 | self.__doHandshake() 92 | 93 | if not self.wasHandshakeDone: 94 | return 95 | 96 | while True: 97 | message = self.messageQueue.get() 98 | 99 | command = message.clientCommand 100 | payload = message.payload 101 | 102 | # Check if the client requested to end the connection 103 | if command == constants.COMMAND_END: 104 | self.connectionManager.destroyClient(self.remoteNick) 105 | self.errorCallback(self.remoteNick, errors.ERR_CONNECTION_ENDED) 106 | return 107 | # Ensure we got a valid command 108 | elif self.wasHandshakeDone and command not in constants.LOOP_COMMANDS: 109 | self.connectionManager.destroyClient(self.remoteNick) 110 | self.errorCallback(self.remoteNick, errors.ERR_INVALID_COMMAND) 111 | return 112 | 113 | # Decrypt the incoming data 114 | payload = self.__getDecryptedPayload(message) 115 | 116 | self.messageQueue.task_done() 117 | 118 | # Handle SMP commands specially 119 | if command in constants.SMP_COMMANDS: 120 | self.__handleSMPCommand(command, payload) 121 | else: 122 | self.recvMessageCallback(command, message.sourceNick, payload) 123 | 124 | 125 | def connect(self): 126 | self.__initiateHandshake() 127 | 128 | 129 | def disconnect(self): 130 | try: 131 | self.sendMessage(constants.COMMAND_END) 132 | except: 133 | pass 134 | 135 | 136 | def __doHandshake(self): 137 | try: 138 | # The caller of this function (should) checks for the initial HELO command 139 | 140 | # Send the ready command 141 | self.sendMessage(constants.COMMAND_REDY) 142 | 143 | # Receive the client's public key 144 | clientPublicKey = self.__getHandshakeMessagePayload(constants.COMMAND_PUBLIC_KEY) 145 | self.crypto.computeDHSecret(long(base64.b64decode(clientPublicKey))) 146 | 147 | # Send our public key 148 | publicKey = base64.b64encode(str(self.crypto.getDHPubKey())) 149 | self.sendMessage(constants.COMMAND_PUBLIC_KEY, publicKey) 150 | 151 | # Switch to AES encryption for the remainder of the connection 152 | self.isEncrypted = True 153 | 154 | self.wasHandshakeDone = True 155 | self.handshakeDoneCallback(self.remoteNick) 156 | except exceptions.ProtocolEnd: 157 | self.disconnect() 158 | self.connectionManager.destroyClient(self.remoteNick) 159 | except (exceptions.ProtocolError, exceptions.CryptoError) as e: 160 | self.__handleHandshakeError(e) 161 | 162 | 163 | def __initiateHandshake(self): 164 | try: 165 | # Send the hello command 166 | self.sendMessage(constants.COMMAND_HELO) 167 | 168 | # Receive the redy command 169 | self.__getHandshakeMessagePayload(constants.COMMAND_REDY) 170 | 171 | # Send our public key 172 | publicKey = base64.b64encode(str(self.crypto.getDHPubKey())) 173 | self.sendMessage(constants.COMMAND_PUBLIC_KEY, publicKey) 174 | 175 | # Receive the client's public key 176 | clientPublicKey = self.__getHandshakeMessagePayload(constants.COMMAND_PUBLIC_KEY) 177 | self.crypto.computeDHSecret(long(base64.b64decode(clientPublicKey))) 178 | 179 | # Switch to AES encryption for the remainder of the connection 180 | self.isEncrypted = True 181 | 182 | self.wasHandshakeDone = True 183 | self.handshakeDoneCallback(self.remoteNick) 184 | except exceptions.ProtocolEnd: 185 | self.disconnect() 186 | self.connectionManager.destroyClient(self.remoteNick) 187 | except (exceptions.ProtocolError, exceptions.CryptoError) as e: 188 | self.__handleHandshakeError(e) 189 | 190 | 191 | def __getHandshakeMessagePayload(self, expectedCommand): 192 | message = self.messageQueue.get() 193 | 194 | if message.clientCommand != expectedCommand: 195 | if message.clientCommand == constants.COMMAND_END: 196 | raise exceptions.ProtocolEnd 197 | elif message.clientCommand == constants.COMMAND_REJECT: 198 | raise exceptions.ProtocolError(errno=errors.ERR_CONNECTION_REJECTED) 199 | else: 200 | raise exceptions.ProtocolError(errno=errors.ERR_BAD_HANDSHAKE) 201 | 202 | payload = self.__getDecryptedPayload(message) 203 | self.messageQueue.task_done() 204 | 205 | return payload 206 | 207 | 208 | def __getDecryptedPayload(self, message): 209 | if self.isEncrypted: 210 | payload = message.getEncryptedPayloadAsBinaryString() 211 | encryptedMessageNumber = message.getMessageNumAsBinaryString() 212 | 213 | # Check the HMAC 214 | if not self.__verifyHmac(message.hmac, payload): 215 | self.errorCallback(message.sourceNick, errors.ERR_BAD_HMAC) 216 | raise exceptions.CryptoError(errno=errors.BAD_HMAC) 217 | 218 | try: 219 | # Check the message number 220 | messageNumber = int(self.crypto.aesDecrypt(encryptedMessageNumber)) 221 | 222 | # If the message number is less than what we're expecting, the message is being replayed 223 | if self.incomingMessageNum > messageNumber: 224 | raise exceptions.ProtocolError(errno=errors.ERR_MESSAGE_REPLAY) 225 | # If the message number is greater than what we're expecting, messages are being deleted 226 | elif self.incomingMessageNum < messageNumber: 227 | raise exceptions.ProtocolError(errno=errors.ERR_MESSAGE_DELETION) 228 | self.incomingMessageNum += 1 229 | 230 | # Decrypt the payload 231 | payload = self.crypto.aesDecrypt(payload) 232 | except exceptions.CryptoError as ce: 233 | self.errorCallback(message.sourceNick, errors.ERR_BAD_DECRYPT) 234 | raise ce 235 | else: 236 | payload = message.payload 237 | 238 | return payload 239 | 240 | 241 | def __verifyHmac(self, givenHmac, payload): 242 | generatedHmac = self.crypto.generateHmac(payload) 243 | return utils.secureStrcmp(generatedHmac, base64.b64decode(givenHmac)) 244 | 245 | 246 | def __handleSMPCommand(self, command, payload): 247 | try: 248 | if command == constants.COMMAND_SMP_0: 249 | # Fire the SMP request callback with the given question 250 | self.smpRequestCallback(constants.SMP_CALLBACK_REQUEST, self.remoteNick, payload) 251 | elif command == constants.COMMAND_SMP_1: 252 | # If there's already an smp object, go ahead to step 1. 253 | # Otherwise, save the payload until we have an answer from the user to respond with 254 | if self.smp: 255 | self.__doSMPStep1(payload) 256 | else: 257 | self.smpStep1 = payload 258 | elif command == constants.COMMAND_SMP_2: 259 | self.__doSMPStep2(payload) 260 | elif command == constants.COMMAND_SMP_3: 261 | self.__doSMPStep3(payload) 262 | elif command == constants.COMMAND_SMP_4: 263 | self.__doSMPStep4(payload) 264 | else: 265 | # This shouldn't happen 266 | raise exceptions.CryptoError(errno=errors.ERR_SMP_CHECK_FAILED) 267 | except exceptions.CryptoError as ce: 268 | self.smpRequestCallback(constants.SMP_CALLBACK_ERROR, self.remoteNick, '', ce.errno) 269 | 270 | 271 | def __doSMPStep1(self, payload): 272 | buffer = self.smp.step2(payload) 273 | self.sendMessage(constants.COMMAND_SMP_2, buffer) 274 | 275 | 276 | def __doSMPStep2(self, payload): 277 | buffer = self.smp.step3(payload) 278 | self.sendMessage(constants.COMMAND_SMP_3, buffer) 279 | 280 | 281 | def __doSMPStep3(self, payload): 282 | buffer = self.smp.step4(payload) 283 | self.sendMessage(constants.COMMAND_SMP_4, buffer) 284 | 285 | # Destroy the SMP object now that we're done 286 | self.smp = None 287 | 288 | 289 | def __doSMPStep4(self, payload): 290 | self.smp.step5(payload) 291 | 292 | if self.__checkSMP(): 293 | self.smpRequestCallback(constants.SMP_CALLBACK_COMPLETE, self.remoteNick) 294 | 295 | # Destroy the SMP object now that we're done 296 | self.smp = None 297 | 298 | 299 | def __checkSMP(self): 300 | if not self.smp.match: 301 | raise exceptions.CryptoError(errno=errors.ERR_SMP_MATCH_FAILED) 302 | return True 303 | 304 | 305 | def __handleHandshakeError(self, exception): 306 | self.errorCallback(self.remoteNick, exception.errno) 307 | 308 | # For all errors except the connection being rejected, tell the client there was an error 309 | if exception.errno != errors.ERR_CONNECTION_REJECTED: 310 | self.sendMessage(constants.COMMAND_ERR) 311 | else: 312 | self.connectionManager.destroyClient(self.remoteNick) 313 | -------------------------------------------------------------------------------- /src/network/connectionManager.py: -------------------------------------------------------------------------------- 1 | import Queue 2 | import socket 3 | import sys 4 | import traceback 5 | 6 | from threading import Thread 7 | 8 | from client import Client 9 | from message import Message 10 | from sock import Socket 11 | 12 | from utils import constants 13 | from utils import exceptions 14 | from utils import errors 15 | from utils import utils 16 | 17 | 18 | class ConnectionManager(object): 19 | def __init__(self, nick, serverAddr, recvMessageCallback, newClientCallback, handshakeDoneCallback, smpRequestCallback, errorCallback): 20 | self.clients = {} 21 | 22 | self.nick = nick 23 | self.sock = Socket(serverAddr) 24 | self.recvMessageCallback = recvMessageCallback 25 | self.newClientCallback = newClientCallback 26 | self.handshakeDoneCallback = handshakeDoneCallback 27 | self.smpRequestCallback = smpRequestCallback 28 | self.errorCallback = errorCallback 29 | self.sendThread = SendThread(self.sock, self.errorCallback) 30 | self.recvThread = RecvThread(self.sock, self.recvMessage, self.errorCallback) 31 | self.messageQueue = Queue.Queue() 32 | 33 | 34 | def connectToServer(self): 35 | self.sock.connect() 36 | self.sendThread.start() 37 | self.recvThread.start() 38 | self.__sendProtocolVersion() 39 | self.__registerNick() 40 | 41 | 42 | def disconnectFromServer(self): 43 | if self.sock.isConnected: 44 | try: 45 | # Send the end command to all clients 46 | for nick, client in self.clients.iteritems(): 47 | client.disconnect() 48 | 49 | # Send the end command to the server 50 | self.__sendServerCommand(constants.COMMAND_END) 51 | except Exception: 52 | pass 53 | 54 | 55 | def openChat(self, destNick): 56 | self.__createClient(destNick.lower(), initiateHandshakeOnStart=True) 57 | 58 | 59 | def __createClient(self, nick, initiateHandshakeOnStart=False): 60 | if type(nick) is not str: 61 | raise TypeError 62 | # Check that we're not connecting to ourself 63 | elif nick == self.nick: 64 | self.errorCallback(nick, errors.ERR_SELF_CONNECT) 65 | return 66 | # Check if a connection to the nick already exists 67 | elif nick in self.clients: 68 | self.errorCallback(nick, errors.ERR_ALREADY_CONNECTED) 69 | return 70 | 71 | newClient = Client(self, nick, self.sendMessage, self.recvMessageCallback, self.handshakeDoneCallback, self.smpRequestCallback, self.errorCallback, initiateHandshakeOnStart) 72 | self.clients[nick] = newClient 73 | newClient.start() 74 | 75 | 76 | def closeChat(self, nick): 77 | client = self.getClient(nick) 78 | if client is None: 79 | return 80 | 81 | # Send the end command to the client 82 | self.sendMessage(Message(clientCommand=constants.COMMAND_END, destNick=nick)) 83 | 84 | # Remove the client from the clients list 85 | self.destroyClient(nick) 86 | 87 | 88 | def destroyClient(self, nick): 89 | del self.clients[nick] 90 | 91 | 92 | def getClient(self, nick): 93 | try: 94 | return self.clients[nick] 95 | except KeyError: 96 | return None 97 | 98 | 99 | def __sendProtocolVersion(self): 100 | self.__sendServerCommand(constants.COMMAND_VERSION, constants.PROTOCOL_VERSION) 101 | 102 | 103 | def __registerNick(self): 104 | self.__sendServerCommand(constants.COMMAND_REGISTER) 105 | 106 | 107 | def __sendServerCommand(self, command, payload=None): 108 | # Send a commend intended for the server, not another client (such as registering a nick) 109 | self.sendThread.messageQueue.put(Message(serverCommand=command, sourceNick=self.nick, payload=payload)) 110 | 111 | 112 | def sendMessage(self, message): 113 | message.serverCommand = constants.COMMAND_RELAY 114 | message.sourceNick = self.nick 115 | self.sendThread.messageQueue.put(message) 116 | 117 | 118 | def recvMessage(self, message): 119 | command = message.clientCommand 120 | sourceNick = message.sourceNick 121 | 122 | # Handle errors/shutdown from the server 123 | if message.serverCommand == constants.COMMAND_ERR: 124 | # If the error was that the nick wasn't found, kill the client trying to connect to that nick 125 | if int(message.error) == errors.ERR_NICK_NOT_FOUND: 126 | try: 127 | del self.clients[str(message.destNick)] 128 | except: 129 | pass 130 | 131 | self.errorCallback(message.destNick, int(message.error)) 132 | return 133 | elif message.serverCommand == constants.COMMAND_END: 134 | self.errorCallback('', int(message.error)) 135 | return 136 | 137 | # Send the payload to it's intended client 138 | try: 139 | self.clients[sourceNick].postMessage(message) 140 | except KeyError as ke: 141 | # Create a new client if we haven't seen this client before 142 | if command == constants.COMMAND_HELO: 143 | self.newClientCallback(sourceNick) 144 | else: 145 | self.sendMessage(Message(clientCommand=constants.COMMAND_ERR, error=errors.INVALID_COMMAND)) 146 | 147 | 148 | def newClientAccepted(self, nick): 149 | self.__createClient(nick) 150 | 151 | 152 | def newClientRejected(self, nick): 153 | # If rejected, send the rejected command to the client 154 | self.sendMessage(Message(clientCommand=constants.COMMAND_REJECT, destNick=nick)) 155 | 156 | 157 | def respondSMP(self, nick, answer): 158 | self.clients[nick].respondSMP(answer) 159 | 160 | 161 | class RecvThread(Thread): 162 | def __init__(self, sock, recvCallback, errorCallback): 163 | Thread.__init__(self) 164 | self.daemon = True 165 | 166 | self.sock = sock 167 | self.errorCallback = errorCallback 168 | self.recvCallback = recvCallback 169 | 170 | 171 | def run(self): 172 | while True: 173 | try: 174 | message = Message.createFromJSON(self.sock.recv()) 175 | 176 | # Send the message to the given callback 177 | self.recvCallback(message) 178 | except exceptions.NetworkError as ne: 179 | # Don't show an error if the connection closing was expected/normal 180 | if hasattr(ne, 'errno') and ne.errno != errors.ERR_CLOSED_CONNECTION: 181 | self.errorCallback('', errors.ERR_NETWORK_ERROR) 182 | return 183 | 184 | 185 | class SendThread(Thread): 186 | def __init__(self, sock, errorCallback): 187 | Thread.__init__(self) 188 | self.daemon = True 189 | 190 | self.sock = sock 191 | self.errorCallback = errorCallback 192 | self.messageQueue = Queue.Queue() 193 | 194 | 195 | def run(self): 196 | while True: 197 | # Get (or wait) for a message in the message queue 198 | message = self.messageQueue.get() 199 | 200 | try: 201 | self.sock.send(str(message)) 202 | 203 | # If the server command is END, shut the socket now that the message ws sent 204 | if message.serverCommand == constants.COMMAND_END: 205 | self.sock.disconnect() 206 | except exceptions.NetworkError as ne: 207 | self.errorCallback('', errors.ERR_NETWORK_ERROR) 208 | return 209 | finally: 210 | # Mark the operation as done 211 | self.messageQueue.task_done() 212 | -------------------------------------------------------------------------------- /src/network/message.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | 4 | 5 | class Message(object): 6 | def __init__(self, serverCommand=None, clientCommand=None, sourceNick=None, destNick=None, 7 | payload=None, hmac=None, error=None, num=None): 8 | self.serverCommand = str(serverCommand) 9 | self.clientCommand = str(clientCommand) 10 | self.sourceNick = str(sourceNick) 11 | self.destNick = str(destNick) 12 | self.payload = str(payload) 13 | self.hmac = str(hmac) 14 | self.error = str(error) 15 | self.num = str(num) 16 | 17 | 18 | def __str__(self): 19 | return json.dumps({'serverCommand': self.serverCommand, 'clientCommand': self.clientCommand, 20 | 'sourceNick': self.sourceNick, 'destNick': self.destNick, 21 | 'payload': self.payload, 'hmac': self.hmac, 'error': self.error, 'num': self.num}) 22 | 23 | 24 | def getEncryptedPayloadAsBinaryString(self): 25 | return base64.b64decode(self.payload) 26 | 27 | 28 | def setEncryptedPayload(self, payload): 29 | self.payload = str(base64.b64encode(payload)) 30 | 31 | 32 | def getHmacAsBinaryString(self): 33 | return base64.b64decode(self.hmac) 34 | 35 | 36 | def setBinaryHmac(self, hmac): 37 | self.hmac = str(base64.b64encode(hmac)) 38 | 39 | def getMessageNumAsBinaryString(self): 40 | return base64.b64decode(self.num) 41 | 42 | 43 | def setBinaryMessageNum(self, num): 44 | self.num = str(base64.b64encode(num)) 45 | 46 | 47 | @staticmethod 48 | def createFromJSON(jsonStr): 49 | jsonStr = json.loads(jsonStr) 50 | return Message(jsonStr['serverCommand'], jsonStr['clientCommand'], jsonStr['sourceNick'], jsonStr['destNick'], 51 | jsonStr['payload'], jsonStr['hmac'], jsonStr['error'], jsonStr['num']) 52 | -------------------------------------------------------------------------------- /src/network/qtThreads.py: -------------------------------------------------------------------------------- 1 | from client import Client 2 | 3 | from PyQt4.QtCore import QThread 4 | from PyQt4.QtCore import pyqtSignal 5 | 6 | from utils import exceptions 7 | 8 | 9 | class QtServerConnectThread(QThread): 10 | successSignal = pyqtSignal() 11 | failureSignal = pyqtSignal(str) 12 | 13 | def __init__(self, connectionManager, successSlot, failureSlot): 14 | QThread.__init__(self) 15 | 16 | self.connectionManager = connectionManager 17 | self.successSignal.connect(successSlot) 18 | self.failureSignal.connect(failureSlot) 19 | 20 | 21 | def run(self): 22 | try: 23 | self.connectionManager.connectToServer() 24 | self.successSignal.emit() 25 | except exceptions.GenericError as ge: 26 | self.failureSignal.emit(str(ge)) 27 | except exceptions.NetworkError as ne: 28 | self.failureSignal.emit(str(ne)) 29 | -------------------------------------------------------------------------------- /src/network/sock.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | 4 | from utils import errors 5 | from utils import exceptions 6 | 7 | 8 | class Socket(object): 9 | def __init__(self, addr, sock=None): 10 | self.addr = addr 11 | self.sock = sock 12 | 13 | # Create a new socket if one was not given 14 | if sock is None: 15 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 16 | self.isConnected = False 17 | else: 18 | self.sock = sock 19 | self.isConnected = True 20 | 21 | 22 | def __str__(self): 23 | return self.addr[0] + ':' + str(self.addr[1]) 24 | 25 | 26 | def connect(self): 27 | try: 28 | self.sock.connect(self.addr) 29 | self.isConnected = True 30 | except socket.error as se: 31 | raise exceptions.GenericError(str(se)) 32 | 33 | 34 | def disconnect(self): 35 | try: 36 | self.sock.shutdown(socket.SHUT_RDWR) 37 | self.sock.close() 38 | except Exception as e: 39 | pass 40 | finally: 41 | self.isConnected = False 42 | 43 | 44 | def send(self, data): 45 | if type(data) is not str: 46 | raise TypeError() 47 | 48 | dataLength = len(data) 49 | 50 | # Send the length of the message (int converted to network byte order and packed as binary data) 51 | self._send(struct.pack("I", socket.htonl(dataLength)), 4) 52 | 53 | # Send the actual data 54 | self._send(data, dataLength) 55 | 56 | 57 | def _send(self, data, length): 58 | sentLen = 0 59 | while sentLen < length: 60 | try: 61 | amountSent = self.sock.send(data[sentLen:]) 62 | except Exception: 63 | self.isConnected = False 64 | raise exceptions.NetworkError(errors.UNEXPECTED_CLOSE_CONNECTION) 65 | 66 | if amountSent == 0: 67 | self.isConnected = False 68 | raise exceptions.NetworkError(errors.UNEXPECTED_CLOSE_CONNECTION) 69 | 70 | sentLen += amountSent 71 | 72 | 73 | def recv(self): 74 | # Receive the length of the incoming message (unpack the binary data) 75 | dataLength = socket.ntohl(struct.unpack("I", self._recv(4))[0]) 76 | 77 | # Receive the actual data 78 | return self._recv(dataLength) 79 | 80 | 81 | def _recv(self, length): 82 | try: 83 | data = '' 84 | recvLen = 0 85 | while recvLen < length: 86 | newData = self.sock.recv(length-recvLen) 87 | 88 | if newData == '': 89 | self.isConnected = False 90 | raise exceptions.NetworkError(errors.CLOSE_CONNECTION, errno=errors.ERR_CLOSED_CONNECTION) 91 | 92 | data = data + newData 93 | recvLen += len(newData) 94 | 95 | return data 96 | except socket.error as se: 97 | raise exceptions.NetworkError(str(se)) 98 | 99 | 100 | def getHostname(self): 101 | return self.addr[0] 102 | -------------------------------------------------------------------------------- /src/qt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/qt/__init__.py -------------------------------------------------------------------------------- /src/qt/qAcceptDialog.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtGui import QIcon 2 | from PyQt4.QtGui import QMessageBox 3 | from PyQt4.QtGui import QPushButton 4 | 5 | class QAcceptDialog(QMessageBox): 6 | def __init__(self, parent, hostname): 7 | QMessageBox.__init__(self, parent) 8 | 9 | self.accepted = None 10 | 11 | self.setWindowTitle("Accept Connection?") 12 | self.setText("Received connection from " + hostname) 13 | self.setIcon(QMessageBox.Question) 14 | 15 | self.acceptButton = QPushButton(QIcon.fromTheme('dialog-ok'), "Accept") 16 | self.rejectButton = QPushButton(QIcon.fromTheme('dialog-cancel'), "Reject") 17 | self.addButton(self.acceptButton, QMessageBox.YesRole) 18 | self.addButton(self.rejectButton, QMessageBox.NoRole) 19 | 20 | self.buttonClicked.connect(self.gotAnswer) 21 | 22 | 23 | def gotAnswer(self, button): 24 | if button is self.acceptButton: 25 | self.accepted = True 26 | else: 27 | self.accepted = False 28 | 29 | self.close() 30 | 31 | 32 | @staticmethod 33 | def getAnswer(parent, hostname): 34 | acceptDialog = QAcceptDialog(parent, hostname) 35 | acceptDialog.exec_() 36 | return acceptDialog.accepted 37 | -------------------------------------------------------------------------------- /src/qt/qChatTab.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | 5 | from PyQt4.QtCore import Qt 6 | from PyQt4.QtGui import QAction 7 | from PyQt4.QtGui import QHBoxLayout 8 | from PyQt4.QtGui import QMessageBox 9 | from PyQt4.QtGui import QStackedWidget 10 | from PyQt4.QtGui import QWidget 11 | 12 | import qtUtils 13 | from qChatWidget import QChatWidget 14 | from qConnectingWidget import QConnectingWidget 15 | from qHelpDialog import QHelpDialog 16 | from qNickInputWidget import QNickInputWidget 17 | 18 | from utils import errors 19 | 20 | 21 | class QChatTab(QWidget): 22 | def __init__(self, chatWindow, nick): 23 | QWidget.__init__(self) 24 | 25 | self.chatWindow = chatWindow 26 | self.nick = nick 27 | self.unreadCount = 0 28 | 29 | self.widgetStack = QStackedWidget(self) 30 | self.widgetStack.addWidget(QNickInputWidget('new_chat.png', 150, self.connectClicked, parent=self)) 31 | self.widgetStack.addWidget(QConnectingWidget(self)) 32 | self.widgetStack.addWidget(QChatWidget(self.chatWindow.connectionManager, self)) 33 | 34 | # Skip the chat layout if the nick was given denoting an incoming connection 35 | if self.nick is None or self.nick == '': 36 | self.widgetStack.setCurrentIndex(0) 37 | else: 38 | self.widgetStack.setCurrentIndex(2) 39 | 40 | layout = QHBoxLayout() 41 | layout.addWidget(self.widgetStack) 42 | self.setLayout(layout) 43 | 44 | 45 | def connectClicked(self, nick): 46 | # Check that the nick isn't already connected 47 | if self.chatWindow.isNickInTabs(nick): 48 | QMessageBox.warning(self, errors.TITLE_ALREADY_CONNECTED, errors.ALREADY_CONNECTED % (nick)) 49 | return 50 | 51 | self.nick = nick 52 | self.widgetStack.widget(1).setConnectingToNick(self.nick) 53 | self.widgetStack.setCurrentIndex(1) 54 | self.chatWindow.connectionManager.openChat(self.nick) 55 | 56 | 57 | def showNowChattingMessage(self): 58 | self.widgetStack.setCurrentIndex(2) 59 | self.widgetStack.widget(2).showNowChattingMessage(self.nick) 60 | 61 | 62 | def appendMessage(self, message, source): 63 | self.widgetStack.widget(2).appendMessage(message, source) 64 | 65 | 66 | def resetOrDisable(self): 67 | # If the connecting widget is showing, reset to the nick input widget 68 | # If the chat widget is showing, disable it to prevent sending of more messages 69 | curWidgetIndex = self.widgetStack.currentIndex() 70 | if curWidgetIndex == 1: 71 | self.widgetStack.setCurrentIndex(0) 72 | elif curWidgetIndex == 2: 73 | self.widgetStack.widget(2).disable() 74 | 75 | 76 | def enable(self): 77 | self.widgetStack.setCurrentIndex(2) 78 | self.widgetStack.widget(2).enable() 79 | -------------------------------------------------------------------------------- /src/qt/qChatWidget.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from PyQt4.QtCore import Qt 4 | from PyQt4.QtCore import QTimer 5 | from PyQt4.QtGui import QFontMetrics 6 | from PyQt4.QtGui import QHBoxLayout 7 | from PyQt4.QtGui import QLabel 8 | from PyQt4.QtGui import QPushButton 9 | from PyQt4.QtGui import QSplitter 10 | from PyQt4.QtGui import QTextBrowser 11 | from PyQt4.QtGui import QTextEdit 12 | from PyQt4.QtGui import QWidget 13 | 14 | import qtUtils 15 | 16 | from utils import constants 17 | from utils import utils 18 | 19 | 20 | class QChatWidget(QWidget): 21 | def __init__(self, connectionManager, parent=None): 22 | QWidget.__init__(self, parent) 23 | 24 | self.connectionManager = connectionManager 25 | self.isDisabled = False 26 | self.wasCleared = False 27 | 28 | self.urlRegex = re.compile(constants.URL_REGEX) 29 | 30 | self.chatLog = QTextBrowser() 31 | self.chatLog.setOpenExternalLinks(True) 32 | 33 | self.chatInput = QTextEdit() 34 | self.chatInput.textChanged.connect(self.chatInputTextChanged) 35 | 36 | self.sendButton = QPushButton("Send") 37 | self.sendButton.clicked.connect(self.sendMessage) 38 | 39 | # Set the min height for the chatlog and a matching fixed height for the send button 40 | chatInputFontMetrics = QFontMetrics(self.chatInput.font()) 41 | self.chatInput.setMinimumHeight(chatInputFontMetrics.lineSpacing() * 3) 42 | self.sendButton.setFixedHeight(chatInputFontMetrics.lineSpacing() * 3) 43 | 44 | hbox = QHBoxLayout() 45 | hbox.addWidget(self.chatInput) 46 | hbox.addWidget(self.sendButton) 47 | 48 | # Put the chatinput and send button in a wrapper widget so they may be added to the splitter 49 | chatInputWrapper = QWidget() 50 | chatInputWrapper.setLayout(hbox) 51 | chatInputWrapper.setMinimumHeight(chatInputFontMetrics.lineSpacing() * 3.7) 52 | 53 | # Put the chat log and chat input into a splitter so the user can resize them at will 54 | splitter = QSplitter(Qt.Vertical) 55 | splitter.addWidget(self.chatLog) 56 | splitter.addWidget(chatInputWrapper) 57 | splitter.setSizes([int(parent.height()), 1]) 58 | 59 | hbox = QHBoxLayout() 60 | hbox.addWidget(splitter) 61 | self.setLayout(hbox) 62 | 63 | self.typingTimer = QTimer() 64 | self.typingTimer.setSingleShot(True) 65 | self.typingTimer.timeout.connect(self.stoppedTyping) 66 | 67 | 68 | def chatInputTextChanged(self): 69 | # Check if the text changed was the text box being cleared to avoid sending an invalid typing status 70 | if self.wasCleared: 71 | self.wasCleared = False 72 | return 73 | 74 | if str(self.chatInput.toPlainText())[-1:] == '\n': 75 | self.sendMessage() 76 | else: 77 | # Start a timer to check for the user stopping typing 78 | self.typingTimer.start(constants.TYPING_TIMEOUT) 79 | self.sendTypingStatus(constants.TYPING_START) 80 | 81 | 82 | def stoppedTyping(self): 83 | self.typingTimer.stop() 84 | if str(self.chatInput.toPlainText()) == '': 85 | self.sendTypingStatus(constants.TYPING_STOP_WITHOUT_TEXT) 86 | else: 87 | self.sendTypingStatus(constants.TYPING_STOP_WITH_TEXT) 88 | 89 | 90 | def sendMessage(self): 91 | if self.isDisabled: 92 | return 93 | 94 | self.typingTimer.stop() 95 | 96 | text = str(self.chatInput.toPlainText())[:-1] 97 | 98 | # Don't send empty messages 99 | if text == '': 100 | return 101 | 102 | # Convert URLs into clickable links 103 | text = self.__linkify(text) 104 | 105 | # Add the message to the message queue to be sent 106 | self.connectionManager.getClient(self.nick).sendChatMessage(text) 107 | 108 | # Clear the chat input 109 | self.wasCleared = True 110 | self.chatInput.clear() 111 | 112 | self.appendMessage(text, constants.SENDER) 113 | 114 | 115 | def sendTypingStatus(self, status): 116 | self.connectionManager.getClient(self.nick).sendTypingMessage(status) 117 | 118 | 119 | def showNowChattingMessage(self, nick): 120 | self.nick = nick 121 | self.appendMessage("You are now securely chatting with " + self.nick + " :)", 122 | constants.SERVICE, showTimestampAndNick=False) 123 | 124 | self.appendMessage("It's a good idea to verify the communcation is secure by selecting " 125 | "\"authenticate buddy\" in the options menu.", constants.SERVICE, showTimestampAndNick=False) 126 | 127 | 128 | def appendMessage(self, message, source, showTimestampAndNick=True): 129 | color = self.__getColor(source) 130 | 131 | if showTimestampAndNick: 132 | timestamp = '(' + utils.getTimestamp() + ') ' + \ 133 | (self.connectionManager.nick if source == constants.SENDER else self.nick) + \ 134 | ': ' 135 | else: 136 | timestamp = '' 137 | 138 | # If the user has scrolled up (current value != maximum), do not move the scrollbar 139 | # to the bottom after appending the message 140 | shouldScroll = True 141 | scrollbar = self.chatLog.verticalScrollBar() 142 | if scrollbar.value() != scrollbar.maximum() and source != constants.SENDER: 143 | shouldScroll = False 144 | 145 | self.chatLog.append(timestamp + message) 146 | 147 | # Move the vertical scrollbar to the bottom of the chat log 148 | if shouldScroll: 149 | scrollbar.setValue(scrollbar.maximum()) 150 | 151 | 152 | def __linkify(self, text): 153 | matches = self.urlRegex.findall(text) 154 | 155 | for match in matches: 156 | text = text.replace(match[0], '%s' % (match[0], match[0])) 157 | 158 | return text 159 | 160 | 161 | def __getColor(self, source): 162 | if source == constants.SENDER: 163 | if qtUtils.isLightTheme: 164 | return '#0000CC' 165 | else: 166 | return '#6666FF' 167 | elif source == constants.RECEIVER: 168 | if qtUtils.isLightTheme: 169 | return '#CC0000' 170 | else: 171 | return '#CC3333' 172 | else: 173 | if qtUtils.isLightTheme: 174 | return '#000000' 175 | else: 176 | return '#FFFFFF' 177 | 178 | 179 | def disable(self): 180 | self.isDisabled = True 181 | self.chatInput.setReadOnly(True) 182 | 183 | 184 | def enable(self): 185 | self.isDisabled = False 186 | self.chatInput.setReadOnly(False) 187 | -------------------------------------------------------------------------------- /src/qt/qChatWindow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | 5 | from PyQt4.QtCore import pyqtSignal 6 | from PyQt4.QtCore import pyqtSlot 7 | from PyQt4.QtCore import Qt 8 | from PyQt4.QtGui import QFontMetrics 9 | from PyQt4.QtGui import QAction 10 | from PyQt4.QtGui import QIcon 11 | from PyQt4.QtGui import QInputDialog 12 | from PyQt4.QtGui import QLabel 13 | from PyQt4.QtGui import QMainWindow 14 | from PyQt4.QtGui import QMenu 15 | from PyQt4.QtGui import QMessageBox 16 | from PyQt4.QtGui import QPushButton 17 | from PyQt4.QtGui import QSplitter 18 | from PyQt4.QtGui import QSystemTrayIcon 19 | from PyQt4.QtGui import QTabWidget 20 | from PyQt4.QtGui import QTextEdit 21 | from PyQt4.QtGui import QToolBar 22 | from PyQt4.QtGui import QToolButton 23 | from PyQt4.QtGui import QVBoxLayout 24 | from PyQt4.QtGui import QWidget 25 | 26 | from qChatTab import QChatTab 27 | from qAcceptDialog import QAcceptDialog 28 | from qHelpDialog import QHelpDialog 29 | from qSMPInitiateDialog import QSMPInitiateDialog 30 | from qSMPRespondDialog import QSMPRespondDialog 31 | import qtUtils 32 | 33 | from utils import constants 34 | from utils import errors 35 | from utils import utils 36 | 37 | 38 | class QChatWindow(QMainWindow): 39 | newClientSignal = pyqtSignal(str) 40 | clientReadySignal = pyqtSignal(str) 41 | smpRequestSignal = pyqtSignal(int, str, str, int) 42 | handleErrorSignal = pyqtSignal(str, int) 43 | sendMessageToTabSignal = pyqtSignal(str, str, str) 44 | 45 | def __init__(self, restartCallback, connectionManager=None, messageQueue=None): 46 | QMainWindow.__init__(self) 47 | 48 | self.restartCallback = restartCallback 49 | self.connectionManager = connectionManager 50 | self.messageQueue = messageQueue 51 | self.newClientSignal.connect(self.newClientSlot) 52 | self.clientReadySignal.connect(self.clientReadySlot) 53 | self.smpRequestSignal.connect(self.smpRequestSlot) 54 | self.handleErrorSignal.connect(self.handleErrorSlot) 55 | self.sendMessageToTabSignal.connect(self.sendMessageToTab) 56 | 57 | self.chatTabs = QTabWidget(self) 58 | self.chatTabs.setTabsClosable(True) 59 | self.chatTabs.setMovable(True) 60 | self.chatTabs.tabCloseRequested.connect(self.closeTab) 61 | self.chatTabs.currentChanged.connect(self.tabChanged) 62 | 63 | self.statusBar = self.statusBar() 64 | self.systemTrayIcon = QSystemTrayIcon(self) 65 | self.systemTrayIcon.setVisible(True) 66 | 67 | self.__setMenubar() 68 | 69 | vbox = QVBoxLayout() 70 | vbox.addWidget(self.chatTabs) 71 | 72 | # Add the completeted layout to the window 73 | self.centralWidget = QWidget() 74 | self.centralWidget.setLayout(vbox) 75 | self.setCentralWidget(self.centralWidget) 76 | 77 | qtUtils.resizeWindow(self, 700, 400) 78 | qtUtils.centerWindow(self) 79 | 80 | # Title and icon 81 | self.setWindowTitle("Cryptully") 82 | self.setWindowIcon(QIcon(qtUtils.getAbsoluteImagePath('icon.png'))) 83 | 84 | 85 | def connectedToServer(self): 86 | # Add an initial tab once connected to the server 87 | self.addNewTab() 88 | 89 | 90 | def newClient(self, nick): 91 | # This function is called from a bg thread. Send a signal to get on the UI thread 92 | self.newClientSignal.emit(nick) 93 | 94 | 95 | @pyqtSlot(str) 96 | def newClientSlot(self, nick): 97 | nick = str(nick) 98 | 99 | # Show a system notifcation of the new client if not the current window 100 | if not self.isActiveWindow(): 101 | qtUtils.showDesktopNotification(self.systemTrayIcon, "Chat request from %s" % nick, '') 102 | 103 | # Show the accept dialog 104 | accept = QAcceptDialog.getAnswer(self, nick) 105 | 106 | if not accept: 107 | self.connectionManager.newClientRejected(nick) 108 | return 109 | 110 | # If nick already has a tab, reuse it 111 | if self.isNickInTabs(nick): 112 | self.getTabByNick(nick)[0].enable() 113 | else: 114 | self.addNewTab(nick) 115 | 116 | self.connectionManager.newClientAccepted(nick) 117 | 118 | 119 | def addNewTab(self, nick=None): 120 | newTab = QChatTab(self, nick) 121 | self.chatTabs.addTab(newTab, nick if nick is not None else "New Chat") 122 | self.chatTabs.setCurrentWidget(newTab) 123 | newTab.setFocus() 124 | 125 | 126 | def clientReady(self, nick): 127 | # Use a signal to call the client ready slot on the UI thread since 128 | # this function is called from a background thread 129 | self.clientReadySignal.emit(nick) 130 | 131 | 132 | @pyqtSlot(str) 133 | def clientReadySlot(self, nick): 134 | nick = str(nick) 135 | tab, tabIndex = self.getTabByNick(nick) 136 | self.chatTabs.setTabText(tabIndex, nick) 137 | tab.showNowChattingMessage() 138 | 139 | # Set the window title if the tab is the selected tab 140 | if tabIndex == self.chatTabs.currentIndex(): 141 | self.setWindowTitle(nick) 142 | 143 | 144 | def smpRequest(self, type, nick, question='', errno=0): 145 | self.smpRequestSignal.emit(type, nick, question, errno) 146 | 147 | 148 | @pyqtSlot(int, str, str, int) 149 | def smpRequestSlot(self, type, nick, question='', errno=0): 150 | if type == constants.SMP_CALLBACK_REQUEST: 151 | answer, clickedButton = QSMPRespondDialog.getAnswer(nick, question) 152 | 153 | if clickedButton == constants.BUTTON_OKAY: 154 | self.connectionManager.respondSMP(str(nick), str(answer)) 155 | elif type == constants.SMP_CALLBACK_COMPLETE: 156 | QMessageBox.information(self, "%s Authenticated" % nick, 157 | "Your chat session with %s has been succesfully authenticated. The conversation is verfied as secure." % nick) 158 | elif type == constants.SMP_CALLBACK_ERROR: 159 | if errno == errors.ERR_SMP_CHECK_FAILED: 160 | QMessageBox.warning(self, errors.TITLE_PROTOCOL_ERROR, errors.PROTOCOL_ERROR % (nick)) 161 | elif errno == errors.ERR_SMP_MATCH_FAILED: 162 | QMessageBox.critical(self, errors.TITLE_SMP_MATCH_FAILED, errors.SMP_MATCH_FAILED) 163 | 164 | 165 | def handleError(self, nick, errno): 166 | self.handleErrorSignal.emit(nick, errno) 167 | 168 | 169 | @pyqtSlot(str, int) 170 | def handleErrorSlot(self, nick, errno): 171 | # If no nick was given, disable all tabs 172 | nick = str(nick) 173 | if nick == '': 174 | self.__disableAllTabs() 175 | else: 176 | try: 177 | tab = self.getTabByNick(nick)[0] 178 | tab.resetOrDisable() 179 | except: 180 | self.__disableAllTabs() 181 | 182 | if errno == errors.ERR_CONNECTION_ENDED: 183 | QMessageBox.warning(self, errors.TITLE_CONNECTION_ENDED, errors.CONNECTION_ENDED % (nick)) 184 | elif errno == errors.ERR_NICK_NOT_FOUND: 185 | QMessageBox.information(self, errors.TITLE_NICK_NOT_FOUND, errors.NICK_NOT_FOUND % (nick)) 186 | tab.nick = None 187 | elif errno == errors.ERR_CONNECTION_REJECTED: 188 | QMessageBox.warning(self, errors.TITLE_CONNECTION_REJECTED, errors.CONNECTION_REJECTED % (nick)) 189 | tab.nick = None 190 | elif errno == errors.ERR_BAD_HANDSHAKE: 191 | QMessageBox.warning(self, errors.TITLE_PROTOCOL_ERROR, errors.PROTOCOL_ERROR % (nick)) 192 | elif errno == errors.ERR_CLIENT_EXISTS: 193 | QMessageBox.information(self, errors.TITLE_CLIENT_EXISTS, errors.CLIENT_EXISTS % (nick)) 194 | elif errno == errors.ERR_SELF_CONNECT: 195 | QMessageBox.warning(self, errors.TITLE_SELF_CONNECT, errors.SELF_CONNECT) 196 | elif errno == errors.ERR_SERVER_SHUTDOWN: 197 | QMessageBox.critical(self, errors.TITLE_SERVER_SHUTDOWN, errors.SERVER_SHUTDOWN) 198 | elif errno == errors.ERR_ALREADY_CONNECTED: 199 | QMessageBox.information(self, errors.TITLE_ALREADY_CONNECTED, errors.ALREADY_CONNECTED % (nick)) 200 | elif errno == errors.ERR_INVALID_COMMAND: 201 | QMessageBox.warning(self, errors.TITLE_INVALID_COMMAND, errors.INVALID_COMMAND % (nick)) 202 | elif errno == errors.ERR_NETWORK_ERROR: 203 | QMessageBox.critical(self, errors.TITLE_NETWORK_ERROR, errors.NETWORK_ERROR) 204 | elif errno == errors.ERR_BAD_HMAC: 205 | QMessageBox.critical(self, errors.TITLE_BAD_HMAC, errors.BAD_HMAC) 206 | elif errno == errors.ERR_BAD_DECRYPT: 207 | QMessageBox.warning(self, errors.TITLE_BAD_DECRYPT, errors.BAD_DECRYPT) 208 | elif errno == errors.ERR_KICKED: 209 | QMessageBox.critical(self, errors.TITLE_KICKED, errors.KICKED) 210 | elif errno == errors.ERR_NICK_IN_USE: 211 | QMessageBox.warning(self, errors.TITLE_NICK_IN_USE, errors.NICK_IN_USE) 212 | self.restartCallback() 213 | elif errno == errors.ERR_MESSAGE_REPLAY: 214 | QMessageBox.critical(self, errors.TITLE_MESSAGE_REPLAY, errors.MESSAGE_REPLAY) 215 | elif errno == errors.ERR_MESSAGE_DELETION: 216 | QMessageBox.critical(self, errors.TITLE_MESSAGE_DELETION, errors.MESSAGE_DELETION) 217 | elif errno == errors.ERR_PROTOCOL_VERSION_MISMATCH: 218 | QMessageBox.critical(self, errors.TITLE_PROTOCOL_VERSION_MISMATCH, errors.PROTOCOL_VERSION_MISMATCH) 219 | self.restartCallback() 220 | else: 221 | QMessageBox.warning(self, errors.TITLE_UNKNOWN_ERROR, errors.UNKNOWN_ERROR % (nick)) 222 | 223 | 224 | def __disableAllTabs(self): 225 | for i in range(0, self.chatTabs.count()): 226 | curTab = self.chatTabs.widget(i) 227 | curTab.resetOrDisable() 228 | 229 | 230 | def postMessage(self, command, sourceNick, payload): 231 | self.sendMessageToTabSignal.emit(command, sourceNick, payload) 232 | 233 | 234 | @pyqtSlot(str, str, str) 235 | def sendMessageToTab(self, command, sourceNick, payload): 236 | # If a typing command, update the typing status in the tab, otherwise 237 | # show the message in the tab 238 | tab, tabIndex = self.getTabByNick(sourceNick) 239 | if command == constants.COMMAND_TYPING: 240 | # Show the typing status in the status bar if the tab is the selected tab 241 | if tabIndex == self.chatTabs.currentIndex(): 242 | payload = int(payload) 243 | if payload == constants.TYPING_START: 244 | self.statusBar.showMessage("%s is typing" % sourceNick) 245 | elif payload == constants.TYPING_STOP_WITHOUT_TEXT: 246 | self.statusBar.showMessage('') 247 | elif payload == constants.TYPING_STOP_WITH_TEXT: 248 | self.statusBar.showMessage("%s has entered text" % sourceNick) 249 | elif command == constants.COMMAND_SMP_0: 250 | print('got request for smp in tab %d' % (tabIndex)) 251 | else: 252 | tab.appendMessage(payload, constants.RECEIVER) 253 | 254 | # Update the unread message count if the message is not intended for the currently selected tab 255 | if tabIndex != self.chatTabs.currentIndex(): 256 | tab.unreadCount += 1 257 | self.chatTabs.setTabText(tabIndex, "%s (%d)" % (tab.nick, tab.unreadCount)) 258 | else: 259 | # Clear the typing status if the current tab 260 | self.statusBar.showMessage('') 261 | 262 | # Show a system notifcation of the new message if not the current window or tab or the 263 | # scrollbar of the tab isn't at the bottom 264 | chatLogScrollbar = tab.widgetStack.widget(2).chatLog.verticalScrollBar() 265 | if not self.isActiveWindow() or tabIndex != self.chatTabs.currentIndex() or \ 266 | chatLogScrollbar.value() != chatLogScrollbar.maximum(): 267 | qtUtils.showDesktopNotification(self.systemTrayIcon, sourceNick, payload) 268 | 269 | 270 | @pyqtSlot(int) 271 | def tabChanged(self, index): 272 | # Reset the unread count for the tab when it's switched to 273 | tab = self.chatTabs.widget(index) 274 | 275 | # Change the window title to the nick 276 | if tab is None or tab.nick is None: 277 | self.setWindowTitle("Cryptully") 278 | else: 279 | self.setWindowTitle(tab.nick) 280 | 281 | if tab is not None and tab.unreadCount != 0: 282 | tab.unreadCount = 0 283 | self.chatTabs.setTabText(index, tab.nick) 284 | 285 | 286 | @pyqtSlot(int) 287 | def closeTab(self, index): 288 | tab = self.chatTabs.widget(index) 289 | self.connectionManager.closeChat(tab.nick) 290 | 291 | self.chatTabs.removeTab(index) 292 | 293 | # Show a new tab if there are now no tabs left 294 | if self.chatTabs.count() == 0: 295 | self.addNewTab() 296 | 297 | 298 | def getTabByNick(self, nick): 299 | for i in range(0, self.chatTabs.count()): 300 | curTab = self.chatTabs.widget(i) 301 | if curTab.nick == nick: 302 | return (curTab, i) 303 | return None 304 | 305 | 306 | def isNickInTabs(self, nick): 307 | for i in range(0, self.chatTabs.count()): 308 | curTab = self.chatTabs.widget(i) 309 | if curTab.nick == nick: 310 | return True 311 | return False 312 | 313 | 314 | def __setMenubar(self): 315 | newChatIcon = QIcon(qtUtils.getAbsoluteImagePath('new_chat.png')) 316 | helpIcon = QIcon(qtUtils.getAbsoluteImagePath('help.png')) 317 | exitIcon = QIcon(qtUtils.getAbsoluteImagePath('exit.png')) 318 | menuIcon = QIcon(qtUtils.getAbsoluteImagePath('menu.png')) 319 | 320 | newChatAction = QAction(newChatIcon, '&New chat', self) 321 | authChatAction = QAction(newChatIcon, '&Authenticate chat', self) 322 | helpAction = QAction(helpIcon, 'Show &help', self) 323 | exitAction = QAction(exitIcon, '&Exit', self) 324 | 325 | newChatAction.triggered.connect(lambda: self.addNewTab()) 326 | authChatAction.triggered.connect(self.__showAuthDialog) 327 | helpAction.triggered.connect(self.__showHelpDialog) 328 | exitAction.triggered.connect(self.__exit) 329 | 330 | newChatAction.setShortcut('Ctrl+N') 331 | helpAction.setShortcut('Ctrl+H') 332 | exitAction.setShortcut('Ctrl+Q') 333 | 334 | optionsMenu = QMenu() 335 | 336 | optionsMenu.addAction(newChatAction) 337 | optionsMenu.addAction(authChatAction) 338 | optionsMenu.addAction(helpAction) 339 | optionsMenu.addAction(exitAction) 340 | 341 | optionsMenuButton = QToolButton() 342 | newChatButton = QToolButton() 343 | exitButton = QToolButton() 344 | 345 | newChatButton.clicked.connect(lambda: self.addNewTab()) 346 | exitButton.clicked.connect(self.__exit) 347 | 348 | optionsMenuButton.setIcon(menuIcon) 349 | newChatButton.setIcon(newChatIcon) 350 | exitButton.setIcon(exitIcon) 351 | 352 | optionsMenuButton.setMenu(optionsMenu) 353 | optionsMenuButton.setPopupMode(QToolButton.InstantPopup) 354 | 355 | toolbar = QToolBar(self) 356 | toolbar.addWidget(optionsMenuButton) 357 | toolbar.addWidget(newChatButton) 358 | toolbar.addWidget(exitButton) 359 | self.addToolBar(Qt.LeftToolBarArea, toolbar) 360 | 361 | 362 | def __showAuthDialog(self): 363 | client = self.connectionManager.getClient(self.chatTabs.currentWidget().nick) 364 | 365 | if client is None: 366 | QMessageBox.information(self, "Not Available", "You must be chatting with someone before you can authenticate the connection.") 367 | return 368 | 369 | try: 370 | question, answer, clickedButton = QSMPInitiateDialog.getQuestionAndAnswer() 371 | except AttributeError: 372 | QMessageBox.information(self, "Not Available", "Encryption keys are not available until you are chatting with someone") 373 | 374 | if clickedButton == constants.BUTTON_OKAY: 375 | client.initiateSMP(str(question), str(answer)) 376 | 377 | 378 | def __showHelpDialog(self): 379 | QHelpDialog(self).show() 380 | 381 | 382 | def __exit(self): 383 | if QMessageBox.Yes == QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No): 384 | qtUtils.exitApp() 385 | -------------------------------------------------------------------------------- /src/qt/qConnectingWidget.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtCore import Qt 2 | from PyQt4.QtGui import QHBoxLayout 3 | from PyQt4.QtGui import QLabel 4 | from PyQt4.QtGui import QMovie 5 | from PyQt4.QtGui import QWidget 6 | 7 | import qtUtils 8 | 9 | 10 | class QConnectingWidget(QWidget): 11 | def __init__(self, parent=None): 12 | QWidget.__init__(self, parent) 13 | 14 | # Create connecting image 15 | self.connectingGif = QMovie(qtUtils.getAbsoluteImagePath('waiting.gif')) 16 | self.connectingGif.start() 17 | self.connetingImageLabel = QLabel(self) 18 | self.connetingImageLabel.setMovie(self.connectingGif) 19 | self.connectingLabel = QLabel(self) 20 | 21 | hbox = QHBoxLayout() 22 | hbox.addStretch(1) 23 | hbox.addWidget(self.connetingImageLabel, alignment=Qt.AlignCenter) 24 | hbox.addSpacing(10) 25 | hbox.addWidget(self.connectingLabel, alignment=Qt.AlignCenter) 26 | hbox.addStretch(1) 27 | 28 | self.setLayout(hbox) 29 | 30 | 31 | def setConnectingToNick(self, nick): 32 | self.connectingLabel.setText("Connecting to " + nick + "...") 33 | -------------------------------------------------------------------------------- /src/qt/qHelpDialog.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtGui import QMessageBox 2 | from PyQt4.QtGui import QIcon 3 | from PyQt4.QtGui import QLabel 4 | from PyQt4.QtGui import QVBoxLayout 5 | 6 | from qLinkLabel import QLinkLabel 7 | 8 | class QHelpDialog(QMessageBox): 9 | def __init__(self, parent=None): 10 | QMessageBox.__init__(self, parent) 11 | 12 | self.setWindowTitle("Help") 13 | 14 | helpText = QLabel("Questions? There's a whole bunch of info on the documentation page.", self) 15 | helpLink = QLinkLabel("Read the docs.", "https://cryptully.readthedocs.org/en/latest/", self) 16 | 17 | self.setIcon(QMessageBox.Question) 18 | self.setStandardButtons(QMessageBox.Ok) 19 | 20 | vbox = QVBoxLayout() 21 | vbox.addStretch(1) 22 | vbox.addWidget(helpText) 23 | vbox.addWidget(helpLink) 24 | vbox.addStretch(1) 25 | 26 | # Replace the default label with our own custom layout 27 | layout = self.layout() 28 | layout.addLayout(vbox, 0, 1) 29 | -------------------------------------------------------------------------------- /src/qt/qLine.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtGui import QFrame 2 | 3 | class QLine(QFrame): 4 | def __init__(self, parent=None): 5 | QFrame.__init__(self, parent) 6 | 7 | self.setFrameShape(QFrame.HLine) 8 | self.setFrameShadow(QFrame.Sunken) 9 | -------------------------------------------------------------------------------- /src/qt/qLinkLabel.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtCore import Qt 2 | from PyQt4.QtGui import QLabel 3 | 4 | class QLinkLabel(QLabel): 5 | def __init__(self, text, link, parent=None): 6 | QLabel.__init__(self, parent) 7 | 8 | self.setText("" + text + "") 9 | self.setTextFormat(Qt.RichText) 10 | self.setTextInteractionFlags(Qt.TextBrowserInteraction) 11 | self.setOpenExternalLinks(True) 12 | -------------------------------------------------------------------------------- /src/qt/qLoginWindow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | from PyQt4.QtCore import Qt 5 | from PyQt4.QtGui import QDialog 6 | from PyQt4.QtGui import QHBoxLayout 7 | from PyQt4.QtGui import QIcon 8 | from PyQt4.QtGui import QLabel 9 | from PyQt4.QtGui import QLineEdit 10 | from PyQt4.QtGui import QPushButton 11 | from PyQt4.QtGui import QVBoxLayout 12 | 13 | from qNickInputWidget import QNickInputWidget 14 | from qLinkLabel import QLinkLabel 15 | import qtUtils 16 | 17 | from utils import constants 18 | from utils import utils 19 | 20 | class QLoginWindow(QDialog): 21 | def __init__(self, parent, nick=""): 22 | QDialog.__init__(self, parent) 23 | self.nick = None 24 | 25 | # Set the title and icon 26 | self.setWindowTitle("Cryptully") 27 | self.setWindowIcon(QIcon(qtUtils.getAbsoluteImagePath('icon.png'))) 28 | 29 | helpLink = QLinkLabel("Confused? Read the docs.", "https://cryptully.readthedocs.org/en/latest/", self) 30 | 31 | vbox = QVBoxLayout() 32 | vbox.addStretch(1) 33 | vbox.addWidget(QNickInputWidget('splash_icon.png', 200, self.connectClicked, nick, self)) 34 | vbox.addStretch(1) 35 | vbox.addWidget(helpLink, alignment=Qt.AlignRight) 36 | 37 | self.setLayout(vbox) 38 | 39 | qtUtils.resizeWindow(self, 500, 200) 40 | qtUtils.centerWindow(self) 41 | 42 | 43 | def connectClicked(self, nick): 44 | self.nick = nick 45 | self.close() 46 | 47 | 48 | @staticmethod 49 | def getNick(parent, nick=""): 50 | if nick is None: 51 | nick = "" 52 | 53 | loginWindow = QLoginWindow(parent, nick) 54 | loginWindow.exec_() 55 | return loginWindow.nick 56 | -------------------------------------------------------------------------------- /src/qt/qNickInputWidget.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtCore import Qt 2 | from PyQt4.QtGui import QHBoxLayout 3 | from PyQt4.QtGui import QLabel 4 | from PyQt4.QtGui import QLineEdit 5 | from PyQt4.QtGui import QMessageBox 6 | from PyQt4.QtGui import QPixmap 7 | from PyQt4.QtGui import QPushButton 8 | from PyQt4.QtGui import QVBoxLayout 9 | from PyQt4.QtGui import QWidget 10 | 11 | import qtUtils 12 | 13 | from utils import constants 14 | from utils import errors 15 | from utils import utils 16 | 17 | 18 | class QNickInputWidget(QWidget): 19 | def __init__(self, image, imageWidth, connectClickedSlot, nick='', parent=None): 20 | QWidget.__init__(self, parent) 21 | 22 | self.connectClickedSlot = connectClickedSlot 23 | 24 | # Image 25 | self.image = QLabel(self) 26 | self.image.setPixmap(QPixmap(qtUtils.getAbsoluteImagePath(image)).scaledToWidth(imageWidth, Qt.SmoothTransformation)) 27 | 28 | # Nick field 29 | self.nickLabel = QLabel("Nickname:", self) 30 | self.nickEdit = QLineEdit(nick, self) 31 | self.nickEdit.setMaxLength(constants.NICK_MAX_LEN) 32 | self.nickEdit.returnPressed.connect(self.__connectClicked) 33 | self.nickEdit.setFocus() 34 | 35 | # Connect button 36 | self.connectButton = QPushButton("Connect", self) 37 | self.connectButton.resize(self.connectButton.sizeHint()) 38 | self.connectButton.setAutoDefault(False) 39 | self.connectButton.clicked.connect(self.__connectClicked) 40 | 41 | hbox = QHBoxLayout() 42 | hbox.addStretch(1) 43 | hbox.addWidget(self.nickLabel) 44 | hbox.addWidget(self.nickEdit) 45 | hbox.addStretch(1) 46 | 47 | vbox = QVBoxLayout() 48 | vbox.addStretch(1) 49 | vbox.addLayout(hbox) 50 | vbox.addWidget(self.connectButton) 51 | vbox.addStretch(1) 52 | 53 | hbox = QHBoxLayout() 54 | hbox.addStretch(1) 55 | hbox.addWidget(self.image) 56 | hbox.addSpacing(10) 57 | hbox.addLayout(vbox) 58 | hbox.addStretch(1) 59 | 60 | self.setLayout(hbox) 61 | 62 | 63 | def __connectClicked(self): 64 | nick = str(self.nickEdit.text()).lower() 65 | 66 | # Validate the given nick 67 | nickStatus = utils.isValidNick(nick) 68 | if nickStatus == errors.VALID_NICK: 69 | self.connectClickedSlot(nick) 70 | elif nickStatus == errors.INVALID_NICK_CONTENT: 71 | QMessageBox.warning(self, errors.TITLE_INVALID_NICK, errors.INVALID_NICK_CONTENT) 72 | elif nickStatus == errors.INVALID_NICK_LENGTH: 73 | QMessageBox.warning(self, errors.TITLE_INVALID_NICK, errors.INVALID_NICK_LENGTH) 74 | elif nickStatus == errors.INVALID_EMPTY_NICK: 75 | QMessageBox.warning(self, errors.TITLE_EMPTY_NICK, errors.EMPTY_NICK) 76 | -------------------------------------------------------------------------------- /src/qt/qPassphraseDialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | from PyQt4.QtGui import QDialog 5 | from PyQt4.QtGui import QHBoxLayout 6 | from PyQt4.QtGui import QIcon 7 | from PyQt4.QtGui import QLabel 8 | from PyQt4.QtGui import QLineEdit 9 | from PyQt4.QtGui import QPushButton 10 | from PyQt4.QtGui import QVBoxLayout 11 | 12 | import qtUtils 13 | 14 | from utils import constants 15 | 16 | class QPassphraseDialog(QDialog): 17 | def __init__(self, verify=False, showForgotButton=True): 18 | QDialog.__init__(self) 19 | 20 | self.passphrase = None 21 | self.clickedButton = constants.BUTTON_CANCEL 22 | 23 | # Set the title and icon 24 | self.setWindowTitle("Save Keys Passphrase") 25 | self.setWindowIcon(QIcon(qtUtils.getAbsoluteImagePath('icon.png'))) 26 | 27 | label = QLabel("Encryption keys passphrase:" if not verify else "Confirm passphrase:", self) 28 | self.passphraseInput = QLineEdit(self) 29 | self.passphraseInput.setEchoMode(QLineEdit.Password) 30 | okayButton = QPushButton(QIcon.fromTheme('dialog-ok'), "OK", self) 31 | cancelButton = QPushButton(QIcon.fromTheme('dialog-cancel'), "Cancel", self) 32 | 33 | if showForgotButton: 34 | forgotButton = QPushButton(QIcon.fromTheme('edit-undo'), "Forgot Passphrase", self) 35 | 36 | okayButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_OKAY)) 37 | cancelButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_CANCEL)) 38 | 39 | if showForgotButton: 40 | forgotButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_FORGOT)) 41 | 42 | # Float the buttons to the right 43 | hbox = QHBoxLayout() 44 | hbox.addStretch(1) 45 | hbox.addWidget(okayButton) 46 | hbox.addWidget(cancelButton) 47 | 48 | if showForgotButton: 49 | hbox.addWidget(forgotButton) 50 | 51 | vbox = QVBoxLayout() 52 | vbox.addStretch(1) 53 | vbox.addWidget(label) 54 | vbox.addWidget(self.passphraseInput) 55 | vbox.addLayout(hbox) 56 | vbox.addStretch(1) 57 | 58 | self.setLayout(vbox) 59 | 60 | 61 | def buttonClicked(self, button): 62 | self.passphrase = self.passphraseInput.text() 63 | self.clickedButton = button 64 | self.close() 65 | 66 | 67 | @staticmethod 68 | def getPassphrase(verify=False, showForgotButton=True): 69 | passphraseDialog = QPassphraseDialog(verify, showForgotButton) 70 | passphraseDialog.exec_() 71 | return passphraseDialog.passphrase, passphraseDialog.clickedButton 72 | -------------------------------------------------------------------------------- /src/qt/qSMPInitiateDialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | from PyQt4.QtCore import Qt 5 | from PyQt4.QtGui import QDialog 6 | from PyQt4.QtGui import QHBoxLayout 7 | from PyQt4.QtGui import QIcon 8 | from PyQt4.QtGui import QLabel 9 | from PyQt4.QtGui import QLineEdit 10 | from PyQt4.QtGui import QPixmap 11 | from PyQt4.QtGui import QPushButton 12 | from PyQt4.QtGui import QVBoxLayout 13 | 14 | from qLine import QLine 15 | import qtUtils 16 | 17 | from utils import constants 18 | 19 | class QSMPInitiateDialog(QDialog): 20 | def __init__(self, parent=None): 21 | QDialog.__init__(self, parent) 22 | self.clickedButton = constants.BUTTON_CANCEL 23 | 24 | # Set the title and icon 25 | self.setWindowTitle("Authenticate Buddy") 26 | self.setWindowIcon(QIcon(qtUtils.getAbsoluteImagePath('icon.png'))) 27 | 28 | smpQuestionLabel = QLabel("Question:", self) 29 | self.smpQuestionInput = QLineEdit(self) 30 | 31 | smpAnswerLabel = QLabel("Answer (case sensitive):", self) 32 | self.smpAnswerInput = QLineEdit(self) 33 | 34 | okayButton = QPushButton(QIcon.fromTheme('dialog-ok'), "OK", self) 35 | cancelButton = QPushButton(QIcon.fromTheme('dialog-cancel'), "Cancel", self) 36 | 37 | keyIcon = QLabel(self) 38 | keyIcon.setPixmap(QPixmap(qtUtils.getAbsoluteImagePath('fingerprint.png')).scaledToWidth(50, Qt.SmoothTransformation)) 39 | 40 | helpLabel = QLabel("In order to ensure that no one is listening in on your conversation\n" 41 | "it's best to verify the identity of your buddy by entering a question\n" 42 | "that only your buddy knows the answer to.") 43 | 44 | okayButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_OKAY)) 45 | cancelButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_CANCEL)) 46 | 47 | helpLayout = QHBoxLayout() 48 | helpLayout.addStretch(1) 49 | helpLayout.addWidget(keyIcon) 50 | helpLayout.addSpacing(15) 51 | helpLayout.addWidget(helpLabel) 52 | helpLayout.addStretch(1) 53 | 54 | # Float the buttons to the right 55 | buttons = QHBoxLayout() 56 | buttons.addStretch(1) 57 | buttons.addWidget(okayButton) 58 | buttons.addWidget(cancelButton) 59 | 60 | vbox = QVBoxLayout() 61 | vbox.addLayout(helpLayout) 62 | vbox.addWidget(QLine()) 63 | vbox.addWidget(smpQuestionLabel) 64 | vbox.addWidget(self.smpQuestionInput) 65 | vbox.addWidget(smpAnswerLabel) 66 | vbox.addWidget(self.smpAnswerInput) 67 | vbox.addLayout(buttons) 68 | 69 | self.setLayout(vbox) 70 | 71 | 72 | def buttonClicked(self, button): 73 | self.smpQuestion = self.smpQuestionInput.text() 74 | self.smpAnswer = self.smpAnswerInput.text() 75 | self.clickedButton = button 76 | self.close() 77 | 78 | 79 | @staticmethod 80 | def getQuestionAndAnswer(): 81 | dialog = QSMPInitiateDialog() 82 | dialog.exec_() 83 | return dialog.smpQuestion, dialog.smpAnswer, dialog.clickedButton 84 | -------------------------------------------------------------------------------- /src/qt/qSMPRespondDialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | from PyQt4.QtCore import Qt 5 | from PyQt4.QtGui import QDialog 6 | from PyQt4.QtGui import QHBoxLayout 7 | from PyQt4.QtGui import QIcon 8 | from PyQt4.QtGui import QLabel 9 | from PyQt4.QtGui import QLineEdit 10 | from PyQt4.QtGui import QPixmap 11 | from PyQt4.QtGui import QPushButton 12 | from PyQt4.QtGui import QVBoxLayout 13 | 14 | from qLine import QLine 15 | import qtUtils 16 | 17 | from utils import constants 18 | 19 | class QSMPRespondDialog(QDialog): 20 | def __init__(self, nick, question, parent=None): 21 | QDialog.__init__(self, parent) 22 | 23 | self.clickedButton = constants.BUTTON_CANCEL 24 | 25 | # Set the title and icon 26 | self.setWindowTitle("Authenticate %s" % nick) 27 | self.setWindowIcon(QIcon(qtUtils.getAbsoluteImagePath('icon.png'))) 28 | 29 | smpQuestionLabel = QLabel("Question: %s" % question, self) 30 | 31 | smpAnswerLabel = QLabel("Answer (case sensitive):", self) 32 | self.smpAnswerInput = QLineEdit(self) 33 | 34 | okayButton = QPushButton(QIcon.fromTheme('dialog-ok'), "OK", self) 35 | cancelButton = QPushButton(QIcon.fromTheme('dialog-cancel'), "Cancel", self) 36 | 37 | keyIcon = QLabel(self) 38 | keyIcon.setPixmap(QPixmap(qtUtils.getAbsoluteImagePath('fingerprint.png')).scaledToWidth(60, Qt.SmoothTransformation)) 39 | 40 | helpLabel = QLabel("%s has requested to authenticate your conversation by asking you a\n" 41 | "question only you should know the answer to. Enter your answer below\n" 42 | "to authenticate your conversation.\n\n" 43 | "You may wish to ask your buddy a question as well." % nick) 44 | 45 | okayButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_OKAY)) 46 | cancelButton.clicked.connect(lambda: self.buttonClicked(constants.BUTTON_CANCEL)) 47 | 48 | helpLayout = QHBoxLayout() 49 | helpLayout.addStretch(1) 50 | helpLayout.addWidget(keyIcon) 51 | helpLayout.addSpacing(15) 52 | helpLayout.addWidget(helpLabel) 53 | helpLayout.addStretch(1) 54 | 55 | # Float the buttons to the right 56 | buttons = QHBoxLayout() 57 | buttons.addStretch(1) 58 | buttons.addWidget(okayButton) 59 | buttons.addWidget(cancelButton) 60 | 61 | vbox = QVBoxLayout() 62 | vbox.addLayout(helpLayout) 63 | vbox.addWidget(QLine()) 64 | vbox.addWidget(smpQuestionLabel) 65 | vbox.addWidget(smpAnswerLabel) 66 | vbox.addWidget(self.smpAnswerInput) 67 | vbox.addLayout(buttons) 68 | 69 | self.setLayout(vbox) 70 | 71 | 72 | def buttonClicked(self, button): 73 | self.smpAnswer = self.smpAnswerInput.text() 74 | self.clickedButton = button 75 | self.close() 76 | 77 | 78 | @staticmethod 79 | def getAnswer(nick, question): 80 | dialog = QSMPRespondDialog(nick, question) 81 | dialog.exec_() 82 | return dialog.smpAnswer, dialog.clickedButton 83 | -------------------------------------------------------------------------------- /src/qt/qWaitingDialog.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtCore import pyqtSignal 2 | from PyQt4.QtGui import QDialog 3 | from PyQt4.QtGui import QFrame 4 | from PyQt4.QtGui import QHBoxLayout 5 | from PyQt4.QtGui import QLabel 6 | from PyQt4.QtGui import QMovie 7 | from PyQt4.QtGui import QVBoxLayout 8 | 9 | import qtUtils 10 | from qLine import QLine 11 | 12 | from utils import constants 13 | 14 | 15 | class QWaitingDialog(QDialog): 16 | def __init__(self, parent, text=""): 17 | QDialog.__init__(self, parent) 18 | 19 | # Create waiting image 20 | waitingImage = QMovie(qtUtils.getAbsoluteImagePath('waiting.gif')) 21 | waitingImage.start() 22 | waitingImageLabel = QLabel(self) 23 | waitingImageLabel.setMovie(waitingImage) 24 | 25 | waitingLabel = QLabel(text, self) 26 | 27 | hbox = QHBoxLayout() 28 | hbox.addStretch(1) 29 | hbox.addWidget(waitingImageLabel) 30 | hbox.addSpacing(10) 31 | hbox.addWidget(waitingLabel) 32 | hbox.addStretch(1) 33 | 34 | self.setLayout(hbox) 35 | -------------------------------------------------------------------------------- /src/qt/qt.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | from network.client import Client 5 | from network.connectionManager import ConnectionManager 6 | from network import qtThreads 7 | 8 | from PyQt4.QtCore import pyqtSlot 9 | from PyQt4.QtCore import QTimer 10 | from PyQt4.QtGui import QApplication 11 | from PyQt4.QtGui import QInputDialog 12 | from PyQt4.QtGui import QMessageBox 13 | from PyQt4.QtGui import QPalette 14 | from PyQt4.QtGui import QWidget 15 | 16 | from qAcceptDialog import QAcceptDialog 17 | from qChatWindow import QChatWindow 18 | from qLoginWindow import QLoginWindow 19 | import qtUtils 20 | from qWaitingDialog import QWaitingDialog 21 | 22 | from utils import constants 23 | from utils import errors 24 | from utils import exceptions 25 | from utils import utils 26 | 27 | 28 | class QtUI(QApplication): 29 | def __init__(self, argv, nick, turn, port): 30 | QApplication.__init__(self, argv) 31 | 32 | self.nick = nick 33 | self.turn = turn 34 | self.port = port 35 | self.isEventLoopRunning = False 36 | 37 | qtUtils.setIsLightTheme(self.palette().color(QPalette.Window)) 38 | 39 | self.aboutToQuit.connect(self.stop) 40 | 41 | 42 | def start(self): 43 | # Start a timer to allow for ctrl+c handling 44 | self.timer = QTimer() 45 | self.timer.start(500) 46 | self.timer.timeout.connect(lambda: None) 47 | 48 | # Show the login window and get the nick 49 | nick = QLoginWindow.getNick(QWidget(), self.nick) 50 | 51 | # If the nick is None, the user closed the window so we should quit the app 52 | if nick is None: 53 | qtUtils.exitApp() 54 | else: 55 | self.nick = str(nick) 56 | 57 | # Show the chat window 58 | self.chatWindow = QChatWindow(self.restart) 59 | self.chatWindow.show() 60 | 61 | self.__connectToServer() 62 | 63 | # Don't start the event loop again if it's already running 64 | if not self.isEventLoopRunning: 65 | self.isEventLoopRunning = True 66 | self.exec_() 67 | 68 | 69 | def stop(self): 70 | if hasattr(self, 'connectionManager'): 71 | self.connectionManager.disconnectFromServer() 72 | 73 | # Give the send thread time to get the disconnect messages out before exiting 74 | # and killing the thread 75 | time.sleep(.25) 76 | 77 | self.quit() 78 | 79 | 80 | def restart(self): 81 | if hasattr(self, 'connectionManager'): 82 | self.connectionManager.disconnectFromServer() 83 | 84 | self.closeAllWindows() 85 | if hasattr(self, 'chatWindow'): 86 | del self.chatWindow 87 | 88 | self.start() 89 | 90 | 91 | def __connectToServer(self): 92 | # Create the connection manager to manage all communcation to the server 93 | self.connectionManager = ConnectionManager(self.nick, (self.turn, self.port), self.chatWindow.postMessage, self.chatWindow.newClient, self.chatWindow.clientReady, self.chatWindow.smpRequest, self.chatWindow.handleError) 94 | self.chatWindow.connectionManager = self.connectionManager 95 | 96 | # Start the connect thread 97 | self.connectThread = qtThreads.QtServerConnectThread(self.connectionManager, self.__postConnect, self.__connectFailure) 98 | self.connectThread.start() 99 | 100 | # Show the waiting dialog 101 | self.waitingDialog = QWaitingDialog(self.chatWindow, "Connecting to server...") 102 | self.waitingDialog.show() 103 | 104 | 105 | @pyqtSlot() 106 | def __postConnect(self): 107 | self.waitingDialog.close() 108 | self.chatWindow.connectedToServer() 109 | 110 | 111 | @pyqtSlot(str) 112 | def __connectFailure(self, errorMessage): 113 | # Show a more friendly error if the connection was refused (errno 111) 114 | if errorMessage.contains('Errno 111'): 115 | errorMessage = "Unable to contact the server. Try again later." 116 | 117 | QMessageBox.critical(self.chatWindow, errors.FAILED_TO_CONNECT, errorMessage) 118 | self.restart() 119 | -------------------------------------------------------------------------------- /src/qt/qtUtils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | 5 | from PyQt4.QtCore import QCoreApplication 6 | from PyQt4.QtGui import QDesktopWidget 7 | from PyQt4.QtGui import QIcon 8 | from PyQt4.QtGui import QInputDialog 9 | from PyQt4.QtGui import QMessageBox 10 | from PyQt4.QtGui import QPixmap 11 | from PyQt4.QtGui import QWidget 12 | 13 | from qPassphraseDialog import QPassphraseDialog 14 | 15 | from utils import constants 16 | from utils import utils 17 | 18 | 19 | def centerWindow(window): 20 | centerPoint = QDesktopWidget().availableGeometry().center() 21 | geo = window.frameGeometry() 22 | geo.moveCenter(centerPoint) 23 | window.move(geo.topLeft()) 24 | 25 | 26 | def resizeWindow(window, width, height): 27 | window.setGeometry(0, 0, width, height) 28 | 29 | 30 | def showDesktopNotification(systemTrayIcon, title, message): 31 | systemTrayIcon.showMessage(title, message) 32 | 33 | 34 | isLightTheme = False 35 | def setIsLightTheme(color): 36 | global isLightTheme 37 | isLightTheme = (color.red() > 100 and color.blue() > 100 and color.green() > 100) 38 | 39 | 40 | def getAbsoluteImagePath(imageName): 41 | global isLightTheme 42 | return utils.getAbsoluteResourcePath('images/' + ('light' if isLightTheme else 'dark') + '/' + imageName) 43 | 44 | 45 | def exitApp(): 46 | os.kill(os.getpid(), signal.SIGINT) 47 | -------------------------------------------------------------------------------- /src/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/server/__init__.py -------------------------------------------------------------------------------- /src/server/console.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | from threading import Thread 5 | 6 | class Console(Thread): 7 | def __init__(self, nickMap, ipMap): 8 | Thread.__init__(self) 9 | self.nickMap = nickMap 10 | self.ipMap = ipMap 11 | self.daemon = True 12 | 13 | self.commands = { 14 | 'list': { 15 | 'callback': self.list, 16 | 'help': 'list\t\tlist active connections' 17 | }, 18 | 'zombies': { 19 | 'callback': self.zombies, 20 | 'help': 'zombies\t\tlist zombie connections' 21 | }, 22 | 'kick': { 23 | 'callback': self.kick, 24 | 'help': 'kick [nick]\tkick the given nick from the server' 25 | }, 26 | 'kill': { 27 | 'callback': self.kill, 28 | 'help': 'kill [ip]\tkill the zombie with the given IP' 29 | }, 30 | 'stop': { 31 | 'callback': self.stop, 32 | 'help': 'stop\t\tstop the server' 33 | }, 34 | 'help': { 35 | 'callback': self.help, 36 | 'help': 'help\t\tdisplay this message' 37 | }, 38 | } 39 | 40 | 41 | def run(self): 42 | while True: 43 | try: 44 | input = raw_input(">> ").split() 45 | 46 | if len(input) == 0: 47 | continue 48 | 49 | command = input[0] 50 | arg = input[1] if len(input) == 2 else None 51 | 52 | self.commands[command]['callback'](arg) 53 | except EOFError: 54 | self.stop() 55 | except KeyError: 56 | print "Unrecognized command" 57 | 58 | 59 | def list(self, arg): 60 | print "Registered nicks" 61 | print "================" 62 | 63 | for nick, client in self.nickMap.iteritems(): 64 | print nick + " - " + str(client.sock) 65 | 66 | 67 | def zombies(self, arg): 68 | print "Zombie Connections" 69 | print "==================" 70 | 71 | for addr, client in self.ipMap.iteritems(): 72 | print addr 73 | 74 | 75 | def kick(self, nick): 76 | if not nick: 77 | print "Kick command requires a nick" 78 | return 79 | 80 | try: 81 | client = self.nickMap[nick] 82 | client.kick() 83 | print "%s kicked from server" % nick 84 | except KeyError: 85 | print "%s is not a registered nick" % nick 86 | 87 | 88 | def kill(self, ip): 89 | if not ip: 90 | print "Kill command requires an IP" 91 | return 92 | 93 | try: 94 | client = self.ipMap[ip] 95 | client.kick() 96 | print "%s killed" % ip 97 | except KeyError: 98 | print "%s is not a zombie" % ip 99 | 100 | 101 | def stop(self, arg=None): 102 | os.kill(os.getpid(), signal.SIGINT) 103 | 104 | 105 | def help(self, arg): 106 | delimeter = '\n\t' 107 | helpMessages = map(lambda (_, command): command['help'], self.commands.iteritems()) 108 | print "Available commands:%s%s" % (delimeter, delimeter.join(helpMessages)) 109 | -------------------------------------------------------------------------------- /src/server/turnServer.py: -------------------------------------------------------------------------------- 1 | import Queue 2 | import socket 3 | import sys 4 | import time 5 | 6 | from threading import Thread 7 | 8 | from console import Console 9 | 10 | from network.message import Message 11 | from network.sock import Socket 12 | 13 | from utils import constants 14 | from utils import errors 15 | from utils import exceptions 16 | from utils import utils 17 | 18 | 19 | # Dict to store connected clients in 20 | nickMap = {} 21 | 22 | # Dict for new clients that haven't registered a nick yet 23 | ipMap = {} 24 | 25 | quiet = False 26 | 27 | 28 | class TURNServer(object): 29 | def __init__(self, listenPort, showConsole=True): 30 | self.listenPort = listenPort 31 | 32 | global quiet 33 | quiet = showConsole 34 | 35 | 36 | def start(self): 37 | self.openLog() 38 | self.serversock = self.startServer() 39 | 40 | if quiet: 41 | Console(nickMap, ipMap).start() 42 | 43 | while True: 44 | # Wait for a client to connect 45 | (clientSock, clientAddr) = self.serversock.accept() 46 | 47 | # Wrap the socket in our socket object 48 | clientSock = Socket(clientAddr, clientSock) 49 | 50 | # Store the client's IP and port in the IP map 51 | printAndLog("Got connection: %s" % str(clientSock)) 52 | ipMap[str(clientSock)] = Client(clientSock) 53 | 54 | 55 | def startServer(self): 56 | printAndLog("Starting server...") 57 | serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 58 | 59 | try: 60 | serversock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 61 | serversock.bind(('0.0.0.0', self.listenPort)) 62 | serversock.listen(10) 63 | 64 | return serversock 65 | except exceptions.NetworkError as ne: 66 | printAndLog("Failed to start server") 67 | sys.exit(1) 68 | 69 | 70 | def stop(self): 71 | printAndLog("Requested to stop server") 72 | 73 | for nick, client in nickMap.iteritems(): 74 | client.send(Message(serverCommand=constants.COMMAND_END, destNick=nick, error=errors.ERR_SERVER_SHUTDOWN)) 75 | 76 | # Give the send threads time to get their messages out 77 | time.sleep(.25) 78 | 79 | if logFile is not None: 80 | logFile.close() 81 | 82 | 83 | def openLog(self): 84 | global logFile 85 | try: 86 | logFile = open('cryptully.log', 'a') 87 | except: 88 | logFile = None 89 | print "Error opening logfile" 90 | 91 | 92 | class Client(object): 93 | def __init__(self, sock): 94 | self.sock = sock 95 | self.nick = None 96 | self.sendThread = SendThread(sock) 97 | self.recvThread = RecvThread(sock, self.__nickRegistered) 98 | 99 | self.sendThread.start() 100 | self.recvThread.start() 101 | 102 | 103 | def send(self, message): 104 | self.sendThread.queue.put(message) 105 | 106 | 107 | def __nickRegistered(self, nick): 108 | # Add the client to the nick map and remove it from the ip map 109 | printAndLog("%s -> %s" % (str(self.sock), nick)) 110 | self.nick = nick 111 | nickMap[nick] = self 112 | try: 113 | del ipMap[str(self.sock)] 114 | except KeyError: 115 | pass 116 | 117 | 118 | def disconnect(self): 119 | self.sock.disconnect() 120 | del nickMap[self.nick] 121 | 122 | 123 | def kick(self): 124 | self.send(Message(serverCommand=constants.COMMAND_ERR, destNick=self.nick, error=errors.ERR_KICKED)) 125 | time.sleep(.25) 126 | self.disconnect() 127 | 128 | 129 | 130 | class SendThread(Thread): 131 | def __init__(self, sock): 132 | Thread.__init__(self) 133 | self.daemon = True 134 | 135 | self.sock = sock 136 | self.queue = Queue.Queue() 137 | 138 | 139 | def run(self): 140 | while True: 141 | message = self.queue.get() 142 | 143 | try: 144 | self.sock.send(str(message)) 145 | except Exception as e: 146 | nick = message.destNick 147 | printAndLog("%s: error sending data to: %s" % (nick, str(e))) 148 | nickMap[nick].disconnect() 149 | return 150 | finally: 151 | self.queue.task_done() 152 | 153 | 154 | class RecvThread(Thread): 155 | def __init__(self, sock, nickRegisteredCallback): 156 | Thread.__init__(self) 157 | self.daemon = True 158 | 159 | self.sock = sock 160 | self.nickRegisteredCallback = nickRegisteredCallback 161 | 162 | 163 | def run(self): 164 | # The client should send the protocol version its using first 165 | try: 166 | message = Message.createFromJSON(self.sock.recv()) 167 | except KeyError: 168 | printAndLog("%s: send a command with missing JSON fields" % self.sock) 169 | self.__handleError(errors.ERR_INVALID_COMMAND) 170 | return 171 | 172 | # Check that the client sent the version command 173 | if message.serverCommand != constants.COMMAND_VERSION: 174 | printAndLog("%s: did not send version command" % self.sock) 175 | self.__handleError(errors.ERR_INVALID_COMMAND) 176 | return 177 | 178 | # Check the protocol versions match 179 | if message.payload != constants.PROTOCOL_VERSION: 180 | printAndLog("%s: is using a mismatched protocol version" % self.sock) 181 | self.__handleError(errors.ERR_PROTOCOL_VERSION_MISMATCH) 182 | return 183 | 184 | # The client should then register a nick 185 | try: 186 | message = Message.createFromJSON(self.sock.recv()) 187 | except KeyError: 188 | printAndLog("%s: send a command with missing JSON fields" % self.sock) 189 | self.__handleError(errors.ERR_INVALID_COMMAND) 190 | return 191 | 192 | # Check that the client sent the register command 193 | if message.serverCommand != constants.COMMAND_REGISTER: 194 | printAndLog("%s: did not register a nick" % self.sock) 195 | self.__handleError(errors.ERR_INVALID_COMMAND) 196 | return 197 | 198 | # Check that the nick is valid 199 | self.nick = message.sourceNick 200 | if utils.isValidNick(self.nick) != errors.VALID_NICK: 201 | printAndLog("%s: tried to register an invalid nick" % self.sock) 202 | self.__handleError(errors.ERR_INVALID_NICK) 203 | return 204 | 205 | # Check that the nick is not already in use 206 | self.nick = self.nick.lower() 207 | if self.nick in nickMap: 208 | printAndLog("%s: tried to register an in-use nick" % self.sock) 209 | self.__handleError(errors.ERR_NICK_IN_USE) 210 | return 211 | 212 | self.nickRegisteredCallback(self.nick) 213 | 214 | while True: 215 | try: 216 | try: 217 | message = Message.createFromJSON(self.sock.recv()) 218 | except KeyError: 219 | printAndLog("%s: send a command with missing JSON fields" % self.sock) 220 | self.__handleError(errors.ERR_INVALID_COMMAND) 221 | return 222 | 223 | if message.serverCommand == constants.COMMAND_END: 224 | printAndLog("%s: requested to end connection" % self.nick) 225 | nickMap[self.nick].disconnect() 226 | return 227 | elif message.serverCommand != constants.COMMAND_RELAY: 228 | printAndLog("%s: sent invalid command" % self.nick) 229 | self.__handleError(errors.ERR_INVALID_COMMAND) 230 | return 231 | 232 | try: 233 | destNick = message.destNick 234 | # Validate the destination nick 235 | if utils.isValidNick(destNick) != errors.VALID_NICK: 236 | printAndLog("%s: requested to send message to invalid nick" % self.nick) 237 | self.__handleError(errors.ERR_INVALID_NICK) 238 | 239 | client = nickMap[destNick.lower()] 240 | 241 | # Rewrite the source nick to prevent nick spoofing 242 | message.sourceNick = self.nick 243 | 244 | client.send(message) 245 | except KeyError: 246 | printAndLog("%s: sent message to non-existant nick" % self.nick) 247 | self.sock.send(str(Message(serverCommand=constants.COMMAND_ERR, destNick=message.destNick, error=errors.ERR_NICK_NOT_FOUND))) 248 | except Exception as e: 249 | if hasattr(e, 'errno') and e.errno != errors.ERR_CLOSED_CONNECTION: 250 | printAndLog("%s: error receiving from: %s" % (self.nick, str(e))) 251 | 252 | if self.nick in nickMap: 253 | nickMap[self.nick].disconnect() 254 | return 255 | 256 | 257 | def __handleError(self, errorCode): 258 | self.sock.send(str(Message(serverCommand=constants.COMMAND_ERR, error=errorCode))) 259 | self.sock.disconnect() 260 | 261 | # Remove the client from the ip or nick maps (it may be in either) 262 | try: 263 | del ipMap[str(self.sock)] 264 | # If found the ip map, don't try to delete from the nick map (it can't be in both) 265 | return 266 | except: 267 | pass 268 | try: 269 | del nickMap[self.nick] 270 | except: 271 | pass 272 | 273 | 274 | def printAndLog(message): 275 | if quiet: 276 | sys.stdout.write("\b\b\b%s\n>> " % message) 277 | sys.stdout.flush() 278 | 279 | log(message) 280 | 281 | 282 | def log(message): 283 | if logFile is not None: 284 | logFile.write('%s\n' % message) 285 | logFile.flush() 286 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python2.7 2 | 3 | import sys 4 | 5 | from tests.mockServer import MockServer 6 | from tests.mockClient import MockClient, Client1, Client2 7 | 8 | 9 | def test(): 10 | # Start a server thread to test against 11 | serverThread = MockServer() 12 | serverThread.start() 13 | 14 | # Wait for the server to start 15 | while not hasattr(serverThread.server, 'serversock'): 16 | pass 17 | 18 | clients = [ 19 | Client1('alice', 'bob'), 20 | Client2('bob', 'alice'), 21 | ] 22 | 23 | for client in clients: 24 | client.start() 25 | 26 | # Wait for each client to finish so we can print exceptions/failures 27 | for client in clients: 28 | client.join() 29 | 30 | print '' 31 | 32 | for client in clients: 33 | client.printExceptions() 34 | 35 | # Exit with failure if any client has exceptions/failures 36 | for client in clients: 37 | if len(client.exceptions) > 0: 38 | sys.exit(1) 39 | 40 | sys.exit(0) 41 | 42 | if __name__ == '__main__': 43 | test() 44 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/mockClient.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import traceback 4 | 5 | from threading import Thread 6 | 7 | from network.connectionManager import ConnectionManager 8 | from utils import constants 9 | from waitingMock import WaitingMock 10 | 11 | 12 | class MockClient(Thread): 13 | def __init__(self, nick, remoteNick): 14 | Thread.__init__(self) 15 | 16 | self.nick = nick 17 | self.remoteNick = remoteNick 18 | self.exceptions = [] 19 | 20 | self.message1 = 'message 1' 21 | self.message2 = 'message 2' 22 | 23 | self.smpQuestion = 'when do we attack?' 24 | self.smpAnswer = 'at dawn' 25 | 26 | self.recvMessageCallback = WaitingMock() 27 | self.newClientCallback = WaitingMock() 28 | self.handshakeDoneCallback = WaitingMock() 29 | self.smpRequestCallback = WaitingMock() 30 | self.errorCallback = WaitingMock() 31 | 32 | self.connectionManager = ConnectionManager(self.nick, ('localhost', constants.DEFAULT_PORT), 33 | self.recvMessageCallback, self.newClientCallback, self.handshakeDoneCallback, self.smpRequestCallback, self.errorCallback) 34 | 35 | def success(self): 36 | sys.stdout.write('.') 37 | 38 | 39 | def failure(self): 40 | sys.stdout.write('F') 41 | 42 | 43 | def exception(self): 44 | sys.stdout.write('E') 45 | 46 | 47 | def printExceptions(self): 48 | for exception in self.exceptions: 49 | print "\n%s\n%s" % (exception[1], exception[0]) 50 | 51 | 52 | class Client1(MockClient): 53 | def __init__(self, nick, remoteNick): 54 | MockClient.__init__(self, nick, remoteNick) 55 | 56 | 57 | def run(self): 58 | try: 59 | self.connectionManager.connectToServer() 60 | 61 | # There's no connected to server callback so wait for all mock clients to connect to the server before trying to connect to one 62 | time.sleep(1) 63 | 64 | # Open the chat again and expect success 65 | self.connectionManager.openChat(self.remoteNick) 66 | self.handshakeDoneCallback.assert_called_with_wait(self.remoteNick) 67 | 68 | client = self.connectionManager.getClient(self.remoteNick) 69 | 70 | # Send two regular chat messages 71 | client.sendChatMessage(self.message1) 72 | client.sendChatMessage(self.message2) 73 | 74 | # Expect client 1 to send two typing messages 75 | self.recvMessageCallback.assert_called_with_wait(constants.COMMAND_TYPING, self.remoteNick, str(constants.TYPING_STOP_WITHOUT_TEXT)) 76 | self.recvMessageCallback.assert_called_with_wait(constants.COMMAND_TYPING, self.remoteNick, str(constants.TYPING_START)) 77 | 78 | # Start an SMP request 79 | client.initiateSMP(self.smpQuestion, self.smpAnswer) 80 | self.smpRequestCallback.assert_called_with_wait(constants.SMP_CALLBACK_COMPLETE, self.remoteNick) 81 | 82 | # End the connection 83 | client.disconnect() 84 | 85 | self.success() 86 | except AssertionError as err: 87 | self.failure() 88 | self.exceptions.append((err, traceback.format_exc())) 89 | except Exception as err: 90 | self.exception() 91 | self.exceptions.append((err, traceback.format_exc())) 92 | 93 | 94 | class Client2(MockClient): 95 | def __init__(self, nick, remoteNick): 96 | MockClient.__init__(self, nick, remoteNick) 97 | 98 | 99 | def run(self): 100 | try: 101 | self.connectionManager.connectToServer() 102 | 103 | # Expect client 1 to open a chat with us and then accept it 104 | self.newClientCallback.assert_called_with_wait(self.remoteNick) 105 | self.connectionManager.newClientAccepted(self.remoteNick) 106 | self.handshakeDoneCallback.assert_called_with_wait(self.remoteNick) 107 | 108 | # Expect client 1 to send us two messages 109 | # Message 1 should arrive first so check for message 2 since the callbacks are stored in a stack 110 | self.recvMessageCallback.assert_called_with_wait(constants.COMMAND_MSG, self.remoteNick, self.message2) 111 | self.recvMessageCallback.assert_called_with_wait(constants.COMMAND_MSG, self.remoteNick, self.message1) 112 | 113 | client = self.connectionManager.getClient(self.remoteNick) 114 | 115 | # Send the typing command and then the stop typing command 116 | client.sendTypingMessage(constants.TYPING_START) 117 | client.sendTypingMessage(constants.TYPING_STOP_WITHOUT_TEXT) 118 | 119 | # Expect an SMP request 120 | self.smpRequestCallback.assert_called_with_wait(constants.SMP_CALLBACK_REQUEST, self.remoteNick, self.smpQuestion) 121 | client.respondSMP(self.smpAnswer) 122 | 123 | # Expec client 1 to end the connection 124 | self.recvMessageCallback.assert_called_with_wait(constants.COMMAND_END, self.remoteNick, None) 125 | 126 | self.success() 127 | except AssertionError as err: 128 | self.failure() 129 | self.exceptions.append((err, traceback.format_exc())) 130 | except: 131 | self.exception() 132 | self.exceptions.append((err, traceback.format_exc())) 133 | -------------------------------------------------------------------------------- /src/tests/mockServer.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | from server.turnServer import TURNServer 4 | from utils import constants 5 | 6 | 7 | class MockServer(Thread): 8 | def __init__(self): 9 | Thread.__init__(self) 10 | self.daemon = True 11 | self.server = None 12 | 13 | 14 | def run(self): 15 | self.server = TURNServer(constants.DEFAULT_PORT, showConsole=False) 16 | self.server.start() 17 | -------------------------------------------------------------------------------- /src/tests/waitingMock.py: -------------------------------------------------------------------------------- 1 | from mock import Mock 2 | from threading import Event 3 | 4 | class WaitingMock(Mock): 5 | TIMEOUT = 3 6 | 7 | def __init__(self, *args, **kwargs): 8 | super(WaitingMock, self).__init__(*args, **kwargs) 9 | self.calledEvent = Event() 10 | 11 | 12 | def _mock_call(self, *args, **kwargs): 13 | retval = super(WaitingMock, self)._mock_call(*args, **kwargs) 14 | 15 | if self.call_count >= 1: 16 | self.calledEvent.set() 17 | 18 | return retval 19 | 20 | def assert_called_with_wait(self, *args, **kargs): 21 | self.calledEvent.clear() 22 | 23 | if self.call_count >= 1: 24 | return True 25 | 26 | self.calledEvent.wait(timeout=self.TIMEOUT) 27 | self.assert_called_with(*args, **kargs) 28 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanet/Cryptully/c1aa026ab9864bfeda6c95aedd7b81aa3ff2559d/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_TURN_SERVER = 'cryptully.com' 2 | DEFAULT_PORT = 9000 3 | 4 | PROTOCOL_VERSION = '1' 5 | NICK_MAX_LEN = 32 6 | DEFAULT_AES_MODE = 'aes_256_cbc' 7 | TYPING_TIMEOUT = 1500 8 | 9 | # Protocol commands 10 | 11 | # Server commands 12 | COMMAND_REGISTER = "REG" 13 | COMMAND_RELAY = "REL" 14 | COMMAND_VERSION = "VERSION" 15 | 16 | # Client commands 17 | 18 | # Handshake commands 19 | COMMAND_HELO = "HELO" 20 | COMMAND_REDY = "REDY" 21 | COMMAND_REJECT = "REJ" 22 | COMMAND_PUBLIC_KEY = "PUB_KEY" 23 | 24 | # Loop commands 25 | COMMAND_MSG = "MSG" 26 | COMMAND_TYPING = "TYPING" 27 | COMMAND_END = "END" 28 | COMMAND_ERR = "ERR" 29 | COMMAND_SMP_0 = "SMP0" 30 | COMMAND_SMP_1 = "SMP1" 31 | COMMAND_SMP_2 = "SMP2" 32 | COMMAND_SMP_3 = "SMP3" 33 | COMMAND_SMP_4 = "SMP4" 34 | 35 | SMP_COMMANDS = [ 36 | COMMAND_SMP_0, 37 | COMMAND_SMP_1, 38 | COMMAND_SMP_2, 39 | COMMAND_SMP_3, 40 | COMMAND_SMP_4, 41 | ] 42 | 43 | LOOP_COMMANDS = [ 44 | COMMAND_MSG, 45 | COMMAND_TYPING, 46 | COMMAND_END, 47 | COMMAND_ERR, 48 | COMMAND_SMP_0, 49 | COMMAND_SMP_1, 50 | COMMAND_SMP_2, 51 | COMMAND_SMP_3, 52 | COMMAND_SMP_4, 53 | ] 54 | 55 | # Message sources 56 | SENDER = 0 57 | RECEIVER = 1 58 | SERVICE = 2 59 | 60 | # Typing statuses 61 | TYPING_START = 0 62 | TYPING_STOP_WITHOUT_TEXT = 1 63 | TYPING_STOP_WITH_TEXT = 2 64 | 65 | # QT UI custom button codes 66 | BUTTON_OKAY = 0 67 | BUTTON_CANCEL = 1 68 | BUTTON_FORGOT = 2 69 | 70 | # Ncurses accept/mode dialog codes 71 | ACCEPT = 0 72 | REJECT = 1 73 | 74 | CONNECT = 0 75 | WAIT = 1 76 | 77 | SMP_CALLBACK_REQUEST = 0 78 | SMP_CALLBACK_COMPLETE = 1 79 | SMP_CALLBACK_ERROR = 2 80 | 81 | URL_REGEX = r"(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?]))" 82 | -------------------------------------------------------------------------------- /src/utils/errors.py: -------------------------------------------------------------------------------- 1 | import constants 2 | 3 | # Nick validation statuses 4 | VALID_NICK = 0 5 | INVALID_NICK_CONTENT = 1 6 | INVALID_NICK_LENGTH = 2 7 | INVALID_EMPTY_NICK = 3 8 | 9 | # UI error messages 10 | TITLE_CONNECTION_ENDED = "Connection Ended" 11 | TITLE_NETWORK_ERROR = "Network Error" 12 | TITLE_CRYPTO_ERROR = "Crypto Error" 13 | TITLE_END_CONNECTION = "Connection Ended" 14 | TITLE_INVALID_NICK = "Invalid Nickname" 15 | TITLE_NICK_NOT_FOUND = "Nickname Not Found" 16 | TITLE_CONNECTION_REJECTED = "Connection Rejected" 17 | TITLE_PROTOCOL_ERROR = "Invalid Response" 18 | TITLE_CLIENT_EXISTS = "Client Exists" 19 | TITLE_SELF_CONNECT = "Self Connection" 20 | TITLE_SERVER_SHUTDOWN = "Server Shutdown" 21 | TITLE_INVALID_COMMAND = "Invalid Command" 22 | TITLE_ALREADY_CONNECTED = "Already Chatting" 23 | TITLE_UNKNOWN_ERROR = "Unknown Error" 24 | TITLE_EMPTY_NICK = "No Nickname Provided" 25 | TITLE_NETWORK_ERROR = "Network Error" 26 | TITLE_BAD_HMAC = "Tampering Detected" 27 | TITLE_BAD_DECRYPT = "Decryption Error" 28 | TITLE_NICK_IN_USE = "Nickname Not Available" 29 | TITLE_KICKED = "Kicked" 30 | TITLE_SMP_MATCH_FAILED = "Eavesdropping Detected" 31 | TITLE_MESSAGE_REPLAY = "Tampering Detected" 32 | TITLE_MESSAGE_DELETION = "Tampering Detected" 33 | TITLE_PROTOCOL_VERSION_MISMATCH = "Incompatible Versions" 34 | 35 | UNEXPECTED_CLOSE_CONNECTION = "Server unexpectedly closed connection" 36 | CLOSE_CONNECTION = "The server closed the connection" 37 | UNEXPECTED_DATA = "Remote sent unexpected data" 38 | UNEXPECTED_COMMAND = "Receieved unexpected command" 39 | NO_COMMAND_SEPARATOR = "Command separator not found in message" 40 | UNKNOWN_ENCRYPTION_TYPE = "Unknown encryption type" 41 | VERIFY_PASSPHRASE_FAILED = "Passphrases do not match" 42 | BAD_PASSPHRASE = "Wrong passphrase" 43 | BAD_PASSPHRASE_VERBOSE = "An incorrect passphrase was entered" 44 | FAILED_TO_START_SERVER = "Error starting server" 45 | FAILED_TO_ACCEPT_CLIENT = "Error accepting client connection" 46 | FAILED_TO_CONNECT = "Error connecting to server" 47 | CLIENT_ENDED_CONNECTION = "The client requested to end the connection" 48 | INVALID_NICK_CONTENT = "Sorry, nicknames can only contain numbers and letters" 49 | INVALID_NICK_LENGTH = "Sorry, nicknames must be less than %d characters" % constants.NICK_MAX_LEN 50 | NICK_NOT_FOUND = "%s is not connected to the server" 51 | CONNECTION_REJECTED = "%s rejected your connection" 52 | PROTOCOL_ERROR = "%s sent unexpected data" 53 | CLIENT_EXISTS = "%s is open in another tab already" 54 | CONNECTION_ENDED = "%s has disconnected" 55 | SELF_CONNECT = "You cannot connect to yourself" 56 | SERVER_SHUTDOWN = "The server is shutting down" 57 | INVALID_COMMAND = "An invalid command was recieved from %s" 58 | ALREADY_CONNECTED = "A chat with %s is already open" 59 | UNKNOWN_ERROR = "An unknown error occured with %s" 60 | EMPTY_NICK = "Please enter a nickname" 61 | NETWORK_ERROR = "A network error occured while communicating with the server. Try connecting to the server again." 62 | BAD_HMAC = "Warning: Automatic data integrity check failed. Someone may be tampering with your conversation." 63 | BAD_DECRYPT = "Unable to decrypt incoming message. This usually happens when the client sends malformed data." 64 | NICK_IN_USE = "Sorry, someone else is already using that nickname" 65 | KICKED = "You have been kicked off the server" 66 | SMP_MATCH_FAILED = "Chat authentication failed. Either your buddy provided the wrong answer to the question or someone may be attempting to eavesdrop on your conversation. Note that answers are case sensitive." 67 | SMP_MATCH_FAILED_SHORT = "Chat authentication failed. Note that answers are case sensitive." 68 | MESSAGE_REPLAY = "Warning: Old message recieved multiple times. Someone may be tampering with your conversation." 69 | MESSAGE_DELETION = "Warning: Message deletion detected. Someone may be tampering with your conversation." 70 | PROTOCOL_VERSION_MISMATCH = "The server is reporting that you are using an outdated version of the program. Are you running the most recent version?" 71 | 72 | # Error codes 73 | ERR_CONNECTION_ENDED = 0 74 | ERR_NICK_NOT_FOUND = 1 75 | ERR_CONNECTION_REJECTED = 2 76 | ERR_BAD_HANDSHAKE = 3 77 | ERR_CLIENT_EXISTS = 4 78 | ERR_SELF_CONNECT = 5 79 | ERR_SERVER_SHUTDOWN = 6 80 | ERR_INVALID_COMMAND = 7 81 | ERR_ALREADY_CONNECTED = 8 82 | ERR_NETWORK_ERROR = 9 83 | ERR_BAD_HMAC = 10 84 | ERR_BAD_DECRYPT = 11 85 | ERR_INVALID_NICK = 12 86 | ERR_NICK_IN_USE = 13 87 | ERR_CLOSED_CONNECTION = 14 88 | ERR_KICKED = 15 89 | ERR_SMP_CHECK_FAILED = 16 90 | ERR_SMP_MATCH_FAILED = 17 91 | ERR_MESSAGE_REPLAY = 18 92 | ERR_MESSAGE_DELETION = 19 93 | ERR_PROTOCOL_VERSION_MISMATCH = 20 94 | -------------------------------------------------------------------------------- /src/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | class GenericError(Exception): 2 | def __init__(self, message=None, errno=0): 3 | Exception.__init__(self) 4 | self.message = message 5 | self.errno = errno 6 | 7 | 8 | class NetworkError(GenericError): 9 | def __init__(self, message=None, errno=0): 10 | GenericError.__init__(self, message, errno) 11 | 12 | 13 | class ProtocolError(GenericError): 14 | def __init__(self, message=None, errno=0): 15 | GenericError.__init__(self, message, errno) 16 | 17 | 18 | class ProtocolEnd(GenericError): 19 | def __init__(self, message=None, errno=0): 20 | GenericError.__init__(self, message, errno) 21 | 22 | 23 | class CryptoError(GenericError): 24 | def __init__(self, message=None, errno=0): 25 | GenericError.__init__(self, message, errno) 26 | -------------------------------------------------------------------------------- /src/utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import constants 5 | import errors 6 | 7 | from time import localtime 8 | from time import strftime 9 | 10 | 11 | def isValidNick(nick): 12 | if nick == "": 13 | return errors.INVALID_EMPTY_NICK 14 | if not nick.isalnum(): 15 | return errors.INVALID_NICK_CONTENT 16 | if len(nick) > constants.NICK_MAX_LEN: 17 | return errors.INVALID_NICK_LENGTH 18 | return errors.VALID_NICK 19 | 20 | 21 | def getTimestamp(): 22 | return strftime('%H:%M:%S', localtime()) 23 | 24 | 25 | def getAbsoluteResourcePath(relativePath): 26 | try: 27 | # PyInstaller stores data files in a tmp folder refered to as _MEIPASS 28 | basePath = sys._MEIPASS 29 | except Exception: 30 | # If not running as a PyInstaller created binary, try to find the data file as 31 | # an installed Python egg 32 | try: 33 | basePath = os.path.dirname(sys.modules['src'].__file__) 34 | except Exception: 35 | basePath = '' 36 | 37 | # If the egg path does not exist, assume we're running as non-packaged 38 | if not os.path.exists(os.path.join(basePath, relativePath)): 39 | basePath = 'src' 40 | 41 | path = os.path.join(basePath, relativePath) 42 | 43 | # If the path still doesn't exist, this function won't help you 44 | if not os.path.exists(path): 45 | return None 46 | 47 | return path 48 | 49 | 50 | def secureStrcmp(left, right): 51 | equal = True 52 | 53 | if len(left) != len(right): 54 | equal = False 55 | 56 | for i in range(0, min(len(left), len(right))): 57 | if left[i] != right[i]: 58 | equal = False 59 | 60 | return equal 61 | --------------------------------------------------------------------------------