AVM Fritz!Box API Getting valid session id

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?

Greetings,
Rolf

Usually both Python and Java are not that different compared to Xojo. Why don’t you post your example code and what you tried in Xojo?

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.

Can you link the docs directly?

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)
2 Likes

The problem with your version is that you need a 32 byte SHA2_256 hash rather than a 65 byte SHA256 hash.

My habit of thinking around 3 corners. Thank you so much!