Source code for xled.security

# -*- coding: utf-8 -*-

"""
xled.security
~~~~~~~~~~~~~

This module contains cryptographic functions to encrypt data with shared
secret so it can be transferred over unencrypted connection.

.. seealso::

    :doc:`protocol_details`
        for various operations.
"""

import os
import base64
import hashlib
import itertools
import warnings

import netaddr

from xled.compat import zip, is_py2

if is_py2:
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        # This import would raise UserWarning on its own
        from cryptography.utils import CryptographyDeprecationWarning

with warnings.catch_warnings():
    if is_py2:
        # Make sure we ignore only CryptographyDeprecationWarning
        warnings.simplefilter("ignore", category=CryptographyDeprecationWarning)

    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms


#: Default key to encrypt challenge in login phase
SHARED_KEY_CHALLANGE = b"evenmoresecret!!"

#: Default key to encrypt WiFi password
SHARED_KEY_WIFI = b"supersecretkey!!"

#: Read buffer size for sha1sum
BUFFER_SIZE = 65536


[docs]def xor_strings(message, key): """ Apply XOR operation on every corresponding byte If key is shorter than message repeats it from the beginning until whole message is processed. :param bytes message: input message to encrypt :param bytes key: encryption key :return: encrypted cypher :rtype: bytearray """ if is_py2: message = bytearray(message) key = bytearray(key) ciphered = bytearray() for m_char, k_char in zip(message, itertools.cycle(key)): ciphered.append(m_char ^ k_char) return bytes(ciphered)
[docs]def derive_key(shared_key, mac_address): """ Derives secret key from shared key and MAC address MAC address is repeated to length of key. Then bytes on corresponding positions are xor-ed. Finally a string is created. :param str shared_key: secret key :param str mac_address: MAC address in any format that netaddr.EUI recognizes :return: derived key :rtype: bytes """ mac = netaddr.EUI(mac_address) return xor_strings(shared_key, mac.packed)
[docs]def rc4(message, key): """ Simple wrapper for RC4 cipher that encrypts message with key :param str message: input to encrypt :param str key: encryption key :return: ciphertext :rtype: str """ algorithm = algorithms.ARC4(key) cipher = Cipher(algorithm, mode=None, backend=default_backend()) encryptor = cipher.encryptor() return encryptor.update(message)
[docs]def generate_challenge(): """ Generates random challenge string :rtype: str """ return os.urandom(32)
[docs]def make_challenge_response(challenge_message, mac_address, key=SHARED_KEY_CHALLANGE): """ Create challenge response from challenge Used in initial login phase of communication with device. Could be used to check that device shares same shared secret and implements same algorithm to show that it is genuine. :param str challenge_message: random message originally sent as challenge with login request :param str mac_address: MAC address of the remote device in any format that netaddr.EUI recognizes :param str key: (optional) shared key that device has to know :return: hashed ciphertext that must be equal to challenge-response in response to login call :rtype: str """ secret_key = derive_key(key, mac_address) rc4_encoded = rc4(challenge_message, secret_key) return hashlib.sha1(rc4_encoded).hexdigest()
[docs]def encrypt_wifi_password(password, mac_address, key=SHARED_KEY_WIFI): """ Encrypts password This can be used to send password for WiFi in encrypted form over unencrypted channel. Ideally only device that knows shared secret key and has defined MAC address should be able to decrypt the message. :param str password: password to encrypt :param str mac_address: MAC address of the remote device in any format that netaddr.EUI recognizes :param str key: (optional) shared key that device has to know :return: Base 64 encoded string of ciphertext of input password :rtype: str """ if not isinstance(password, bytes): password = bytes(password, "utf-8") secret_key = derive_key(key, mac_address) data = password.ljust(64, b"\x00") rc4_encoded = rc4(data, secret_key) return base64.b64encode(rc4_encoded)
[docs]def sha1sum(fileobj): """ Computes SHA1 from file-like object It is up to caller to open file for reading and close it afterwards. :param fileobj: file-like object :return: SHA1 digest as hexdecimal digits only :rtype: str """ sha1 = hashlib.sha1() while True: data = fileobj.read(BUFFER_SIZE) if not data: break sha1.update(data) return sha1.hexdigest()