Add AEAD ciphers support (#775)
* Add AEAD ciphers support, add manger api * fix test_encrypt_all * Add manager api requirements * #775 fix UDP decrypt_all issue * fix udp replay: decrypt_all return a list remove manager api * fix indent according pep8 * remove abc requirement * remove unused import * fix pep8 format * fix test_aes_256_gcm()
This commit is contained in:
parent
c4731de532
commit
445a3c9c7e
15 changed files with 1086 additions and 80 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -29,13 +29,17 @@ htmlcov
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
tags
|
||||||
|
|
||||||
#Emacs
|
#Emacs
|
||||||
.#*
|
.#*
|
||||||
|
venv/
|
||||||
|
|
||||||
#vscode
|
# VS-code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Pycharm
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
|
|
||||||
#ss
|
#ss
|
||||||
config.json
|
config.json
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f:
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="shadowsocks",
|
name="shadowsocks",
|
||||||
version="2.9.1",
|
version="3.0.0",
|
||||||
license='http://www.apache.org/licenses/LICENSE-2.0',
|
license='http://www.apache.org/licenses/LICENSE-2.0',
|
||||||
description="A fast tunnel proxy that help you get through firewalls",
|
description="A fast tunnel proxy that help you get through firewalls",
|
||||||
author='clowwindy',
|
author='clowwindy',
|
||||||
|
|
285
shadowsocks/crypto/aead.py
Normal file
285
shadowsocks/crypto/aead.py
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Void Copyright NO ONE
|
||||||
|
#
|
||||||
|
# Void License
|
||||||
|
#
|
||||||
|
# The code belongs to no one. Do whatever you want.
|
||||||
|
# Forget about boring open source license.
|
||||||
|
#
|
||||||
|
# AEAD cipher for shadowsocks
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function, \
|
||||||
|
with_statement
|
||||||
|
|
||||||
|
from ctypes import create_string_buffer
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from struct import pack, unpack
|
||||||
|
|
||||||
|
from shadowsocks.crypto import hkdf
|
||||||
|
from shadowsocks.common import ord, chr
|
||||||
|
|
||||||
|
|
||||||
|
EVP_CTRL_GCM_SET_IVLEN = 0x9
|
||||||
|
EVP_CTRL_GCM_GET_TAG = 0x10
|
||||||
|
EVP_CTRL_GCM_SET_TAG = 0x11
|
||||||
|
EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
|
||||||
|
EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG
|
||||||
|
EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG
|
||||||
|
|
||||||
|
EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
|
||||||
|
EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG
|
||||||
|
EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG
|
||||||
|
|
||||||
|
AEAD_MSG_LEN_UNKNOWN = 0
|
||||||
|
AEAD_CHUNK_SIZE_LEN = 2
|
||||||
|
AEAD_CHUNK_SIZE_MASK = 0x3FFF
|
||||||
|
|
||||||
|
CIPHER_NONCE_LEN = {
|
||||||
|
'aes-128-gcm': 12,
|
||||||
|
'aes-192-gcm': 12,
|
||||||
|
'aes-256-gcm': 12,
|
||||||
|
'chacha20-poly1305': 12,
|
||||||
|
'chacha20-ietf-poly1305': 12,
|
||||||
|
'xchacha20-ietf-poly1305': 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
CIPHER_TAG_LEN = {
|
||||||
|
'aes-128-gcm': 16,
|
||||||
|
'aes-192-gcm': 16,
|
||||||
|
'aes-256-gcm': 16,
|
||||||
|
'chacha20-poly1305': 16,
|
||||||
|
'chacha20-ietf-poly1305': 16,
|
||||||
|
'xchacha20-ietf-poly1305': 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBKEY_INFO = b"ss-subkey"
|
||||||
|
|
||||||
|
|
||||||
|
def nonce_increment(nonce, nlen):
|
||||||
|
"""
|
||||||
|
Increase nonce by 1 in little endian
|
||||||
|
From libsodium sodium_increment():
|
||||||
|
for (; i < nlen; i++) {
|
||||||
|
c += (uint_fast16_t) n[i];
|
||||||
|
n[i] = (unsigned char) c;
|
||||||
|
c >>= 8;
|
||||||
|
}
|
||||||
|
:param nonce: string_buffer nonce
|
||||||
|
:param nlen: nonce length
|
||||||
|
:return: nonce plus by 1
|
||||||
|
"""
|
||||||
|
c = 1
|
||||||
|
i = 0
|
||||||
|
# n = create_string_buffer(nlen)
|
||||||
|
while i < nlen:
|
||||||
|
c += ord(nonce[i])
|
||||||
|
nonce[i] = chr(c & 0xFF)
|
||||||
|
c >>= 8
|
||||||
|
i += 1
|
||||||
|
return # n.raw
|
||||||
|
|
||||||
|
|
||||||
|
class AeadCryptoBase(object):
|
||||||
|
"""
|
||||||
|
Handles basic aead process of shadowsocks protocol
|
||||||
|
|
||||||
|
TCP Chunk (after encryption, *ciphertext*)
|
||||||
|
+--------------+---------------+--------------+------------+
|
||||||
|
| *DataLen* | DataLen_TAG | *Data* | Data_TAG |
|
||||||
|
+--------------+---------------+--------------+------------+
|
||||||
|
| 2 | Fixed | Variable | Fixed |
|
||||||
|
+--------------+---------------+--------------+------------+
|
||||||
|
|
||||||
|
UDP (after encryption, *ciphertext*)
|
||||||
|
+--------+-----------+-----------+
|
||||||
|
| NONCE | *Data* | Data_TAG |
|
||||||
|
+-------+-----------+-----------+
|
||||||
|
| Fixed | Variable | Fixed |
|
||||||
|
+--------+-----------+-----------+
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cipher_name, key, iv, op):
|
||||||
|
self._op = int(op)
|
||||||
|
self._salt = iv
|
||||||
|
self._nlen = CIPHER_NONCE_LEN[cipher_name]
|
||||||
|
self._nonce = create_string_buffer(self._nlen)
|
||||||
|
self._tlen = CIPHER_TAG_LEN[cipher_name]
|
||||||
|
|
||||||
|
crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1)
|
||||||
|
self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key))
|
||||||
|
# _chunk['mlen']:
|
||||||
|
# -1, waiting data len header
|
||||||
|
# n, n > 0, waiting data
|
||||||
|
self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''}
|
||||||
|
|
||||||
|
self.encrypt_once = self.aead_encrypt
|
||||||
|
self.decrypt_once = self.aead_decrypt
|
||||||
|
|
||||||
|
def cipher_ctx_init(self):
|
||||||
|
"""
|
||||||
|
Increase nonce to make it unique for the same key
|
||||||
|
:return: void
|
||||||
|
"""
|
||||||
|
nonce_increment(self._nonce, self._nlen)
|
||||||
|
# print("".join("%02x" % ord(b) for b in self._nonce))
|
||||||
|
|
||||||
|
def aead_encrypt(self, data):
|
||||||
|
"""
|
||||||
|
Encrypt data with authenticate tag
|
||||||
|
|
||||||
|
:param data: plain text
|
||||||
|
:return: cipher text with tag
|
||||||
|
"""
|
||||||
|
raise Exception("Must implement aead_encrypt method")
|
||||||
|
|
||||||
|
def encrypt_chunk(self, data):
|
||||||
|
"""
|
||||||
|
Encrypt a chunk for TCP chunks
|
||||||
|
|
||||||
|
:param data: str
|
||||||
|
:return: (str, int)
|
||||||
|
"""
|
||||||
|
plen = len(data)
|
||||||
|
l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2
|
||||||
|
|
||||||
|
# network byte order
|
||||||
|
ctext = self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))
|
||||||
|
if len(ctext) != AEAD_CHUNK_SIZE_LEN + self._tlen:
|
||||||
|
raise Exception("data length invalid")
|
||||||
|
|
||||||
|
self.cipher_ctx_init()
|
||||||
|
ctext += self.aead_encrypt(data)
|
||||||
|
if len(ctext) != l:
|
||||||
|
raise Exception("data length invalid")
|
||||||
|
|
||||||
|
self.cipher_ctx_init()
|
||||||
|
return ctext, l
|
||||||
|
|
||||||
|
def encrypt(self, data):
|
||||||
|
"""
|
||||||
|
Encrypt data, for TCP divided into chunks
|
||||||
|
For UDP data, call aead_encrypt instead
|
||||||
|
|
||||||
|
:param data: str data bytes
|
||||||
|
:return: str encrypted data
|
||||||
|
"""
|
||||||
|
plen = len(data)
|
||||||
|
if plen <= AEAD_CHUNK_SIZE_MASK:
|
||||||
|
ctext, _ = self.encrypt_chunk(data)
|
||||||
|
return ctext
|
||||||
|
ctext, clen = b"", 0
|
||||||
|
while plen > 0:
|
||||||
|
mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \
|
||||||
|
else AEAD_CHUNK_SIZE_MASK
|
||||||
|
r, l = self.encrypt_chunk(data[:mlen])
|
||||||
|
ctext += r
|
||||||
|
clen += l
|
||||||
|
data = data[mlen:]
|
||||||
|
plen -= mlen
|
||||||
|
|
||||||
|
return ctext
|
||||||
|
|
||||||
|
def aead_decrypt(self, data):
|
||||||
|
"""
|
||||||
|
Decrypt data and authenticate tag
|
||||||
|
|
||||||
|
:param data: str cipher text with tag
|
||||||
|
:return: str plain text
|
||||||
|
"""
|
||||||
|
raise Exception("Must implement aead_decrypt method")
|
||||||
|
|
||||||
|
def decrypt_chunk_size(self, data):
|
||||||
|
"""
|
||||||
|
Decrypt chunk size
|
||||||
|
|
||||||
|
:param data: str encrypted msg
|
||||||
|
:return: (int, str) msg length and remaining encrypted data
|
||||||
|
"""
|
||||||
|
if self._chunk['mlen'] > 0:
|
||||||
|
return self._chunk['mlen'], data
|
||||||
|
data = self._chunk['data'] + data
|
||||||
|
self._chunk['data'] = b""
|
||||||
|
|
||||||
|
hlen = AEAD_CHUNK_SIZE_LEN + self._tlen
|
||||||
|
if hlen > len(data):
|
||||||
|
self._chunk['data'] = data
|
||||||
|
return 0, b""
|
||||||
|
plen = self.aead_decrypt(data[:hlen])
|
||||||
|
plen, = unpack("!H", plen)
|
||||||
|
if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0:
|
||||||
|
raise Exception('Invalid message length')
|
||||||
|
|
||||||
|
self.cipher_ctx_init()
|
||||||
|
return plen, data[hlen:]
|
||||||
|
|
||||||
|
def decrypt_chunk_payload(self, plen, data):
|
||||||
|
"""
|
||||||
|
Decrypted encrypted msg payload
|
||||||
|
|
||||||
|
:param plen: int payload length
|
||||||
|
:param data: str encrypted data
|
||||||
|
:return: (str, str) plain text and remaining encrypted data
|
||||||
|
"""
|
||||||
|
data = self._chunk['data'] + data
|
||||||
|
if len(data) < plen + self._tlen:
|
||||||
|
self._chunk['mlen'] = plen
|
||||||
|
self._chunk['data'] = data
|
||||||
|
return b"", b""
|
||||||
|
self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN
|
||||||
|
self._chunk['data'] = b""
|
||||||
|
|
||||||
|
plaintext = self.aead_decrypt(data[:plen + self._tlen])
|
||||||
|
|
||||||
|
if len(plaintext) != plen:
|
||||||
|
raise Exception("plaintext length invalid")
|
||||||
|
|
||||||
|
self.cipher_ctx_init()
|
||||||
|
|
||||||
|
return plaintext, data[plen + self._tlen:]
|
||||||
|
|
||||||
|
def decrypt_chunk(self, data):
|
||||||
|
"""
|
||||||
|
Decrypt a TCP chunk
|
||||||
|
|
||||||
|
:param data: str encrypted msg
|
||||||
|
:return: (str, str) decrypted msg and remaining encrypted data
|
||||||
|
"""
|
||||||
|
plen, data = self.decrypt_chunk_size(data)
|
||||||
|
if plen <= 0:
|
||||||
|
return b"", b""
|
||||||
|
return self.decrypt_chunk_payload(plen, data)
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
"""
|
||||||
|
Decrypt data for TCP data divided into chunks
|
||||||
|
For UDP data, call aead_decrypt instead
|
||||||
|
|
||||||
|
:param data: str
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
ptext, left = self.decrypt_chunk(data)
|
||||||
|
while len(left) > 0:
|
||||||
|
pnext, left = self.decrypt_chunk(left)
|
||||||
|
ptext += pnext
|
||||||
|
return ptext
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonce_increment():
|
||||||
|
buf = create_string_buffer(12)
|
||||||
|
print("".join("%02x" % ord(b) for b in buf))
|
||||||
|
nonce_increment(buf, 12)
|
||||||
|
nonce_increment(buf, 12)
|
||||||
|
nonce_increment(buf, 12)
|
||||||
|
nonce_increment(buf, 12)
|
||||||
|
print("".join("%02x" % ord(b) for b in buf))
|
||||||
|
for i in range(256):
|
||||||
|
nonce_increment(buf, 12)
|
||||||
|
print("".join("%02x" % ord(b) for b in buf))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_nonce_increment()
|
98
shadowsocks/crypto/hkdf.py
Normal file
98
shadowsocks/crypto/hkdf.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Void Copyright NO ONE
|
||||||
|
#
|
||||||
|
# Void License
|
||||||
|
#
|
||||||
|
# The code belongs to no one. Do whatever you want.
|
||||||
|
# Forget about boring open source license.
|
||||||
|
#
|
||||||
|
# HKDF for AEAD ciphers
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
def buffer(x):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256):
|
||||||
|
"""
|
||||||
|
Extract a pseudorandom key suitable for use with hkdf_expand
|
||||||
|
from the input_key_material and a salt using HMAC with the
|
||||||
|
provided hash (default SHA-256).
|
||||||
|
|
||||||
|
salt should be a random, application-specific byte string. If
|
||||||
|
salt is None or the empty string, an all-zeros string of the same
|
||||||
|
length as the hash's block size will be used instead per the RFC.
|
||||||
|
|
||||||
|
See the HKDF draft RFC and paper for usage notes.
|
||||||
|
"""
|
||||||
|
hash_len = algorithm().digest_size
|
||||||
|
if salt is None or len(salt) == 0:
|
||||||
|
salt = bytearray((0,) * hash_len)
|
||||||
|
return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\
|
||||||
|
.digest()
|
||||||
|
|
||||||
|
|
||||||
|
def hkdf_expand(pseudo_random_key, info=b"", length=32,
|
||||||
|
algorithm=hashlib.sha256):
|
||||||
|
"""
|
||||||
|
Expand `pseudo_random_key` and `info` into a key of length `bytes` using
|
||||||
|
HKDF's expand function based on HMAC with the provided hash (default
|
||||||
|
SHA-256). See the HKDF draft RFC and paper for usage notes.
|
||||||
|
"""
|
||||||
|
hash_len = algorithm().digest_size
|
||||||
|
length = int(length)
|
||||||
|
if length > 255 * hash_len:
|
||||||
|
raise Exception("Cannot expand to more than 255 * %d = %d "
|
||||||
|
"bytes using the specified hash function" %
|
||||||
|
(hash_len, 255 * hash_len))
|
||||||
|
blocks_needed = length // hash_len \
|
||||||
|
+ (0 if length % hash_len == 0 else 1) # ceil
|
||||||
|
okm = b""
|
||||||
|
output_block = b""
|
||||||
|
for counter in range(blocks_needed):
|
||||||
|
output_block = hmac.new(
|
||||||
|
pseudo_random_key,
|
||||||
|
buffer(output_block + info + bytearray((counter + 1,))),
|
||||||
|
algorithm
|
||||||
|
).digest()
|
||||||
|
okm += output_block
|
||||||
|
return okm[:length]
|
||||||
|
|
||||||
|
|
||||||
|
class Hkdf(object):
|
||||||
|
"""
|
||||||
|
Wrapper class for HKDF extract and expand functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, salt, input_key_material, algorithm=hashlib.sha256):
|
||||||
|
"""
|
||||||
|
Extract a pseudorandom key from `salt` and `input_key_material`
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
See the HKDF draft RFC for guidance on setting these values.
|
||||||
|
The constructor optionally takes a `algorithm` argument defining
|
||||||
|
the hash function use, defaulting to hashlib.sha256.
|
||||||
|
"""
|
||||||
|
self._hash = algorithm
|
||||||
|
self._prk = hkdf_extract(salt, input_key_material, self._hash)
|
||||||
|
|
||||||
|
def expand(self, info, length=32):
|
||||||
|
"""
|
||||||
|
Generate output key material based on an `info` value
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- info - context to generate the OKM
|
||||||
|
- length - length in bytes of the key to generate
|
||||||
|
|
||||||
|
See the HKDF draft RFC for guidance.
|
||||||
|
"""
|
||||||
|
return hkdf_expand(self._prk, info, length, self._hash)
|
|
@ -22,6 +22,8 @@ from ctypes import c_char_p, c_int, c_long, byref,\
|
||||||
|
|
||||||
from shadowsocks import common
|
from shadowsocks import common
|
||||||
from shadowsocks.crypto import util
|
from shadowsocks.crypto import util
|
||||||
|
from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \
|
||||||
|
nonce_increment, EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG
|
||||||
|
|
||||||
__all__ = ['ciphers']
|
__all__ = ['ciphers']
|
||||||
|
|
||||||
|
@ -30,6 +32,8 @@ loaded = False
|
||||||
|
|
||||||
buf_size = 2048
|
buf_size = 2048
|
||||||
|
|
||||||
|
CIPHER_ENC_UNCHANGED = -1
|
||||||
|
|
||||||
|
|
||||||
def load_openssl():
|
def load_openssl():
|
||||||
global loaded, libcrypto, buf, ctx_cleanup
|
global loaded, libcrypto, buf, ctx_cleanup
|
||||||
|
@ -45,10 +49,13 @@ def load_openssl():
|
||||||
|
|
||||||
libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_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)
|
c_char_p, c_char_p, c_int)
|
||||||
|
libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = (c_void_p, c_int, c_int, c_void_p)
|
||||||
|
|
||||||
libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
|
libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
|
||||||
c_char_p, c_int)
|
c_char_p, c_int)
|
||||||
|
|
||||||
|
libcrypto.EVP_CipherFinal_ex.argtypes = (c_void_p, c_void_p, c_void_p)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
|
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
|
||||||
ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup
|
ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup
|
||||||
|
@ -64,7 +71,7 @@ def load_openssl():
|
||||||
|
|
||||||
|
|
||||||
def load_cipher(cipher_name):
|
def load_cipher(cipher_name):
|
||||||
func_name = 'EVP_' + cipher_name.replace('-', '_')
|
func_name = b'EVP_' + cipher_name.replace(b'-', b'_')
|
||||||
if bytes != str:
|
if bytes != str:
|
||||||
func_name = str(func_name, 'utf-8')
|
func_name = str(func_name, 'utf-8')
|
||||||
cipher = getattr(libcrypto, func_name, None)
|
cipher = getattr(libcrypto, func_name, None)
|
||||||
|
@ -74,9 +81,13 @@ def load_cipher(cipher_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class OpenSSLCrypto(object):
|
class OpenSSLCryptoBase(object):
|
||||||
def __init__(self, cipher_name, key, iv, op):
|
"""
|
||||||
|
OpenSSL crypto base class
|
||||||
|
"""
|
||||||
|
def __init__(self, cipher_name):
|
||||||
self._ctx = None
|
self._ctx = None
|
||||||
|
self._cipher = None
|
||||||
if not loaded:
|
if not loaded:
|
||||||
load_openssl()
|
load_openssl()
|
||||||
cipher_name = common.to_bytes(cipher_name)
|
cipher_name = common.to_bytes(cipher_name)
|
||||||
|
@ -85,26 +96,30 @@ class OpenSSLCrypto(object):
|
||||||
cipher = load_cipher(cipher_name)
|
cipher = load_cipher(cipher_name)
|
||||||
if not cipher:
|
if not cipher:
|
||||||
raise Exception('cipher %s not found in libcrypto' % cipher_name)
|
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()
|
self._ctx = libcrypto.EVP_CIPHER_CTX_new()
|
||||||
|
self._cipher = cipher
|
||||||
if not self._ctx:
|
if not self._ctx:
|
||||||
raise Exception('can not create cipher context')
|
raise Exception('can not create cipher context')
|
||||||
r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None,
|
|
||||||
key_ptr, iv_ptr, c_int(op))
|
self.encrypt_once = self.update
|
||||||
if not r:
|
self.decrypt_once = self.update
|
||||||
self.clean()
|
|
||||||
raise Exception('can not initialize cipher context')
|
|
||||||
|
|
||||||
def update(self, data):
|
def update(self, data):
|
||||||
|
"""
|
||||||
|
Encrypt/decrypt data
|
||||||
|
:param data: str
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
global buf_size, buf
|
global buf_size, buf
|
||||||
cipher_out_len = c_long(0)
|
cipher_out_len = c_long(0)
|
||||||
l = len(data)
|
l = len(data)
|
||||||
if buf_size < l:
|
if buf_size < l:
|
||||||
buf_size = l * 2
|
buf_size = l * 2
|
||||||
buf = create_string_buffer(buf_size)
|
buf = create_string_buffer(buf_size)
|
||||||
libcrypto.EVP_CipherUpdate(self._ctx, byref(buf),
|
libcrypto.EVP_CipherUpdate(
|
||||||
byref(cipher_out_len), c_char_p(data), l)
|
self._ctx, byref(buf),
|
||||||
|
byref(cipher_out_len), c_char_p(data), l
|
||||||
|
)
|
||||||
# buf is copied to a str object when we access buf.raw
|
# buf is copied to a str object when we access buf.raw
|
||||||
return buf.raw[:cipher_out_len.value]
|
return buf.raw[:cipher_out_len.value]
|
||||||
|
|
||||||
|
@ -117,39 +132,190 @@ class OpenSSLCrypto(object):
|
||||||
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
|
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase):
|
||||||
|
"""
|
||||||
|
Implement OpenSSL Aead mode: gcm, ocb
|
||||||
|
"""
|
||||||
|
def __init__(self, cipher_name, key, iv, op):
|
||||||
|
super(OpenSSLAeadCrypto, self).__init__(cipher_name)
|
||||||
|
AeadCryptoBase.__init__(self, cipher_name, key, iv, op)
|
||||||
|
|
||||||
|
r = libcrypto.EVP_CipherInit_ex(
|
||||||
|
self._ctx,
|
||||||
|
self._cipher, None,
|
||||||
|
None, None, c_int(op)
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
self.clean()
|
||||||
|
raise Exception('can not initialize cipher context')
|
||||||
|
|
||||||
|
r = libcrypto.EVP_CIPHER_CTX_ctrl(
|
||||||
|
self._ctx,
|
||||||
|
c_int(EVP_CTRL_AEAD_SET_IVLEN),
|
||||||
|
c_int(self._nlen),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
raise Exception('Set ivlen failed')
|
||||||
|
|
||||||
|
self.cipher_ctx_init()
|
||||||
|
|
||||||
|
def cipher_ctx_init(self):
|
||||||
|
"""
|
||||||
|
Need init cipher context after EVP_CipherFinal_ex to reuse context
|
||||||
|
:return: void
|
||||||
|
"""
|
||||||
|
key_ptr = c_char_p(self._skey)
|
||||||
|
iv_ptr = c_char_p(self._nonce.raw)
|
||||||
|
|
||||||
|
r = libcrypto.EVP_CipherInit_ex(
|
||||||
|
self._ctx,
|
||||||
|
None, None,
|
||||||
|
key_ptr, iv_ptr,
|
||||||
|
c_int(CIPHER_ENC_UNCHANGED)
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
self.clean()
|
||||||
|
raise Exception('can not initialize cipher context')
|
||||||
|
|
||||||
|
nonce_increment(self._nonce, self._nlen)
|
||||||
|
# print("".join("%02x" % ord(b) for b in self._nonce))
|
||||||
|
|
||||||
|
def set_tag(self, tag):
|
||||||
|
"""
|
||||||
|
Set tag before decrypt any data (update)
|
||||||
|
:param tag: authenticated tag
|
||||||
|
:return: void
|
||||||
|
"""
|
||||||
|
tag_len = self._tlen
|
||||||
|
r = libcrypto.EVP_CIPHER_CTX_ctrl(
|
||||||
|
self._ctx,
|
||||||
|
c_int(EVP_CTRL_AEAD_SET_TAG),
|
||||||
|
c_int(tag_len), c_char_p(tag)
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
raise Exception('Set tag failed')
|
||||||
|
|
||||||
|
def get_tag(self):
|
||||||
|
"""
|
||||||
|
Get authenticated tag, called after EVP_CipherFinal_ex
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
tag_len = self._tlen
|
||||||
|
tag_buf = create_string_buffer(tag_len)
|
||||||
|
r = libcrypto.EVP_CIPHER_CTX_ctrl(
|
||||||
|
self._ctx,
|
||||||
|
c_int(EVP_CTRL_AEAD_GET_TAG),
|
||||||
|
c_int(tag_len), byref(tag_buf)
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
raise Exception('Get tag failed')
|
||||||
|
return tag_buf.raw[:tag_len]
|
||||||
|
|
||||||
|
def final(self):
|
||||||
|
"""
|
||||||
|
Finish encrypt/decrypt a chunk (<= 0x3FFF)
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
global buf_size, buf
|
||||||
|
cipher_out_len = c_long(0)
|
||||||
|
r = libcrypto.EVP_CipherFinal_ex(
|
||||||
|
self._ctx,
|
||||||
|
byref(buf), byref(cipher_out_len)
|
||||||
|
)
|
||||||
|
if not r:
|
||||||
|
# print(self._nonce.raw, r, cipher_out_len)
|
||||||
|
raise Exception('Verify data failed')
|
||||||
|
return buf.raw[:cipher_out_len.value]
|
||||||
|
|
||||||
|
def aead_encrypt(self, data):
|
||||||
|
"""
|
||||||
|
Encrypt data with authenticate tag
|
||||||
|
|
||||||
|
:param data: plain text
|
||||||
|
:return: cipher text with tag
|
||||||
|
"""
|
||||||
|
ctext = self.update(data) + self.final() + self.get_tag()
|
||||||
|
return ctext
|
||||||
|
|
||||||
|
def aead_decrypt(self, data):
|
||||||
|
"""
|
||||||
|
Decrypt data and authenticate tag
|
||||||
|
|
||||||
|
:param data: cipher text with tag
|
||||||
|
:return: plain text
|
||||||
|
"""
|
||||||
|
clen = len(data)
|
||||||
|
if clen < self._tlen:
|
||||||
|
raise Exception('Data too short')
|
||||||
|
|
||||||
|
self.set_tag(data[clen - self._tlen:])
|
||||||
|
plaintext = self.update(data[:clen - self._tlen]) + self.final()
|
||||||
|
return plaintext
|
||||||
|
|
||||||
|
|
||||||
|
class OpenSSLStreamCrypto(OpenSSLCryptoBase):
|
||||||
|
"""
|
||||||
|
Crypto for stream modes: cfb, ofb, ctr
|
||||||
|
"""
|
||||||
|
def __init__(self, cipher_name, key, iv, op):
|
||||||
|
super(OpenSSLStreamCrypto, self).__init__(cipher_name)
|
||||||
|
key_ptr = c_char_p(key)
|
||||||
|
iv_ptr = c_char_p(iv)
|
||||||
|
r = libcrypto.EVP_CipherInit_ex(self._ctx, self._cipher, None,
|
||||||
|
key_ptr, iv_ptr, c_int(op))
|
||||||
|
if not r:
|
||||||
|
self.clean()
|
||||||
|
raise Exception('can not initialize cipher context')
|
||||||
|
self.encrypt = self.update
|
||||||
|
self.decrypt = self.update
|
||||||
|
|
||||||
|
|
||||||
ciphers = {
|
ciphers = {
|
||||||
'aes-128-cfb': (16, 16, OpenSSLCrypto),
|
'aes-128-cfb': (16, 16, OpenSSLStreamCrypto),
|
||||||
'aes-192-cfb': (24, 16, OpenSSLCrypto),
|
'aes-192-cfb': (24, 16, OpenSSLStreamCrypto),
|
||||||
'aes-256-cfb': (32, 16, OpenSSLCrypto),
|
'aes-256-cfb': (32, 16, OpenSSLStreamCrypto),
|
||||||
'aes-128-ofb': (16, 16, OpenSSLCrypto),
|
'aes-128-gcm': (16, 16, OpenSSLAeadCrypto),
|
||||||
'aes-192-ofb': (24, 16, OpenSSLCrypto),
|
'aes-192-gcm': (24, 24, OpenSSLAeadCrypto),
|
||||||
'aes-256-ofb': (32, 16, OpenSSLCrypto),
|
'aes-256-gcm': (32, 32, OpenSSLAeadCrypto),
|
||||||
'aes-128-ctr': (16, 16, OpenSSLCrypto),
|
'aes-128-ofb': (16, 16, OpenSSLStreamCrypto),
|
||||||
'aes-192-ctr': (24, 16, OpenSSLCrypto),
|
'aes-192-ofb': (24, 16, OpenSSLStreamCrypto),
|
||||||
'aes-256-ctr': (32, 16, OpenSSLCrypto),
|
'aes-256-ofb': (32, 16, OpenSSLStreamCrypto),
|
||||||
'aes-128-cfb8': (16, 16, OpenSSLCrypto),
|
'aes-128-ctr': (16, 16, OpenSSLStreamCrypto),
|
||||||
'aes-192-cfb8': (24, 16, OpenSSLCrypto),
|
'aes-192-ctr': (24, 16, OpenSSLStreamCrypto),
|
||||||
'aes-256-cfb8': (32, 16, OpenSSLCrypto),
|
'aes-256-ctr': (32, 16, OpenSSLStreamCrypto),
|
||||||
'aes-128-cfb1': (16, 16, OpenSSLCrypto),
|
'aes-128-cfb8': (16, 16, OpenSSLStreamCrypto),
|
||||||
'aes-192-cfb1': (24, 16, OpenSSLCrypto),
|
'aes-192-cfb8': (24, 16, OpenSSLStreamCrypto),
|
||||||
'aes-256-cfb1': (32, 16, OpenSSLCrypto),
|
'aes-256-cfb8': (32, 16, OpenSSLStreamCrypto),
|
||||||
'bf-cfb': (16, 8, OpenSSLCrypto),
|
'aes-128-cfb1': (16, 16, OpenSSLStreamCrypto),
|
||||||
'camellia-128-cfb': (16, 16, OpenSSLCrypto),
|
'aes-192-cfb1': (24, 16, OpenSSLStreamCrypto),
|
||||||
'camellia-192-cfb': (24, 16, OpenSSLCrypto),
|
'aes-256-cfb1': (32, 16, OpenSSLStreamCrypto),
|
||||||
'camellia-256-cfb': (32, 16, OpenSSLCrypto),
|
'bf-cfb': (16, 8, OpenSSLStreamCrypto),
|
||||||
'cast5-cfb': (16, 8, OpenSSLCrypto),
|
'camellia-128-cfb': (16, 16, OpenSSLStreamCrypto),
|
||||||
'des-cfb': (8, 8, OpenSSLCrypto),
|
'camellia-192-cfb': (24, 16, OpenSSLStreamCrypto),
|
||||||
'idea-cfb': (16, 8, OpenSSLCrypto),
|
'camellia-256-cfb': (32, 16, OpenSSLStreamCrypto),
|
||||||
'rc2-cfb': (16, 8, OpenSSLCrypto),
|
'cast5-cfb': (16, 8, OpenSSLStreamCrypto),
|
||||||
'rc4': (16, 0, OpenSSLCrypto),
|
'des-cfb': (8, 8, OpenSSLStreamCrypto),
|
||||||
'seed-cfb': (16, 16, OpenSSLCrypto),
|
'idea-cfb': (16, 8, OpenSSLStreamCrypto),
|
||||||
|
'rc2-cfb': (16, 8, OpenSSLStreamCrypto),
|
||||||
|
'rc4': (16, 0, OpenSSLStreamCrypto),
|
||||||
|
'seed-cfb': (16, 16, OpenSSLStreamCrypto),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def run_method(method):
|
def run_method(method):
|
||||||
|
|
||||||
cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1)
|
cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1)
|
||||||
decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0)
|
decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0)
|
||||||
|
|
||||||
|
util.run_cipher(cipher, decipher)
|
||||||
|
|
||||||
|
|
||||||
|
def run_aead_method(method, key_len=16):
|
||||||
|
|
||||||
|
key_len = int(key_len)
|
||||||
|
cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
|
||||||
|
decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0)
|
||||||
|
|
||||||
util.run_cipher(cipher, decipher)
|
util.run_cipher(cipher, decipher)
|
||||||
|
|
||||||
|
@ -158,6 +324,16 @@ def test_aes_128_cfb():
|
||||||
run_method('aes-128-cfb')
|
run_method('aes-128-cfb')
|
||||||
|
|
||||||
|
|
||||||
|
def test_aes_gcm(bits=128):
|
||||||
|
method = "aes-{0}-gcm".format(bits)
|
||||||
|
print(method, int(bits / 8))
|
||||||
|
run_aead_method(method, bits / 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_aes_256_gcm():
|
||||||
|
test_aes_gcm(256)
|
||||||
|
|
||||||
|
|
||||||
def test_aes_256_cfb():
|
def test_aes_256_cfb():
|
||||||
run_method('aes-256-cfb')
|
run_method('aes-256-cfb')
|
||||||
|
|
||||||
|
@ -184,3 +360,7 @@ def test_rc4():
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_aes_128_cfb()
|
test_aes_128_cfb()
|
||||||
|
test_aes_gcm(128)
|
||||||
|
test_aes_gcm(192)
|
||||||
|
test_aes_gcm(256)
|
||||||
|
test_aes_256_gcm()
|
||||||
|
|
|
@ -18,7 +18,6 @@ from __future__ import absolute_import, division, print_function, \
|
||||||
with_statement
|
with_statement
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from shadowsocks.crypto import openssl
|
from shadowsocks.crypto import openssl
|
||||||
|
|
||||||
__all__ = ['ciphers']
|
__all__ = ['ciphers']
|
||||||
|
@ -30,7 +29,7 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None,
|
||||||
md5.update(key)
|
md5.update(key)
|
||||||
md5.update(iv)
|
md5.update(iv)
|
||||||
rc4_key = md5.digest()
|
rc4_key = md5.digest()
|
||||||
return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op)
|
return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op)
|
||||||
|
|
||||||
|
|
||||||
ciphers = {
|
ciphers = {
|
||||||
|
|
|
@ -21,6 +21,7 @@ from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \
|
||||||
create_string_buffer, c_void_p
|
create_string_buffer, c_void_p
|
||||||
|
|
||||||
from shadowsocks.crypto import util
|
from shadowsocks.crypto import util
|
||||||
|
from shadowsocks.crypto.aead import AeadCryptoBase
|
||||||
|
|
||||||
__all__ = ['ciphers']
|
__all__ = ['ciphers']
|
||||||
|
|
||||||
|
@ -59,6 +60,67 @@ def load_libsodium():
|
||||||
c_ulong,
|
c_ulong,
|
||||||
c_char_p)
|
c_char_p)
|
||||||
|
|
||||||
|
# chacha20-poly1305
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_encrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_encrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p, # c, clen
|
||||||
|
c_char_p, c_ulonglong, # m, mlen
|
||||||
|
c_char_p, c_ulonglong, # ad, adlen
|
||||||
|
c_char_p, # nsec, not used
|
||||||
|
c_char_p, c_char_p # npub, k
|
||||||
|
)
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_decrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_decrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p, # m, mlen
|
||||||
|
c_char_p, # nsec, not used
|
||||||
|
c_char_p, c_ulonglong, # c, clen
|
||||||
|
c_char_p, c_ulonglong, # ad, adlen
|
||||||
|
c_char_p, c_char_p # npub, k
|
||||||
|
)
|
||||||
|
|
||||||
|
# chacha20-ietf-poly1305, same api structure as above
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p,
|
||||||
|
c_char_p, c_char_p
|
||||||
|
)
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p,
|
||||||
|
c_char_p,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_char_p
|
||||||
|
)
|
||||||
|
|
||||||
|
# xchacha20-ietf-poly1305, same api structure as above
|
||||||
|
if hasattr(libsodium, 'crypto_aead_xchacha20poly1305_ietf_encrypt'):
|
||||||
|
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p,
|
||||||
|
c_char_p, c_char_p
|
||||||
|
)
|
||||||
|
|
||||||
|
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.restype = c_int
|
||||||
|
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.argtypes = (
|
||||||
|
c_void_p, c_void_p,
|
||||||
|
c_char_p,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_ulonglong,
|
||||||
|
c_char_p, c_char_p
|
||||||
|
)
|
||||||
|
|
||||||
|
libsodium.sodium_increment.restype = c_void_p
|
||||||
|
libsodium.sodium_increment.argtypes = (
|
||||||
|
c_void_p, c_int
|
||||||
|
)
|
||||||
|
|
||||||
buf = create_string_buffer(buf_size)
|
buf = create_string_buffer(buf_size)
|
||||||
loaded = True
|
loaded = True
|
||||||
|
|
||||||
|
@ -81,6 +143,10 @@ class SodiumCrypto(object):
|
||||||
raise Exception('Unknown cipher')
|
raise Exception('Unknown cipher')
|
||||||
# byte counter, not block counter
|
# byte counter, not block counter
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
|
self.encrypt = self.update
|
||||||
|
self.decrypt = self.update
|
||||||
|
self.encrypt_once = self.update
|
||||||
|
self.decrypt_once = self.update
|
||||||
|
|
||||||
def update(self, data):
|
def update(self, data):
|
||||||
global buf_size, buf
|
global buf_size, buf
|
||||||
|
@ -103,10 +169,85 @@ class SodiumCrypto(object):
|
||||||
return buf.raw[padding:padding + l]
|
return buf.raw[padding:padding + l]
|
||||||
|
|
||||||
|
|
||||||
|
class SodiumAeadCrypto(AeadCryptoBase):
|
||||||
|
def __init__(self, cipher_name, key, iv, op):
|
||||||
|
if not loaded:
|
||||||
|
load_libsodium()
|
||||||
|
AeadCryptoBase.__init__(self, cipher_name, key, iv, op)
|
||||||
|
|
||||||
|
if cipher_name == 'chacha20-poly1305':
|
||||||
|
self.encryptor = libsodium.crypto_aead_chacha20poly1305_encrypt
|
||||||
|
self.decryptor = libsodium.crypto_aead_chacha20poly1305_decrypt
|
||||||
|
elif cipher_name == 'chacha20-ietf-poly1305':
|
||||||
|
self.encryptor = libsodium.\
|
||||||
|
crypto_aead_chacha20poly1305_ietf_encrypt
|
||||||
|
self.decryptor = libsodium.\
|
||||||
|
crypto_aead_chacha20poly1305_ietf_decrypt
|
||||||
|
elif cipher_name == 'xchacha20-ietf-poly1305':
|
||||||
|
if hasattr(libsodium,
|
||||||
|
'crypto_aead_xchacha20poly1305_ietf_encrypt'):
|
||||||
|
self.encryptor = libsodium.\
|
||||||
|
crypto_aead_xchacha20poly1305_ietf_encrypt
|
||||||
|
self.decryptor = libsodium.\
|
||||||
|
crypto_aead_xchacha20poly1305_ietf_decrypt
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown cipher')
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown cipher')
|
||||||
|
|
||||||
|
def cipher_ctx_init(self):
|
||||||
|
|
||||||
|
libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen))
|
||||||
|
# print("".join("%02x" % ord(b) for b in self._nonce))
|
||||||
|
|
||||||
|
def aead_encrypt(self, data):
|
||||||
|
global buf, buf_size
|
||||||
|
plen = len(data)
|
||||||
|
if buf_size < plen + self._tlen:
|
||||||
|
buf_size = (plen + self._tlen) * 2
|
||||||
|
buf = create_string_buffer(buf_size)
|
||||||
|
cipher_out_len = c_ulonglong(0)
|
||||||
|
self.encryptor(
|
||||||
|
byref(buf), byref(cipher_out_len),
|
||||||
|
c_char_p(data), c_ulonglong(plen),
|
||||||
|
None, c_ulonglong(0), None,
|
||||||
|
c_char_p(self._nonce.raw), c_char_p(self._skey)
|
||||||
|
)
|
||||||
|
if cipher_out_len.value != plen + self._tlen:
|
||||||
|
raise Exception("Encrypt failed")
|
||||||
|
|
||||||
|
return buf.raw[:cipher_out_len.value]
|
||||||
|
|
||||||
|
def aead_decrypt(self, data):
|
||||||
|
global buf, buf_size
|
||||||
|
clen = len(data)
|
||||||
|
if buf_size < clen:
|
||||||
|
buf_size = clen * 2
|
||||||
|
buf = create_string_buffer(buf_size)
|
||||||
|
cipher_out_len = c_ulonglong(0)
|
||||||
|
r = self.decryptor(
|
||||||
|
byref(buf), byref(cipher_out_len),
|
||||||
|
None,
|
||||||
|
c_char_p(data), c_ulonglong(clen),
|
||||||
|
None, c_ulonglong(0),
|
||||||
|
c_char_p(self._nonce.raw), c_char_p(self._skey)
|
||||||
|
)
|
||||||
|
if r != 0:
|
||||||
|
raise Exception("Decrypt failed")
|
||||||
|
|
||||||
|
if cipher_out_len.value != clen - self._tlen:
|
||||||
|
raise Exception("Encrypt failed")
|
||||||
|
|
||||||
|
return buf.raw[:cipher_out_len.value]
|
||||||
|
|
||||||
|
|
||||||
ciphers = {
|
ciphers = {
|
||||||
'salsa20': (32, 8, SodiumCrypto),
|
'salsa20': (32, 8, SodiumCrypto),
|
||||||
'chacha20': (32, 8, SodiumCrypto),
|
'chacha20': (32, 8, SodiumCrypto),
|
||||||
'chacha20-ietf': (32, 12, SodiumCrypto),
|
'chacha20-ietf': (32, 12, SodiumCrypto),
|
||||||
|
'chacha20-poly1305': (32, 32, SodiumAeadCrypto),
|
||||||
|
'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
|
||||||
|
'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,7 +274,31 @@ def test_chacha20_ietf():
|
||||||
util.run_cipher(cipher, decipher)
|
util.run_cipher(cipher, decipher)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chacha20_poly1305():
|
||||||
|
|
||||||
|
print("Test chacha20-poly1305")
|
||||||
|
cipher = SodiumAeadCrypto('chacha20-poly1305',
|
||||||
|
b'k' * 32, b'i' * 32, 1)
|
||||||
|
decipher = SodiumAeadCrypto('chacha20-poly1305',
|
||||||
|
b'k' * 32, b'i' * 32, 0)
|
||||||
|
|
||||||
|
util.run_cipher(cipher, decipher)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chacha20_ietf_poly1305():
|
||||||
|
|
||||||
|
print("Test chacha20-ietf-poly1305")
|
||||||
|
cipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
|
||||||
|
b'k' * 32, b'i' * 32, 1)
|
||||||
|
decipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
|
||||||
|
b'k' * 32, b'i' * 32, 0)
|
||||||
|
|
||||||
|
util.run_cipher(cipher, decipher)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_chacha20()
|
test_chacha20()
|
||||||
test_salsa20()
|
test_salsa20()
|
||||||
test_chacha20_ietf()
|
test_chacha20_ietf()
|
||||||
|
test_chacha20_poly1305()
|
||||||
|
test_chacha20_ietf_poly1305()
|
||||||
|
|
|
@ -58,6 +58,10 @@ class TableCipher(object):
|
||||||
def __init__(self, cipher_name, key, iv, op):
|
def __init__(self, cipher_name, key, iv, op):
|
||||||
self._encrypt_table, self._decrypt_table = init_table(key)
|
self._encrypt_table, self._decrypt_table = init_table(key)
|
||||||
self._op = op
|
self._op = op
|
||||||
|
self.encrypt = self.update
|
||||||
|
self.decrypt = self.update
|
||||||
|
self.encrypt_once = self.update
|
||||||
|
self.decrypt_once = self.update
|
||||||
|
|
||||||
def update(self, data):
|
def update(self, data):
|
||||||
if self._op:
|
if self._op:
|
||||||
|
|
|
@ -33,7 +33,7 @@ def find_library_nt(name):
|
||||||
results.append(fname)
|
results.append(fname)
|
||||||
if fname.lower().endswith(".dll"):
|
if fname.lower().endswith(".dll"):
|
||||||
continue
|
continue
|
||||||
fname = fname + ".dll"
|
fname += ".dll"
|
||||||
if os.path.isfile(fname):
|
if os.path.isfile(fname):
|
||||||
results.append(fname)
|
results.append(fname)
|
||||||
return results
|
return results
|
||||||
|
@ -92,14 +92,27 @@ def find_library(possible_lib_names, search_symbol, library_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mode(cipher_nme):
|
||||||
|
"""
|
||||||
|
Parse the cipher mode from cipher name
|
||||||
|
e.g. aes-128-gcm, the mode is gcm
|
||||||
|
:param cipher_nme: str cipher name, aes-128-cfb, aes-128-gcm ...
|
||||||
|
:return: str/None The mode, cfb, gcm ...
|
||||||
|
"""
|
||||||
|
hyphen = cipher_nme.rfind('-')
|
||||||
|
if hyphen > 0:
|
||||||
|
return cipher_nme[hyphen:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def run_cipher(cipher, decipher):
|
def run_cipher(cipher, decipher):
|
||||||
from os import urandom
|
from os import urandom
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
BLOCK_SIZE = 16384
|
block_size = 16384
|
||||||
rounds = 1 * 1024
|
rounds = 1 * 1024
|
||||||
plain = urandom(BLOCK_SIZE * rounds)
|
plain = urandom(block_size * rounds)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
pos = 0
|
pos = 0
|
||||||
|
@ -107,18 +120,18 @@ def run_cipher(cipher, decipher):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while pos < len(plain):
|
while pos < len(plain):
|
||||||
l = random.randint(100, 32768)
|
l = random.randint(100, 32768)
|
||||||
c = cipher.update(plain[pos:pos + l])
|
c = cipher.encrypt(plain[pos:pos + l])
|
||||||
results.append(c)
|
results.append(c)
|
||||||
pos += l
|
pos += l
|
||||||
pos = 0
|
pos = 0
|
||||||
c = b''.join(results)
|
c = b''.join(results)
|
||||||
results = []
|
results = []
|
||||||
while pos < len(plain):
|
while pos < len(c):
|
||||||
l = random.randint(100, 32768)
|
l = random.randint(100, 32768)
|
||||||
results.append(decipher.update(c[pos:pos + l]))
|
results.append(decipher.decrypt(c[pos:pos + l]))
|
||||||
pos += l
|
pos += l
|
||||||
end = time.time()
|
end = time.time()
|
||||||
print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start)))
|
print('speed: %d bytes/s' % (block_size * rounds / (end - start)))
|
||||||
assert b''.join(results) == plain
|
assert b''.join(results) == plain
|
||||||
|
|
||||||
|
|
||||||
|
|
235
shadowsocks/cryptor.py
Normal file
235
shadowsocks/cryptor.py
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright 2012-2015 clowwindy
|
||||||
|
#
|
||||||
|
# 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 __future__ import absolute_import, division, print_function, \
|
||||||
|
with_statement
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from shadowsocks import common
|
||||||
|
from shadowsocks.crypto import rc4_md5, openssl, sodium, table
|
||||||
|
|
||||||
|
|
||||||
|
CIPHER_ENC_ENCRYPTION = 1
|
||||||
|
CIPHER_ENC_DECRYPTION = 0
|
||||||
|
|
||||||
|
METHOD_INFO_KEY_LEN = 0
|
||||||
|
METHOD_INFO_IV_LEN = 1
|
||||||
|
METHOD_INFO_CRYPTO = 2
|
||||||
|
|
||||||
|
method_supported = {}
|
||||||
|
method_supported.update(rc4_md5.ciphers)
|
||||||
|
method_supported.update(openssl.ciphers)
|
||||||
|
method_supported.update(sodium.ciphers)
|
||||||
|
method_supported.update(table.ciphers)
|
||||||
|
|
||||||
|
|
||||||
|
def random_string(length):
|
||||||
|
return os.urandom(length)
|
||||||
|
|
||||||
|
cached_keys = {}
|
||||||
|
|
||||||
|
|
||||||
|
def try_cipher(key, method=None):
|
||||||
|
Cryptor(key, method)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
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):
|
||||||
|
md5 = hashlib.md5()
|
||||||
|
data = password
|
||||||
|
if i > 0:
|
||||||
|
data = m[i - 1] + password
|
||||||
|
md5.update(data)
|
||||||
|
m.append(md5.digest())
|
||||||
|
i += 1
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Cryptor(object):
|
||||||
|
def __init__(self, password, method):
|
||||||
|
self.password = password
|
||||||
|
self.key = None
|
||||||
|
self.method = method
|
||||||
|
self.iv_sent = False
|
||||||
|
self.cipher_iv = b''
|
||||||
|
self.decipher = None
|
||||||
|
self.decipher_iv = None
|
||||||
|
method = method.lower()
|
||||||
|
self._method_info = Cryptor.get_method_info(method)
|
||||||
|
if self._method_info:
|
||||||
|
self.cipher = self.get_cipher(
|
||||||
|
password, method, CIPHER_ENC_ENCRYPTION,
|
||||||
|
random_string(self._method_info[METHOD_INFO_IV_LEN])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.error('method %s not supported' % method)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_method_info(method):
|
||||||
|
method = method.lower()
|
||||||
|
m = method_supported.get(method)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def iv_len(self):
|
||||||
|
return len(self.cipher_iv)
|
||||||
|
|
||||||
|
def get_cipher(self, password, method, op, iv):
|
||||||
|
password = common.to_bytes(password)
|
||||||
|
m = self._method_info
|
||||||
|
if m[METHOD_INFO_KEY_LEN] > 0:
|
||||||
|
key, _ = EVP_BytesToKey(password,
|
||||||
|
m[METHOD_INFO_KEY_LEN],
|
||||||
|
m[METHOD_INFO_IV_LEN])
|
||||||
|
else:
|
||||||
|
# key_length == 0 indicates we should use the key directly
|
||||||
|
key, iv = password, b''
|
||||||
|
self.key = key
|
||||||
|
iv = iv[:m[METHOD_INFO_IV_LEN]]
|
||||||
|
if op == CIPHER_ENC_ENCRYPTION:
|
||||||
|
# this iv is for cipher not decipher
|
||||||
|
self.cipher_iv = iv
|
||||||
|
return m[METHOD_INFO_CRYPTO](method, key, iv, op)
|
||||||
|
|
||||||
|
def encrypt(self, buf):
|
||||||
|
if len(buf) == 0:
|
||||||
|
return buf
|
||||||
|
if self.iv_sent:
|
||||||
|
return self.cipher.encrypt(buf)
|
||||||
|
else:
|
||||||
|
self.iv_sent = True
|
||||||
|
return self.cipher_iv + self.cipher.encrypt(buf)
|
||||||
|
|
||||||
|
def decrypt(self, buf):
|
||||||
|
if len(buf) == 0:
|
||||||
|
return buf
|
||||||
|
if self.decipher is None:
|
||||||
|
decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN]
|
||||||
|
decipher_iv = buf[:decipher_iv_len]
|
||||||
|
self.decipher_iv = decipher_iv
|
||||||
|
self.decipher = self.get_cipher(
|
||||||
|
self.password, self.method,
|
||||||
|
CIPHER_ENC_DECRYPTION,
|
||||||
|
iv=decipher_iv
|
||||||
|
)
|
||||||
|
buf = buf[decipher_iv_len:]
|
||||||
|
if len(buf) == 0:
|
||||||
|
return buf
|
||||||
|
return self.decipher.decrypt(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_key_iv(password, method):
|
||||||
|
method = method.lower()
|
||||||
|
(key_len, iv_len, m) = method_supported[method]
|
||||||
|
if key_len > 0:
|
||||||
|
key, _ = EVP_BytesToKey(password, key_len, iv_len)
|
||||||
|
else:
|
||||||
|
key = password
|
||||||
|
iv = random_string(iv_len)
|
||||||
|
return key, iv, m
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_all_m(key, iv, m, method, data):
|
||||||
|
result = [iv]
|
||||||
|
cipher = m(method, key, iv, 1)
|
||||||
|
result.append(cipher.encrypt_once(data))
|
||||||
|
return b''.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_all(password, method, data):
|
||||||
|
result = []
|
||||||
|
method = method.lower()
|
||||||
|
(key, iv, m) = gen_key_iv(password, method)
|
||||||
|
iv = data[:len(iv)]
|
||||||
|
data = data[len(iv):]
|
||||||
|
cipher = m(method, key, iv, CIPHER_ENC_DECRYPTION)
|
||||||
|
result.append(cipher.decrypt_once(data))
|
||||||
|
return b''.join(result), key, iv
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_all(password, method, data):
|
||||||
|
result = []
|
||||||
|
method = method.lower()
|
||||||
|
(key, iv, m) = gen_key_iv(password, method)
|
||||||
|
result.append(iv)
|
||||||
|
cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION)
|
||||||
|
result.append(cipher.encrypt_once(data))
|
||||||
|
return b''.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
CIPHERS_TO_TEST = [
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'rc4-md5',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'table',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_encryptor():
|
||||||
|
from os import urandom
|
||||||
|
plain = urandom(10240)
|
||||||
|
for method in CIPHERS_TO_TEST:
|
||||||
|
logging.warn(method)
|
||||||
|
encryptor = Cryptor(b'key', method)
|
||||||
|
decryptor = Cryptor(b'key', method)
|
||||||
|
cipher = encryptor.encrypt(plain)
|
||||||
|
plain2 = decryptor.decrypt(cipher)
|
||||||
|
assert plain == plain2
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypt_all():
|
||||||
|
from os import urandom
|
||||||
|
plain = urandom(10240)
|
||||||
|
for method in CIPHERS_TO_TEST:
|
||||||
|
logging.warn(method)
|
||||||
|
cipher = encrypt_all(b'key', method, plain)
|
||||||
|
plain2, key, iv = decrypt_all(b'key', method, cipher)
|
||||||
|
assert plain == plain2
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypt_all_m():
|
||||||
|
from os import urandom
|
||||||
|
plain = urandom(10240)
|
||||||
|
for method in CIPHERS_TO_TEST:
|
||||||
|
logging.warn(method)
|
||||||
|
key, iv, m = gen_key_iv(b'key', method)
|
||||||
|
cipher = encrypt_all_m(key, iv, m, method, plain)
|
||||||
|
plain2, key, iv = decrypt_all(b'key', method, cipher)
|
||||||
|
assert plain == plain2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_encrypt_all()
|
||||||
|
test_encryptor()
|
||||||
|
test_encrypt_all_m()
|
|
@ -153,7 +153,7 @@ def encrypt_all_m(key, iv, m, method, data):
|
||||||
return b''.join(result)
|
return b''.join(result)
|
||||||
|
|
||||||
|
|
||||||
def decrypt_all(password, method, data):
|
def dencrypt_all(password, method, data):
|
||||||
result = []
|
result = []
|
||||||
method = method.lower()
|
method = method.lower()
|
||||||
(key_len, iv_len, m) = method_supported[method]
|
(key_len, iv_len, m) = method_supported[method]
|
||||||
|
@ -228,7 +228,7 @@ def test_encrypt_all_m():
|
||||||
logging.warn(method)
|
logging.warn(method)
|
||||||
key, iv, m = gen_key_iv(b'key', method)
|
key, iv, m = gen_key_iv(b'key', method)
|
||||||
cipher = encrypt_all_m(key, iv, m, method, plain)
|
cipher = encrypt_all_m(key, iv, m, method, plain)
|
||||||
plain2, key, iv = decrypt_all(b'key', method, cipher)
|
plain2, key, iv = dencrypt_all(b'key', method, cipher)
|
||||||
assert plain == plain2
|
assert plain == plain2
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -202,7 +202,7 @@ def test():
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import struct
|
import struct
|
||||||
from shadowsocks import encrypt
|
from shadowsocks import cryptor
|
||||||
|
|
||||||
logging.basicConfig(level=5,
|
logging.basicConfig(level=5,
|
||||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||||
|
@ -252,7 +252,7 @@ def test():
|
||||||
|
|
||||||
# test statistics for TCP
|
# test statistics for TCP
|
||||||
header = common.pack_addr(b'google.com') + struct.pack('>H', 80)
|
header = common.pack_addr(b'google.com') + struct.pack('>H', 80)
|
||||||
data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1,
|
data = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb',
|
||||||
header + b'GET /\r\n\r\n')
|
header + b'GET /\r\n\r\n')
|
||||||
tcp_cli = socket.socket()
|
tcp_cli = socket.socket()
|
||||||
tcp_cli.connect(('127.0.0.1', 7001))
|
tcp_cli.connect(('127.0.0.1', 7001))
|
||||||
|
@ -270,7 +270,7 @@ def test():
|
||||||
|
|
||||||
# test statistics for UDP
|
# test statistics for UDP
|
||||||
header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80)
|
header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80)
|
||||||
data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1,
|
data = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb',
|
||||||
header + b'test')
|
header + b'test')
|
||||||
udp_cli = socket.socket(type=socket.SOCK_DGRAM)
|
udp_cli = socket.socket(type=socket.SOCK_DGRAM)
|
||||||
udp_cli.sendto(data, ('127.0.0.1', 8382))
|
udp_cli.sendto(data, ('127.0.0.1', 8382))
|
||||||
|
|
|
@ -28,7 +28,7 @@ import traceback
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from shadowsocks.common import to_bytes, to_str, IPNetwork
|
from shadowsocks.common import to_bytes, to_str, IPNetwork
|
||||||
from shadowsocks import encrypt
|
from shadowsocks import cryptor
|
||||||
|
|
||||||
|
|
||||||
VERBOSE_LEVEL = 5
|
VERBOSE_LEVEL = 5
|
||||||
|
@ -200,7 +200,7 @@ def check_config(config, is_local):
|
||||||
if config.get('dns_server', None) is not None:
|
if config.get('dns_server', None) is not None:
|
||||||
logging.info('Specified DNS server: %s' % config['dns_server'])
|
logging.info('Specified DNS server: %s' % config['dns_server'])
|
||||||
|
|
||||||
encrypt.try_cipher(config['password'], config['method'])
|
cryptor.try_cipher(config['password'], config['method'])
|
||||||
|
|
||||||
|
|
||||||
def get_config(is_local):
|
def get_config(is_local):
|
||||||
|
@ -362,6 +362,18 @@ Proxy options:
|
||||||
-l LOCAL_PORT local port, default: 1080
|
-l LOCAL_PORT local port, default: 1080
|
||||||
-k PASSWORD password
|
-k PASSWORD password
|
||||||
-m METHOD encryption method, default: aes-256-cfb
|
-m METHOD encryption method, default: aes-256-cfb
|
||||||
|
supported method:
|
||||||
|
chacha20-poly1305, chacha20-ietf-poly1305,
|
||||||
|
*xchacha20-ietf-poly1305,
|
||||||
|
aes-128-gcm, aes-192-gcm, aes-256-gcm,
|
||||||
|
aes-128-cfb, aes-192-cfb, aes-256-cfb,
|
||||||
|
es-128-ctr, aes-192-ctr, aes-256-ctr,
|
||||||
|
camellia-128-cfb, camellia-192-cfb,
|
||||||
|
camellia-256-cfb,
|
||||||
|
salsa20, chacha20, chacha20-ietf,
|
||||||
|
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
|
||||||
|
rc2-cfb, seed-cfb,
|
||||||
|
rc4, rc4-md5, table.
|
||||||
-t TIMEOUT timeout in seconds, default: 300
|
-t TIMEOUT timeout in seconds, default: 300
|
||||||
-a ONE_TIME_AUTH one time auth
|
-a ONE_TIME_AUTH one time auth
|
||||||
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
||||||
|
@ -392,6 +404,18 @@ Proxy options:
|
||||||
-p SERVER_PORT server port, default: 8388
|
-p SERVER_PORT server port, default: 8388
|
||||||
-k PASSWORD password
|
-k PASSWORD password
|
||||||
-m METHOD encryption method, default: aes-256-cfb
|
-m METHOD encryption method, default: aes-256-cfb
|
||||||
|
supported method:
|
||||||
|
chacha20-poly1305, chacha20-ietf-poly1305,
|
||||||
|
*xchacha20-ietf-poly1305,
|
||||||
|
aes-128-gcm, aes-192-gcm, aes-256-gcm,
|
||||||
|
aes-128-cfb, aes-192-cfb, aes-256-cfb,
|
||||||
|
es-128-ctr, aes-192-ctr, aes-256-ctr,
|
||||||
|
camellia-128-cfb, camellia-192-cfb,
|
||||||
|
camellia-256-cfb,
|
||||||
|
salsa20, chacha20, chacha20-ietf,
|
||||||
|
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
|
||||||
|
rc2-cfb, seed-cfb,
|
||||||
|
rc4, rc4-md5, table.
|
||||||
-t TIMEOUT timeout in seconds, default: 300
|
-t TIMEOUT timeout in seconds, default: 300
|
||||||
-a ONE_TIME_AUTH one time auth
|
-a ONE_TIME_AUTH one time auth
|
||||||
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
||||||
|
|
|
@ -26,7 +26,7 @@ import logging
|
||||||
import traceback
|
import traceback
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from shadowsocks import encrypt, eventloop, shell, common
|
from shadowsocks import cryptor, eventloop, shell, common
|
||||||
from shadowsocks.common import parse_header, onetimeauth_verify, \
|
from shadowsocks.common import parse_header, onetimeauth_verify, \
|
||||||
onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \
|
onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \
|
||||||
ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH
|
ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH
|
||||||
|
@ -94,9 +94,9 @@ WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING
|
||||||
|
|
||||||
BUF_SIZE = 32 * 1024
|
BUF_SIZE = 32 * 1024
|
||||||
|
|
||||||
|
|
||||||
# helper exceptions for TCPRelayHandler
|
# helper exceptions for TCPRelayHandler
|
||||||
|
|
||||||
|
|
||||||
class BadSocksHeader(Exception):
|
class BadSocksHeader(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class TCPRelayHandler(object):
|
||||||
# if is_local, this is sslocal
|
# if is_local, this is sslocal
|
||||||
self._is_local = is_local
|
self._is_local = is_local
|
||||||
self._stage = STAGE_INIT
|
self._stage = STAGE_INIT
|
||||||
self._encryptor = encrypt.Encryptor(config['password'],
|
self._cryptor = cryptor.Cryptor(config['password'],
|
||||||
config['method'])
|
config['method'])
|
||||||
self._ota_enable = config.get('one_time_auth', False)
|
self._ota_enable = config.get('one_time_auth', False)
|
||||||
self._ota_enable_session = self._ota_enable
|
self._ota_enable_session = self._ota_enable
|
||||||
|
@ -257,7 +257,7 @@ class TCPRelayHandler(object):
|
||||||
return
|
return
|
||||||
if self._ota_enable_session:
|
if self._ota_enable_session:
|
||||||
data = self._ota_chunk_data_gen(data)
|
data = self._ota_chunk_data_gen(data)
|
||||||
data = self._encryptor.encrypt(data)
|
data = self._cryptor.encrypt(data)
|
||||||
self._data_to_write_to_remote.append(data)
|
self._data_to_write_to_remote.append(data)
|
||||||
|
|
||||||
if self._config['fast_open'] and not self._fastopen_connected:
|
if self._config['fast_open'] and not self._fastopen_connected:
|
||||||
|
@ -347,7 +347,7 @@ class TCPRelayHandler(object):
|
||||||
offset = header_length + ONETIMEAUTH_BYTES
|
offset = header_length + ONETIMEAUTH_BYTES
|
||||||
_hash = data[header_length: offset]
|
_hash = data[header_length: offset]
|
||||||
_data = data[:header_length]
|
_data = data[:header_length]
|
||||||
key = self._encryptor.decipher_iv + self._encryptor.key
|
key = self._cryptor.decipher_iv + self._cryptor.key
|
||||||
if onetimeauth_verify(_hash, _data, key) is False:
|
if onetimeauth_verify(_hash, _data, key) is False:
|
||||||
logging.warn('one time auth fail')
|
logging.warn('one time auth fail')
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
@ -368,11 +368,11 @@ class TCPRelayHandler(object):
|
||||||
# ATYP & 0x10 == 0x10, then OTA is enabled.
|
# ATYP & 0x10 == 0x10, then OTA is enabled.
|
||||||
if self._ota_enable_session:
|
if self._ota_enable_session:
|
||||||
data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:]
|
data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:]
|
||||||
key = self._encryptor.cipher_iv + self._encryptor.key
|
key = self._cryptor.cipher_iv + self._cryptor.key
|
||||||
_header = data[:header_length]
|
_header = data[:header_length]
|
||||||
sha110 = onetimeauth_gen(data, key)
|
sha110 = onetimeauth_gen(data, key)
|
||||||
data = _header + sha110 + data[header_length:]
|
data = _header + sha110 + data[header_length:]
|
||||||
data_to_send = self._encryptor.encrypt(data)
|
data_to_send = self._cryptor.encrypt(data)
|
||||||
self._data_to_write_to_remote.append(data_to_send)
|
self._data_to_write_to_remote.append(data_to_send)
|
||||||
# notice here may go into _handle_dns_resolved directly
|
# notice here may go into _handle_dns_resolved directly
|
||||||
self._dns_resolver.resolve(self._chosen_server[0],
|
self._dns_resolver.resolve(self._chosen_server[0],
|
||||||
|
@ -475,7 +475,7 @@ class TCPRelayHandler(object):
|
||||||
_hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:]
|
_hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:]
|
||||||
_data = self._ota_buff_data
|
_data = self._ota_buff_data
|
||||||
index = struct.pack('>I', self._ota_chunk_idx)
|
index = struct.pack('>I', self._ota_chunk_idx)
|
||||||
key = self._encryptor.decipher_iv + index
|
key = self._cryptor.decipher_iv + index
|
||||||
if onetimeauth_verify(_hash, _data, key) is False:
|
if onetimeauth_verify(_hash, _data, key) is False:
|
||||||
logging.warn('one time auth fail, drop chunk !')
|
logging.warn('one time auth fail, drop chunk !')
|
||||||
else:
|
else:
|
||||||
|
@ -490,7 +490,7 @@ class TCPRelayHandler(object):
|
||||||
def _ota_chunk_data_gen(self, data):
|
def _ota_chunk_data_gen(self, data):
|
||||||
data_len = struct.pack(">H", len(data))
|
data_len = struct.pack(">H", len(data))
|
||||||
index = struct.pack('>I', self._ota_chunk_idx)
|
index = struct.pack('>I', self._ota_chunk_idx)
|
||||||
key = self._encryptor.cipher_iv + index
|
key = self._cryptor.cipher_iv + index
|
||||||
sha110 = onetimeauth_gen(data, key)
|
sha110 = onetimeauth_gen(data, key)
|
||||||
self._ota_chunk_idx += 1
|
self._ota_chunk_idx += 1
|
||||||
return data_len + sha110 + data
|
return data_len + sha110 + data
|
||||||
|
@ -499,7 +499,7 @@ class TCPRelayHandler(object):
|
||||||
if self._is_local:
|
if self._is_local:
|
||||||
if self._ota_enable_session:
|
if self._ota_enable_session:
|
||||||
data = self._ota_chunk_data_gen(data)
|
data = self._ota_chunk_data_gen(data)
|
||||||
data = self._encryptor.encrypt(data)
|
data = self._cryptor.encrypt(data)
|
||||||
self._write_to_sock(data, self._remote_sock)
|
self._write_to_sock(data, self._remote_sock)
|
||||||
else:
|
else:
|
||||||
if self._ota_enable_session:
|
if self._ota_enable_session:
|
||||||
|
@ -564,7 +564,7 @@ class TCPRelayHandler(object):
|
||||||
return
|
return
|
||||||
self._update_activity(len(data))
|
self._update_activity(len(data))
|
||||||
if not is_local:
|
if not is_local:
|
||||||
data = self._encryptor.decrypt(data)
|
data = self._cryptor.decrypt(data)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
if self._stage == STAGE_STREAM:
|
if self._stage == STAGE_STREAM:
|
||||||
|
@ -598,9 +598,9 @@ class TCPRelayHandler(object):
|
||||||
return
|
return
|
||||||
self._update_activity(len(data))
|
self._update_activity(len(data))
|
||||||
if self._is_local:
|
if self._is_local:
|
||||||
data = self._encryptor.decrypt(data)
|
data = self._cryptor.decrypt(data)
|
||||||
else:
|
else:
|
||||||
data = self._encryptor.encrypt(data)
|
data = self._cryptor.encrypt(data)
|
||||||
try:
|
try:
|
||||||
self._write_to_sock(data, self._local_sock)
|
self._write_to_sock(data, self._local_sock)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -68,7 +68,7 @@ import struct
|
||||||
import errno
|
import errno
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from shadowsocks import encrypt, eventloop, lru_cache, common, shell
|
from shadowsocks import cryptor, eventloop, lru_cache, common, shell
|
||||||
from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \
|
from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \
|
||||||
onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH
|
onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ class UDPRelay(object):
|
||||||
else:
|
else:
|
||||||
data = data[3:]
|
data = data[3:]
|
||||||
else:
|
else:
|
||||||
data, key, iv = encrypt.decrypt_all(self._password,
|
data, key, iv = cryptor.decrypt_all(self._password,
|
||||||
self._method,
|
self._method,
|
||||||
data)
|
data)
|
||||||
# decrypt data
|
# decrypt data
|
||||||
|
@ -234,11 +234,11 @@ class UDPRelay(object):
|
||||||
self._eventloop.add(client, eventloop.POLL_IN, self)
|
self._eventloop.add(client, eventloop.POLL_IN, self)
|
||||||
|
|
||||||
if self._is_local:
|
if self._is_local:
|
||||||
key, iv, m = encrypt.gen_key_iv(self._password, self._method)
|
key, iv, m = cryptor.gen_key_iv(self._password, self._method)
|
||||||
# spec https://shadowsocks.org/en/spec/one-time-auth.html
|
# spec https://shadowsocks.org/en/spec/one-time-auth.html
|
||||||
if self._ota_enable_session:
|
if self._ota_enable_session:
|
||||||
data = self._ota_chunk_data_gen(key, iv, data)
|
data = self._ota_chunk_data_gen(key, iv, data)
|
||||||
data = encrypt.encrypt_all_m(key, iv, m, self._method, data)
|
data = cryptor.encrypt_all_m(key, iv, m, self._method, data)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -267,13 +267,12 @@ class UDPRelay(object):
|
||||||
# drop
|
# drop
|
||||||
return
|
return
|
||||||
data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
|
data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
|
||||||
response = encrypt.encrypt_all(self._password, self._method, 1,
|
response = cryptor.encrypt_all(self._password, self._method, data)
|
||||||
data)
|
|
||||||
if not response:
|
if not response:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
data = encrypt.encrypt_all(self._password, self._method, 0,
|
data, key, iv = cryptor.decrypt_all(self._password,
|
||||||
data)
|
self._method, data)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
header_result = parse_header(data)
|
header_result = parse_header(data)
|
||||||
|
|
Loading…
Reference in a new issue