diff --git a/shadowsocks/crypto/ctypes_libsodium.py b/shadowsocks/crypto/ctypes_libsodium.py index 69b9de9..4845dd1 100644 --- a/shadowsocks/crypto/ctypes_libsodium.py +++ b/shadowsocks/crypto/ctypes_libsodium.py @@ -27,7 +27,7 @@ import logging from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ create_string_buffer, c_void_p -__all__ = ['ciphers'] +__all__ = ['ciphers', 'auths'] libsodium = None loaded = False @@ -39,7 +39,7 @@ BLOCK_SIZE = 64 def load_libsodium(): - global loaded, libsodium, buf + global loaded, libsodium, buf, tag_buf from ctypes.util import find_library for p in ('sodium', 'libsodium'): @@ -62,9 +62,18 @@ def load_libsodium(): c_char_p, c_ulonglong, c_char_p) + libsodium.crypto_onetimeauth.restype = c_int + libsodium.crypto_onetimeauth.argtypes = (c_void_p, c_char_p, + c_ulonglong, c_char_p) + + libsodium.crypto_onetimeauth_verify.restype = c_int + libsodium.crypto_onetimeauth_verify.argtypes = (c_char_p, c_char_p, + c_ulonglong, c_char_p) + libsodium.sodium_init() buf = create_string_buffer(buf_size) + tag_buf = create_string_buffer(16) loaded = True @@ -106,11 +115,32 @@ class Salsa20Crypto(object): return buf.raw[padding:padding + l] +class Poly1305(object): + @staticmethod + def auth(method, key, data): + global tag_buf + if not loaded: + load_libsodium() + libsodium.crypto_onetimeauth(byref(tag_buf), data, len(data), key) + return tag_buf.raw + + @staticmethod + def verify(method, key, data, tag): + if not loaded: + load_libsodium() + r = libsodium.crypto_onetimeauth_verify(tag, data, len(data), key) + return r == 0 + + ciphers = { b'salsa20': (32, 8, Salsa20Crypto), b'chacha20': (32, 8, Salsa20Crypto), } +auths = { + b'poly1305': (32, 16, Poly1305) +} + def test_salsa20(): from shadowsocks.crypto import util diff --git a/shadowsocks/crypto/hmac.py b/shadowsocks/crypto/hmac.py new file mode 100644 index 0000000..0fbf4e9 --- /dev/null +++ b/shadowsocks/crypto/hmac.py @@ -0,0 +1,49 @@ +#!/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import hmac + +from shadowsocks import common + +__all__ = ['auths'] + + +class HMAC(object): + @staticmethod + def auth(method, key, data): + digest = common.to_str(method.replace(b'hmac-', b'')) + return hmac.new(key, data, digest).digest() + + @staticmethod + def verify(method, key, data, tag): + digest = common.to_str(method.replace(b'hmac-', b'')) + t = hmac.new(key, data, digest).digest() + return hmac.compare_digest(t, tag) + + +auths = { + b'hmac-md5': (32, 16, HMAC), + b'hmac-sha256': (32, 32, HMAC), +} diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index ba02101..b6a931d 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -29,17 +29,23 @@ import hashlib import logging from shadowsocks.crypto import m2, rc4_md5, salsa20_ctr,\ - ctypes_openssl, ctypes_libsodium, table + ctypes_openssl, ctypes_libsodium, table, hmac +from shadowsocks import common -method_supported = {} -method_supported.update(rc4_md5.ciphers) -method_supported.update(salsa20_ctr.ciphers) -method_supported.update(ctypes_openssl.ciphers) -method_supported.update(ctypes_libsodium.ciphers) +ciphers_supported = {} +ciphers_supported.update(rc4_md5.ciphers) +ciphers_supported.update(salsa20_ctr.ciphers) +ciphers_supported.update(ctypes_openssl.ciphers) +ciphers_supported.update(ctypes_libsodium.ciphers) # let M2Crypto override ctypes_openssl -method_supported.update(m2.ciphers) -method_supported.update(table.ciphers) +ciphers_supported.update(m2.ciphers) +ciphers_supported.update(table.ciphers) + + +auths_supported = {} +auths_supported.update(hmac.auths) +auths_supported.update(ctypes_libsodium.auths) def random_string(length): @@ -50,22 +56,14 @@ def random_string(length): return os.urandom(length) -cached_keys = {} - - -def try_cipher(key, method=None): +def try_cipher(key, method=None, auth=None): Encryptor(key, method) + auth_create(b'test', key, b'test', auth) def EVP_BytesToKey(password, key_len, iv_len): # equivalent to OpenSSL's EVP_BytesToKey() with count 1 # so that we make the same key and iv as nodejs version - if hasattr(password, 'encode'): - password = password.encode('utf-8') - cached_key = '%s-%d-%d' % (password, key_len, iv_len) - r = cached_keys.get(cached_key, None) - if r: - return r m = [] i = 0 while len(b''.join(m)) < (key_len + iv_len): @@ -79,7 +77,6 @@ def EVP_BytesToKey(password, key_len, iv_len): ms = b''.join(m) key = ms[:key_len] iv = ms[key_len:key_len + iv_len] - cached_keys[cached_key] = (key, iv) return key, iv @@ -102,15 +99,14 @@ class Encryptor(object): def get_method_info(self, method): method = method.lower() - m = method_supported.get(method) + m = ciphers_supported.get(method) return m def iv_len(self): return len(self.cipher_iv) def get_cipher(self, password, method, op, iv): - if hasattr(password, 'encode'): - password = password.encode('utf-8') + password = common.to_bytes(password) m = self._method_info if m[0] > 0: key, iv_ = EVP_BytesToKey(password, m[0], m[1]) @@ -150,7 +146,8 @@ class Encryptor(object): def encrypt_all(password, method, op, data): result = [] method = method.lower() - (key_len, iv_len, m) = method_supported[method] + password = common.to_bytes(password) + (key_len, iv_len, m) = ciphers_supported[method] if key_len > 0: key, _ = EVP_BytesToKey(password, key_len, iv_len) else: @@ -166,6 +163,42 @@ def encrypt_all(password, method, op, data): return b''.join(result) +def auth_create(data, password, iv, method): + if method is None: + return data + # prepend hmac to data + password = common.to_bytes(password) + method = method.lower() + method_info = auths_supported.get(method) + if not method_info: + logging.error('method %s not supported' % method) + sys.exit(1) + key_len, tag_len, m = method_info + key, _ = EVP_BytesToKey(password + iv, key_len, 0) + tag = m.auth(method, key, data) + return tag + data + + +def auth_open(data, password, iv, method): + if method is None: + return data + # verify hmac and remove the hmac or return None + password = common.to_bytes(password) + method = method.lower() + method_info = auths_supported.get(method) + if not method_info: + logging.error('method %s not supported' % method) + sys.exit(1) + key_len, tag_len, m = method_info + key, _ = EVP_BytesToKey(password + iv, key_len, 0) + if len(data) <= tag_len: + return None + result = data[tag_len:] + if not m.verify(method, key, result, data[:tag_len]): + return None + return result + + CIPHERS_TO_TEST = [ b'aes-128-cfb', b'aes-256-cfb', @@ -175,6 +208,13 @@ CIPHERS_TO_TEST = [ b'table', ] +AUTHS_TO_TEST = [ + None, + b'hmac-md5', + b'hmac-sha256', + b'poly1305', +] + def test_encryptor(): from os import urandom @@ -198,6 +238,22 @@ def test_encrypt_all(): assert plain == plain2 +def test_auth(): + from os import urandom + plain = urandom(10240) + for method in AUTHS_TO_TEST: + logging.warn(method) + boxed = auth_create(plain, b'key', b'iv', method) + unboxed = auth_open(boxed, b'key', b'iv', method) + assert plain == unboxed + if method is not None: + b = common.ord(boxed[0]) + b ^= 1 + attack = common.chr(b) + boxed[1:] + assert auth_open(attack, b'key', b'iv', method) is None + + if __name__ == '__main__': test_encrypt_all() test_encryptor() + test_auth() diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 994b6d8..44e9fca 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -49,7 +49,7 @@ def main(): utils.print_shadowsocks() - encrypt.try_cipher(config['password'], config['method']) + encrypt.try_cipher(config['password'], config['method'], config['auth']) try: logging.info("starting local at %s:%d" % diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 8eed4ad..0ddcda5 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -57,7 +57,7 @@ def main(): else: config['port_password'][str(server_port)] = config['password'] - encrypt.try_cipher(config['password'], config['method']) + encrypt.try_cipher(config['password'], config['method'], config['auth']) tcp_servers = [] udp_servers = [] dns_resolver = asyncdns.DNSResolver()