diff --git a/.travis.yml b/.travis.yml index 1d716f2..b10edd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ script: - pep8 . - python tests/test.py -c tests/table.json - python tests/test.py -c tests/aes.json + - python tests/test.py -c tests/aes-cfb1.json + - python tests/test.py -c tests/aes-cfb8.json - python tests/test.py -c tests/rc4-md5.json - python tests/test.py -c tests/salsa20.json - python tests/test.py -c tests/server-multi-ports.json diff --git a/setup.py b/setup.py index 460dc69..8ddf3c5 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( author='clowwindy', author_email='clowwindy42@gmail.com', url='https://github.com/clowwindy/shadowsocks', - packages=['shadowsocks'], + packages=['shadowsocks', 'shadowsocks.crypto'], package_data={ 'shadowsocks': ['README.rst', 'LICENSE'] }, diff --git a/shadowsocks/crypto/__init__.py b/shadowsocks/crypto/__init__.py new file mode 100644 index 0000000..bd3a926 --- /dev/null +++ b/shadowsocks/crypto/__init__.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# 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/shadowsocks/crypto/ctypes_openssl.py b/shadowsocks/crypto/ctypes_openssl.py new file mode 100644 index 0000000..8cbd9e5 --- /dev/null +++ b/shadowsocks/crypto/ctypes_openssl.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# 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. + +import logging + +__all__ = ['ciphers'] + +loaded = False + + +def load_openssl(): + global loaded, libcrypto, CDLL, c_char_p, c_int, c_long, byref,\ + create_string_buffer, c_void_p, buf + from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ + create_string_buffer, c_void_p + from ctypes.util import find_library + libcrypto_path = find_library('crypto') + logging.info('loading libcrypto from %s', libcrypto_path) + libcrypto = CDLL(libcrypto_path) + libcrypto.EVP_get_cipherbyname.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p) + + libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p, c_char_p, c_int) + + libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, + c_char_p, c_int) + + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + + buf = create_string_buffer(65536) + loaded = True + + +class CtypesCrypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_openssl() + self._ctx = None + cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libcrypto' % cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + self._ctx = libcrypto.EVP_CIPHER_CTX_new(cipher, None, + key_ptr, iv_ptr) + if not self._ctx: + raise Exception('can not create cipher context') + r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def update(self, data): + cipher_out_len = c_long(0) + libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), + len(data)) + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + libcrypto.EVP_CIPHER_CTX_free(self._ctx) + + +ciphers = { + 'aes-128-cfb8': (16, 16, CtypesCrypto), + 'aes-192-cfb8': (24, 16, CtypesCrypto), + 'aes-256-cfb8': (32, 16, CtypesCrypto), + 'aes-128-cfb1': (16, 16, CtypesCrypto), + 'aes-192-cfb1': (24, 16, CtypesCrypto), + 'aes-256-cfb1': (32, 16, CtypesCrypto), +} diff --git a/shadowsocks/crypto/m2.py b/shadowsocks/crypto/m2.py new file mode 100644 index 0000000..cff3682 --- /dev/null +++ b/shadowsocks/crypto/m2.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# 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. + +import sys +import logging + +__all__ = ['ciphers'] + +has_m2 = True +try: + __import__('M2Crypto') +except ImportError: + has_m2 = False + + +def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, + padding=1): + + import M2Crypto.EVP + return M2Crypto.EVP.Cipher('rc4', key, iv, op, key_as_bytes=0, + d='md5', salt=None, i=1, padding=1) + + +def err(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1): + logging.error(('M2Crypto is required to use %s, please run' + ' `apt-get install python-m2crypto`') % alg) + sys.exit(1) + + +if not has_m2: + create_cipher = err + +ciphers = { + 'aes-128-cfb': (16, 16, create_cipher), + 'aes-192-cfb': (24, 16, create_cipher), + 'aes-256-cfb': (32, 16, create_cipher), + 'bf-cfb': (16, 8, create_cipher), + 'camellia-128-cfb': (16, 16, create_cipher), + 'camellia-192-cfb': (24, 16, create_cipher), + 'camellia-256-cfb': (32, 16, create_cipher), + 'cast5-cfb': (16, 8, create_cipher), + 'des-cfb': (8, 8, create_cipher), + 'idea-cfb': (16, 8, create_cipher), + 'rc2-cfb': (16, 8, create_cipher), + 'rc4': (16, 0, create_cipher), + 'seed-cfb': (16, 16, create_cipher), +} diff --git a/shadowsocks/encrypt_rc4_md5.py b/shadowsocks/crypto/rc4_md5.py similarity index 94% rename from shadowsocks/encrypt_rc4_md5.py rename to shadowsocks/crypto/rc4_md5.py index 8295153..571f666 100644 --- a/shadowsocks/encrypt_rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -20,9 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + import hashlib +__all__ = ['ciphers'] + + def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1): md5 = hashlib.md5() @@ -33,3 +37,8 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, import M2Crypto.EVP return M2Crypto.EVP.Cipher('rc4', rc4_key, '', op, key_as_bytes=0, d='md5', salt=None, i=1, padding=1) + + +ciphers = { + 'rc4-md5': (16, 16, create_cipher), +} diff --git a/shadowsocks/encrypt_salsa20.py b/shadowsocks/crypto/salsa20_ctr.py similarity index 98% rename from shadowsocks/encrypt_salsa20.py rename to shadowsocks/crypto/salsa20_ctr.py index be8cb8b..22a8f35 100644 --- a/shadowsocks/encrypt_salsa20.py +++ b/shadowsocks/crypto/salsa20_ctr.py @@ -149,5 +149,10 @@ def test(): assert ''.join(results) == plain +ciphers = { + 'salsa20-ctr': (32, 8, Salsa20Cipher), +} + + if __name__ == '__main__': test() diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index d144e64..9675e85 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -26,8 +26,19 @@ import hashlib import string import struct import logging -import encrypt_salsa20 -import encrypt_rc4_md5 +import crypto.m2 +import crypto.rc4_md5 +import crypto.salsa20_ctr +import crypto.ctypes_openssl + + +method_supported = { +} + +method_supported.update(crypto.m2.ciphers) +method_supported.update(crypto.rc4_md5.ciphers) +method_supported.update(crypto.salsa20_ctr.ciphers) +method_supported.update(crypto.ctypes_openssl.ciphers) def random_string(length): @@ -57,13 +68,6 @@ def get_table(key): def init_table(key, method=None): if method is not None and method == 'table': method = None - if method: - try: - __import__('M2Crypto') - except ImportError: - logging.error(('M2Crypto is required to use %s, please run' - ' `apt-get install python-m2crypto`') % method) - sys.exit(1) if not method: if key in cached_tables: return cached_tables[key] @@ -103,25 +107,6 @@ def EVP_BytesToKey(password, key_len, iv_len): return (key, iv) -method_supported = { - 'aes-128-cfb': (16, 16), - 'aes-192-cfb': (24, 16), - 'aes-256-cfb': (32, 16), - 'bf-cfb': (16, 8), - 'camellia-128-cfb': (16, 16), - 'camellia-192-cfb': (24, 16), - 'camellia-256-cfb': (32, 16), - 'cast5-cfb': (16, 8), - 'des-cfb': (8, 8), - 'idea-cfb': (16, 8), - 'rc2-cfb': (16, 8), - 'rc4': (16, 0), - 'rc4-md5': (16, 16), - 'seed-cfb': (16, 16), - 'salsa20-ctr': (32, 8), -} - - class Encryptor(object): def __init__(self, key, method=None): if method == 'table': @@ -138,7 +123,7 @@ class Encryptor(object): self.encrypt_table, self.decrypt_table = init_table(key) self.cipher = None - def get_cipher_len(self, method): + def get_cipher_param(self, method): method = method.lower() m = method_supported.get(method, None) return m @@ -149,7 +134,7 @@ class Encryptor(object): def get_cipher(self, password, method, op, iv=None): password = password.encode('utf-8') method = method.lower() - m = self.get_cipher_len(method) + m = self.get_cipher_param(method) if m: key, iv_ = EVP_BytesToKey(password, m[0], m[1]) if iv is None: @@ -158,15 +143,7 @@ class Encryptor(object): if op == 1: # this iv is for cipher not decipher self.cipher_iv = iv[:m[1]] - if method == 'salsa20-ctr': - return encrypt_salsa20.Salsa20Cipher(method, key, iv, op) - elif method == 'rc4-md5': - return encrypt_rc4_md5.create_cipher(method, key, iv, op) - else: - import M2Crypto.EVP - return M2Crypto.EVP.Cipher(method.replace('-', '_'), key, iv, - op, key_as_bytes=0, d='md5', - salt=None, i=1, padding=1) + return m[2](method, key, iv, op) logging.error('method %s not supported' % method) sys.exit(1) @@ -190,7 +167,7 @@ class Encryptor(object): return string.translate(buf, self.decrypt_table) else: if self.decipher is None: - decipher_iv_len = self.get_cipher_len(self.method)[1] + decipher_iv_len = self.get_cipher_param(self.method)[1] decipher_iv = buf[:decipher_iv_len] self.decipher = self.get_cipher(self.key, self.method, 0, iv=decipher_iv) @@ -210,10 +187,9 @@ def encrypt_all(password, method, op, data): else: return string.translate(data, decrypt_table) else: - import M2Crypto.EVP result = [] method = method.lower() - (key_len, iv_len) = method_supported[method] + (key_len, iv_len, m) = method_supported[method] (key, _) = EVP_BytesToKey(password, key_len, iv_len) if op: iv = random_string(iv_len) @@ -221,13 +197,6 @@ def encrypt_all(password, method, op, data): else: iv = data[:iv_len] data = data[iv_len:] - if method == 'salsa20-ctr': - cipher = encrypt_salsa20.Salsa20Cipher(method, key, iv, op) - elif method == 'rc4-md5': - cipher = encrypt_rc4_md5.create_cipher(method, key, iv, op) - else: - cipher = M2Crypto.EVP.Cipher(method.replace('-', '_'), key, iv, - op, key_as_bytes=0, d='md5', - salt=None, i=1, padding=1) + cipher = m(method, key, iv, op) result.append(cipher.update(data)) return ''.join(result) diff --git a/tests/aes-cfb1.json b/tests/aes-cfb1.json new file mode 100644 index 0000000..40d0b21 --- /dev/null +++ b/tests/aes-cfb1.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb1", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-cfb8.json b/tests/aes-cfb8.json new file mode 100644 index 0000000..fb7014b --- /dev/null +++ b/tests/aes-cfb8.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb8", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/test.py b/tests/test.py index 18aff4b..632312f 100755 --- a/tests/test.py +++ b/tests/test.py @@ -11,8 +11,8 @@ from subprocess import Popen, PIPE sys.path.insert(0, './') if 'salsa20' in sys.argv[-1]: - from shadowsocks import encrypt_salsa20 - encrypt_salsa20.test() + from shadowsocks.crypto import salsa20_ctr + salsa20_ctr.test() print 'encryption test passed' p1 = Popen(['python', 'shadowsocks/server.py', '-c', sys.argv[-1]], stdin=PIPE,