├── .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 | [](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 |
--------------------------------------------------------------------------------