/********************************************************************************* * MIT License * * Copyright (c) 2020-2023 Gregg E. Berman * * https://github.com/HomeSpan/HomeSpan * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * ********************************************************************************/ #include #include #include #include #include "HAP.h" ////////////////////////////////////// void HAPClient::init(){ size_t len; // not used but required to read blobs from NVS nvs_open("SRP",NVS_READWRITE,&srpNVS); // open SRP data namespace in NVS nvs_open("HAP",NVS_READWRITE,&hapNVS); // open HAP data namespace in NVS if(strlen(homeSpan.spanOTA.otaPwd)==0){ // OTA password has not been specified in sketch if(!nvs_get_str(homeSpan.otaNVS,"OTADATA",NULL,&len)){ // if found OTA data in NVS... nvs_get_str(homeSpan.otaNVS,"OTADATA",homeSpan.spanOTA.otaPwd,&len); // ...retrieve data. } else { // otherwise... homeSpan.spanOTA.setPassword(DEFAULT_OTA_PASSWORD); // ...use default password } } if(strlen(homeSpan.pairingCodeCommand)){ // load verification setup code if provided homeSpan.processSerialCommand(homeSpan.pairingCodeCommand); // if load failed due to invalid code, the logic below still runs and will pick up previous code or use the default one } struct { // temporary structure to hold SRP verification code and salt stored in NVS uint8_t salt[16]; uint8_t verifyCode[384]; } verifyData; if(!nvs_get_blob(srpNVS,"VERIFYDATA",NULL,&len)){ // if found verification code data in NVS nvs_get_blob(srpNVS,"VERIFYDATA",&verifyData,&len); // retrieve data srp.loadVerifyCode(verifyData.verifyCode,verifyData.salt); // load verification code and salt into SRP structure } else { LOG0("Generating SRP verification data for default Setup Code: %.3s-%.2s-%.3s\n",homeSpan.defaultSetupCode,homeSpan.defaultSetupCode+3,homeSpan.defaultSetupCode+5); srp.createVerifyCode(homeSpan.defaultSetupCode,verifyData.verifyCode,verifyData.salt); // create verification code from default Setup Code and random salt nvs_set_blob(srpNVS,"VERIFYDATA",&verifyData,sizeof(verifyData)); // update data nvs_commit(srpNVS); // commit to NVS LOG0("Setup Payload for Optional QR Code: %s\n\n",homeSpan.qrCode.get(atoi(homeSpan.defaultSetupCode),homeSpan.qrID,atoi(homeSpan.category))); } if(!strlen(homeSpan.qrID)){ // Setup ID has not been specified in sketch if(!nvs_get_str(hapNVS,"SETUPID",NULL,&len)){ // check for saved value nvs_get_str(hapNVS,"SETUPID",homeSpan.qrID,&len); // retrieve data } else { sprintf(homeSpan.qrID,"%s",DEFAULT_QR_ID); // use default } } if(!nvs_get_blob(hapNVS,"ACCESSORY",NULL,&len)){ // if found long-term Accessory data in NVS nvs_get_blob(hapNVS,"ACCESSORY",&accessory,&len); // retrieve data } else { LOG0("Generating new random Accessory ID and Long-Term Ed25519 Signature Keys...\n\n"); uint8_t buf[6]; char cBuf[18]; randombytes_buf(buf,6); // generate 6 random bytes using libsodium (which uses the ESP32 hardware-based random number generator) sprintf(cBuf,"%02X:%02X:%02X:%02X:%02X:%02X", // create ID in form "XX:XX:XX:XX:XX:XX" (HAP Table 6-7) buf[0],buf[1],buf[2],buf[3],buf[4],buf[5]); memcpy(accessory.ID,cBuf,17); // copy into Accessory ID for permanent storage crypto_sign_keypair(accessory.LTPK,accessory.LTSK); // generate new random set of keys using libsodium public-key signature nvs_set_blob(hapNVS,"ACCESSORY",&accessory,sizeof(accessory)); // update data nvs_commit(hapNVS); // commit to NVS } if(!nvs_get_blob(hapNVS,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS TempBuffer tBuf(len/sizeof(Controller), "HAPClient::init"); nvs_get_blob(hapNVS,"CONTROLLERS",tBuf.get(),&len); // retrieve data for(int i=0;iMAX_HTTP){ // exceeded maximum number of bytes allowed badRequestError(); LOG0("\n*** ERROR: HTTP message of %d bytes exceeds maximum allowed (%d)\n\n",messageSize,MAX_HTTP); return; } TempBuffer httpBuf(messageSize+1, "HAPClient::processRequest"); // leave room for null character added below if(cPair){ // expecting encrypted message LOG2("<<<< #### "); LOG2(client.remoteIP()); LOG2(" #### <<<<\n"); nBytes=receiveEncrypted(httpBuf.get(),messageSize); // decrypt and return number of bytes read if(!nBytes){ // decryption failed (error message already printed in function) badRequestError(); return; } } else { // expecting plaintext message LOG2("<<<<<<<<< "); LOG2(client.remoteIP()); LOG2(" <<<<<<<<<\n"); nBytes=client.read(httpBuf.get(),messageSize); // read expected number of bytes if(nBytes!=messageSize || client.available()!=0){ badRequestError(); LOG0("\n*** ERROR: HTTP message not read correctly. Expected %d bytes, read %d bytes, %d bytes remaining\n\n",messageSize,nBytes,client.available()); return; } } // encrypted/plaintext httpBuf.get()[nBytes]='\0'; // add null character to enable string functions char *body=(char *)httpBuf.get(); // char pointer to start of HTTP Body char *p; // char pointer used for searches if(!(p=strstr((char *)httpBuf.get(),"\r\n\r\n"))){ badRequestError(); LOG0("\n*** ERROR: Malformed HTTP request (can't find blank line indicating end of BODY)\n\n"); return; } *p='\0'; // null-terminate end of HTTP Body to faciliate additional string processing uint8_t *content=(uint8_t *)p+4; // byte pointer to start of optional HTTP Content int cLen=0; // length of optional HTTP Content if((p=strstr(body,"Content-Length: "))) // Content-Length is specified cLen=atoi(p+16); if(nBytes!=strlen(body)+4+cLen){ badRequestError(); LOG0("\n*** ERROR: Malformed HTTP request (Content-Length plus Body Length does not equal total number of bytes read)\n\n"); return; } LOG2(body); LOG2("\n------------ END BODY! ------------\n"); if(!strncmp(body,"POST ",5)){ // this is a POST request if(cLen==0){ badRequestError(); LOG0("\n*** ERROR: HTTP POST request contains no Content\n\n"); return; } if(!strncmp(body,"POST /pair-setup ",17) && // POST PAIR-SETUP strstr(body,"Content-Type: application/pairing+tlv8") && // check that content is TLV8 tlv8.unpack(content,cLen)){ // read TLV content tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)" LOG2("------------ END TLVS! ------------\n"); postPairSetupURL(); // process URL return; } if(!strncmp(body,"POST /pair-verify ",18) && // POST PAIR-VERIFY strstr(body,"Content-Type: application/pairing+tlv8") && // check that content is TLV8 tlv8.unpack(content,cLen)){ // read TLV content tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)" LOG2("------------ END TLVS! ------------\n"); postPairVerifyURL(); // process URL return; } if(!strncmp(body,"POST /pairings ",15) && // POST PAIRINGS strstr(body,"Content-Type: application/pairing+tlv8") && // check that content is TLV8 tlv8.unpack(content,cLen)){ // read TLV content tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)" LOG2("------------ END TLVS! ------------\n"); postPairingsURL(); // process URL return; } notFoundError(); LOG0("\n*** ERROR: Bad POST request - URL not found\n\n"); return; } // POST request if(!strncmp(body,"PUT ",4)){ // this is a PUT request if(cLen==0){ badRequestError(); LOG0("\n*** ERROR: HTTP PUT request contains no Content\n\n"); return; } if(!strncmp(body,"PUT /characteristics ",21) && // PUT CHARACTERISTICS strstr(body,"Content-Type: application/hap+json")){ // check that content is JSON content[cLen]='\0'; // add a trailing null on end of JSON LOG2((char *)content); // print JSON LOG2("\n------------ END JSON! ------------\n"); putCharacteristicsURL((char *)content); // process URL return; } if(!strncmp(body,"PUT /prepare ",13) && // PUT PREPARE strstr(body,"Content-Type: application/hap+json")){ // check that content is JSON content[cLen]='\0'; // add a trailing null on end of JSON LOG2((char *)content); // print JSON LOG2("\n------------ END JSON! ------------\n"); putPrepareURL((char *)content); // process URL return; } notFoundError(); LOG0("\n*** ERROR: Bad PUT request - URL not found\n\n"); return; } // PUT request if(!strncmp(body,"GET ",4)){ // this is a GET request if(!strncmp(body,"GET /accessories ",17)){ // GET ACCESSORIES getAccessoriesURL(); return; } if(!strncmp(body,"GET /characteristics?",21)){ // GET CHARACTERISTICS getCharacteristicsURL(body+21); return; } if(homeSpan.webLog.isEnabled && !strncmp(body,homeSpan.webLog.statusURL.c_str(),homeSpan.webLog.statusURL.length())){ // GET STATUS - AN OPTIONAL, NON-HAP-R2 FEATURE getStatusURL(); return; } notFoundError(); LOG0("\n*** ERROR: Bad GET request - URL not found\n\n"); return; } // GET request badRequestError(); LOG0("\n*** ERROR: Unknown or malformed HTTP request\n\n"); } // processHAP ////////////////////////////////////// int HAPClient::notFoundError(){ char s[]="HTTP/1.1 404 Not Found\r\n\r\n"; LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(s); client.print(s); LOG2("------------ SENT! --------------\n"); delay(1); client.stop(); return(-1); } ////////////////////////////////////// int HAPClient::badRequestError(){ char s[]="HTTP/1.1 400 Bad Request\r\n\r\n"; LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(s); client.print(s); LOG2("------------ SENT! --------------\n"); delay(1); client.stop(); return(-1); } ////////////////////////////////////// int HAPClient::unauthorizedError(){ char s[]="HTTP/1.1 470 Connection Authorization Required\r\n\r\n"; LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(s); client.print(s); LOG2("------------ SENT! --------------\n"); delay(1); client.stop(); return(-1); } ////////////////////////////////////// int HAPClient::postPairSetupURL(){ LOG1("In Pair Setup..."); int tlvState=tlv8.val(kTLVType_State); char buf[64]; if(tlvState==-1){ // missing STATE TLV LOG0("\n*** ERROR: Missing State TLV\n\n"); badRequestError(); // return with 400 error, which closes connection return(0); } if(nAdminControllers()){ // error: Device already paired (i.e. there is at least one admin Controller). We should not be receiving any requests for Pair-Setup! LOG0("\n*** ERROR: Device already paired!\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,tlvState+1); // set response STATE to requested state+1 (which should match the state that was expected by the controller) tlv8.val(kTLVType_Error,tagError_Unavailable); // set Error=Unavailable tlvRespond(); // send response to client return(0); }; sprintf(buf,"Found . Expected \n",tlvState,pairStatus); LOG2(buf); if(tlvState!=pairStatus){ // error: Device is not yet paired, but out-of-sequence pair-setup STATE was received LOG0("\n*** ERROR: Out-of-Sequence Pair-Setup request!\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,tlvState+1); // set response STATE to requested state+1 (which should match the state that was expected by the controller) tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for out-of-sequence steps) tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired accessory (M1) return(0); }; switch(tlvState){ // valid and in-sequence Pair-Setup STATE received -- process request! (HAP Section 5.6) case pairState_M1: // 'SRP Start Request' if(tlv8.val(kTLVType_Method)!=0){ // error: "Pair Setup" method must always be 0 to indicate setup without MiFi Authentification (HAP Table 5-3) LOG0("\n*** ERROR: Pair Method not set to 0\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M2); // set State= tlv8.val(kTLVType_Error,tagError_Unavailable); // set Error=Unavailable tlvRespond(); // send response to client return(0); }; tlv8.clear(); tlv8.val(kTLVType_State,pairState_M2); // set State= srp.createPublicKey(); // create accessory public key from random Pair-Setup code (displayed to user) srp.loadTLV(kTLVType_PublicKey,&srp.B,384); // load server public key, B srp.loadTLV(kTLVType_Salt,&srp.s,16); // load salt, s tlvRespond(); // send response to client pairStatus=pairState_M3; // set next expected pair-state request from client return(1); break; case pairState_M3: // 'SRP Verify Request' if(!srp.writeTLV(kTLVType_PublicKey,&srp.A) || // try to write TLVs into mpi structures !srp.writeTLV(kTLVType_Proof,&srp.M1)){ LOG0("\n*** ERROR: One or both of the required 'PublicKey' and 'Proof' TLV records for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); }; srp.createSessionKey(); // create session key, K, from receipt of HAP Client public key, A if(!srp.verifyProof()){ // verify proof, M1, received from HAP Client LOG0("\n*** ERROR: SRP Proof Verification Failed\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); }; srp.createProof(); // M1 has been successully verified; now create accessory proof M2 tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= srp.loadTLV(kTLVType_Proof,&srp.M2,64); // load M2 counter-proof tlvRespond(); // send response to client pairStatus=pairState_M5; // set next expected pair-state request from client return(1); break; case pairState_M5: // 'Exchange Request' if(!tlv8.len(kTLVType_EncryptedData)){ LOG0("\n*** ERROR: Required 'EncryptedData' TLV record for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M6); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); }; // THIS NEXT STEP IS MISSING FROM HAP DOCUMENTATION! // // Must FIRST use HKDF to create a Session Key from the SRP Shared Secret for use in subsequent ChaCha20-Poly1305 decryption // of the encrypted data TLV (HAP Sections 5.6.5.2 and 5.6.6.1). // // Note the SALT and INFO text fields used by HKDF to create this Session Key are NOT the same as those for creating iosDeviceX. // The iosDeviceX HKDF calculations are separate and will be performed further below with the SALT and INFO as specified in the HAP docs. hkdf.create(sessionKey, srp.sharedSecret,64,"Pair-Setup-Encrypt-Salt","Pair-Setup-Encrypt-Info"); // create SessionKey uint8_t decrypted[1024]; // temporary storage for decrypted data unsigned long long decryptedLen; // length (in bytes) of decrypted data if(crypto_aead_chacha20poly1305_ietf_decrypt( // use SessionKey to decrypt encryptedData TLV with padded nonce="PS-Msg05" decrypted, &decryptedLen, NULL, tlv8.buf(kTLVType_EncryptedData), tlv8.len(kTLVType_EncryptedData), NULL, 0, (unsigned char *)"\x00\x00\x00\x00PS-Msg05", sessionKey)==-1){ LOG0("\n*** ERROR: Exchange-Request Authentication Failed\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M6); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); } if(!tlv8.unpack(decrypted,decryptedLen)){ LOG0("\n*** ERROR: Can't parse decrypted data into separate TLV records\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M6); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); } tlv8.print(2); // print decrypted TLV data LOG2("------- END DECRYPTED TLVS! -------\n"); if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_PublicKey) || !tlv8.len(kTLVType_Signature)){ LOG0("\n*** ERROR: One or more of required 'Identifier,' 'PublicKey,' and 'Signature' TLV records for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M6); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); }; // Next, verify the authenticity of the TLV Records using the Signature provided by the Client. // But the Client does not send the entire message that was used to generate the Signature. // Rather, it purposely does not transmit "iosDeviceX", which is derived from the SRP Shared Secret that only the Client and this Server know. // Note that the SALT and INFO text fields now match those in HAP Section 5.6.6.1 uint8_t iosDeviceX[32]; hkdf.create(iosDeviceX,srp.sharedSecret,64,"Pair-Setup-Controller-Sign-Salt","Pair-Setup-Controller-Sign-Info"); // derive iosDeviceX from SRP Shared Secret using HKDF size_t iosDeviceXLen=32; uint8_t *iosDevicePairingID = tlv8.buf(kTLVType_Identifier); // set iosDevicePairingID from TLV record size_t iosDevicePairingIDLen = tlv8.len(kTLVType_Identifier); uint8_t *iosDeviceLTPK = tlv8.buf(kTLVType_PublicKey); // set iosDeviceLTPK (Ed25519 long-term public key) from TLV record size_t iosDeviceLTPKLen = tlv8.len(kTLVType_PublicKey); size_t iosDeviceInfoLen=iosDeviceXLen+iosDevicePairingIDLen+iosDeviceLTPKLen; // total size of re-constituted message, iosDeviceInfo uint8_t iosDeviceInfo[iosDeviceInfoLen]; memcpy(iosDeviceInfo,iosDeviceX,iosDeviceXLen); // iosDeviceInfo = iosDeviceX memcpy(iosDeviceInfo+iosDeviceXLen,iosDevicePairingID,iosDevicePairingIDLen); // +iosDevicePairingID memcpy(iosDeviceInfo+iosDeviceXLen+iosDevicePairingIDLen,iosDeviceLTPK,iosDeviceLTPKLen); // +iosDeviceLTPK uint8_t *iosDeviceSignature = tlv8.buf(kTLVType_Signature); // set iosDeviceSignature from TLV record (an Ed25519 should always be 64 bytes) if(crypto_sign_verify_detached(iosDeviceSignature, iosDeviceInfo, iosDeviceInfoLen, iosDeviceLTPK) != 0){ // verify signature of iosDeviceInfo using iosDeviceLTPK LOG0("\n*** ERROR: LPTK Signature Verification Failed\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M6); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client pairStatus=pairState_M1; // reset pairStatus to first step of unpaired return(0); } addController(iosDevicePairingID,iosDeviceLTPK,true); // save Pairing ID and LTPK for this Controller with admin privileges // Now perform the above steps in reverse to securely transmit the AccessoryLTPK to the Controller (HAP Section 5.6.6.2) uint8_t accessoryX[32]; hkdf.create(accessoryX,srp.sharedSecret,64,"Pair-Setup-Accessory-Sign-Salt","Pair-Setup-Accessory-Sign-Info"); // derive accessoryX from SRP Shared Secret using HKDF size_t accessoryXLen=32; uint8_t *accessoryPairingID=accessory.ID; // set accessoryPairingID from storage size_t accessoryPairingIDLen=17; uint8_t *accessoryLTPK=accessory.LTPK; // set accessoryLTPK (Ed25519 long-term public key) from storage size_t accessoryLTPKLen=32; size_t accessoryInfoLen=accessoryXLen+accessoryPairingIDLen+accessoryLTPKLen; // total size of accessoryInfo uint8_t accessoryInfo[accessoryInfoLen]; memcpy(accessoryInfo,accessoryX,accessoryXLen); // accessoryInfo = accessoryX memcpy(accessoryInfo+accessoryXLen,accessoryPairingID,accessoryPairingIDLen); // +accessoryPairingID memcpy(accessoryInfo+accessoryXLen+accessoryPairingIDLen,accessoryLTPK,accessoryLTPKLen); // +accessoryLTPK tlv8.clear(); // clear existing TLV records crypto_sign_detached(tlv8.buf(kTLVType_Signature,64),NULL,accessoryInfo,accessoryInfoLen,accessory.LTSK); // produce signature of accessoryInfo using AccessoryLTSK (Ed25519 long-term secret key) tlv8.buf(kTLVType_Identifier,accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID tlv8.buf(kTLVType_PublicKey,accessoryLTPK,accessoryLTPKLen); // set PublicKey TLV record as accessoryLTPK LOG2("------- ENCRYPTING SUB-TLVS -------\n"); tlv8.print(2); size_t subTLVLen=tlv8.pack(NULL); // get size of buffer needed to store sub-TLV uint8_t subTLV[subTLVLen]; subTLVLen=tlv8.pack(subTLV); // create sub-TLV by packing Identifier, PublicKey, and Signature TLV records together tlv8.clear(); // clear existing TLV records // Final step is to encrypt the subTLV data using the same sessionKey as above with ChaCha20-Poly1305 unsigned long long edLen; crypto_aead_chacha20poly1305_ietf_encrypt(tlv8.buf(kTLVType_EncryptedData),&edLen,subTLV,subTLVLen,NULL,0,NULL,(unsigned char *)"\x00\x00\x00\x00PS-Msg06",sessionKey); LOG2("---------- END SUB-TLVS! ----------\n"); tlv8.buf(kTLVType_EncryptedData,edLen); // set length of EncryptedData TLV record, which should now include the Authentication Tag at the end as required by HAP tlv8.val(kTLVType_State,pairState_M6); // set State= tlvRespond(); // send response to client mdns_service_txt_item_set("_hap","_tcp","sf","0"); // broadcast new status LOG1("\n*** ACCESSORY PAIRED! ***\n"); STATUS_UPDATE(on(),HS_PAIRED) if(homeSpan.pairCallback) // if set, invoke user-defined Pairing Callback to indicate device has been paired homeSpan.pairCallback(true); return(1); break; } // switch return(1); } // postPairSetup ////////////////////////////////////// int HAPClient::postPairVerifyURL(){ LOG2("In Pair Verify #"); LOG2(conNum); LOG2(" ("); LOG2(client.remoteIP()); LOG2(")..."); char buf[64]; int tlvState=tlv8.val(kTLVType_State); if(tlvState==-1){ // missing STATE TLV LOG0("\n*** ERROR: Missing State TLV\n\n"); badRequestError(); // return with 400 error, which closes connection return(0); } if(!nAdminControllers()){ // error: Device not yet paired - we should not be receiving any requests for Pair-Verify! LOG0("\n*** ERROR: Device not yet paired!\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,tlvState+1); // set response STATE to requested state+1 (which should match the state that was expected by the controller) tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown tlvRespond(); // send response to client return(0); }; sprintf(buf,"Found \n",tlvState); // unlike pair-setup, out-of-sequencing can be handled gracefully for pair-verify (HAP requirement). No need to keep track of pairStatus LOG2(buf); switch(tlvState){ // Pair-Verify STATE received -- process request! (HAP Section 5.7) case pairState_M1: // 'Verify Start Request' if(!tlv8.len(kTLVType_PublicKey)){ LOG0("\n*** ERROR: Required 'PublicKey' TLV record for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M2); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client return(0); } else { uint8_t secretCurveKey[32]; // Accessory's secret key for Curve25519 encryption (32 bytes). Ephemeral usage - created below and used only in this block crypto_box_keypair(publicCurveKey,secretCurveKey); // generate Curve25519 public key pair (will persist until end of verification process) memcpy(iosCurveKey,tlv8.buf(kTLVType_PublicKey),32); // save iosCurveKey (will persist until end of verification process) crypto_scalarmult_curve25519(sharedCurveKey,secretCurveKey,iosCurveKey); // generate (and persist) Pair Verify SharedSecret CurveKey from Accessory's Curve25519 secret key and Controller's Curve25519 public key (32 bytes) uint8_t *accessoryPairingID = accessory.ID; // set accessoryPairingID size_t accessoryPairingIDLen = 17; size_t accessoryInfoLen=32+accessoryPairingIDLen+32; // total size of accessoryInfo uint8_t accessoryInfo[accessoryInfoLen]; memcpy(accessoryInfo,publicCurveKey,32); // accessoryInfo = Accessory's Curve25519 public key memcpy(accessoryInfo+32,accessoryPairingID,accessoryPairingIDLen); // +accessoryPairingID memcpy(accessoryInfo+32+accessoryPairingIDLen,iosCurveKey,32); // +Controller's Curve25519 public key tlv8.clear(); // clear existing TLV records crypto_sign_detached(tlv8.buf(kTLVType_Signature,64),NULL,accessoryInfo,accessoryInfoLen,accessory.LTSK); // produce signature of accessoryInfo using AccessoryLTSK (Ed25519 long-term secret key) tlv8.buf(kTLVType_Identifier,accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID LOG2("------- ENCRYPTING SUB-TLVS -------\n"); tlv8.print(2); size_t subTLVLen=tlv8.pack(NULL); // get size of buffer needed to store sub-TLV uint8_t subTLV[subTLVLen]; subTLVLen=tlv8.pack(subTLV); // create sub-TLV by packing Identifier and Signature TLV records together tlv8.clear(); // clear existing TLV records // create SessionKey from Curve25519 SharedSecret using HKDF-SHA-512, then encrypt subTLV data with SessionKey using ChaCha20-Poly1305. Output stored in EncryptedData TLV unsigned long long edLen; hkdf.create(sessionKey,sharedCurveKey,32,"Pair-Verify-Encrypt-Salt","Pair-Verify-Encrypt-Info"); // create SessionKey (32 bytes) crypto_aead_chacha20poly1305_ietf_encrypt(tlv8.buf(kTLVType_EncryptedData),&edLen,subTLV,subTLVLen,NULL,0,NULL,(unsigned char *)"\x00\x00\x00\x00PV-Msg02",sessionKey); LOG2("---------- END SUB-TLVS! ----------\n"); tlv8.buf(kTLVType_EncryptedData,edLen); // set length of EncryptedData TLV record, which should now include the Authentication Tag at the end as required by HAP tlv8.val(kTLVType_State,pairState_M2); // set State= tlv8.buf(kTLVType_PublicKey,publicCurveKey,32); // set PublicKey to Accessory's Curve25519 public key tlvRespond(); // send response to client return(1); } break; case pairState_M3: // 'Verify Finish Request' if(!tlv8.len(kTLVType_EncryptedData)){ LOG0("\n*** ERROR: Required 'EncryptedData' TLV record for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client return(0); }; uint8_t decrypted[1024]; // temporary storage for decrypted data unsigned long long decryptedLen; // length (in bytes) of decrypted data if(crypto_aead_chacha20poly1305_ietf_decrypt( // use SessionKey to decrypt encrypytedData TLV with padded nonce="PV-Msg03" decrypted, &decryptedLen, NULL, tlv8.buf(kTLVType_EncryptedData), tlv8.len(kTLVType_EncryptedData), NULL, 0, (unsigned char *)"\x00\x00\x00\x00PV-Msg03", sessionKey)==-1){ LOG0("\n*** ERROR: Verify Authentication Failed\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client return(0); } if(!tlv8.unpack(decrypted,decryptedLen)){ LOG0("\n*** ERROR: Can't parse decrypted data into separate TLV records\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client return(0); } tlv8.print(2); // print decrypted TLV data LOG2("------- END DECRYPTED TLVS! -------\n"); if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_Signature)){ LOG0("\n*** ERROR: One or more of required 'Identifier,' and 'Signature' TLV records for this step is bad or missing\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data) tlvRespond(); // send response to client return(0); } Controller *tPair; // temporary pointer to Controller if(!(tPair=findController(tlv8.buf(kTLVType_Identifier)))){ LOG0("\n*** ERROR: Unrecognized Controller ID: "); charPrintRow(tlv8.buf(kTLVType_Identifier),36,2); LOG0("\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client return(0); } LOG2("\n*** Verifying session with Controller ID: "); charPrintRow(tPair->ID,36,2); LOG2("...\n"); size_t iosDeviceInfoLen=32+36+32; uint8_t iosDeviceInfo[iosDeviceInfoLen]; memcpy(iosDeviceInfo,iosCurveKey,32); memcpy(iosDeviceInfo+32,tPair->ID,36); memcpy(iosDeviceInfo+32+36,publicCurveKey,32); if(crypto_sign_verify_detached(tlv8.buf(kTLVType_Signature), iosDeviceInfo, iosDeviceInfoLen, tPair->LTPK) != 0){ // verify signature of iosDeviceInfo using iosDeviceLTPK LOG0("\n*** ERROR: LPTK Signature Verification Failed\n\n"); tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication tlvRespond(); // send response to client return(0); } tlv8.clear(); // clear TLV records tlv8.val(kTLVType_State,pairState_M4); // set State= tlvRespond(); // send response to client (unencrypted since cPair=NULL) cPair=tPair; // save Controller for this connection slot - connection is now verified and should be encrypted going forward hkdf.create(a2cKey,sharedCurveKey,32,"Control-Salt","Control-Read-Encryption-Key"); // create AccessoryToControllerKey (HAP Section 6.5.2) hkdf.create(c2aKey,sharedCurveKey,32,"Control-Salt","Control-Write-Encryption-Key"); // create ControllerToAccessoryKey (HAP Section 6.5.2) a2cNonce.zero(); // reset Nonces for this session to zero c2aNonce.zero(); LOG2("\n*** SESSION VERIFICATION COMPLETE *** \n"); return(1); break; } // switch return(1); } // postPairVerify ////////////////////////////////////// int HAPClient::getAccessoriesURL(){ if(!cPair){ // unverified, unencrypted session unauthorizedError(); return(0); } LOG1("In Get Accessories #"); LOG1(conNum); LOG1(" ("); LOG1(client.remoteIP()); LOG1(")...\n"); int nBytes = homeSpan.sprintfAttributes(NULL); // get size of HAP attributes JSON // TempBuffer jBuf(nBytes+1,"HAPClient::getAccessoriesURL"); // homeSpan.sprintfAttributes(jBuf.get()); // create JSON database (will need to re-cast to uint8_t* below) // char *body; // asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); // LOG2("\n>>>>>>>>>> "); // LOG2(client.remoteIP()); // LOG2(" >>>>>>>>>>\n"); // LOG2(body); // LOG2(jBuf.get()); // LOG2("\n"); // sendEncrypted(body,(uint8_t *)jBuf.get(),nBytes); // free(body); SendEncryptedContext context(*this); context.printf("HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); homeSpan.sprintfAttributes(NULL, GET_VALUE|GET_META|GET_PERMS|GET_TYPE|GET_DESC, &context); return(1); } // getAccessories ////////////////////////////////////// int HAPClient::postPairingsURL(){ if(!cPair){ // unverified, unencrypted session unauthorizedError(); return(0); } LOG1("In Post Pairings #"); LOG1(conNum); LOG1(" ("); LOG1(client.remoteIP()); LOG1(")..."); if(tlv8.val(kTLVType_State)!=1){ LOG0("\n*** ERROR: 'State' TLV record is either missing or not set to as required\n\n"); badRequestError(); // return with 400 error, which closes connection return(0); } switch(tlv8.val(kTLVType_Method)){ case 3: { LOG1("Add...\n"); if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_PublicKey) || !tlv8.len(kTLVType_Permissions)){ LOG0("\n*** ERROR: One or more of required 'Identifier,' 'PublicKey,' and 'Permissions' TLV records for this step is bad or missing\n\n"); tlv8.clear(); tlv8.val(kTLVType_Error,tagError_Unknown); } else if(!cPair->admin){ LOG0("\n*** ERROR: Controller making request does not have admin privileges to add/update other Controllers\n\n"); tlv8.clear(); tlv8.val(kTLVType_Error,tagError_Authentication); } else { tagError err=addController(tlv8.buf(kTLVType_Identifier),tlv8.buf(kTLVType_PublicKey),tlv8.val(kTLVType_Permissions)); tlv8.clear(); if(err!=tagError_None) tlv8.val(kTLVType_Error,err); } tlv8.val(kTLVType_State,pairState_M2); tlvRespond(); return(1); } case 4: { LOG1("Remove...\n"); uint8_t id[36]; if(!tlv8.len(kTLVType_Identifier)){ LOG0("\n*** ERROR: Required 'Identifier' TLV record for this step is bad or missing\n\n"); tlv8.clear(); tlv8.val(kTLVType_Error,tagError_Unknown); } else if(!cPair->admin){ LOG0("\n*** ERROR: Controller making request does not have admin privileges to remove Controllers\n\n"); tlv8.clear(); tlv8.val(kTLVType_Error,tagError_Authentication); } else { memcpy(id,tlv8.buf(kTLVType_Identifier),36); tlv8.clear(); } tlv8.val(kTLVType_State,pairState_M2); tlvRespond(); // must send response before removing Controller below if(tlv8.val(kTLVType_Error)==-1) removeController(id); return(1); } case 5: { LOG1("List...\n"); TempBuffer tBuf(listControllers(NULL), "HAPClient::postPairingsURL listControllers"); char *body; asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",tBuf.len()); // create Body with Content Length = size of TLV data LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); listControllers(tBuf.get()); sendEncrypted(body,tBuf.get(),tBuf.len()); free(body); return(1); } default: { LOG0("\n*** ERROR: 'Method' TLV record is either missing or not set to either 3, 4, or 5 as required\n\n"); badRequestError(); // return with 400 error, which closes connection return(0); } } // switch return(1); } ////////////////////////////////////// int HAPClient::getCharacteristicsURL(char *urlBuf){ if(!cPair){ // unverified, unencrypted session unauthorizedError(); return(0); } LOG1("In Get Characteristics #"); LOG1(conNum); LOG1(" ("); LOG1(client.remoteIP()); LOG1(")...\n"); int len=strlen(urlBuf); // determine number of IDs specificed by counting commas in URL int numIDs=1; for(int i=0;i>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); LOG2(jsonBuf); LOG2("\n"); sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* free(body); return(1); } ////////////////////////////////////// int HAPClient::putCharacteristicsURL(char *json){ if(!cPair){ // unverified, unencrypted session unauthorizedError(); return(0); } LOG1("In Put Characteristics #"); LOG1(conNum); LOG1(" ("); LOG1(client.remoteIP()); LOG1(")...\n"); int n=homeSpan.countCharacteristics(json); // count number of objects in JSON request if(n==0) // if no objects found, return return(0); SpanBuf pObj[n]; // reserve space for objects if(!homeSpan.updateCharacteristics(json, pObj)) // perform update return(0); // return if failed to update (error message will have been printed in update) int multiCast=0; // check if all status is OK, or if multicast response is request for(int i=0;i>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); sendEncrypted(body,NULL,0); } else { // multicast respose is required int nBytes=homeSpan.sprintfAttributes(pObj,n,NULL); // get JSON response - includes terminating null (will be recast to uint8_t* below) char jsonBuf[nBytes+1]; homeSpan.sprintfAttributes(pObj,n,jsonBuf); char *body; asprintf(&body,"HTTP/1.1 207 Multi-Status\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); LOG2(jsonBuf); LOG2("\n"); sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* free(body); } // Create and send Event Notifications if needed eventNotify(pObj,n,HAPClient::conNum); // transmit EVENT Notification for "n" pObj objects, except DO NOT notify client making request return(1); } ////////////////////////////////////// int HAPClient::putPrepareURL(char *json){ if(!cPair){ // unverified, unencrypted session unauthorizedError(); return(0); } LOG1("In Put Prepare #"); LOG1(conNum); LOG1(" ("); LOG1(client.remoteIP()); LOG1(")...\n"); char ttlToken[]="\"ttl\":"; char pidToken[]="\"pid\":"; char *cBuf; uint32_t ttl; uint64_t pid; if((cBuf=strstr(json,ttlToken))) sscanf(cBuf+strlen(ttlToken),"%u",&ttl); if((cBuf=strstr(json,pidToken))) sscanf(cBuf+strlen(ttlToken),"%llu",&pid); char jsonBuf[32]; StatusCode status=StatusCode::OK; if(ttl>0 && pid>0){ // found required elements homeSpan.TimedWrites[pid]=ttl+millis(); // store this pid/alarmTime combination } else { // problems parsing request status=StatusCode::InvalidValue; } sprintf(jsonBuf,"{\"status\":%d}",(int)status); int nBytes=strlen(jsonBuf); char *body; asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); LOG2(jsonBuf); LOG2("\n"); sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* free(body); return(1); } ////////////////////////////////////// int HAPClient::getStatusURL(){ char clocktime[33]; if(homeSpan.webLog.timeInit){ struct tm timeinfo; getLocalTime(&timeinfo,10); strftime(clocktime,sizeof(clocktime),"%c",&timeinfo); } else { sprintf(clocktime,"Unknown"); } char uptime[32]; int seconds=esp_timer_get_time()/1e6; int secs=seconds%60; int mins=(seconds/=60)%60; int hours=(seconds/=60)%24; int days=(seconds/=24); sprintf(uptime,"%d:%02d:%02d:%02d",days,hours,mins,secs); String response="HTTP/1.1 200 OK\r\nContent-type: text/html; charset=utf-8\r\n\r\n"; response+="" + String(homeSpan.displayName) + "\n"; response+="\n"; response+="

