diff --git a/NOTICE.rst b/NOTICE.rst index 4836a0d..477f601 100644 --- a/NOTICE.rst +++ b/NOTICE.rst @@ -7,7 +7,30 @@ All rights reserved. All Authors and Contributors are named in the AUTHORS.rst file. -Used libraries -============== +Used Third Party Code +===================== -None. +SleekXMPP +========= + +``` +Copyright (c) 2010 Nathanael C. Fritz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` diff --git a/README b/README index 6ce2d1f..248eec7 100644 --- a/README +++ b/README @@ -4,9 +4,11 @@ CryptoIM .. image:: https://travis-ci.org/oskopek/CryptoIM.png?branch=develop :target: https://travis-ci.org/oskopek/CryptoIM + :alt: TravisCI .. image:: https://coveralls.io/repos/oskopek/CryptoIM/badge.png?branch=develop :target: https://coveralls.io/r/oskopek/CryptoIM?branch=develop + :alt: Coveralls .. image:: https://pypip.in/v/CryptoIM/badge.png :target: https://pypi.python.org/pypi/CryptoIM/ @@ -20,18 +22,76 @@ CryptoIM :target: https://pypi.python.org/pypi/CryptoIM/ :alt: License -A crypto-aware instant messenger written in Python out of sheer fun. +A secure* instant messenger written in Python out of sheer fun. + +\* not really secure (at least not yet) + +Building +======== + +1. **Build**: ``python setup.py build`` and ``sudo python setup.py install`` + +2. To **run**: ``python main.py`` + +3. To run **unit tests**: ``python setup.py nosetests`` + +4. To **clean**, run: ``python setup.py clean --all`` + +5. To **edit** connection options, edit the ``main.cfg`` file. + +Building on Linux +================= + +* Make sure to **install** these packages using your package manager: ``python`` and ``python-setuptools`` + +* Follow steps 1., 2. and 5. from **Building** + +Building on Windows +=================== + +* **Install** `Python `_ and `Setuptools `_. + +* Follow steps 1., 2. and 5. from **Building** + +Contributing +============ + +**Everyone** is encouraged to help improve this project. + +Here are some ways *you* can contribute: + +* by using alpha, beta, and prerelease versions +* by reporting bugs +* by suggesting new features +* by translating to a new language +* by writing or editing documentation +* by writing specifications +* by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) +* by refactoring code +* by closing `issues `_ +* by reviewing patches Info ==== CryptoIM uses `semantic versioning `_ and branching model similar to `this `_. -Building -======== +Submitting an Issue +=================== -* **Build**: ``python setup.py build`` and ``sudo python setup.py install`` +We use the `GitHub issue tracker `_ to track bugs and features. Before +submitting a bug report or feature request, check to make sure it hasn't +already been submitted. When submitting a bug report, please include a `Gist `_ +that includes a stack trace and any details that may be necessary to reproduce +the bug, including your Java version and operating system. -* To run **unit tests**: ``python setup.py nosetests`` +Submitting a Pull Request +========================= -* To **clean**, run: ``python setup.py clean --all`` +1. `Fork the repository `_. +2. `Create a topic branch `_. +3. Implement your feature or bug fix. +4. Run ``python setup.py nosetests``. If the tests fail, return to step 3. +5. If applicable, add tests for your feature or bug fix. +6. Add, commit, and push your changes. +7. `Submit a pull request `_. diff --git a/README.rst b/README.rst index 6ce2d1f..248eec7 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,11 @@ CryptoIM .. image:: https://travis-ci.org/oskopek/CryptoIM.png?branch=develop :target: https://travis-ci.org/oskopek/CryptoIM + :alt: TravisCI .. image:: https://coveralls.io/repos/oskopek/CryptoIM/badge.png?branch=develop :target: https://coveralls.io/r/oskopek/CryptoIM?branch=develop + :alt: Coveralls .. image:: https://pypip.in/v/CryptoIM/badge.png :target: https://pypi.python.org/pypi/CryptoIM/ @@ -20,18 +22,76 @@ CryptoIM :target: https://pypi.python.org/pypi/CryptoIM/ :alt: License -A crypto-aware instant messenger written in Python out of sheer fun. +A secure* instant messenger written in Python out of sheer fun. + +\* not really secure (at least not yet) + +Building +======== + +1. **Build**: ``python setup.py build`` and ``sudo python setup.py install`` + +2. To **run**: ``python main.py`` + +3. To run **unit tests**: ``python setup.py nosetests`` + +4. To **clean**, run: ``python setup.py clean --all`` + +5. To **edit** connection options, edit the ``main.cfg`` file. + +Building on Linux +================= + +* Make sure to **install** these packages using your package manager: ``python`` and ``python-setuptools`` + +* Follow steps 1., 2. and 5. from **Building** + +Building on Windows +=================== + +* **Install** `Python `_ and `Setuptools `_. + +* Follow steps 1., 2. and 5. from **Building** + +Contributing +============ + +**Everyone** is encouraged to help improve this project. + +Here are some ways *you* can contribute: + +* by using alpha, beta, and prerelease versions +* by reporting bugs +* by suggesting new features +* by translating to a new language +* by writing or editing documentation +* by writing specifications +* by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) +* by refactoring code +* by closing `issues `_ +* by reviewing patches Info ==== CryptoIM uses `semantic versioning `_ and branching model similar to `this `_. -Building -======== +Submitting an Issue +=================== -* **Build**: ``python setup.py build`` and ``sudo python setup.py install`` +We use the `GitHub issue tracker `_ to track bugs and features. Before +submitting a bug report or feature request, check to make sure it hasn't +already been submitted. When submitting a bug report, please include a `Gist `_ +that includes a stack trace and any details that may be necessary to reproduce +the bug, including your Java version and operating system. -* To run **unit tests**: ``python setup.py nosetests`` +Submitting a Pull Request +========================= -* To **clean**, run: ``python setup.py clean --all`` +1. `Fork the repository `_. +2. `Create a topic branch `_. +3. Implement your feature or bug fix. +4. Run ``python setup.py nosetests``. If the tests fail, return to step 3. +5. If applicable, add tests for your feature or bug fix. +6. Add, commit, and push your changes. +7. `Submit a pull request `_. diff --git a/cryptoim/__init__.py b/cryptoim/__init__.py index 94038e3..7997d59 100644 --- a/cryptoim/__init__.py +++ b/cryptoim/__init__.py @@ -14,6 +14,6 @@ limitations under the License. """ -from cryptoim import decryptor_core, encryptor_core, const, common +from cryptoim import decryptor_core, encryptor_core, const, common, xmpp, cli, key_exchange -__all__ = ["decryptor_core", "encryptor_core", "const", "common", ] +__all__ = ['decryptor_core', 'encryptor_core', 'const', 'common', 'xmpp', 'cli', 'key_exchange', ] diff --git a/cryptoim/cli.py b/cryptoim/cli.py new file mode 100644 index 0000000..e0c9f0a --- /dev/null +++ b/cryptoim/cli.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import cmd, sys, copy + +import cryptoim.xmpp + +if sys.version_info < (3, 0): + import ConfigParser as configparser +else: + import configparser as configparser + + +class CryptoShell(cmd.Cmd): + """ + CryptoShell + """ + + intro = 'Welcome to CryptoIM! Type help or ? to list commands.\n' + prompt = '(cryptoim) ' + test_mode = False + + def __init__(self, config_file): + """ + CryptoShell init + """ + + # super().__init__() # Python 3 only + cmd.Cmd.__init__(self) + self.config = configparser.ConfigParser() + self.config.read(config_file) + + import os + if not os.path.exists(config_file): + self.config.add_section('friends') + with open(config_file, 'w') as c_file: + self.config.write(c_file) + + self.config_file = config_file + + self.xmpp_client = None + self.current_chat = None + + # Logging + self.received_msg_list = [] + self.received_jid_list = [] + self.sent_msg_list = [] + self.sent_jid_list = [] + + # -- basic commands -- + def do_exit(self, arg): + """ + Quit CryptoIM + """ + + self.do_disconnect(arg) + self.print_cmd('Thank you for using CryptoIM!') + sys.exit(0) + + def do_q(self, arg): + """ + Usage: q (Exit) + """ + self.do_exit(arg) + + + # -- overrides -- + def emptyline(self): + pass + + # -- xmpp commands -- + def do_connect(self, arg): + """ + Usage: connect or connect + (Connects to your JID, if you dont have connection use addconnection) + """ + splitted = arg.split(' ') + + if not sanit_arg_count(splitted, 0, 2): + self.print_cmd('Invalid number of arguments!') + return self.return_cli(False) + + if self.xmpp_client and self.xmpp_client.is_connected(): + self.print_cmd('Already connected!') + return self.return_cli(False) + + conn_jid = None + conn_pass = None + + if sanit_arg_count_exact(splitted, 1): + if splitted[0] in self.config.sections(): + username = self.config.get(arg, 'Username') # self.config[arg]['Username'] + host = self.config.get(arg, 'Host') # self.config[arg]['Host'] + conn_jid = username + '@' + host + conn_pass = self.config.get(arg, 'Password') # self.config[arg]['Password'] + else: + self.print_cmd('Connection ' + splitted[0] + ' doesn\'t exist') + return self.return_cli(False) + + elif sanit_arg_count_exact(splitted, 2): + conn_jid = splitted[0] + conn_pass = splitted[1] + + self.xmpp_client = cryptoim.xmpp.XMPPClient(conn_jid, conn_pass, self) + self.xmpp_client.connect_server() + self.print_cmd('Connecting...') + return self.return_cli(True) + + + def do_disconnect(self, arg): + """ + Usage: disconnect + """ + + if not self.xmpp_client or not self.xmpp_client.is_connected(): + self.print_cmd('Already disconnected!') + return self.return_cli(False) + + if arg: # arg nonempty + self.print_cmd('Usage: disconnect, not disconnect ') + + self.xmpp_client.disconnect_server() + self.print_cmd('Disconnected from server.') + return self.return_cli(True) + + + def do_addconnection(self, arg): + """ + Usage: addconnection + """ + splitted = arg.split(' ') + + if self.config.has_section(splitted[0]): + self.print_cmd(splitted[0] + ' is already in your connection list') + return self.return_cli(False) + if not sanit_arg_count_exact(splitted, 3): + self.print_cmd('Usage: addconnection ') + return self.return_cli(False) + if not sanit_is_jid(splitted[1]): + self.print_cmd('JID has form of username@host.') + self.print_cmd('Usage: addconnection ') + return self.return_cli(False) + + self.config.add_section(splitted[0]) + self.config.set(splitted[0], 'username', splitted[0]) + self.config.set(splitted[0], 'host', (splitted[1].split('@') [1]) ) + self.config.set(splitted[0], 'password', splitted[2]) + + with open(self.config_file, 'w') as conf: + self.config.write(conf) + return self.return_cli(True) + + def do_removeconnection(self, arg): + """ + Usage: removeconnection + """ + splitted = arg.split(' ') + + if not self.config.has_section(splitted[0]): + self.print_cmd(splitted[0] + ' is not in your connection list') + return self.return_cli(False) + + if not sanit_arg_count_exact(splitted, 1) or sanit_is_jid(splitted[0]): + self.print_cmd('Usage: removeconnection ') + return self.return_cli(False) + + self.config.remove_section(splitted[0]) + + with open(self.config_file, 'w') as conf: + self.config.write(conf) + return self.return_cli(True) + + def do_s(self, arg): + """ + Usage: s or s + (Shorthand for send) + """ + return(self.do_send(arg)) + + def do_send(self, arg): + """ + Usage: send or send + """ + if not self.xmpp_client or not self.xmpp_client.is_in_session(): + self.print_cmd('Connect first!') + return self.return_cli(False) + + splitted = arg.split(' ') + + if self.current_chat: # if chat mode + if len(arg)==0: + self.print_cmd('Please enter your message.') + return self.return_cli(False) + recipient = self.current_chat + message = ' '.join(splitted) + + else: # if chat mode off + + if self.config_find(splitted[0]): # if sending to friend + recipient = self.config_find(splitted[0]) + elif sanit_is_jid(splitted[0]): # if sending to jid + recipient = splitted[0] + else: # error: username not defined or jid isnt jid + if splitted[0]: + self.print_cmd(splitted[0] + ' is not recognized. Please enter valid JID or username.') + self.print_cmd('Usage: send or send ') + return self.return_cli(False) + + message = ' '.join(splitted[1:]) + if len(message) == 0: + self.print_cmd('Please enter your message.') + return self.return_cli(False) + + self.xmpp_client.send_message(recipient, message) + self.print_cmd(address_format(self.xmpp_client.xmpp.jid, message)) + + return self.return_cli(True) + + def do_addfriend(self, arg): + """ + Usage: addfriend + """ + splitted = arg.split(' ') + + if not sanit_arg_count_exact(splitted, 2): + self.print_cmd('Usage: addfriend ') + return self.return_cli(False) + + if self.config_find(splitted[0]): + self.print_cmd('Already in your friend list.') + return self.return_cli(False) + + self.config.set('friends', splitted[0], splitted[1]) + with open(self.config_file, 'w') as conf: + self.config.write(conf) + return self.return_cli(True) + + def do_removefriend(self, arg): + """ + Usage: removefriend + """ + splitted = arg.split(' ') + + if not arg: + self.print_cmd('Usage: removefriend ') + return self.return_cli(False) + + if not self.config_find(splitted[0]): + self.print_cmd('Not in your friend list.') + return self.return_cli(False) + + self.config.remove_option('friends', splitted[0]) + with open(self.config_file, 'w') as conf: + self.config.write(conf) + return self.return_cli(True) + + def do_chat(self, arg): + """ + Usage: chat or chat + """ + if not arg: + self.print_cmd('Usage: chat or chat ') + return self.return_cli(False) + + if sanit_is_jid(arg): + self.print_cmd('Opening chat window with: ' + arg.split(' ')[0]) + self.current_chat = arg.split(' ')[0] + self.prompt = '(' + self.current_chat.split('@')[0] + ') ' + return self.return_cli(True) + + if not sanit_is_jid(arg) and self.config_find(arg): + arg = self.config_find(arg) + self.print_cmd('Opening chat window with: ' + arg.split(' ')[0]) + self.current_chat = arg.split(' ')[0] + self.prompt = '(' + self.current_chat.split('@')[0] + ') ' + return self.return_cli(True) + + if not sanit_is_jid(arg) and not self.config_find(arg): + self.print_cmd('Unknown JID or username, please check JID or try addfriend ') + return self.return_cli(False) + + def do_friendlist(self, arg): + """ + Displays all friends from config file friends section + """ + + if not arg: + self.print_cmd('Usage: friendlist, not friendlist ') + + for friend in self.config.items('friends'): + self.print_cmd(' - '.join(friend)) + + + def do_stopchat(self, arg): + """ + Usage: stopchat (Stops current chat) + """ + if not self.current_chat: + self.print_cmd('No open chat to close.') + return self.return_cli(False) + if not arg: + self.print_cmd('Usage: stopchat, not stopchat ') + + self.prompt = '(cryptoim) ' + self.current_chat = None + self.print_cmd('Closing chat window.') + return self.return_cli(True) + + # -- tools -- + + def print_cmd(self, string): + """ + Prints a string to the console + """ + self.stdout.write(string + '\n') + self.stdout.flush() + + def print_msg(self, jid, msg): + """ + Prints a message (jid + msg), correctly formatted using address_format + without erasing typed content. + """ + + backup = copy.copy(self.prompt) + self.stdout.write('\r') + self.stdout.flush() + self.print_cmd(address_format(jid, msg)) + self.stdout.write(backup) + self.stdout.flush() + + def print_debug(self, msg): + """ + Prints debug messages + """ + if self.test_mode: + self.print_cmd('DEBUG: ' + msg) + + def config_find(self, param, section='friends'): + """ + Finds a parameter in section in config, returns the value, or None if not found + """ + if self.config: + if self.config.has_option(section, param): + return self.config.get(section, param) + return None + + def return_cli(self, value): + """ + If in test mode, return value from param + """ + + if self.test_mode: + return value + else: + return + +# End of class + +def sanit_arg_count(input_array, number_lo, number_hi): + """ + Returns True, if length of input array is in + """ + if len(input_array) <= number_hi and len(input_array) >= number_lo: + return True + return False + +def sanit_arg_count_exact(input_array, number): + """ + Returns True, if length of input_array is equal to number + """ + return sanit_arg_count(input_array, number, number) + +def sanit_is_jid(string): + """ + Returns true if the string is a JID + """ + if string.count('@') != 1 or string.count('/') > 1: + return False + + splitted = string.split('@') + username = splitted[0] + + host = splitted[1] + if host.count('.') > 1: + return False + host = host.replace('.', '') + + if '/' in host: + host_resource = host.split('/') + host = host_resource[0] + resource = host_resource[1] + + # Test resource: + if not resource.isalnum(): + return False + + return username.isalnum() and host.isalnum() + +def address_format(jid, msg): + """ + Formats a jid and message to correctly display in the log + """ + return(jid + ': ' + msg) diff --git a/cryptoim/const.py b/cryptoim/const.py index e7dde9a..06928b0 100644 --- a/cryptoim/const.py +++ b/cryptoim/const.py @@ -141,3 +141,75 @@ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] + +PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, + 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, + 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, + 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, + 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, + 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, + 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, + 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, + 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, + 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, + 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, + 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, + 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, + 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, + 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, + 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, + 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, + 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, + 2383, 2389, 2393, 2399, 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, + 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, + 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, + 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909, + 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, + 3067, 3079, 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, + 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, + 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, + 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, + 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727, 3733, 3739, 3761, + 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, + 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, + 4049, 4051, 4057, 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, + 4201, 4211, 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, + 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, + 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, 4637, + 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, + 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, + 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023, 5039, 5051, + 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, + 5227, 5231, 5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, + 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, + 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, + 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, 5801, + 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, + 5927, 5939, 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, + 6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, + 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, + 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, 6481, 6491, 6521, 6529, 6547, 6551, + 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, + 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, + 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, + 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, + 7151, 7159, 7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, + 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, + 7489, 7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591, + 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727, 7741, + 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, + 7919, 7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, + 8089, 8093, 8101, 8111, 8117, 8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219, 8221, 8231, 8233, + 8237, 8243, 8263, 8269, 8273, 8287, 8291, 8293, 8297, 8311, 8317, 8329, 8353, 8363, 8369, 8377, 8387, + 8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467, 8501, 8513, 8521, 8527, 8537, 8539, 8543, 8563, + 8573, 8581, 8597, 8599, 8609, 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677, 8681, 8689, 8693, 8699, + 8707, 8713, 8719, 8731, 8737, 8741, 8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819, 8821, 8831, 8837, + 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923, 8929, 8933, 8941, 8951, 8963, 8969, 8971, 8999, 9001, + 9007, 9011, 9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, 9103, 9109, 9127, 9133, 9137, 9151, 9157, + 9161, 9173, 9181, 9187, 9199, 9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283, 9293, 9311, + 9319, 9323, 9337, 9341, 9343, 9349, 9371, 9377, 9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, 9437, + 9439, 9461, 9463, 9467, 9473, 9479, 9491, 9497, 9511, 9521, 9533, 9539, 9547, 9551, 9587, 9601, 9613, + 9619, 9623, 9629, 9631, 9643, 9649, 9661, 9677, 9679, 9689, 9697, 9719, 9721, 9733, 9739, 9743, 9749, + 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829, 9833, 9839, 9851, 9857, 9859, 9871, 9883, 9887, + 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973] diff --git a/cryptoim/encryptor_core.py b/cryptoim/encryptor_core.py index fbb4b88..53f427f 100644 --- a/cryptoim/encryptor_core.py +++ b/cryptoim/encryptor_core.py @@ -23,7 +23,7 @@ def encrypt(plaintext, key): """ plaintext = string - key = string (256 bytes) + key = string (256 bits) """ messages = __split_message(plaintext) @@ -35,7 +35,7 @@ def encrypt_round(messages, roundkeys): """ encrypt_round """ - ciphertext = "" + ciphertext = '' for msg in messages: msg = __add_roundkey(msg, roundkeys[14]) for i in range(14): @@ -65,7 +65,7 @@ def __split_message(plaintext): message_chunks.append(message_chunk) message_chunk = '' if i == (len(plaintext)-1) and len(message_chunk) < 16: - message_chunk += (16-len(message_chunk))*"\x00" + message_chunk += (16-len(message_chunk)) * '\x00' message_chunks.append(message_chunk) message_chunk = '' messages = [] @@ -80,7 +80,6 @@ def __split_message(plaintext): messages.append(matrix) return messages - # TODO: Check if this works as expected def __sub_bytes(message): """ @@ -91,7 +90,6 @@ def __sub_bytes(message): hexadecimal = __convert_char_hex(message[i][j]) message[i][j] = const.SBOX[int(hexadecimal[0], 16)][int(hexadecimal[1], 16)] return message - # TODO: Check if returns in decimal or hexadecimal def __shift_rows(message): """ diff --git a/cryptoim/key_exchange.py b/cryptoim/key_exchange.py new file mode 100644 index 0000000..34af5c1 --- /dev/null +++ b/cryptoim/key_exchange.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import random +from cryptoim.const import PRIMES + +# Random generator useable for cryptography +RAND = random.SystemRandom() + +def generate_random(limit_lo, limit_hi): + """ + Returns a random integer inside the (limit_lo, limit_hi) interval + """ + return RAND.randint(limit_lo, limit_hi) + +def prime_pick(): + """ + Returns a random number from the const.PRIMES array + """ + rnd = generate_random(0, len(PRIMES) - 1) + return PRIMES[rnd] + +def base_pick(): + """ + Returns a random number from the const.PRIMES array from indexes interval (0, 15) + """ + rnd = generate_random(2, 15) + return rnd + +def make_public_key(prime, base, rnumber): + """ + Returns (base^number) mod prime, the public key used for the key exchange + """ + pub_key = (base ** rnumber) % prime + return pub_key + +def make_final_key(prime, public, private): + """ + Returns (pub_key^p_number) mod prime, the key used for encryption + """ + key = (public ** private) % prime + return key + +def encode_syn(prime, base, A): + """ + Encodes the numbers in a standardized format + """ + return 'SYN;%i;%i;%i' % (prime, base, A) + +def decode_syn(msg): + """ + Decodes the numbers in a standardized format + """ + cut = msg[4:] # Omit the first 4 chars ('SYN;') + spl = cut.split(';') + prime = int(spl[0]) + base = int(spl[1]) + A = int(spl[2]) + return prime, base, A + +def encode_ack(B): + """ + Encodes the number in a standardized format + """ + return 'ACK;%i' % (B) + +def decode_ack(msg): + """ + Decodes the number in a standardized format + """ + cut = msg[4:] # Omit the first 4 chars ('ACK;') + return int(cut) diff --git a/cryptoim/xmpp.py b/cryptoim/xmpp.py new file mode 100644 index 0000000..dfbb588 --- /dev/null +++ b/cryptoim/xmpp.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Original LICENSE: + + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file NOTICE.rst file for copying permission. +""" + +import logging +import sleekxmpp + +import cryptoim.encryptor_core as encryptor +import cryptoim.decryptor_core as decryptor +import cryptoim.key_exchange as keyex + +class CryptoXMPP(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP client. + """ + + def __init__(self, jid, password, parent): + # Add a static resource + if '/' in jid: + jid = jid[:jid.index('/')] + jid += '/cryptoim' + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can initialize + # our roster. + self.add_event_handler('session_start', self.start) + self.add_event_handler('session_end', self.session_end) + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. + self.add_event_handler('message', self.message) + + self.add_event_handler('connected', self.connected) + self.add_event_handler('disconnected', self.disconnected) + + self.parent = parent + self.in_session = False + self.is_connected = False + + self.msg_queue = dict() + self.key_queue = dict() + + def connected(self, event): + """ + Process the connected event. + """ + + self.is_connected = True + + def disconnected(self, event): + """ + Process the disconnected event. + """ + + self.is_connected = False + + def session_end(self, event): + """ + Process the session_end event. + """ + + self.in_session = False + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + self.in_session = True + self.parent.print_cmd('Connected!') + + def message(self, msg): + """ + Process incoming message stanzas. Be aware that this also + includes MUC messages and error messages. It is usually + a good idea to check the messages's type before processing + or sending replies. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + + if msg['type'] not in ('chat', 'normal'): + return # Ignore nonchat messages + + sender = msg['from'].bare + text = msg['body'] + + # DH key exchange: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange#Explanation_including_encryption_mathematics + + if text.startswith('SYN;'): # receiving + prime, base, A = keyex.decode_syn(msg['body']) + b = keyex.generate_random(2, 100) + B = keyex.make_public_key(prime, base, b) + key = str(keyex.make_final_key(prime, A, b)) + + self.send_message(mto = sender, mbody = keyex.encode_ack(B), mtype = 'chat') + self.key_queue[sender] = key + + elif text.startswith('ACK;'): # sending + q_entry = self.msg_queue[sender] + msg_text = q_entry[0] + prime = q_entry[1] + a = q_entry[2] + B = keyex.decode_ack(msg['body']) + key = str(keyex.make_final_key(prime, B, a)) + ciphertext = encryptor.encrypt(msg_text, key) + self.send_message(mto = sender, mbody = ciphertext, mtype = 'chat') + + del q_entry # TODO check if it actually gets removed + + # Log: + self.parent.sent_jid_list.append(sender) + self.parent.sent_msg_list.append(msg_text) + + else: + ciphertext = text + key = self.key_queue[sender] + decrypted_message = decryptor.decrypt(ciphertext, key) + self.parent.print_msg(sender, decrypted_message) + + del key # TODO check if it actually gets removed + + # Log: + self.parent.received_jid_list.append(sender) + self.parent.received_msg_list.append(decrypted_message) + + +class XMPPClient(object): + """ + The XMPP client object, used as a wrapper for the SleekXMPP client. + """ + + xmpp = None + + def __init__(self, jid, password, parent, loglevel=logging.CRITICAL): + """ + Initializes the ClientXMPP, logging, etc + """ + + # Setup logging. + logging.basicConfig(level=loglevel, + format='%(levelname)-8s %(message)s') + + # Setup the ClientXMPP and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + self.xmpp = CryptoXMPP(jid, password, parent) + self.xmpp.register_plugin('xep_0030') # Service Discovery + self.xmpp.register_plugin('xep_0004') # Data Forms + self.xmpp.register_plugin('xep_0060') # PubSub + self.xmpp.register_plugin('xep_0199') # XMPP Ping + + + def connect_server(self, should_block=False, should_reattempt=True): + """ + Connects the ClientXMPP to the server, specify thread blocking. + """ + + # Connect to the XMPP server and start processing XMPP stanzas. + if self.xmpp.connect(reattempt=should_reattempt): + # If you do not have the dnspython library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + self.xmpp.process(block=should_block) + + def disconnect_server(self): + """ + Disconnects the ClientXMPP from the server. + """ + + self.xmpp.disconnect(wait=True) + + def is_connected(self): + """ + Checks if the ClientXMPP is currently connected to the server. + """ + + return self.xmpp.is_connected + + def is_in_session(self): + """ + Checks if the ClientXMPP is currently in a session + """ + + return self.xmpp.in_session + + def send_message(self, recipient, msg): + """ + Sends a chat message to the designated recipient. + """ + prime = keyex.prime_pick() + base = keyex.base_pick() + a = keyex.generate_random(2, 100) + A = keyex.make_public_key(prime, base, a) + + self.xmpp.send_message(mto = recipient, mbody = keyex.encode_syn(prime, base, A), mtype = 'chat') + self.xmpp.msg_queue[recipient] = (msg, prime, a) diff --git a/main.cfg b/main.cfg new file mode 100644 index 0000000..c33a720 --- /dev/null +++ b/main.cfg @@ -0,0 +1,14 @@ +[cryptoim] +username = cryptoim +host = jabber.de +password = crypto_test + +[cryptoim2] +username = cryptoim2 +host = jabber.de +password = crypto_test2 + +[friends] +cryptoim = cryptoim@jabber.de +cryptoim2 = cryptoim2@jabber.de + diff --git a/main.py b/main.py new file mode 100755 index 0000000..6fa9b46 --- /dev/null +++ b/main.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from cryptoim.cli import CryptoShell + +if __name__ == '__main__': + CryptoShell('main.cfg').cmdloop() diff --git a/setup.cfg b/setup.cfg index a2ec557..215100c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,8 @@ [nosetests] +verbosity=3 with-coverage=1 cover-package=cryptoim cover-tests=0 cover-erase=1 cover-branches=1 +#with-timer=1 diff --git a/setup.py b/setup.py index 4030ec8..2b1a39e 100755 --- a/setup.py +++ b/setup.py @@ -2,48 +2,48 @@ # encoding: utf-8 """ - Copyright 2013-2014 CryptoIM Development Team + Copyright 2014 CryptoIM Development Team - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. """ from setuptools import setup, find_packages setup( - name = "CryptoIM", - version = "0.1.0dev", - author = "CryptoIM Development Team", - author_email = "skopekondrej@gmail.com", - packages = find_packages(exclude=["tests"]), - url = "http://pypi.python.org/pypi/CryptoIM/", - license = "Apache License 2.0", - description = "Crypto Instant Messenger", - keywords = "crypto instant messenger", - test_suite = "nose.collector", + name = 'CryptoIM', + version = '0.1.0', + author = 'CryptoIM Development Team', + author_email = 'skopekondrej@gmail.com', + packages = find_packages(exclude=['tests']), + scripts = ['main.py'], + url = 'http://pypi.python.org/pypi/CryptoIM/', + license = 'Apache License 2.0', + description = 'Crypto Instant Messenger', + keywords = 'crypto instant messenger', + test_suite = 'nose.collector', classifiers = [ - "Development Status :: 1 - Planning", - "Environment :: Console", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Programming Language :: Python", - "Topic :: Communications :: Chat", - "Topic :: Security :: Cryptography", + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Programming Language :: Python', + 'Topic :: Communications :: Chat', + 'Topic :: Security :: Cryptography', ], - long_description = open("README").read(), + long_description = open('README').read(), install_requires = [ - "docutils >= 0.3", - "nose >= 1.3.0", - "nosexcover >= 1.0.8", - "python-coveralls >= 2.4.2", + 'nose >= 1.3.0', + 'python-coveralls >= 2.4.2', + 'sleekxmpp >= 1.1.11' ], ) diff --git a/tests/TestCli.py b/tests/TestCli.py new file mode 100644 index 0000000..7d5917e --- /dev/null +++ b/tests/TestCli.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from cryptoim.cli import CryptoShell +import cryptoim.cli as cli +import TestXMPP + +from nose.tools import ok_, eq_ + +def test_connect_disconnect(): + + cshell = CryptoShell('main.cfg') + cshell.test_mode = True + eq_(cshell.do_connect(''), False) + eq_(cshell.do_connect('invalid number of arguments'), False) + eq_(cshell.do_connect('cryptoim'), True) + eq_(cshell.do_connect('cryptoim'), False) + eq_(cshell.do_disconnect('random_string'), True) # branch coverage + eq_(cshell.do_disconnect(''), False) + + exit_code = -1 + try: + cshell.do_q('') + except SystemExit: + exit_code = 0 + eq_(0, exit_code) + +def test_connect_disconnect_jid(): + + cshell = CryptoShell('main.cfg') + cshell.test_mode = True + eq_(cshell.do_connect('cryptoim@jabber.de/random_resource_gets_stripped crypto_test'), True) + eq_(cshell.do_disconnect(''), True) + eq_(cshell.do_disconnect(''), False) + +def test_send(): + + cshell = CryptoShell('main.cfg') + cshell.test_mode = True + eq_(cshell.do_send('cryptoim message before connection'), False) + eq_(cshell.do_connect('cryptoim2'), True) + TestXMPP.waitForSession(cshell.xmpp_client, True) + eq_(cshell.do_send(''), False) + eq_(cshell.onecmd(''), None) # just empty line command - emptyline() test + eq_(cshell.do_send('shouldntwork message'), False) + eq_(cshell.do_send('cryptoim message'), True) + eq_(cshell.do_send('cryptoim2@jabber.de message'), True) + eq_(cshell.do_send('cryptoim'), False) + cshell.do_disconnect('') + +def test_chat_stopchat_exit(): + + cshell = CryptoShell('main.cfg') + cshell.test_mode = True + eq_(cshell.do_chat(''), False) + eq_(cshell.do_chat('cryptoim@jabber.de'), True) + eq_(cshell.do_chat('cryptoim'), True) + eq_(cshell.do_chat('shouldntwork'), False) + eq_(cshell.do_connect('cryptoim2'), True) + TestXMPP.waitForSession(cshell.xmpp_client, True) + eq_(cshell.do_send('Test message'), True) + eq_(cshell.do_s('Test message for short version'), True) + eq_(cshell.do_send(''), False) + eq_(cshell.do_stopchat(''), True) + eq_(cshell.do_stopchat(''), False) + eq_(cshell.do_send('Test message after stopchat'), False) + eq_(cshell.do_s('Alsto testing the short version'), False) + cshell.do_disconnect('') + + exit_code = -1 + try: + cshell.do_exit('') + except SystemExit: + exit_code = 0 + eq_(0, exit_code) + +def test_addfriend_removefriend(): + + cshell = CryptoShell('tests/test_config.cfg') + cshell.test_mode = True + + eq_(cshell.do_addfriend('testfriend testfriend@jabber.de'), True) + eq_(cshell.do_addfriend('testfriend testfriend@jabber.de'), False) + eq_(cshell.do_addfriend(''), False) + eq_(cshell.do_removefriend('testfriend'), True) + eq_(cshell.do_removefriend('testfriend another few lines'), False) + eq_(cshell.do_removefriend(''), False) + eq_(cshell.do_removefriend('testfriend'), False) + +def test_addconnection_removeconnection(): + + cshell = CryptoShell('tests/test_config.cfg') + cshell.test_mode = True + + eq_(cshell.do_addconnection('testuser2 testuser2@jabber.de testpass'), True) + eq_(cshell.do_addconnection('testuser2 testuser2@jabber.de testpass'), False) + eq_(cshell.do_addconnection('testuser2'), False) + eq_(cshell.do_addconnection('testuser3'), False) + eq_(cshell.do_addconnection('testuser3 thisisnotajid testpass'), False) + eq_(cshell.do_removeconnection('testuser3'), False) + eq_(cshell.do_removeconnection('testuser2 testuser3@jabber.de'), False) + eq_(cshell.do_removeconnection('testuser2@jabber.de'), False) + eq_(cshell.do_removeconnection('testuser2'), True) + +def test_friendlist(): + + cshell = CryptoShell('main.cfg') + cshell.test_mode = True + eq_(cshell.do_friendlist(''), None) + eq_(cshell.do_friendlist('whatever string'), None) + +def test_return_cli(): + + cshell = CryptoShell('tests/test_config.cfg') + cshell.test_mode = True + eq_(cshell.return_cli(False), False) + eq_(cshell.return_cli(True), True) + eq_(cshell.return_cli('test'), 'test') + eq_(cshell.return_cli(123), 123) + cshell.test_mode = False + eq_(cshell.return_cli(False), None) + eq_(cshell.return_cli(True), None) + eq_(cshell.return_cli('test'), None) + eq_(cshell.return_cli(123), None) + +def test_create_config(): + config_file = 'tests/test_config_nonexistant.cfg' + cshell = CryptoShell(config_file) + import os + os.remove(config_file) + +# Test tools +def test_sanit_is_jid(): + is_jid = cli.sanit_is_jid + eq_(True, is_jid('test@jabber.de')) + eq_(True, is_jid('test@jabber.de/resourceHere123')) + eq_(True, is_jid('test@localhost')) + eq_(True, is_jid('tes1234tBigSmall@jabber.DE')) + + eq_(False, is_jid('testjabber.de')) + eq_(False, is_jid('test/jabber@de')) + eq_(False, is_jid('test&jabber.de')) + eq_(False, is_jid('test@jabber&de')) + eq_(False, is_jid('te@st@jabber.de')) + eq_(False, is_jid('test@jabber..de')) + eq_(False, is_jid('te.st@jabber.de')) + eq_(False, is_jid('te&st@jabber.de')) + eq_(False, is_jid('test@jabber.de/resourceHere.123')) + eq_(False, is_jid('test@jabber.de/resource&&Here123')) diff --git a/tests/TestDecryptorCore.py b/tests/TestDecryptorCore.py index fc4defc..1657076 100644 --- a/tests/TestDecryptorCore.py +++ b/tests/TestDecryptorCore.py @@ -20,6 +20,44 @@ import cryptoim.decryptor_core as decryptor_core import cryptoim.encryptor_core as encryptor_core from nose.tools import ok_, eq_ +import random, string + + +def random_message_range(lo, hi): + length = random.randint(lo, hi) + return ''.join(random.choice(string.printable) for _ in range(length)) + +def random_message(limit): + return random_message_range(1, limit) + +def test_random_encrypt_decrypt(): + test_count = 10 + limit = 100 + + for _ in range(test_count): + originaltext = random_message(limit) + key = random_message(limit) + ciphertext = encryptor_core.encrypt(originaltext, key) + check_decrypt(originaltext, ciphertext, key) + +def check_decrypt(originaltext, ciphertext, key): + decryptedtext = decryptor_core.decrypt(ciphertext, key) + eq_(originaltext, decryptedtext) + +def test_random_key(): + length = 100 + originaltext = 'Secret message!' + key = random_message_range(length, length) + ciphertext = encryptor_core.encrypt(originaltext, key) + check_decrypt(originaltext, ciphertext, key) + +def test_long_string(): + length = 1000 + originaltext = random_message_range(length, length) + key = 'This is a secret key!' + ciphertext = encryptor_core.encrypt(originaltext, key) + check_decrypt(originaltext, ciphertext, key) + def test_decrypt(): """ @@ -35,19 +73,17 @@ def rand_str(limit): from string import ascii_letters from random import choice - rand = "" + rand = '' for _ in range(limit): rand += choice(ascii_letters) return rand - message = "This is a test message" + message = 'This is a test message' key = rand_str(32) ctext = encrypt(message, key) ptext = decrypt(ctext, key) eq_(message, ptext) - #TODO ok_(len(decrypt(message, key)) != 0, "Length wasn't supposed to be 0") - def test_ciphertext_fission(): """ Test for decryptor_core.__ciphertext_fission diff --git a/tests/TestEncryptorCore.py b/tests/TestEncryptorCore.py index c993f7e..e5b0a00 100644 --- a/tests/TestEncryptorCore.py +++ b/tests/TestEncryptorCore.py @@ -25,7 +25,7 @@ def test_encrypt(): Test for encryptor_core.encrypt """ encrypt = encryptor_core.encrypt - message = "This is a test message!" + message = 'This is a test message!' def rand_str(limit): """ @@ -44,12 +44,12 @@ def rand_str(limit): encrypted_str = encrypt(message, key) print(encrypted_str) - ok_(len(encrypted_str) >= 0, "Length wasn't supposed to be 0") + ok_(len(encrypted_str) >= 0, 'Length wasn\'t supposed to be 0') - encrypted1 = encrypt("014730f80ac625fe84f026c60bfd547d", "0000000000000000000000000000000000000000000000000000000000000000") + encrypted1 = encrypt('014730f80ac625fe84f026c60bfd547d', '0000000000000000000000000000000000000000000000000000000000000000') print(encrypted1) - #expected1 = "5c9d844ed46f9885085e5d6a4f94c7d7" - ok_(len(encrypted1) >= 0, "Length wasn't supposed to be 0") + #expected1 = '5c9d844ed46f9885085e5d6a4f94c7d7' + ok_(len(encrypted1) >= 0, 'Length wasn\'t supposed to be 0') # TODO eq_(encrypted1, expected1) @@ -96,8 +96,8 @@ def test_message_fusion(): [0, 0, 0, 0], [0, 0, 0, 0]] - ok_(type(message_fusion(input_mat)) == str, "Not a string!") - ok_(type(message_fusion(input_mat_zeros)) == str, "Zeros - Not a string!") + ok_(type(message_fusion(input_mat)) == str, 'Not a string!') + ok_(type(message_fusion(input_mat_zeros)) == str, 'Zeros - Not a string!') eq_(len(message_fusion(input_mat)), 32) eq_(len(message_fusion(input_mat_zeros)), 32) @@ -115,7 +115,7 @@ def rand_str(limit): from string import ascii_letters from random import choice - rand = "" + rand = '' for _ in range(limit): rand += choice(ascii_letters) return rand diff --git a/tests/TestKeyExchange.py b/tests/TestKeyExchange.py new file mode 100644 index 0000000..b8040b2 --- /dev/null +++ b/tests/TestKeyExchange.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import cryptoim.key_exchange as k_ex +from nose.tools import ok_, eq_ + +def test_generate_random(): + """ + Test for key_exchange.generate_random + """ + generate_random = k_ex.generate_random + + random_numero = generate_random(1, 100) + eq_(type(random_numero), int or long) + ok_(random_numero >= 1 and random_numero <= 100) + +def test_prime_pick(): + """ + Test for key_exchange.prime_prick + """ + prime_pick = k_ex.prime_pick + + prime = prime_pick() + eq_(type(prime), int) + +def test_base_pick(): + """ + Test for key_exchange.base_pick + """ + base_pick = k_ex.base_pick + + base = base_pick() + eq_(type(base), int) + +def test_make_public_key(): + """ + Test for key_exchange.make_public_key + """ + make_public_key = k_ex.make_public_key + prime_pick = k_ex.prime_pick + generate_random = k_ex.generate_random + base_pick = k_ex.base_pick + + prime = prime_pick() + base = base_pick() + rnumber = generate_random(1, 100) + + public_key = make_public_key(prime, base, rnumber) + manual_public_key = (base**rnumber)%prime + + ok_(type(public_key) == int or type(public_key) == long) + eq_(public_key, manual_public_key) + +def test_make_final_key(): + """ + Test for key_exchange.make_final_key + """ + make_public_key = k_ex.make_public_key + prime_pick = k_ex.prime_pick + generate_random = k_ex.generate_random + base_pick = k_ex.base_pick + make_final_key = k_ex.make_final_key + + for i in range(10): + a = generate_random(2, 100) + b = generate_random(2, 100) + p = prime_pick() + g = base_pick() + + A = make_public_key(p, g, a) + B = make_public_key(p, g, b) + + keyA = make_final_key(p, B, a) + keyB = make_final_key(p, A, b) + + ok_(type(keyA) == int or type(keyA) == long) + ok_(type(keyB) == int or type(keyB) == long) + eq_(keyA, keyB) diff --git a/tests/TestXMPP.py b/tests/TestXMPP.py new file mode 100644 index 0000000..81ef6b7 --- /dev/null +++ b/tests/TestXMPP.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + Copyright 2014 CryptoIM Development Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import cryptoim.xmpp as xmpp +from cryptoim.cli import CryptoShell + +from nose.tools import ok_, eq_, nottest +import time + +def init_xmpp_clients(): + """ + Initializes the xmpp_clients and connects them + """ + crypto_shell = CryptoShell('main.cfg') + + xmpp_client = xmpp.XMPPClient('cryptoim@jabber.de', 'crypto_test', crypto_shell) + xmpp_client.connect_server(should_block=False) + + crypto_shell2 = CryptoShell('main.cfg') + xmpp_client2 = xmpp.XMPPClient('cryptoim2@jabber.de', 'crypto_test2', crypto_shell2) + xmpp_client2.connect_server(should_block=False) + + waitForConnection(xmpp_client, True) + waitForConnection(xmpp_client2, True) + return xmpp_client, xmpp_client2 + +def test_connect(): + """ + Test for xmpp.XMPPClient.connect_server and disconnect_server + """ + xmpp_client, xmpp_client2 = init_xmpp_clients() + eq_(xmpp_client.is_connected(), True) + eq_(xmpp_client2.is_connected(), True) + + xmpp_client.disconnect_server() + waitForConnection(xmpp_client, False) + xmpp_client2.disconnect_server() + waitForConnection(xmpp_client2, False) + +def test_send_message(): + """ + Test for xmpp.XMPPClient.send_message + """ + xmpp_client, xmpp_client2 = init_xmpp_clients() + crypto_shell = xmpp_client.xmpp.parent + waitForConnection(xmpp_client, True) + waitForConnection(xmpp_client2, True) + waitForSession(xmpp_client, True) + waitForSession(xmpp_client2, True) + msg = 'Hello, CryptoIM check_send_message!' + recipient = xmpp_client2.xmpp.jid + xmpp_client.xmpp.send_message(mto = recipient, mbody = 'test', mtype = 'error') # Test for dropping non-chat messages + xmpp_client.send_message(recipient, msg) + + while len(crypto_shell.sent_msg_list) < 1: + time.sleep(0.1) + + xmpp_client.disconnect_server() + xmpp_client2.disconnect_server() + waitForConnection(xmpp_client, False) + waitForConnection(xmpp_client2, False) + + # Assert that xmpp_client sent the message (it is bound to be sent after disconnect if it waits) + ok_(1 == len(crypto_shell.sent_msg_list)) + eq_(len(crypto_shell.sent_jid_list), len(crypto_shell.sent_msg_list)) + eq_(msg, crypto_shell.sent_msg_list[-1]) + eq_(recipient, crypto_shell.sent_jid_list[-1]) + +def test_not_connect(): + """ + Failproofing test for xmpp.XMPPClient.connect_server and disconnect_server + """ + + crypto_shell = CryptoShell('main.cfg') + + # Wrong host + xmpp_client = xmpp.XMPPClient('cryptoim@jabber2.de', 'crypto_test', crypto_shell) + assertDisconnect(xmpp_client) + + + # Wrong pass + xmpp_client = xmpp.XMPPClient('cryptoim@jabber.de', 'wrong_pass', crypto_shell) + assertDisconnect(xmpp_client) + + # Wrong name + xmpp_client = xmpp.XMPPClient('cryptoim0@jabber.de', 'crypto_test', crypto_shell) + assertDisconnect(xmpp_client) + +def assertDisconnect(xmpp_client): + """ + Conencts, disconnects and asserts it happened + """ + xmpp_client.connect_server(should_block=False, should_reattempt=False) + + waitForConnection(xmpp_client, False) + + xmpp_client.disconnect_server() + waitForConnection(xmpp_client, False) + +def test_receive_message(): + """ + Test for CryptoXMPP.message (receive message) + """ + xmpp_client, xmpp_client2 = init_xmpp_clients() + crypto_shell2 = xmpp_client2.xmpp.parent + + # Assert connected + waitForConnection(xmpp_client, True) + waitForConnection(xmpp_client2, True) + waitForSession(xmpp_client, True) + waitForSession(xmpp_client2, True) + + # Send and receive message + plaintext = 'Hello, CryptoIM check_receive_message!' + ciphertext = xmpp_client.send_message(xmpp_client2.xmpp.jid, plaintext) + + while len(crypto_shell2.received_msg_list) < 1: + time.sleep(0.1) + + # Disconnect + xmpp_client.disconnect_server() + waitForConnection(xmpp_client, False) + xmpp_client2.disconnect_server() + waitForConnection(xmpp_client2, False) + + # Assert that xmpp_client2 got it (it is bound to be received after disconnect if it waits) + print('Received msg list: ', crypto_shell2.received_msg_list) + print('Received jid list: ', crypto_shell2.received_jid_list) + + ok_(1 == len(crypto_shell2.received_msg_list)) + eq_(len(crypto_shell2.received_jid_list), len(crypto_shell2.received_msg_list)) + eq_(plaintext, crypto_shell2.received_msg_list[-1]) + eq_(xmpp_client.xmpp.jid, crypto_shell2.received_jid_list[-1]) + +def waitForConnection(xmpp_client, should_be_connected): + """ + Waits until a connection is estabilished + """ + while not xmpp_client.is_connected() == should_be_connected: + time.sleep(0.1) + eq_(xmpp_client.is_connected(), should_be_connected) + +def waitForSession(xmpp_client, should_be_in_session): + """ + Waits until a session is estabilished + """ + while not xmpp_client.is_in_session() == should_be_in_session: + time.sleep(0.1) + eq_(xmpp_client.is_in_session(), should_be_in_session) + diff --git a/tests/test_config.cfg b/tests/test_config.cfg new file mode 100644 index 0000000..e7d52c0 --- /dev/null +++ b/tests/test_config.cfg @@ -0,0 +1,7 @@ +[testuser1] +username = testuser1 +host = jabber.de +password = crypto_test + +[friends] +