diff --git a/README.md b/README.md index 83adcdb..dcc8f07 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file * [openwebnet documentation](https://developer.legrand.com/Documentation/) * [old password algorithm](https://rosettacode.org/wiki/OpenWebNet_password#Python) +* [java implementation](https://github.com/mvalla/openwebnet4j/) used in OpenHab developed by [mvalla](https://github.com/mvalla) +* [another python implementation](https://github.com/karel1980/ReOpenWebNet) developed by [karel1980](https://github.com/karel1980) diff --git a/pyown/auth/hmac.py b/pyown/auth/hmac.py new file mode 100644 index 0000000..92900dc --- /dev/null +++ b/pyown/auth/hmac.py @@ -0,0 +1,154 @@ +from enum import IntEnum + +from secrets import token_bytes + +from hmac import HMAC, compare_digest +from hashlib import sha1, sha256 + + +__all__ = [ + "AuthAlgorithm", + "client_hmac", + "server_hmac", + "compare_hmac", + "create_key", + "hex_to_digits", +] + + +class AuthAlgorithm(IntEnum): + SHA1 = 1 + SHA256 = 2 + + +def client_hmac( + server_key: str, + client_key: str, + password: str, + client_identity: str = "736F70653E", + server_identity: str = "636F70653E", + hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256, +) -> bytes: + """ + Generate the HMAC authentication for the client. + + Args: + server_key: The key sent by the server (Ra) + client_key: The key generated by the client (Rb) + password: The open password of the server (Kab = sha(kab)) + client_identity: string used to identify the client (A) + server_identity: string used to identify the server (B) + hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256) + + Returns: + str: the client authentication string in bytes + """ + if hash_algorithm == AuthAlgorithm.SHA1: + hash_function = sha1 + elif hash_algorithm == AuthAlgorithm.SHA256: + hash_function = sha256 + else: + raise ValueError("Invalid hash algorithm") + + kab = hash_function(password.encode()).hexdigest() + hmac_message = f"{server_key}{client_key}{client_identity}{server_identity}{kab}" + + hmac = HMAC( + key=server_key.encode(), + msg=hmac_message.encode(), + digestmod=hash_function, + ) + + return hmac.digest() + + +def server_hmac( + server_key: str, + client_key: str, + password: str, + hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256, +) -> bytes: + """ + Generate the HMAC authentication for the server. + + Args: + server_key: The key sent by the server (Ra) + client_key: The key generated by the client (Rb) + password: The open password of the server (Kab = sha(kab)) + hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256) + + Returns: + str: the server confirmation string in bytes + """ + if hash_algorithm == AuthAlgorithm.SHA1: + hash_function = sha1 + elif hash_algorithm == AuthAlgorithm.SHA256: + hash_function = sha256 + else: + raise ValueError("Invalid hash algorithm") + + kab = hash_function(password.encode()).hexdigest() + hmac_message = f"{client_key}{server_key}{kab}" + + hmac = HMAC( + key=server_key.encode(), + msg=hmac_message.encode(), + digestmod=hash_function, + ) + + return hmac.digest() + +def compare_hmac( + hmac1: bytes, + hmac2: bytes, +) -> bool: + """ + Compare two hmacs in constant time. + + Args: + hmac1: The first hmac + hmac2: The second hmac + + Returns: + bool: True if the hmacs are equal, False otherwise + """ + return compare_digest(hmac1, hmac2) + +def create_key( + hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256, +) -> str: + """ + Create a random key for the hmac. + + Args: + hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256) + + Returns: + str: the key in hex format + """ + if hash_algorithm == AuthAlgorithm.SHA1: + hash_function = sha1 + elif hash_algorithm == AuthAlgorithm.SHA256: + hash_function = sha256 + else: + raise ValueError("Invalid hash algorithm") + + return hash_function(token_bytes(32)).hexdigest() + +def hex_to_digits( + hex_string: str, +) -> str: + """ + Convert a hex string to digits. + + Args: + hex_string: The hex string + + Returns: + str: the digits string + """ + out = "" + for c in hex_string: + value = int(c, 16) + out += str(value // 10) + str(value % 10) + return out