" + String(homeSpan.displayName) + "

\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; response+="\n"; char mbtlsv[64]; mbedtls_version_get_string_full(mbtlsv); response+="\n"; response+="\n"; response+="\n"; if(homeSpan.weblogCallback) homeSpan.weblogCallback(response); response+="
Up Time:" + String(uptime) + "
Current Time:" + String(clocktime) + "
Boot Time:" + String(homeSpan.webLog.bootTime) + "
Reset Reason:"; switch(esp_reset_reason()) { case ESP_RST_UNKNOWN: response += "Cannot be determined"; break; case ESP_RST_POWERON: response += "Power-on event"; break; case ESP_RST_EXT: response += "External pin"; break; case ESP_RST_SW: response += "Software reboot via esp_restart"; break; case ESP_RST_PANIC: response += "Software Exception/Panic"; break; case ESP_RST_INT_WDT: response += "Interrupt watchdog"; break; case ESP_RST_TASK_WDT: response += "Task watchdog"; break; case ESP_RST_WDT: response += "Other watchdogs"; break; case ESP_RST_DEEPSLEEP: response += "Exiting deep sleep mode"; break; case ESP_RST_BROWNOUT: response += "Brownout"; break; case ESP_RST_SDIO: response += "SDIO"; break; default: response += "Unknown Reset Code"; } response+=" (" + String(esp_reset_reason()) + ")
WiFi Disconnects:" + String(homeSpan.connected/2) + "
WiFi Signal:" + String(WiFi.RSSI()) + " dBm
WiFi Gateway:" + WiFi.gatewayIP().toString() + "
ESP32 Board:" + String(ARDUINO_BOARD) + "
Arduino-ESP Version:" + String(ARDUINO_ESP_VERSION) + "
ESP-IDF Version:" + String(ESP_IDF_VERSION_MAJOR) + "." + String(ESP_IDF_VERSION_MINOR) + "." + String(ESP_IDF_VERSION_PATCH) + "
HomeSpan Version:" + String(HOMESPAN_VERSION) + "
Sketch Version:" + String(homeSpan.getSketchVersion()) + "
Sodium Version:" + String(sodium_version_string()) + " Lib " + String(sodium_library_version_major()) + "." + String(sodium_library_version_minor()) +"
MbedTLS Version:" + String(mbtlsv) + "
HomeKit Status:" + String(HAPClient::nAdminControllers()?"PAIRED":"NOT PAIRED") + "
Max Log Entries:" + String(homeSpan.webLog.maxEntries) + "
\n"; response+="

