├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── examples ├── test_client.py └── test_server.py ├── pytest.ini ├── secret_handshake ├── __init__.py ├── boxstream.py ├── crypto.py ├── network.py ├── test_boxstream.py ├── test_crypto.py ├── test_network.py └── util.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | .cache 3 | .eggs 4 | .coverage 5 | __pycache__ 6 | *.pyc 7 | node_modules 8 | build/ 9 | dist/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - "3.5" 5 | - "3.6" 6 | - "3.7" 7 | - "3.8-dev" 8 | install: 9 | - pip install .[tests] 10 | - pip install coveralls 11 | script: pytest 12 | after_success: 13 | - coveralls 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Main author: Pedro Ferreira 2 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pferreir/PySecretHandshake/bfad3177349adf6a89ccd3dd41d739f2410a9dfd/CHANGES.rst -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include AUTHORS 3 | include LICENSE 4 | include README.rst 5 | include pytest.ini 6 | 7 | exclude examples 8 | recursive-exclude examples *.py 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **WORK IN PROGRESS** 2 | 3 | PySecretHandshake - Secret Handshake in Python 4 | ============================================== 5 | 6 | 7 | |build-status| |code-coverage| 8 | 9 | This module implements Secret Handshake as specified in Dominc Tarr's paper `"Designing a Secret Handshake: Authenticated 10 | Key Exchange as a Capability System" `_ (Dominic Tarr, 2015). 11 | 12 | **Please, don't use this package in production. The implementation hasn't yet been carefully checked.** 13 | 14 | .. |build-status| image:: https://travis-ci.org/pferreir/PySecretHandshake.svg?branch=master 15 | :alt: Travis Build Status 16 | :target: https://travis-ci.org/pferreir/PySecretHandshake 17 | .. |code-coverage| image:: https://coveralls.io/repos/github/pferreir/PySecretHandshake/badge.svg?branch=master 18 | :alt: Code Coverage 19 | :target: https://coveralls.io/github/pferreir/PySecretHandshake?branch=master 20 | -------------------------------------------------------------------------------- /examples/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from asyncio import get_event_loop 3 | from base64 import b64decode 4 | 5 | import yaml 6 | from nacl.signing import SigningKey 7 | 8 | from secret_handshake import SHSClient 9 | 10 | with open(os.path.expanduser('~/.ssb/secret')) as f: 11 | config = yaml.load(f) 12 | 13 | 14 | async def main(): 15 | server_pub_key = b64decode(config['public'][:-8]) 16 | client = SHSClient('localhost', 8008, SigningKey.generate(), server_pub_key) 17 | await client.open() 18 | 19 | async for msg in client: 20 | print(msg) 21 | 22 | 23 | loop = get_event_loop() 24 | loop.run_until_complete(main()) 25 | loop.close() 26 | -------------------------------------------------------------------------------- /examples/test_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from asyncio import get_event_loop 3 | from base64 import b64decode 4 | 5 | import yaml 6 | from nacl.signing import SigningKey 7 | 8 | from secret_handshake import SHSServer 9 | 10 | with open(os.path.expanduser('~/.ssb/secret')) as f: 11 | config = yaml.load(f) 12 | 13 | 14 | async def _on_connect(conn): 15 | async for msg in conn: 16 | print(msg) 17 | 18 | 19 | async def main(): 20 | server_keypair = SigningKey(b64decode(config['private'][:-8])[:32]) 21 | server = SHSServer('localhost', 8008, server_keypair) 22 | server.on_connect(_on_connect) 23 | await server.listen() 24 | 25 | loop = get_event_loop() 26 | loop.run_until_complete(main()) 27 | loop.run_forever() 28 | loop.close() 29 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov secret_handshake --cov-report term-missing --no-cov-on-fail 3 | python_files = secret_handshake/test_*.py 4 | -------------------------------------------------------------------------------- /secret_handshake/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | from .network import SHSClient, SHSServer 23 | 24 | __all__ = ('SHSClient', 'SHSServer') 25 | -------------------------------------------------------------------------------- /secret_handshake/boxstream.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from asyncio import IncompleteReadError 3 | 4 | from async_generator import async_generator, yield_ 5 | from nacl.secret import SecretBox 6 | 7 | from .util import inc_nonce, split_chunks 8 | 9 | HEADER_LENGTH = 2 + 16 + 16 10 | MAX_SEGMENT_SIZE = 4 * 1024 11 | TERMINATION_HEADER = (b'\x00' * 18) 12 | 13 | 14 | def get_stream_pair(reader, writer, **kwargs): 15 | """Return a tuple with `(unbox_stream, box_stream)` (reader/writer). 16 | 17 | :return: (:class:`secret_handshake.boxstream.UnboxStream`, 18 | :class:`secret_handshake.boxstream.BoxStream`) """ 19 | box_args = { 20 | 'key': kwargs['encrypt_key'], 21 | 'nonce': kwargs['encrypt_nonce'], 22 | } 23 | unbox_args = { 24 | 'key': kwargs['decrypt_key'], 25 | 'nonce': kwargs['decrypt_nonce'], 26 | } 27 | return UnboxStream(reader, **unbox_args), BoxStream(writer, **box_args) 28 | 29 | 30 | class UnboxStream(object): 31 | def __init__(self, reader, key, nonce): 32 | self.reader = reader 33 | self.key = key 34 | self.nonce = nonce 35 | self.closed = False 36 | 37 | async def read(self): 38 | try: 39 | data = await self.reader.readexactly(HEADER_LENGTH) 40 | except IncompleteReadError: 41 | self.closed = True 42 | return None 43 | 44 | box = SecretBox(self.key) 45 | 46 | header = box.decrypt(data, self.nonce) 47 | 48 | if header == TERMINATION_HEADER: 49 | self.closed = True 50 | return None 51 | 52 | length = struct.unpack('>H', header[:2])[0] 53 | mac = header[2:] 54 | 55 | data = await self.reader.readexactly(length) 56 | 57 | body = box.decrypt(mac + data, inc_nonce(self.nonce)) 58 | 59 | self.nonce = inc_nonce(inc_nonce(self.nonce)) 60 | return body 61 | 62 | @async_generator 63 | async def __aiter__(self): 64 | while True: 65 | data = await self.read() 66 | if data is None: 67 | return 68 | await yield_(data) 69 | 70 | 71 | class BoxStream(object): 72 | def __init__(self, writer, key, nonce): 73 | self.writer = writer 74 | self.key = key 75 | self.box = SecretBox(self.key) 76 | self.nonce = nonce 77 | 78 | def write(self, data): 79 | for chunk in split_chunks(data, MAX_SEGMENT_SIZE): 80 | body = self.box.encrypt(chunk, inc_nonce(self.nonce))[24:] 81 | header = struct.pack('>H', len(body) - 16) + body[:16] 82 | 83 | hdrbox = self.box.encrypt(header, self.nonce)[24:] 84 | self.writer.write(hdrbox) 85 | 86 | self.nonce = inc_nonce(inc_nonce(self.nonce)) 87 | self.writer.write(body[16:]) 88 | 89 | def close(self): 90 | self.writer.write(self.box.encrypt(b'\x00' * 18, self.nonce)[24:]) 91 | -------------------------------------------------------------------------------- /secret_handshake/crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | import hashlib 23 | import hmac 24 | from base64 import b64decode 25 | 26 | from nacl.bindings import (crypto_box_afternm, crypto_box_open_afternm, 27 | crypto_scalarmult) 28 | from nacl.exceptions import CryptoError 29 | from nacl.public import PrivateKey 30 | from nacl.signing import VerifyKey 31 | 32 | APPLICATION_KEY = b64decode('1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=') 33 | 34 | 35 | class SHSError(Exception): 36 | """A SHS exception.""" 37 | pass 38 | 39 | 40 | class SHSCryptoBase(object): 41 | def __init__(self, local_key, ephemeral_key=None, application_key=None): 42 | self.local_key = local_key 43 | self.application_key = application_key or APPLICATION_KEY 44 | self._reset_keys(ephemeral_key or PrivateKey.generate()) 45 | 46 | def _reset_keys(self, ephemeral_key): 47 | self.local_ephemeral_key = ephemeral_key 48 | self.local_app_hmac = (hmac.new(self.application_key, bytes(ephemeral_key.public_key), digestmod='sha512') 49 | .digest()[:32]) 50 | 51 | def generate_challenge(self): 52 | """Generate and return a challenge to be sent to the server.""" 53 | return self.local_app_hmac + bytes(self.local_ephemeral_key.public_key) 54 | 55 | def verify_challenge(self, data): 56 | """Verify the correctness of challenge sent from the client.""" 57 | assert len(data) == 64 58 | sent_hmac, remote_ephemeral_key = data[:32], data[32:] 59 | 60 | h = hmac.new(self.application_key, remote_ephemeral_key, digestmod='sha512') 61 | self.remote_app_hmac = h.digest()[:32] 62 | ok = self.remote_app_hmac == sent_hmac 63 | 64 | if ok: 65 | # this is (a * b) 66 | self.shared_secret = crypto_scalarmult(bytes(self.local_ephemeral_key), remote_ephemeral_key) 67 | self.remote_ephemeral_key = remote_ephemeral_key 68 | # this is hash(a * b) 69 | self.shared_hash = hashlib.sha256(self.shared_secret).digest() 70 | return ok 71 | 72 | def clean(self, new_ephemeral_key=None): 73 | self._reset_keys(new_ephemeral_key or PrivateKey.generate()) 74 | self.shared_secret = None 75 | self.shared_hash = None 76 | self.remote_ephemeral_key = None 77 | 78 | def get_box_keys(self): 79 | shared_secret = hashlib.sha256(self.box_secret).digest() 80 | return { 81 | 'shared_secret': shared_secret, 82 | 'encrypt_key': hashlib.sha256(shared_secret + bytes(self.remote_pub_key)).digest(), 83 | 'decrypt_key': hashlib.sha256(shared_secret + bytes(self.local_key.verify_key)).digest(), 84 | 'encrypt_nonce': self.remote_app_hmac[:24], 85 | 'decrypt_nonce': self.local_app_hmac[:24] 86 | } 87 | 88 | 89 | class SHSServerCrypto(SHSCryptoBase): 90 | def verify_client_auth(self, data): 91 | assert len(data) == 112 92 | a_bob = crypto_scalarmult(bytes(self.local_key.to_curve25519_private_key()), self.remote_ephemeral_key) 93 | box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob).digest() 94 | self.hello = crypto_box_open_afternm(data, b'\x00' * 24, box_secret) 95 | signature, public_key = self.hello[:64], self.hello[64:] 96 | signed = self.application_key + bytes(self.local_key.verify_key) + self.shared_hash 97 | pkey = VerifyKey(public_key) 98 | 99 | # will raise an exception if verification fails 100 | pkey.verify(signed, signature) 101 | self.remote_pub_key = pkey 102 | b_alice = crypto_scalarmult(bytes(self.local_ephemeral_key), 103 | bytes(self.remote_pub_key.to_curve25519_public_key())) 104 | self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob + b_alice).digest()[:32] 105 | return True 106 | 107 | def generate_accept(self): 108 | okay = self.local_key.sign(self.application_key + self.hello + self.shared_hash).signature 109 | d = crypto_box_afternm(okay, b'\x00' * 24, self.box_secret) 110 | return d 111 | 112 | def clean(self, new_ephemeral_key=None): 113 | super(SHSServerCrypto, self).clean(new_ephemeral_key=new_ephemeral_key) 114 | self.hello = None 115 | self.b_alice = None 116 | 117 | 118 | class SHSClientCrypto(SHSCryptoBase): 119 | """An object that encapsulates all the SHS client-side crypto. 120 | 121 | :param local_key: the keypair used by the client (:class:`nacl.public.PrivateKey` object) 122 | :param server_pub_key: the server's public key (``byte`` string) 123 | :param ephemeral_key: a fresh local :class:`nacl.public.PrivateKey` 124 | :param application_key: the unique application key (``byte`` string), defaults to SSB's 125 | """ 126 | 127 | def __init__(self, local_key, server_pub_key, ephemeral_key, application_key=None): 128 | super(SHSClientCrypto, self).__init__(local_key, ephemeral_key, application_key) 129 | self.remote_pub_key = VerifyKey(server_pub_key) 130 | 131 | def verify_server_challenge(self, data): 132 | """Verify the correctness of challenge sent from the server.""" 133 | assert super(SHSClientCrypto, self).verify_challenge(data) 134 | curve_pkey = self.remote_pub_key.to_curve25519_public_key() 135 | 136 | # a_bob is (a * B) 137 | a_bob = crypto_scalarmult(bytes(self.local_ephemeral_key), bytes(curve_pkey)) 138 | self.a_bob = a_bob 139 | # this shall be hash(K | a * b | a * B) 140 | self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob).digest() 141 | 142 | # and message_to_box will correspond to H = sign(A)[K | Bp | hash(a * b)] | Ap 143 | signed_message = self.local_key.sign(self.application_key + bytes(self.remote_pub_key) + self.shared_hash) 144 | message_to_box = signed_message.signature + bytes(self.local_key.verify_key) 145 | self.hello = message_to_box 146 | return True 147 | 148 | def generate_client_auth(self): 149 | """Generate box[K|a*b|a*B](H)""" 150 | 151 | nonce = b"\x00" * 24 152 | # return box(K | a * b | a * B)[H] 153 | return crypto_box_afternm(self.hello, nonce, self.box_secret) 154 | 155 | def verify_server_accept(self, data): 156 | """Verify that the server's accept message is sane""" 157 | curve_lkey = self.local_key.to_curve25519_private_key() 158 | # b_alice is (A * b) 159 | b_alice = crypto_scalarmult(bytes(curve_lkey), self.remote_ephemeral_key) 160 | self.b_alice = b_alice 161 | # this is hash(K | a * b | a * B | A * b) 162 | self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + self.a_bob + 163 | b_alice).digest() 164 | 165 | nonce = b"\x00" * 24 166 | 167 | try: 168 | # let's use the box secret to unbox our encrypted message 169 | signature = crypto_box_open_afternm(data, nonce, self.box_secret) 170 | except CryptoError: 171 | raise SHSError('Error decrypting server acceptance message') 172 | 173 | # we should have received sign(B)[K | H | hash(a * b)] 174 | # let's see if that signature can verify the reconstructed data on our side 175 | self.remote_pub_key.verify(self.application_key + self.hello + self.shared_hash, signature) 176 | return True 177 | 178 | def clean(self, new_ephemeral_key=None): 179 | super(SHSClientCrypto, self).clean(new_ephemeral_key=new_ephemeral_key) 180 | self.a_bob = None 181 | self.b_alice = None 182 | -------------------------------------------------------------------------------- /secret_handshake/network.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | import asyncio 23 | 24 | from async_generator import async_generator, yield_ 25 | 26 | from .boxstream import get_stream_pair 27 | from .crypto import SHSClientCrypto, SHSServerCrypto 28 | 29 | 30 | class SHSClientException(Exception): 31 | pass 32 | 33 | 34 | class SHSDuplexStream(object): 35 | def __init__(self): 36 | self.write_stream = None 37 | self.read_stream = None 38 | self.is_connected = False 39 | 40 | def write(self, data): 41 | self.write_stream.write(data) 42 | 43 | async def read(self): 44 | return await self.read_stream.read() 45 | 46 | def close(self): 47 | self.write_stream.close() 48 | self.read_stream.close() 49 | self.is_connected = False 50 | 51 | @async_generator 52 | async def __aiter__(self): 53 | async for msg in self.read_stream: 54 | await yield_(msg) 55 | 56 | 57 | class SHSEndpoint(object): 58 | def __init__(self): 59 | self._on_connect = None 60 | self.crypto = None 61 | 62 | def on_connect(self, cb): 63 | self._on_connect = cb 64 | 65 | def disconnect(self): 66 | raise NotImplementedError 67 | 68 | 69 | class SHSServer(SHSEndpoint): 70 | def __init__(self, host, port, server_kp, application_key=None): 71 | super(SHSServer, self).__init__() 72 | self.host = host 73 | self.port = port 74 | self.crypto = SHSServerCrypto(server_kp, application_key=application_key) 75 | self.connections = [] 76 | 77 | async def _handshake(self, reader, writer): 78 | data = await reader.readexactly(64) 79 | if not self.crypto.verify_challenge(data): 80 | raise SHSClientException('Client challenge is not valid') 81 | 82 | writer.write(self.crypto.generate_challenge()) 83 | 84 | data = await reader.readexactly(112) 85 | if not self.crypto.verify_client_auth(data): 86 | raise SHSClientException('Client auth is not valid') 87 | 88 | writer.write(self.crypto.generate_accept()) 89 | 90 | async def handle_connection(self, reader, writer): 91 | self.crypto.clean() 92 | await self._handshake(reader, writer) 93 | keys = self.crypto.get_box_keys() 94 | self.crypto.clean() 95 | 96 | conn = SHSServerConnection.from_byte_streams(reader, writer, **keys) 97 | self.connections.append(conn) 98 | 99 | if self._on_connect: 100 | asyncio.ensure_future(self._on_connect(conn)) 101 | 102 | async def listen(self): 103 | await asyncio.start_server(self.handle_connection, self.host, self.port) 104 | 105 | def disconnect(self): 106 | for connection in self.connections: 107 | connection.close() 108 | 109 | 110 | class SHSServerConnection(SHSDuplexStream): 111 | def __init__(self, read_stream, write_stream): 112 | super(SHSServerConnection, self).__init__() 113 | self.read_stream = read_stream 114 | self.write_stream = write_stream 115 | 116 | @classmethod 117 | def from_byte_streams(cls, reader, writer, **keys): 118 | reader, writer = get_stream_pair(reader, writer, **keys) 119 | return cls(reader, writer) 120 | 121 | 122 | class SHSClient(SHSDuplexStream, SHSEndpoint): 123 | def __init__(self, host, port, client_kp, server_pub_key, ephemeral_key=None, application_key=None): 124 | SHSDuplexStream.__init__(self) 125 | SHSEndpoint.__init__(self) 126 | self.host = host 127 | self.port = port 128 | self.crypto = SHSClientCrypto(client_kp, server_pub_key, ephemeral_key=ephemeral_key, 129 | application_key=application_key) 130 | 131 | async def _handshake(self, reader, writer): 132 | writer.write(self.crypto.generate_challenge()) 133 | 134 | data = await reader.readexactly(64) 135 | if not self.crypto.verify_server_challenge(data): 136 | raise SHSClientException('Server challenge is not valid') 137 | 138 | writer.write(self.crypto.generate_client_auth()) 139 | 140 | data = await reader.readexactly(80) 141 | if not self.crypto.verify_server_accept(data): 142 | raise SHSClientException('Server accept is not valid') 143 | 144 | async def open(self): 145 | reader, writer = await asyncio.open_connection(self.host, self.port) 146 | await self._handshake(reader, writer) 147 | 148 | keys = self.crypto.get_box_keys() 149 | self.crypto.clean() 150 | 151 | self.read_stream, self.write_stream = get_stream_pair(reader, writer, **keys) 152 | self.writer = writer 153 | self.is_connected = True 154 | if self._on_connect: 155 | await self._on_connect() 156 | 157 | def disconnect(self): 158 | self.close() 159 | -------------------------------------------------------------------------------- /secret_handshake/test_boxstream.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | import pytest 23 | 24 | from secret_handshake.boxstream import HEADER_LENGTH, BoxStream, UnboxStream 25 | from secret_handshake.util import AsyncBuffer, async_comprehend 26 | 27 | from .test_crypto import CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE 28 | 29 | MESSAGE_1 = (b'\xcev\xedE\x06l\x02\x13\xc8\x17V\xfa\x8bZ?\x88B%O\xb0L\x9f\x8e\x8c0y\x1dv\xc0\xc9\xf6\x9d\xc2\xdf\xdb' 30 | b'\xee\x9d') 31 | MESSAGE_2 = b"\x141\xd63\x13d\xd1\xecZ\x9b\xd0\xd4\x03\xcdR?'\xaa.\x89I\x92I\xf9guL\xaa\x06?\xea\xca/}\x88*\xb2" 32 | MESSAGE_3 = (b'\xcbYY\xf1\x0f\xa5O\x13r\xa6"\x15\xc5\x9d\r.*\x0b\x92\x10m\xa6(\x0c\x0c\xc61\x80j\x81)\x800\xed\xda' 33 | b'\xad\xa1') 34 | MESSAGE_CLOSED = b'\xb1\x14hU\'\xb5M\xa6"\x03\x9duy\xa1\xd4evW,\xdcE\x18\xe4+ C4\xe8h\x96\xed\xc5\x94\x80' 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_boxstream(): 39 | buffer = AsyncBuffer() 40 | box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) 41 | box_stream.write(b'foo') 42 | buffer.seek(0) 43 | assert await buffer.read() == MESSAGE_1 44 | 45 | pos = buffer.tell() 46 | box_stream.write(b'foo') 47 | buffer.seek(pos) 48 | assert await buffer.read() == MESSAGE_2 49 | 50 | pos = buffer.tell() 51 | box_stream.write(b'bar') 52 | buffer.seek(pos) 53 | assert await buffer.read() == MESSAGE_3 54 | 55 | pos = buffer.tell() 56 | box_stream.close() 57 | buffer.seek(pos) 58 | assert await buffer.read() == MESSAGE_CLOSED 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_unboxstream(): 63 | buffer = AsyncBuffer(MESSAGE_1 + MESSAGE_2 + MESSAGE_3 + MESSAGE_CLOSED) 64 | buffer.seek(0) 65 | 66 | unbox_stream = UnboxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) 67 | assert not unbox_stream.closed 68 | assert (await async_comprehend(unbox_stream)) == [b'foo', b'foo', b'bar'] 69 | assert unbox_stream.closed 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_long_packets(): 74 | data_size = 6 * 1024 75 | data = bytes(n % 256 for n in range(data_size)) 76 | 77 | # box 6K buffer 78 | buffer = AsyncBuffer() 79 | box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) 80 | box_stream.write(data) 81 | # the size overhead corresponds to the two packet headers 82 | assert buffer.tell() == data_size + (HEADER_LENGTH * 2) 83 | buffer.seek(0) 84 | 85 | # now let's unbox it and check whether it's OK 86 | unbox_stream = UnboxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) 87 | first_packet = await unbox_stream.read() 88 | assert first_packet == data[:4096] 89 | second_packet = await unbox_stream.read() 90 | assert second_packet == data[4096:] 91 | -------------------------------------------------------------------------------- /secret_handshake/test_crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | import hashlib 23 | 24 | import pytest 25 | from nacl.public import PrivateKey 26 | from nacl.signing import SigningKey 27 | 28 | from secret_handshake.crypto import SHSClientCrypto, SHSServerCrypto 29 | 30 | APP_KEY = hashlib.sha256(b'app_key').digest() 31 | SERVER_KEY_SEED = b'\xcaw\x01\xc2cQ\xfd\x94\x9f\x14\x84\x0c0\x12\\\x96\xcd\x9b\x0c\x02z&\x96!\xe0\xa2' 32 | CLIENT_KEY_SEED = b'\xbf\x02<\xd3e\x9d\xac-\xd1\x9e-{\xe5q\x90\x03\x11\xba\x8cSQ\xa0\xc3p~\x89\xe6\xeeb\xaa\x1c\x17' 33 | SERVER_EPH_KEY_SEED = b"ed\x1c\x01\x03s\x04\xdc\x8e`\xd6Z\xd0u;\xcbX\x91\xd8ZO\xf8\xf0\xd6'\xd5\xb1Yy\x13yH" 34 | CLIENT_EPH_KEY_SEED = b'u8\xd0\xe3\x85d_Pz\x0c\xf5\xfd\x15\xce2p#\xb0\xf0\x9f\xe6!\xe1\xcb\xf6\x93\t\xebr{1\x8b' 35 | 36 | 37 | @pytest.fixture() 38 | def server(): 39 | server_key = SigningKey(SERVER_KEY_SEED) 40 | server_eph_key = PrivateKey(SERVER_EPH_KEY_SEED) 41 | return SHSServerCrypto(server_key, server_eph_key, application_key=APP_KEY) 42 | 43 | 44 | @pytest.fixture() 45 | def client(): 46 | client_key = SigningKey(CLIENT_KEY_SEED) 47 | server_key = SigningKey(SERVER_KEY_SEED) 48 | client_eph_key = PrivateKey(CLIENT_EPH_KEY_SEED) 49 | return SHSClientCrypto(client_key, bytes(server_key.verify_key), client_eph_key, application_key=APP_KEY) 50 | 51 | 52 | CLIENT_CHALLENGE = (b'd\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7j' 53 | b'\xd7\xf0uc\xf0r\xf3\x7f\t\x18\xec\x8c\xf7\xff\x8e\xa9\xc83\x13\x18R\x16\x1d' 54 | b'\xe5\xc6K\xae\x94\xdbVt\x84\xdc\x1c@+D\x1c%') 55 | CLIENT_AUTH = (b'\xf2\xaf?z\x15\x10\xd0\xf0\xdf\xe3\x91\xfe\x14\x1c}z\xab\xeey\xf5\xef\xfc\xa1EdV\xf2T\x95s[!$z' 56 | b'\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t\xc4' 57 | b'\xd8\xc3$[\xc5\x94je\x83\x00%\x99\x10\x16\xb1\xa2\xb2\xb7\xbf\xc9\x88\x14\xb9\xbb^\tzq\xa4\xef\xc5' 58 | b'\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3') 59 | SERVER_CHALLENGE = (b'S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9\xeawl\xb9}\x85\xc3ik' 60 | b'\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{') 61 | SERVER_ACCEPT = (b'\xb4\xd0\xea\xfb\xfb\xf6s\xcc\x10\xc4\x99\x95"\x13 y\xa6\xea.G\xeed\x8d=t9\x88|\x94\xd1\xbcK\xd47' 62 | b'\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d7}\xb5\xce' 63 | b'(k\x15\xe3L\x9d)\xd5\xa1|:') 64 | INTER_SHARED_SECRET = (b'vf\xd82\xaeU\xda]\x08\x9eZ\xd6\x06\xcc\xd3\x99\xfd\xce\xc5\x16e8n\x9a\x04\x04\x84\xc5\x1a' 65 | b'\x8f\xf2M') 66 | BOX_SECRET = b'\x03\xfe\xe3\x8c u\xbcl^\x17eD\x96\xa3\xa6\x880f\x11\x7f\x85\xf2:\xa3[`\x06[#l\xbcr' 67 | 68 | SHARED_SECRET = b'UV\xad*\x8e\xce\x88\xf2\x87l\x13iZ\x12\xd7\xa6\xd1\x9c-\x9d\x07\xf5\xa96\x03w\x11\xe5\x96$m\x1d' 69 | CLIENT_ENCRYPT_KEY = (b'\xec\x1f,\x82\x9f\xedA\xc0\xda\x87[\xf9u\xbf\xac\x9cI\xa5T\xd1\x91\xff\xa8.\xd0 \xfbU\xc7\x14' 70 | b')\xc7') 71 | CLIENT_DECRYPT_KEY = b'\xf9e\xa0As\xb2=\xb7P~\xf3\xf9(\xfd\x7f\xfe\xb7TZhn\xd7\x8c=\xea.o\x9e\x8c9)\x10' 72 | CLIENT_ENCRYPT_NONCE = b'S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9' 73 | CLIENT_DECRYPT_NONCE = b'd\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7' 74 | 75 | 76 | def test_handshake(client, server): 77 | client_challenge = client.generate_challenge() 78 | assert client_challenge == CLIENT_CHALLENGE 79 | assert server.verify_challenge(client_challenge) 80 | 81 | server_challenge = server.generate_challenge() 82 | assert server_challenge == SERVER_CHALLENGE 83 | assert client.verify_server_challenge(server_challenge) 84 | 85 | assert client.shared_secret == INTER_SHARED_SECRET 86 | 87 | client_auth = client.generate_client_auth() 88 | assert client_auth == CLIENT_AUTH 89 | assert server.verify_client_auth(client_auth) 90 | 91 | assert server.shared_secret == client.shared_secret 92 | 93 | server_accept = server.generate_accept() 94 | assert server_accept == SERVER_ACCEPT 95 | assert client.verify_server_accept(server_accept) 96 | 97 | assert client.box_secret == BOX_SECRET 98 | assert client.box_secret == server.box_secret 99 | 100 | client_keys = client.get_box_keys() 101 | server_keys = server.get_box_keys() 102 | 103 | assert client_keys['shared_secret'] == SHARED_SECRET 104 | assert client_keys['encrypt_key'] == CLIENT_ENCRYPT_KEY 105 | assert client_keys['decrypt_key'] == CLIENT_DECRYPT_KEY 106 | assert client_keys['encrypt_nonce'] == CLIENT_ENCRYPT_NONCE 107 | assert client_keys['decrypt_nonce'] == CLIENT_DECRYPT_NONCE 108 | 109 | assert client_keys['shared_secret'] == server_keys['shared_secret'] 110 | assert client_keys['encrypt_key'] == server_keys['decrypt_key'] 111 | assert client_keys['encrypt_nonce'] == server_keys['decrypt_nonce'] 112 | -------------------------------------------------------------------------------- /secret_handshake/test_network.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import os 22 | from asyncio import Event, wait_for 23 | 24 | import pytest 25 | from nacl.signing import SigningKey 26 | 27 | from secret_handshake.util import AsyncBuffer 28 | 29 | 30 | class DummyCrypto(object): 31 | """Dummy crypto module, pretends everything is fine.""" 32 | def verify_server_challenge(self, data): 33 | return True 34 | 35 | def verify_challenge(self, data): 36 | return True 37 | 38 | def verify_server_accept(self, data): 39 | return True 40 | 41 | def generate_challenge(self): 42 | return b'CHALLENGE' 43 | 44 | def generate_client_auth(self): 45 | return b'AUTH' 46 | 47 | def verify_client_auth(self, data): 48 | return True 49 | 50 | def generate_accept(self): 51 | return b'ACCEPT' 52 | 53 | def get_box_keys(self): 54 | return { 55 | 'encrypt_key': b'x' * 32, 56 | 'encrypt_nonce': b'x' * 32, 57 | 'decrypt_key': b'x' * 32, 58 | 'decrypt_nonce': b'x' * 32 59 | } 60 | 61 | def clean(self): 62 | return 63 | 64 | 65 | def _dummy_boxstream(stream, **kwargs): 66 | """Identity boxstream, no tansformation.""" 67 | return stream 68 | 69 | 70 | def _client_stream_mocker(): 71 | reader = AsyncBuffer(b'xxx') 72 | writer = AsyncBuffer(b'xxx') 73 | 74 | async def _create_mock_streams(host, port): 75 | return reader, writer 76 | 77 | return reader, writer, _create_mock_streams 78 | 79 | 80 | def _server_stream_mocker(): 81 | reader = AsyncBuffer(b'xxx') 82 | writer = AsyncBuffer(b'xxx') 83 | 84 | async def _create_mock_server(cb, host, port): 85 | await cb(reader, writer) 86 | 87 | return reader, writer, _create_mock_server 88 | 89 | 90 | @pytest.mark.asyncio 91 | async def test_client(mocker): 92 | reader, writer, _create_mock_streams = _client_stream_mocker() 93 | mocker.patch('asyncio.open_connection', new=_create_mock_streams) 94 | mocker.patch('secret_handshake.boxstream.BoxStream', new=_dummy_boxstream) 95 | mocker.patch('secret_handshake.boxstream.UnboxStream', new=_dummy_boxstream) 96 | 97 | from secret_handshake import SHSClient 98 | 99 | client = SHSClient('shop.local', 1111, SigningKey.generate(), os.urandom(32)) 100 | client.crypto = DummyCrypto() 101 | 102 | await client.open() 103 | reader.append(b'TEST') 104 | assert (await client.read()) == b'TEST' 105 | client.disconnect() 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_server(mocker): 110 | from secret_handshake import SHSServer 111 | 112 | resolve = Event() 113 | 114 | async def _on_connect(conn): 115 | server.disconnect() 116 | resolve.set() 117 | 118 | reader, writer, _create_mock_server = _server_stream_mocker() 119 | mocker.patch('asyncio.start_server', new=_create_mock_server) 120 | mocker.patch('secret_handshake.boxstream.BoxStream', new=_dummy_boxstream) 121 | mocker.patch('secret_handshake.boxstream.UnboxStream', new=_dummy_boxstream) 122 | 123 | server = SHSServer('shop.local', 1111, SigningKey.generate(), os.urandom(32)) 124 | server.crypto = DummyCrypto() 125 | 126 | server.on_connect(_on_connect) 127 | 128 | await server.listen() 129 | await wait_for(resolve.wait(), 5) 130 | -------------------------------------------------------------------------------- /secret_handshake/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | import struct 23 | from io import BytesIO 24 | 25 | NONCE_SIZE = 24 26 | MAX_NONCE = (8 * NONCE_SIZE) 27 | 28 | 29 | class AsyncBuffer(BytesIO): 30 | """Just a BytesIO with an async read method.""" 31 | async def read(self, n=None): 32 | v = super(AsyncBuffer, self).read(n) 33 | return v 34 | readexactly = read 35 | 36 | def append(self, data): 37 | """Append data to the buffer without changing the current position.""" 38 | pos = self.tell() 39 | self.write(data) 40 | self.seek(pos) 41 | 42 | 43 | async def async_comprehend(generator): 44 | """Emulate ``[elem async for elem in generator]``.""" 45 | results = [] 46 | async for msg in generator: 47 | results.append(msg) 48 | return results 49 | 50 | 51 | def inc_nonce(nonce): 52 | num = bytes_to_long(nonce) + 1 53 | if num > 2 ** MAX_NONCE: 54 | num = 0 55 | bnum = long_to_bytes(num) 56 | bnum = b'\x00' * (NONCE_SIZE - len(bnum)) + bnum 57 | return bnum 58 | 59 | 60 | def split_chunks(seq, n): 61 | """Split sequence in equal-sized chunks. 62 | The last chunk is not padded.""" 63 | while seq: 64 | yield seq[:n] 65 | seq = seq[n:] 66 | 67 | 68 | # Stolen from PyCypto (Public Domain) 69 | def b(s): 70 | return s.encode("latin-1") # utf-8 would cause some side-effects we don't want 71 | 72 | 73 | def long_to_bytes(n, blocksize=0): 74 | """long_to_bytes(n:long, blocksize:int) : string 75 | Convert a long integer to a byte string. 76 | If optional blocksize is given and greater than zero, pad the front of the 77 | byte string with binary zeros so that the length is a multiple of 78 | blocksize. 79 | """ 80 | # after much testing, this algorithm was deemed to be the fastest 81 | s = b('') 82 | pack = struct.pack 83 | while n > 0: 84 | s = pack('>I', n & 0xffffffff) + s 85 | n = n >> 32 86 | # strip off leading zeros 87 | for i in range(len(s)): 88 | if s[i] != b('\000')[0]: 89 | break 90 | else: 91 | # only happens when n == 0 92 | s = b('\000') 93 | i = 0 94 | s = s[i:] 95 | # add back some pad bytes. this could be done more efficiently w.r.t. the 96 | # de-padding being done above, but sigh... 97 | if blocksize > 0 and len(s) % blocksize: 98 | s = (blocksize - len(s) % blocksize) * b('\000') + s 99 | return s 100 | 101 | 102 | def bytes_to_long(s): 103 | """bytes_to_long(string) : long 104 | Convert a byte string to a long integer. 105 | This is (essentially) the inverse of long_to_bytes(). 106 | """ 107 | acc = 0 108 | unpack = struct.unpack 109 | length = len(s) 110 | if length % 4: 111 | extra = (4 - length % 4) 112 | s = b('\000') * extra + s 113 | length = length + extra 114 | for i in range(0, length, 4): 115 | acc = (acc << 32) + unpack('>I', s[i:i+4])[0] 116 | return acc 117 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | 4 | [aliases] 5 | test=pytest 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """A module that implements Secret Handshake as specified in "Designing a Secret Handshake: Authenticated 22 | Key Exchange as a Capability System" (Dominic Tarr, 2015).""" 23 | 24 | from setuptools import find_packages, setup 25 | 26 | readme = open('README.rst').read() 27 | history = open('CHANGES.rst').read() 28 | 29 | tests_require = [ 30 | 'check-manifest>=0.39', 31 | 'coverage==4.5.3', 32 | 'isort>=4.3.20', 33 | 'pydocstyle==3.0.0', 34 | 'pytest-cov==2.7.1', 35 | 'pytest==4.6.3', 36 | 'pytest-asyncio==0.10.0', 37 | 'pytest-mock==1.10.4' 38 | ] 39 | 40 | extras_require = { 41 | 'docs': [ 42 | 'Sphinx>=2.1.1', 43 | ], 44 | 'tests': tests_require, 45 | } 46 | extras_require['all'] = sum((lst for lst in extras_require.values()), []) 47 | 48 | install_requires = [ 49 | 'async-generator==1.10', 50 | 'pynacl==1.3.0' 51 | ] 52 | 53 | setup_requires = [ 54 | 'pytest-runner' 55 | ] 56 | 57 | packages = find_packages() 58 | 59 | setup( 60 | name='secret-handshake', 61 | version='0.1.0.dev4', 62 | description=__doc__, 63 | long_description=(readme + '\n\n' + history), 64 | license='MIT', 65 | author='PySecretHandshake Contributors', 66 | author_email='pedro@dete.st', 67 | url='https://github.com/pferreir/PySecretHandshake', 68 | packages=packages, 69 | include_package_data=True, 70 | extras_require=extras_require, 71 | install_requires=install_requires, 72 | setup_requires=setup_requires, 73 | tests_require=tests_require, 74 | zip_safe=False, 75 | classifiers=[ 76 | 'Intended Audience :: Developers', 77 | 'License :: OSI Approved :: MIT License', 78 | 'Operating System :: OS Independent', 79 | 'Programming Language :: Python', 80 | 'Topic :: Software Development :: Libraries :: Python Modules', 81 | 'Programming Language :: Python :: 3.5', 82 | 'Programming Language :: Python :: 3.6' 83 | ], 84 | ) 85 | --------------------------------------------------------------------------------