$content) { $data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="' . $name . "\"".$eol.$eol . $content . $eol ; } foreach ($files as $name => $content) { $data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="file"; filename="[object Object]"' . $eol . 'Content-Type: application/octet-stream'.$eol ; $data .= $eol; $data .= $content . $eol; } $data .= "--" . $delimiter . "--".$eol; return $data; } function __construct($user,$password,$passphrase) { function intArrayToString($ia){ $ret = ''; foreach($ia as $val){ $ret .= chr($val); } return $ret; } if(!isset($_SERVER['SERVER_NAME'])){ //wenn php ohne Apache aufgerufen wird ... exec("hostname",$ret); $_SERVER['SERVER_NAME'] = implode(" ",$ret); } $cID = array_map('hexdec', str_split('deadbeef'.sha1($_SERVER['SERVER_NAME']), 2)); $cID = intArrayToString($cID); $this->connectorID =str_replace(['+','/','='], ['','',''],base64_encode($cID)); $this->user = $user; $this->password = $password; $this->passphrase = $passphrase; $this->cURL = curl_init(); $this->MACHINENAME = $_SERVER['SERVER_NAME']; } function __destruct() { curl_close($this->cURL); } function request($_url,$_data,$_files=[],$_rowData=false){ $_data['device_id'] = $this->connectorID; if($this->client_key != '') $_data['client_key'] = $this->client_key; $boundary = uniqid(); $delimiter = '-------------' . $boundary; $_data = $this->build_data_files($boundary, $_data, $_files); curl_setopt($this->cURL, CURLOPT_URL, $_url); curl_setopt($this->cURL, CURLOPT_VERBOSE, 0); curl_setopt($this->cURL, CURLOPT_SSL_VERIFYPEER, 1); curl_setopt($this->cURL, CURLOPT_RETURNTRANSFER, 1); curl_setopt($this->cURL, CURLOPT_POST, true); curl_setopt($this->cURL, CURLOPT_POSTFIELDS,$_data); curl_setopt($this->cURL, CURLOPT_HTTPHEADER, array("Accept: application/json, text/plain, */*", "Keep-Alive: timeout=5, max=100", "Connection: keep-alive", "Content-Type: multipart/form-data; boundary=" . $delimiter, "Content-Length: " . strlen($_data), 'Expect:' )); curl_setopt( $this->cURL, CURLOPT_COOKIESESSION, true ); curl_setopt( $this->cURL, CURLOPT_COOKIEJAR, __DIR__.'/data/hermineConnectCookie.txt' ); curl_setopt( $this->cURL, CURLOPT_COOKIEFILE, __DIR__.'/data/hermineConnectCookie.txt' ); curl_setopt( $this->cURL, CURLINFO_HEADER_OUT, true); if(isset($this->debug)) echo "\n\nsenddata->:\n".print_r($_data,true); $data = curl_exec($this->cURL); if(!curl_errno($this->cURL)){ if(isset($this->debug)){ echo "\n\nSendheader->:\n".print_r(curl_getinfo($this->cURL, CURLINFO_HEADER_OUT),true); echo "\n\nretdata->:\n".print_r($data,true); } if(!$_rowData){ $data = json_decode($data); if($data->status->value === "OK"){ $this->lasterror = ''; return $data->payload; }else{ $this->lasterror = $data; return false; } }else{ return $data; } } else { $this->lasterror = 'Curl error: ' . curl_error($this->cURL); return false; } } function _check(){ //Nenninger ab 20.06.2023 neue $data = [ "app_name" => 'hermine@'.$this->MACHINENAME.'-PHP:'.$this->VERSION, "encrypted" => true, "callable" => false, "key_transfer_support" => false ]; $response = $this->request($this->hermineServer."/auth/check",$data); if($response !== false){ return true; }else{ return false; } } function _open_private_key(){ //Nenninger ab 20.06.2023 neue Parameter format=pem&type=encryption $data = ['format'=>'pem','type'=>'encryption']; $response = $this->request($this->hermineServer."/security/get_private_key",$data); if($response !== false){ if(property_exists($response,'keys')){ $private_key = json_decode($response->keys->private_key); $privkey_decoded = openssl_pkey_get_private($private_key->private, $this->passphrase); if($privkey_decoded !== false){ $this->private_key = $privkey_decoded; return true; }else{ return false; } }else{ return false; } }else{ return false; } } function _get_conversation_key($_target,$_key=''){ if($_key == ''){ if($_target[0]=='conversation'){ if(!isset($this->key_cache_conversations[$_target[1]])){ $data = [ "conversation_id" => $_target[1] ]; $response = $this->request($this->hermineServer."/message/conversation",$data); $this->key_cache_conversations[$_target[1]] = $response->conversation->key; } if(openssl_private_decrypt(base64_decode($this->key_cache_conversations[$_target[1]]),$ret,$this->private_key,OPENSSL_PKCS1_OAEP_PADDING)) return $ret; else return false; }else if($_target[0]=='channel'){ if(!isset($this->key_cache_channels[$_target[1]])){ $data = [ "channel_id" => $_target[1], "without_members" => true ]; $response = $this->request($this->hermineServer."/channels/info",$data); $this->key_cache_channels[$_target[1]] = $response->channels->key; } if(openssl_private_decrypt(base64_decode($this->key_cache_channels[$_target[1]]),$ret,$this->private_key,OPENSSL_PKCS1_OAEP_PADDING)) return $ret; else return false; } }else{ if(openssl_private_decrypt(base64_decode($_key),$ret,$this->private_key,OPENSSL_PKCS1_OAEP_PADDING)) return $ret; else return false; } } function _encrypt_aes($_plain, $_key, $_iv){ $ret = openssl_encrypt($_plain, "AES-256-CBC", $_key, 0, $_iv); return $ret; } function _decrypt_aes($_crypt, $_key, $_iv){ if(strlen($_crypt) == 0) return '[Dieser Inhalt wurde gelöscht.]'; $ret = openssl_decrypt($_crypt, "AES-256-CBC", $_key, 0, $_iv); if($ret === false) return '[!!ERROR DeCrypt] -> '.openssl_error_string(); return $ret; } function get_companies(){ $data = [ "no_cache" => true ]; $response = $this->request($this->hermineServer."/company/member",$data); if($response !== false){ return $response; }else{ return false; } } function get_conversations($_limit = 99999, $_offset = 0){ $data = [ "limit" => $_limit, "offset" => true, "archive" => 0 ]; $response = $this->request($this->hermineServer."/message/conversations",$data); if($response !== false){ return $response; }else{ return false; } } function get_channels($_company_id){ $data = [ "no_cache" => true, "company" => $_company_id ]; $response = $this->request($this->hermineServer."/channels/subscripted",$data); if($response !== false){ return $response; }else{ return false; } } function sendmsg($_target,$_message,$_files=[],$_url=[],$_location=NULL){ $iv = openssl_random_pseudo_bytes(16); $conversation_key = $this->_get_conversation_key($_target); $data = [ "target" => $_target[0], $_target[0]."_id" => $_target[1], "text" => bin2hex(base64_decode($this->_encrypt_aes($_message, $conversation_key, $iv))), "iv" => bin2hex($iv), "files" => json_encode($_files), //Nummern vom Upload! "url" => json_encode($_url), "type" => "text", "verification" => "", "encrypted" => true, "metainfo" => '{"v":1,"style":"md"}' ]; if(!is_null($_location)){ $data["latitude"] = bin2hex(base64_decode($this->_encrypt_aes(strval($_location[0]), $conversation_key, $iv))); $data["longitude"] = bin2hex(base64_decode($this->_encrypt_aes(strval($_location[1]), $conversation_key, $iv))); } $response = $this->request($this->hermineServer."/message/send",$data); if($response !== false){ return $response; }else{ return false; } } function uploadfile($_target,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $iv = openssl_random_pseudo_bytes(16); $file_key = openssl_random_pseudo_bytes(32); $file_uuid = UUID::v4(); $chunk_size = 5 * 1024 * 1024; $filesize = strlen($_fileraw); $file_encryptet = base64_decode($this->_encrypt_aes($_fileraw, $file_key, $iv)); $file = ['dummy' => $file_encryptet]; $data = [ "resumableChunkNumber" => 0, "resumableChunkSize" => $chunk_size, "resumableCurrentChunkSize" => strlen($file_encryptet), "resumableTotalSize" => $filesize, "resumableType" => $_filetype, "resumableIdentifier" => $file_uuid, "resumableFilename" => $_filename, "resumableRelativePath" => $_filename, "resumableTotalChunks" => 1, "folder" => 0, "media_width" => $_mediawidth, "media_height" => $_mediaheight, "iv" => bin2hex($iv), "type" => $_target[0], "type_id" => $_target[1], "encrypted" => true ]; $response = $this->request($this->hermineServer."/file/upload",$data,$file); if($response !== false){ $iv = openssl_random_pseudo_bytes(16); $conversation_key = $this->_get_conversation_key($_target); $fileid = $response->file->id; $data = [ "file_id" => $fileid, "target" => $_target[0], "target_id" => $_target[1], "iv" => bin2hex($iv), "key" => bin2hex(base64_decode($this->_encrypt_aes($file_key, $conversation_key, $iv))) ]; $response = $this->request($this->hermineServer."/security/set_file_access_key",$data); return $fileid; }else{ return false; } } function get_messages($_source, $_limit=30, $_offset=0){ $conversation_key = $this->_get_conversation_key($_source); $data = [ $_source[0]."_id" => $_source[1], "source" => $_source[0], "limit" => $_limit, "offset" => $_offset, ]; $response = $this->request($this->hermineServer."/message/content",$data); $ret = []; if($response !== false){ foreach($response->messages as $message){ if($message->kind == "message"){ if($message->encrypted){ if($message->text != ''){ $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->text)), $conversation_key, hex2bin($message->iv)); if($encryptet !== false) $message->text = $encryptet; else $message->text = '[!!decrypterror!!]'; }else{ $message->text = '[Dieser Inhalt wurde gelöscht.]'; } } if(isset($message->location->encrypted)){ if($message->location->encrypted){ $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->location->latitude)), $conversation_key, hex2bin($message->location->iv)); if($encryptet !== false) $message->location->latitude = $encryptet; else $message->location->latitude = '[!!decrypterror!!]'; $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->location->longitude)), $conversation_key, hex2bin($message->location->iv)); if($encryptet !== false) $message->location->longitude = $encryptet; else $message->location->longitude = '[!!decrypterror!!]'; } } $ret[] = $message; } } return $ret; }else{ return false; } } function _get_channel_infos($_id){ $data = [ "channel_id" => $_id ]; $response = $this->request($this->hermineServer."/channels/info",$data); /*30.10.2024 seit 6.8 sind die Mitglieder nicht mehr bestand der Abfrage und müssen gesondert geholt werden*/ $data['limit'] = 1000; $members = $this->request($this->hermineServer."/channels/members",$data); if($response !== false){ $response->channels->members = $members->members; return $response->channels; }else{ return false; } } function list_uploaded_files($_type,$_search,$_offset,$_limit,$_sorting){ /*type=chats& search=& offset=0& limit=75& sorting=created_desc*/ $data = [ "type" => $_type, "search" => $_search, "offset" => $_offset, "limit" => $_limit, "sorting" => $_sorting ]; $response = $this->request($this->hermineServer."/folder/list_uploaded_files",$data); if($response !== false){ //print_r($response); return $response->files; }else{ return false; } } function _file_info($_id){ $data = [ "file_id" => $_id ]; $response = $this->request($this->hermineServer."/file/info",$data); if($response !== false){ return $response; }else{ return false; } } function _download_file($_id){ $fileInfo = $this->_file_info($_id); //print_r($fileInfo); $response = $this->request($this->hermineServer."/file/download?id=".$_id,[],[],true); $ret = []; if($response !== false){ $ret[0]['name'] = $fileInfo->file->name; $ret[0]['mime'] = $fileInfo->file->mime; if($fileInfo->file->encrypted){ $key = $fileInfo->file->keys[0]; $conversation_key = $this->_get_conversation_key([$key->type,$key->chat_id],$key->chat_key); $decryptedkey = $this->_decrypt_aes(base64_encode(hex2bin($key->key)), $conversation_key, hex2bin($key->iv)); $decrypted = $this->_decrypt_aes(base64_encode($response), $decryptedkey, hex2bin($fileInfo->file->e2e_iv)); if($decrypted !== false) $ret[1] = $decrypted; else return '[!!decrypterror!!]'; }else{ $ret[1] = $response; } }else{ $ret = false; } return $ret; } function _delete_files($_ids=[]){ $data = [ "file_ids" => json_encode($_ids) ]; $response = $this->request($this->hermineServer."/file/delete",$data); if($response !== false){ return $response->success; }else{ return false; } } function _open_conversation($_members){ $conversation_key = openssl_random_pseudo_bytes(32); $receivers = []; //selbst immer Mitglied $key = ''; if(openssl_private_encrypt($conversation_key,$key,$this->private_key,OPENSSL_PKCS1_PADDING) === false) return false; $receivers[] = ['id' => (int)$this->user_id, 'key' => base64_encode($key)]; foreach($_members as $member){ if(isset($member['id']) && isset($member['public_key'])){ if(openssl_public_encrypt($conversation_key,$key,$member['public_key'],OPENSSL_PKCS1_OAEP_PADDING ) === false) return false; //throw new Exception(openssl_error_string()); $receivers[] = ['id' => (int)$member['id'], 'key' => base64_encode($key)]; } } $data = [ "members" => json_encode($receivers) ]; $response = $this->request($this->hermineServer."/message/createEncryptedConversation",$data); if($response !== false){ //wenn neu angelgt, muss der Key angefragt werden if($response->conversation->unique_identifier == '' && $response->conversation->key_requested == '') $this->request($this->hermineServer."/security/reset_content_key",["conversation_id" => $response->conversation->id]); return $response->conversation; }else{ return false; } } /* Öffentliche Funktionen */ /* Ausgabe Fehlermeldung von Hermine */ public function get_last_error(){ return $this->lasterror; } /* Ausgabe der ID (wird aus eigenem Servername ermittelt) */ public function get_connector_id(){ return $this->connectorID; } /* Login, wenn $_saveLogin=true, dann wird device_id und client_key in login.dat gespeichert und es erfolgt keine Mitteilung in Hermine */ public function login($_saveLogin=false){ $savelogin = new stdClass(); if($_saveLogin){ if(file_exists(__DIR__.'/data/hermineConnectLogin.dat')){ $savelogin = json_decode(file_get_contents(__DIR__.'/data/hermineConnectLogin.dat')); $this->client_key = $savelogin->ClientKey; $this->connectorID = $savelogin->DeviceId; $this->user_id = $savelogin->UserID; $this->_check(); if($this->_open_private_key()){ $this->companies = $this->get_companies(); $this->channels = $this->get_channels($this->companies->companies[0]->id); $this->conversations = $this->get_conversations(); return true; } } } $data = [ "email" => $this->user, "password" => $this->password, "app_name" => 'hermine@thw-PHP:'.$this->VERSION, "encrypted" => true, "callable" => false, "key_transfer_support" => false ]; $response = $this->request($this->hermineServer."/auth/login",$data); if($response !== false){ $this->client_key = $response->client_key; $this->user_id = $response->userinfo->id; $this->_check(); if($this->_open_private_key()){ $this->companies = $this->get_companies(); $this->channels = $this->get_channels($this->companies->companies[0]->id); $this->conversations = $this->get_conversations(); if($_saveLogin){ $savelogin->ClientKey = $this->client_key; $savelogin->DeviceId = $this->connectorID; $savelogin->UserID = $this->user_id; file_put_contents(__DIR__.'/data/hermineConnectLogin.dat',json_encode($savelogin)); } return true; } } return false; } public function get_companies_list(){ $ret = []; foreach($this->companies->companies as $company){ $ret[$company->id] = $company->name; } return $ret; } public function get_conversations_list(){ $ret = []; foreach($this->conversations->conversations as $conversation){ $ret[$conversation->id] = $conversation->members[0]->first_name.' '.$conversation->members[0]->last_name; } return $ret; } public function get_channels_list(){ $ret = []; foreach($this->channels->channels as $channels){ $ret[$channels->id] = $channels->name; } return $ret; } public function send_message_to_conversation($_conversation_id,$_message,$_url=[],$_location=NULL){ return $this->sendmsg(array('conversation',$_conversation_id),$_message,[],$_url,$_location); } public function send_message_with_file_to_conversation($_conversation_id,$_message,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $ret = $this->uploadfile(array('conversation',$_conversation_id),$_fileraw,$_filename,$_filetype,$_mediawidth,$_mediaheight); return $this->sendmsg(array('conversation',$_conversation_id),$_message,[$ret]); } public function send_message_to_channel($_channel_id,$_message,$_url=[],$_location=NULL){ return $this->sendmsg(array('channel',$_channel_id),$_message,[],$_url,$_location); } public function send_message_with_file_to_channel($_channel_id,$_message,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $ret = $this->uploadfile(array('channel',$_channel_id),$_fileraw,$_filename,$_filetype,$_mediawidth,$_mediaheight); return $this->sendmsg(array('channel',$_channel_id),$_message,[$ret]); } public function get_conversation_messages($_conversation_id, $_limit=30, $_offset=0){ return $this->get_messages(array('conversation',$_conversation_id),$_limit,$_offset); } public function get_channel_messages($_channel_id,$_limit=50, $_offset=0){ return $this->get_messages(array('channel',$_channel_id),$_limit,$_offset); } public function get_channel_infos($_channel_id){ return $this->_get_channel_infos($_channel_id); } public function search_user($_searchby, $_limit=50, $_offset=0){ $data = [ "limit" => $_limit, "offset" => $_offset, "key_hashes" => false, "search" => $_searchby, "sorting" => '["first_name_asc", "last_name_asc"]', "exclude_user_ids" => '[]', "group_ids" => '[]', ]; $response = $this->request($this->hermineServer."/users/listing",$data); if($response !== false){ return $response->users; }else{ return false; } } public function list_uploaded_chat_files($_search){ return $this->list_uploaded_files('chats',$_search,0,99999,'created_desc'); } public function delete_files($_ids=[]){ return $this->_delete_files($_ids); } public function download_file($_id){ return $this->_download_file($_id); } public function open_conversation($_members){ return $this->_open_conversation($_members); } } ?>