Hello.
I am trying to read and write commands to AVM home-automation devices (like DECT200),
but I don’t know how to get a valid session id (sid).
The technical note from avm only provide login examples in python and java, and i did not manage to translate them to xojo.
Has someone already achieved this or is willing to help out?
b.t.w. Is it allowed to post download links for external pdf documents here?
My xojo code is not in a state to be shown yet (spagetti code trying to figure out whats wrong with the calculations), i will rebuild it and send it tomorrow…
The python code is as follows:
#!/usr/bin/env python3
# vim: expandtab sw=4 ts=4
"""
FRITZ!OS WebGUI Login
Get a sid (session ID) via PBKDF2 based challenge response algorithm.
Fallback to MD5 if FRITZ!OS has no PBKDF2 support.
AVM 2020-09-25
"""
import sys
import hashlib
import time
import urllib.request
import urllib.parse
import xml.etree.ElementTree as ET
LOGIN_SID_ROUTE = "/login_sid.lua?version=2"
class LoginState:
def __init__(self, challenge: str, blocktime: int):
self.challenge = challenge
self.blocktime = blocktime
self.is_pbkdf2 = challenge.startswith("2$")
def get_sid(box_url: str, username: str, password: str) -> str:
""" Get a sid by solving the PBKDF2 (or MD5) challenge-response
process. """
try:
state = get_login_state(box_url)
except Exception as ex:
raise Exception("failed to get challenge") from ex
if state.is_pbkdf2:
print("PBKDF2 supported")
challenge_response = calculate_pbkdf2_response(state.challenge,
password)
else:
print("Falling back to MD5")
challenge_response = calculate_md5_response(state.challenge,
password)
if state.blocktime > 0:
print(f"Waiting for {state.blocktime} seconds...")
time.sleep(state.blocktime)
try:
sid = send_response(box_url, username, challenge_response)
except Exception as ex:
raise Exception("failed to login") from ex
if sid == "0000000000000000":
raise Exception("wrong username or password")
return sid
def get_login_state(box_url: str) -> LoginState:
""" Get login state from FRITZ!Box using login_sid.lua?version=2 """
url = box_url + LOGIN_SID_ROUTE
http_response = urllib.request.urlopen(url)
xml = ET.fromstring(http_response.read())
# print(f"xml: {xml}")
challenge = xml.find("Challenge").text
blocktime = int(xml.find("BlockTime").text)
return LoginState(challenge, blocktime)
def calculate_pbkdf2_response(challenge: str, password: str) -> str:
""" Calculate the response for a given challenge via PBKDF2 """
challenge_parts = challenge.split("$")
# Extract all necessary values encoded into the challenge
iter1 = int(challenge_parts[1])
salt1 = bytes.fromhex(challenge_parts[2])
iter2 = int(challenge_parts[3])
salt2 = bytes.fromhex(challenge_parts[4])
# Hash twice, once with static salt...
hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1)
# Once with dynamic salt.
hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2)
return f"{challenge_parts[4]}${hash2.hex()}"
def calculate_md5_response(challenge: str, password: str) -> str:
""" Calculate the response for a challenge using legacy MD5 """
response = challenge + "-" + password
# the legacy response needs utf_16_le encoding
response = response.encode("utf_16_le")
md5_sum = hashlib.md5()
md5_sum.update(response)
response = challenge + "-" + md5_sum.hexdigest()
return response
def send_response(box_url: str, username: str, challenge_response: str) ->
str:
""" Send the response and return the parsed sid. raises an Exception on
error """
# Build response params
post_data_dict = {"username": username, "response": challenge_response}
post_data = urllib.parse.urlencode(post_data_dict).encode()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
url = box_url + LOGIN_SID_ROUTE
# Send response
http_request = urllib.request.Request(url, post_data, headers)
http_response = urllib.request.urlopen(http_request)
# Parse SID from resulting XML.
xml = ET.fromstring(http_response.read())
return xml.find("SID").text
def main():
if len(sys.argv) < 4:
print(
f"Usage: {sys.argv[0]} http://fritz.box user pass"
)
exit(1)
url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
sid = get_sid(url, username, password)
print(f"Successful login for user: {username}")
print(f"sid: {sid}")
if __name__ == "__
main__":
main()
I gave up trying to convert the python code. I ended up with 12 functions and no valid return.
The docs say in short how the response has to be calculated from the given challenge code.
I have the monkeybread and einhugur plugins but did not find the proper methods.
Here the chapter how to compute it. Any hints very welcome!
The format of the challenge is defined as follows, separated by $ signs:
2$<iter1>$<salt1>$<iter2>$<salt2>
The response is formed as follows:
<hash1> = pbdkf2_hmac_sha256(<password>, <salt1>, <iter1>)
<response> = <salt2>$ + pbdkf2_hmac_sha256(<hash1>, <salt2>, <iter2>)
The example challenge “2$10000$5A1711$2000$5A1722” and the password “1example!“
(utf8-encoded) results in:
hash1 = pbdkf2_hmac_sha256(“1example!”, 5A1711, 10000)
=> 0x23428e9dec39d95ac7a40514062df0f9e94f996e17c398c79898d0403b332d3b (hex)
response = 5A1722$ + pbdkf2_hmac_sha256(hash1, 5A1722, 2000).hex()
=> 5A1722$1798a1672bca7c6463d6b245f82b53703b0f50813401b03e4045a5861e689adb
Note: the hash1 is not stringified, but remains a raw bytes value.
The numbers iter1 und iter2 are the “iteration” parameters for PBKDF2 of the 1. und 2. round,
respectively. These are kept variabel and may change in future releases of FRITZ!OS, in order to
PBKDF2-based login future-proof.
The code below is the try with minimum code, but even the hash1 computation is wrong:
Funktion Get_ResponseString(challenge as string, password as string) as string
// Debug/Test
Var d_challenge As String = "2$10000$5A1711$2000$5A1722"
Var d_password As String = "1example!"
challenge = d_challenge
password = d_password
// Debug/Text
Var challengeParts() As String = challenge.Split("$")
If UBound(challengeParts) = 4 Then
' Extract challenge parameters
Var iter1 As Integer = Val(challengeParts(1))
Var salt1 As MemoryBlock = HexDecode(challengeParts(2))
Var iter2 As Integer = Val(challengeParts(3))
Var salt2 As MemoryBlock = HexDecode(challengeParts(4))
' Compute hash1
' Test-Result shoud be: 0x23428e9dec39d95ac7a40514062df0f9e94f996e17c398c79898d0403b332d3b (hex)
Var hash1 As MemoryBlock = Crypto.PBKDF2(password, salt1, iter1, 65, Crypto.HashAlgorithms.SHA256)
' Compute hash2
Var hash1b As String = hash1
Var hash2 As MemoryBlock = Crypto.PBKDF2(hash1b, salt2, iter2, 65, Crypto.HashAlgorithms.SHA256)
' Build the response string
' Test-Result should be: 5A1722$1798a1672bca7c6463d6b245f82b53703b0f50813401b03e4045a5861e689adb
Return challengeParts(4) + "$" + BytesToHexString(hash2)
Else
' Handle incorrect challenge format
Return ""
End If
End Function
Function HexDecode (hexString as string) as Memoryblock
Var hexChars As String = "0123456789ABCDEF"
hexString = ReplaceAll(hexString, " ", "")
hexString = Uppercase(hexString)
If Len(hexString) Mod 2 <> 0 Then
hexString = "0" + hexString
End If
Var result As New MemoryBlock(Len(hexString) / 2)
For i As Integer = 1 To Len(hexString) Step 2
Var hexByte As String = Mid(hexString, i, 2)
Var byteValue As Integer = 16 * InStr(hexChars, Mid(hexByte, 1, 1)) - 1 + InStr(hexChars, Mid(hexByte, 2, 1)) - 1
result.Byte(i / 2) = byteValue
Next
Return result
Function BytesToHexString(bytes as MemoryBlock) as string
Var hexChars As String = "0123456789ABCDEF"
Var hexString As String = ""
For i As Integer = 0 To bytes.Size - 1
Var byteValue As Integer = bytes.Byte(i)
hexString = hexString + Mid(hexChars, (byteValue \ 16) + 1, 1) + Mid(hexChars, (byteValue Mod 16) + 1, 1)
Next
Return hexString
Dim challenge As String = "2$10000$5A1711$2000$5A1722"
Dim password As String = "1example!"
Dim iter1 As Integer = Val(NthField(challenge, "$", 2))
Dim iter2 As Integer = Val(NthField(challenge, "$", 4))
Dim salt1 As String = DecodeHex(NthField(challenge, "$", 3))
Dim salt2 As String = DecodeHex(NthField(challenge, "$", 5))
Dim hash1 As String = Crypto.PBKDF2(salt1, password, iter1, 32, Crypto.HashAlgorithms.SHA2_256)
Dim hash2 As String = Crypto.PBKDF2(salt2, hash1, iter2, 32, Crypto.HashAlgorithms.SHA2_256)
Dim response As String = EncodeHex(salt2) + "$" + EncodeHex(hash2)