"; if(homeSpan.webLog.maxEntries>0){ response+="\n"; int lastIndex=homeSpan.webLog.nEntries-homeSpan.webLog.maxEntries; if(lastIndex<0) lastIndex=0; for(int i=homeSpan.webLog.nEntries-1;i>=lastIndex;i--){ int index=i%homeSpan.webLog.maxEntries; seconds=homeSpan.webLog.log[index].upTime/1e6; secs=seconds%60; mins=(seconds/=60)%60; hours=(seconds/=60)%24; days=(seconds/=24); sprintf(uptime,"%d:%02d:%02d:%02d",days,hours,mins,secs); if(homeSpan.webLog.log[index].clockTime.tm_year>0) strftime(clocktime,sizeof(clocktime),"%c",&homeSpan.webLog.log[index].clockTime); else sprintf(clocktime,"Unknown"); response+="\n"; } response+="
EntryUp TimeLog TimeClientMessage
" + String(i+1) + "" + String(uptime) + "" + String(clocktime) + "" + homeSpan.webLog.log[index].clientIP + "" + String(homeSpan.webLog.log[index].message) + "
\n"; } response+=""; LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(response); LOG2("\n"); client.print(response); LOG2("------------ SENT! --------------\n"); delay(1); client.stop(); return(1); } ////////////////////////////////////// void HAPClient::checkNotifications(){ if(!homeSpan.Notifications.empty()){ // if there are Notifications to process eventNotify(&homeSpan.Notifications[0],homeSpan.Notifications.size()); // transmit EVENT Notifications homeSpan.Notifications.clear(); // clear Notifications vector } } ////////////////////////////////////// void HAPClient::checkTimedWrites(){ unsigned long cTime=millis(); // get current time char c[64]; auto tw=homeSpan.TimedWrites.begin(); while(tw!=homeSpan.TimedWrites.end()){ if(cTime>tw->second){ // timer has expired sprintf(c,"Removing PID=%llu ALARM=%u\n",tw->first,tw->second); LOG2(c); tw=homeSpan.TimedWrites.erase(tw); } else tw++; } } ////////////////////////////////////// void HAPClient::eventNotify(SpanBuf *pObj, int nObj, int ignoreClient){ for(int cNum=0;cNumclient && cNum!=ignoreClient){ // if there is a client connected to this slot and it is NOT flagged to be ignored (in cases where it is the client making a PUT request) int nBytes=homeSpan.sprintfNotify(pObj,nObj,NULL,cNum); // get JSON response for notifications to client cNum - includes terminating null (will be recast to uint8_t* below) if(nBytes>0){ // if there are notifications to send to client cNum char jsonBuf[nBytes+1]; homeSpan.sprintfNotify(pObj,nObj,jsonBuf,cNum); char *body; asprintf(&body,"EVENT/1.0 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); LOG2("\n>>>>>>>>>> "); LOG2(hap[cNum]->client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); LOG2(jsonBuf); LOG2("\n"); hap[cNum]->sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* free(body); } // if there are characteristic updates to notify client cNum } // if client exists } } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// void HAPClient::tlvRespond(){ TempBuffer tBuf(tlv8.pack(NULL), "HAPClient::tlvRespond"); // create buffer to hold TLV data tlv8.pack(tBuf.get()); // pack TLV records into buffer char *body; asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",tBuf.len()); // create Body with Content Length = size of TLV data LOG2("\n>>>>>>>>>> "); LOG2(client.remoteIP()); LOG2(" >>>>>>>>>>\n"); LOG2(body); tlv8.print(2); if(!cPair){ // unverified, unencrypted session client.print(body); client.write(tBuf.get(),tBuf.len()); LOG2("------------ SENT! --------------\n"); } else { sendEncrypted(body,tBuf.get(),tBuf.len()); } free(body); } // tlvRespond ////////////////////////////////////// int HAPClient::receiveEncrypted(uint8_t *httpBuf, int messageSize){ uint8_t aad[2]; int nBytes=0; while(client.read(aad,2)==2){ // read initial 2-byte AAD record int n=aad[0]+aad[1]*256; // compute number of bytes expected in message after decoding if(nBytes+n>messageSize){ // exceeded maximum number of bytes allowed in plaintext message LOG0("\n\n*** ERROR: Decrypted message of %d bytes exceeded maximum expected message length of %d bytes\n\n",nBytes+n,messageSize); return(0); } TempBuffer tBuf(n+16, "HAPClient::receiveEncrypted"); // expected number of total bytes = n bytes in encoded message + 16 bytes for appended authentication tag if(client.read(tBuf.get(),tBuf.len())!=tBuf.len()){ LOG0("\n\n*** ERROR: Malformed encrypted message frame\n\n"); return(0); } if(crypto_aead_chacha20poly1305_ietf_decrypt(httpBuf+nBytes, NULL, NULL, tBuf.get(), tBuf.len(), aad, 2, c2aNonce.get(), c2aKey)==-1){ LOG0("\n\n*** ERROR: Can't Decrypt Message\n\n"); return(0); } c2aNonce.inc(); nBytes+=n; // increment total number of bytes in plaintext message } // while return(nBytes); } // receiveEncrypted ////////////////////////////////////// void HAPClient::sendEncrypted(char *body, uint8_t *dataBuf, int dataLen){ const int FRAME_SIZE=1024; // number of bytes to use in each ChaCha20-Poly1305 encrypted frame when sending encrypted JSON content to Client int bodyLen=strlen(body); unsigned long long nBytes; int maxFrameSize=bodyLen>dataLen?bodyLen:dataLen; // set maxFrameSize to greater of bodyLen or dataLen if(maxFrameSize>FRAME_SIZE) // cap maxFrameSize by FRAME_SIZE (HAP restriction) maxFrameSize=FRAME_SIZE; TempBuffer tBuf(2+maxFrameSize+16, "HAPClient::sendEncrypted"); // 2-byte AAD + encrytped data + 16-byte authentication tag tBuf.get()[0]=bodyLen%256; // store number of bytes in first frame that encrypts the Body (AAD bytes) tBuf.get()[1]=bodyLen/256; crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.get()+2,&nBytes,(uint8_t *)body,bodyLen,tBuf.get(),2,NULL,a2cNonce.get(),a2cKey); // encrypt the Body with authentication tag appended client.write(tBuf.get(),nBytes+2); // transmit encrypted frame a2cNonce.inc(); // increment nonce for(int i=0;iFRAME_SIZE) // maximum number of bytes to encrypt=FRAME_SIZE n=FRAME_SIZE; tBuf.get()[0]=n%256; // store number of bytes that encrypts this frame (AAD bytes) tBuf.get()[1]=n/256; crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.get()+2,&nBytes,dataBuf+i,n,tBuf.get(),2,NULL,a2cNonce.get(),a2cKey); // encrypt the next portion of dataBuf with authentication tag appended client.write(tBuf.get(),nBytes+2); // transmit encrypted frame a2cNonce.inc(); // increment nonce } LOG2("-------- SENT ENCRYPTED! --------\n"); } // sendEncrypted ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// void HAPClient::hexPrintColumn(uint8_t *buf, int n, int minLogLevel){ if(homeSpan.logLevelLTPK,sizeof(cTemp->LTPK))){ // existing controller with same LTPK LOG2("\n*** Updated Controller: "); charPrintRow(id,36,2); LOG2(" from %s to %s\n\n",cTemp->admin?"(admin)":"(regular)",admin?"(admin)":"(regular)"); cTemp->admin=admin; saveControllers(); } else { LOG0("\n*** ERROR: Invalid request to update the LTPK of an existing Controller\n\n"); err=tagError_Unknown; } return(err); } ////////////////////////////////////// void HAPClient::removeController(uint8_t *id){ auto it=std::find_if(controllerList.begin(), controllerList.end(), [id](const Controller& cTemp){return(!memcmp(cTemp.ID,id,sizeof(cTemp.ID)));}); if(it==controllerList.end()){ LOG2("\n*** Request to Remove Controller Ignored - Controller Not Found: "); charPrintRow(id,36,2); LOG2("\n"); return; } LOG1("\n*** Removing Controller: "); charPrintRow((*it).ID,36,2); LOG1((*it).admin?" (admin)\n":" (regular)\n"); tearDown((*it).ID); // teardown any connections using this Controller controllerList.erase(it); // remove Controller if(!nAdminControllers()){ // no more admin Controllers LOG1("That was last Admin Controller! Removing any remaining Regular Controllers and unpairing Accessory\n"); tearDown(NULL); // teardown all remaining connections controllerList.clear(); // remove all remaining Controllers mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8) STATUS_UPDATE(start(LED_PAIRING_NEEDED),HS_PAIRING_NEEDED) // set optional Status LED if(homeSpan.pairCallback) // if set, invoke user-defined Pairing Callback to indicate device has been un-paired homeSpan.pairCallback(false); } saveControllers(); } ////////////////////////////////////// void HAPClient::tearDown(uint8_t *id){ for(int i=0;iclient && (id==NULL || (hap[i]->cPair && !memcmp(id,hap[i]->cPair->ID,36)))){ LOG1("*** Terminating Client #%d\n",i); hap[i]->client.stop(); } } } ////////////////////////////////////// int HAPClient::listControllers(uint8_t *tlvBuf){ int nBytes=0; int n; tlv8.clear(); tlv8.val(kTLVType_State,pairState_M2); for(auto it=controllerList.begin();it!=controllerList.end();it++){ if((*it).allocated){ if(tlv8.val(kTLVType_State)==-1) // if State is not set then this is not the first controller found tlv8.val(kTLVType_Separator,1); tlv8.val(kTLVType_Permissions,(*it).admin); tlv8.buf(kTLVType_Identifier,(*it).ID,36); tlv8.buf(kTLVType_PublicKey,(*it).LTPK,32); n=tlv8.pack(tlvBuf); nBytes+=n; if(tlvBuf){ tlvBuf+=n; tlv8.print(); } tlv8.clear(); } } return(nBytes); } ////////////////////////////////////// void HAPClient::printControllers(int minLogLevel){ if(homeSpan.logLevel tBuf(controllerList.size(), "HAPClient::saveControllers"); // create temporary buffer to hold Controller data std::copy(controllerList.begin(),controllerList.end(),tBuf.get()); // copy data from linked list to buffer nvs_set_blob(hapNVS,"CONTROLLERS",tBuf.get(),tBuf.len()); // update data nvs_commit(hapNVS); // commit to NVS } ////////////////////////////////////// Nonce::Nonce(){ zero(); } ////////////////////////////////////// void Nonce::zero(){ memset(x,0,12); } ////////////////////////////////////// uint8_t *Nonce::get(){ return(x); } ////////////////////////////////////// void Nonce::inc(){ x[4]++; if(x[4]==0) x[5]++; } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// // instantiate all static HAP Client structures and data TLV HAPClient::tlv8; nvs_handle HAPClient::hapNVS; nvs_handle HAPClient::srpNVS; HKDF HAPClient::hkdf; pairState HAPClient::pairStatus; Accessory HAPClient::accessory; list HAPClient::controllerList; SRP6A HAPClient::srp; int HAPClient::conNum;