· 6 years ago · Dec 31, 2019, 04:18 PM
1#!/usr/bin/perl
2use CGI::Carp qw(fatalsToBrowser); # Fehlermeldungen an den Browser ausgeben (sonst nur in der Konsole sichtbar)
3print "Content-type: text/plain\n\n"; # für die Ausgabe im Browser, nur plain text, keine Lust auf HTML Formatierung
4
5# Crashkurs in Perl damit das Lesen vom Code ggf. einfacher fällt :)
6# Punkt (".") verbindet strings
7# Variablen (Skalare) beginnen mit einem $ Zeichen, z.B. $name="Alexa"; (Arrays beginnen mit @ und Hashes mit %)
8# "use XXX::YYYY" lädt ein Modul
9
10# IP Adresse vom Miniserver sowie Benutzername und Passwort vom zu verwendenden User
11$miniserver_ip = "192.168.0.21";
12$user = 'alexa';
13$password = '********';
14$permission = 4; # 4=App (Token ist mehrere Wochen gültig) siehe schrottige Loxone API Doku
15$uuid = "aaaaaaaa-bbbb-cccc-dddddddddddddd01"; # beliebige ID mit der sich die Anwendung beim Miniserver identifiziert.
16$info = "TVserver"; # beliebiger Name von der App o.ä.
17
18# notwendige Module laden
19# (werden am Server z.B. mit "cpan -i URI::Escape" installiert)
20use LWP::Simple; # einfacher web crawler
21use JSON::Parse qw(parse_json valid_json); # JSON parser, es werden nur die Funktionen zum parsen und validieren von JSON benötigt
22use Digest::SHA1 qw(sha1_hex);
23use Digest::HMAC_SHA1 qw(hmac_sha1_hex);
24use MIME::Base64; # Base64 en/decoder
25use Crypt::Mode::CBC; # universelles Crypto-Modul, wir verwenden aber nur AES
26use Crypt::PK::RSA; # RSA Crypto-Modul
27use URI::Escape; # Modul zum URI/URL escapen
28
29# Key und Salt vom user vom Miniserver holen
30$content = get("http://$miniserver_ip/jdev/sys/getkey2/$user");
31# JSON response parsen
32$json = parse_json($content);
33# key und salt aus der JSON response in Variablen speichern
34$key_hex = $json->{LL}->{value}->{key};
35$salt_hex = $json->{LL}->{value}->{salt};
36
37# Anzeige vom Key in HEX und BIN/String Form (enthält nur alphanumerische Zeichen)
38print "key_hex: $key_hex\n";
39$key_bin = pack('H*',$key_hex); # HEX-String in BIN-String umwandeln
40print "key_bin: $key_bin\n";
41# das gleiche mit dem Salt
42print "salt_hex: $salt_hex\n";
43$salt_bin = pack('H*',$salt_hex); # HEX-String in BIN-String umwandeln
44print "salt_bin: $salt_bin\n";
45
46print "\n"; # Zeilenumbruch zwecks Übersichtlichkeit
47
48# Erstellen vom ersten Hash, der zu bilden ist aus dem Benutzerpasswort und dem zuvor abgerufenen Salt:
49$password_salt=$password.":".$salt_hex; # /!\ Salt in HEX Form!!!
50print "password:salt $password_salt\n";
51# SHA1 Hash erzeugen.
52# /!\ Mit uc() muss der Hash noch in Uppercase konvertiert werden, sonst funktioniert es nicht!
53$pwHash = uc(sha1_hex($password_salt));
54print "pwHash: $pwHash\n";
55
56# HMAC SHA1 aus Benutzername und dem zuvor erzeugten SHA1 Hash mit dem anfangs abgerufenen Key erzeugen:
57# /!\ Anders als der Salt, muss der Key hier in Binary Form verwendet werden!
58$data=$user.":".$pwHash;
59print "user_pwHash: $data\n";
60$hash = hmac_sha1_hex($data, $key_bin);
61print "hash: $hash\n";
62
63print "\n";
64
65# Kommando vorbereiten
66# $hash der zuletzt erzeugt wurde
67# $user Benutzername, sowie $permission, $uuid und $info wie anfangs definiert
68$cmd="jdev/sys/gettoken/$hash/$user/$permission/$uuid/$info";
69print "cmd: $cmd\n";
70
71print "\n";
72
73# Public Key vom Miniserver holen:
74$content = get("http://$miniserver_ip/jdev/sys/getPublicKey");
75# JSON response parsen:
76$json = parse_json($content);
77# Public Key aus dem JSON in $publicKey_content speichern:
78$publicKey_content = $json->{LL}->{value};
79print "publicKey_content: $publicKey_content\n";
80# Loxone gibt den Key fälschlicherweise als Zertifikat aus, also schreiben wir das mit RegEx um:
81$publicKey_content=~s/-----BEGIN CERTIFICATE-----/-----BEGIN PUBLIC KEY-----/;
82$publicKey_content=~s/-----END CERTIFICATE-----/-----END PUBLIC KEY-----/;
83print "publicKey_content: $publicKey_content\n";
84
85print "\n";
86
87# beliebiger Salt, 2 Bytes
88$salt2="1122";
89# und zuvor erzeugtes Kommando anhängen
90$plaintext = "salt/$salt2/".$cmd;
91print "plaintext: $plaintext\n";
92
93print "\n";
94
95# beliebiger AES-256 cbc Key (32 Byte) und Initialisierungsvektor (16 Byte):
96$AESkey_hex = "4141414141414141414141414141414141414141414141414141414141414141";
97$AESiv_hex = "42424242424242424242424242424242";
98print "AESkey_hex: $AESkey_hex\n";
99print "AESiv_hex: $AESiv_hex\n";
100# beides noch ins Binärformat umwandeln:
101$AESkey = pack('H*', $AESkey_hex);
102$AESiv = pack('H*', $AESiv_hex);
103
104# $plaintext mit dem gerade erstellen AES Key und iv verschlüsseln
105# /!\ WICHTIG! zero-padding verwenden!!!
106$cbc = Crypt::Mode::CBC->new('AES',4); # 4: padding=zero-padding
107$cipher_bin = $cbc->encrypt($plaintext, $AESkey, $AESiv);
108# und den Cipher noch in Base63 encodieren
109$cipher_base64 = encode_base64($cipher_bin,'');
110print "ciphertext_base64: $cipher\n";
111# sowie URI escapen damit keine reservierten Zeichen (+ = / etc.) in der URL vorkommen
112$cipher_base64_uri_encoded = uri_escape($cipher_base64);
113print "cipher_uri_encoded: $cipher_base64_uri_encoded\n";
114
115print "\n";
116
117# eigentlichen Aufruf vorbereiten
118# bei /enc/ antwortet der Miniserver unverschlüsselt, bei /fenc/ verschlüsselt
119$cmd = "jdev/sys/enc/".$cipher_base64_uri_encoded;
120print "cmd: $cmd\n";
121
122print "\n";
123
124# der eigene AES key und iv müssen dem Miniserver noch mitgegeben werden mit dem Aufruf
125# verschlüsselt werden diese mit RSA und dem Public Key vom Miniserver
126# /!\ RSA Parameter: ECB, PKCS1, Base64 with NoWrap
127
128# payload mit AES key und iv erstellen:
129$payload = $AESkey.":".$AESiv;
130print "payload: $payload\n";
131
132# Public Key mit Referenz auf die Variable einlesen
133$pub = Crypt::PK::RSA->new(\$publicKey_content);
134# verschlüsseln
135# /!\ WICHTIG: der 2. Parameter, 'v1.5' sagt dem Modul, dass PKCS1 Padding verwendet werden soll statt dem default oaep Padding
136$session_key = $pub->encrypt($payload, 'v1.5');
137# Base64 encodieren
138$session_key_base64 = encode_base64($session_key,'');
139print "session_key_base64: $session_key_base64\n";
140# und noch URI escapen
141$enc_session_key_base64 = uri_escape($session_key_base64);
142print "enc_session_key_base64: $enc_session_key_base64\n";
143
144print "\n";
145
146# vollständige URL basteln
147# Der String mit dem RSA verschlüsselten AES key und iv wird am Ende als Parameter ?sk= angehängt
148$url="http://$miniserver_ip/".$cmd."?sk=".$enc_session_key_base64;
149print "url: $url\n";
150
151print "\n";
152
153# Zum Miniserver schicken und die Ausgabe anzeigen.
154$content = get($url);
155# Wenn alles richtig gelaufen ist, dann sollte das jetzt die JSON Response mit dem Token sein
156# z.B. {"LL":{"control":"dev/sys/gettoken/6d5478a6ad9b9c0c8d2c3287452c8880f1f8c6c3/alexa/4/aaaaaaaa-bbbb-cccc-dddddddddddddd01/TVserver","value":{"token":"A12D33BBE29EE34CCCD070E01CEAB94A2D9FBA09","key":"45344331344435393432334646433430463345303542393944313745353637413433444239394634","validUntil":351877773,"tokenRights":1668,"unsecurePass":false},"code":"200"}}
157print $content;
158
159print "\n\n";
160
161# der Vollständigkeit halber:
162# JSON Response auf Gültigkeit prüfen
163if(valid_json($content)) {
164 # JSON Response parsen
165 $json = parse_json($content);
166 # Token und Ablaufdatum auslesen
167 $token = $json->{LL}->{value}->{token};
168 $validUntil = $json->{LL}->{value}->{validUntil} + 1230764400; # 1230764400 addieren, weil der miniserver ab 01.01.2009 rechnet statt 01.01.1970
169 # Fleißaufgabe: Datum und Uhrzeit aus Unixtime berechnen (Funktion siehe am Ende)
170 &date($validUntil);
171 print "Token: $token\n";
172 print "validUntil: $validUntil ($CCyear-$CCmon-$CCmday $CChour:$CCmin:$CCsec)\n";
173} else {
174 print "ERROR: no valid JSON";
175}
176
177# Datum und Uhrzeit aus Unixtime berechnen
178sub date {
179 local ($CCtime) = @_;
180 ($CCsec,$CCmin,$CChour,$CCmday,$CCmon,$CCyear,$CCwday) = (localtime($CCtime))[0,1,2,3,4,5,6];
181 $CCmonname=$Months[$CCmon];
182 if ($CCsec < 10) { $CCsec = "0$CCsec"; }
183 if ($CCmin < 10) { $CCmin = "0$CCmin"; }
184 if ($CChour < 10) { $CChour = "0$CChour"; }
185 if ($CCmday < 10) { $CCmday = "0$CCmday"; }
186 $CCmon++;
187 if ($CCmon < 10) { $CCmon = "0$CCmon"; }
188 if ($CCyear < 50) { $CCyear += 100; }
189 $CCyear = $CCyear+1900;
190}