1672 lines
		
	
	
		
			71 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			1672 lines
		
	
	
		
			71 KiB
		
	
	
	
		
			C++
		
	
	
	
| /*********************************************************************************
 | |
|  *  MIT License
 | |
|  *  
 | |
|  *  Copyright (c) 2020-2021 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 <ESPmDNS.h>
 | |
| #include <sodium.h>
 | |
| #include <MD5Builder.h>
 | |
| 
 | |
| #include "HAP.h"
 | |
| #include "HomeSpan.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
 | |
|   nvs_open("OTA",NVS_READWRITE,&otaNVS);        // open OTA data namespace in NVS
 | |
| 
 | |
|   if(!nvs_get_str(otaNVS,"OTADATA",NULL,&len)){                     // if found OTA data in NVS
 | |
|     nvs_get_str(otaNVS,"OTADATA",homeSpan.otaPwd,&len);              // retrieve data  
 | |
|   } else {
 | |
|     MD5Builder otaPwdHash;
 | |
|     otaPwdHash.begin();
 | |
|     otaPwdHash.add(DEFAULT_OTA_PASSWORD);
 | |
|     otaPwdHash.calculate();
 | |
|     otaPwdHash.getChars(homeSpan.otaPwd);
 | |
|   }
 | |
| 
 | |
|   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 {
 | |
|     
 | |
|     char c[128];
 | |
|     sprintf(c,"Generating SRP verification data for default Setup Code: %.3s-%.2s-%.3s\n",homeSpan.defaultSetupCode,homeSpan.defaultSetupCode+3,homeSpan.defaultSetupCode+5);
 | |
|     Serial.print(c);
 | |
|     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
 | |
|     Serial.print("Setup Payload for Optional QR Code: ");
 | |
|     Serial.print(homeSpan.qrCode.get(atoi(homeSpan.defaultSetupCode),homeSpan.qrID,atoi(homeSpan.category)));
 | |
|     Serial.print("\n\n");          
 | |
|   }
 | |
| 
 | |
|   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 {      
 | |
|     Serial.print("Generating new random Accessory ID and Long-Term Ed25519 Signature Keys...\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
 | |
|     nvs_get_blob(hapNVS,"CONTROLLERS",controllers,&len);             // retrieve data
 | |
|   } else {
 | |
|     Serial.print("Initializing storage for Paired Controllers data...\n\n");               
 | |
|     
 | |
|     HAPClient::removeControllers();                                             // clear all Controller data
 | |
|         
 | |
|     nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers));      // update data
 | |
|     nvs_commit(hapNVS);                                                      // commit to NVS
 | |
|   }
 | |
| 
 | |
|   Serial.print("Accessory ID:      ");
 | |
|   charPrintRow(accessory.ID,17);
 | |
|   Serial.print("                               LTPK: ");
 | |
|   hexPrintRow(accessory.LTPK,32);
 | |
|   Serial.print("\n");
 | |
| 
 | |
|   printControllers();                                                         
 | |
| 
 | |
|   // create broadcaset name from server base name plus accessory ID (without ':')
 | |
|   
 | |
|   int nChars=snprintf(NULL,0,"%s-%2.2s%2.2s%2.2s%2.2s%2.2s%2.2s",homeSpan.hostNameBase,accessory.ID,accessory.ID+3,accessory.ID+6,accessory.ID+9,accessory.ID+12,accessory.ID+15);       
 | |
|   homeSpan.hostName=(char *)malloc(nChars+1);
 | |
|   sprintf(homeSpan.hostName,"%s-%2.2s%2.2s%2.2s%2.2s%2.2s%2.2s",homeSpan.hostNameBase,accessory.ID,accessory.ID+3,accessory.ID+6,accessory.ID+9,accessory.ID+12,accessory.ID+15);
 | |
| 
 | |
|   tlv8.create(kTLVType_State,1,"STATE");                 // define the actual TLV records needed for the implementation of HAP; one for each kTLVType needed (HAP Table 5-6)
 | |
|   tlv8.create(kTLVType_PublicKey,384,"PUBKEY");
 | |
|   tlv8.create(kTLVType_Method,1,"METHOD");
 | |
|   tlv8.create(kTLVType_Salt,16,"SALT");
 | |
|   tlv8.create(kTLVType_Error,1,"ERROR");
 | |
|   tlv8.create(kTLVType_Proof,64,"PROOF");
 | |
|   tlv8.create(kTLVType_EncryptedData,1024,"ENC.DATA");
 | |
|   tlv8.create(kTLVType_Signature,64,"SIGNATURE");
 | |
|   tlv8.create(kTLVType_Identifier,64,"IDENTIFIER");
 | |
|   tlv8.create(kTLVType_Permissions,1,"PERMISSION");
 | |
| 
 | |
|   if(!nvs_get_blob(hapNVS,"HAPHASH",NULL,&len)){                 // if found HAP HASH structure
 | |
|     nvs_get_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,&len);     // retrieve data    
 | |
|   } else {
 | |
|     Serial.print("Resetting Accessory Configuration number...\n");
 | |
|     nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig));     // update data
 | |
|     nvs_commit(hapNVS);                                                                // commit to NVS
 | |
|   }
 | |
| 
 | |
|   Serial.print("\n");
 | |
| 
 | |
|   uint8_t tHash[48];
 | |
|   TempBuffer <char> tBuf(homeSpan.sprintfAttributes(NULL)+1);
 | |
|   homeSpan.sprintfAttributes(tBuf.buf);  
 | |
|   mbedtls_sha512_ret((uint8_t *)tBuf.buf,tBuf.len(),tHash,1);     // create SHA-384 hash of JSON (can be any hash - just looking for a unique key)
 | |
| 
 | |
|   if(memcmp(tHash,homeSpan.hapConfig.hashCode,48)){           // if hash code of current HAP database does not match stored hash code
 | |
|     memcpy(homeSpan.hapConfig.hashCode,tHash,48);             // update stored hash code
 | |
|     homeSpan.hapConfig.configNumber++;                        // increment configuration number
 | |
|     if(homeSpan.hapConfig.configNumber==65536)                // reached max value
 | |
|       homeSpan.hapConfig.configNumber=1;                      // reset to 1
 | |
|                    
 | |
|     Serial.print("Accessory configuration has changed.  Updating configuration number to ");
 | |
|     Serial.print(homeSpan.hapConfig.configNumber);
 | |
|     Serial.print("\n\n");
 | |
|     nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig));     // update data
 | |
|     nvs_commit(hapNVS);                                                                // commit to NVS
 | |
|   } else {
 | |
|     Serial.print("Accessory configuration number: ");
 | |
|     Serial.print(homeSpan.hapConfig.configNumber);
 | |
|     Serial.print("\n\n");    
 | |
|   }
 | |
| 
 | |
|   for(int i=0;i<homeSpan.Accessories.size();i++){                             // identify all services with over-ridden loop() methods
 | |
|     for(int j=0;j<homeSpan.Accessories[i]->Services.size();j++){
 | |
|       SpanService *s=homeSpan.Accessories[i]->Services[j];      
 | |
|       if((void(*)())(s->*(&SpanService::loop)) != (void(*)())(&SpanService::loop))    // save pointers to services in Loops vector
 | |
|         homeSpan.Loops.push_back(s);
 | |
|     }
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::processRequest(){
 | |
| 
 | |
|   int nBytes;
 | |
|   
 | |
|   if(cPair){                           // expecting encrypted message
 | |
|     LOG2("<<<< #### ");
 | |
|     LOG2(client.remoteIP());
 | |
|     LOG2(" #### <<<<\n");
 | |
| 
 | |
|     nBytes=receiveEncrypted();           // decrypt and return number of bytes       
 | |
|         
 | |
|     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,MAX_HTTP+1);   // read all available bytes up to maximum allowed+1
 | |
|        
 | |
|     if(nBytes>MAX_HTTP){                              // exceeded maximum number of bytes allowed
 | |
|       badRequestError();
 | |
|       Serial.print("\n*** ERROR:  Exceeded maximum HTTP message length\n\n");
 | |
|       return;
 | |
|     }
 | |
|         
 | |
|   } // encrypted/plaintext
 | |
|       
 | |
|   httpBuf[nBytes]='\0';         // add null character to enable string functions
 | |
|       
 | |
|   char *body=(char *)httpBuf;   // char pointer to start of HTTP Body
 | |
|   char *p;                          // char pointer used for searches
 | |
|       
 | |
|   if(!(p=strstr((char *)httpBuf,"\r\n\r\n"))){
 | |
|     badRequestError();
 | |
|     Serial.print("\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();
 | |
|     Serial.print("\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();
 | |
|       Serial.print("\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
 | |
|        if(homeSpan.logLevel>1) tlv8.print();                                                        // 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
 | |
|        if(homeSpan.logLevel>1) tlv8.print();                                                        // 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
 | |
|        if(homeSpan.logLevel>1) tlv8.print();                                                        // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)"
 | |
|       LOG2("------------ END TLVS! ------------\n");
 | |
|                
 | |
|       postPairingsURL();                  // 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
 | |
|        if(homeSpan.logLevel>1) tlv8.print();                                                        // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)"
 | |
|       LOG2("------------ END TLVS! ------------\n");
 | |
|                
 | |
|       postPairingsURL();                  // process URL    
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     notFoundError();
 | |
|     Serial.print("\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();
 | |
|       Serial.print("\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();
 | |
|     Serial.print("\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;
 | |
|     }
 | |
| 
 | |
|     notFoundError();
 | |
|     Serial.print("\n*** ERROR:  Bad GET request - URL not found\n\n");
 | |
|     return;                  
 | |
| 
 | |
|   } // GET request
 | |
|       
 | |
|   badRequestError();
 | |
|   Serial.print("\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
 | |
|     Serial.print("\n*** ERROR: Missing <M#> 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!
 | |
|     Serial.print("\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 <M%d>.  Expected <M%d>\n",tlvState,pairStatus);
 | |
|   LOG2(buf);
 | |
| 
 | |
|   if(tlvState!=pairStatus){                             // error: Device is not yet paired, but out-of-sequence pair-setup STATE was received
 | |
|     Serial.print("\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)
 | |
|         Serial.print("\n*** ERROR: Pair Method not set to 0\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         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=<M2>
 | |
|       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)){
 | |
|             
 | |
|         Serial.print("\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=<M4>
 | |
|         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
 | |
|         Serial.print("\n*** ERROR: SRP Proof Verification Failed\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M4);                // set State=<M4>
 | |
|         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=<M4>
 | |
|       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.buf(kTLVType_EncryptedData)){            
 | |
|         Serial.print("\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=<M6>
 | |
|         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){
 | |
|           
 | |
|         Serial.print("\n*** ERROR: Exchange-Request Authentication Failed\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M6);                // set State=<M6>
 | |
|         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)){
 | |
|         Serial.print("\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=<M6>
 | |
|         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);
 | |
|       }
 | |
| 
 | |
|       if(homeSpan.logLevel>1) tlv8.print();             // print decrypted TLV data
 | |
|       LOG2("------- END DECRYPTED TLVS! -------\n");
 | |
|        
 | |
|       if(!tlv8.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_PublicKey) || !tlv8.buf(kTLVType_Signature)){            
 | |
|         Serial.print("\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=<M6>
 | |
|         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   
 | |
|         Serial.print("\n*** ERROR: LPTK Signature Verification Failed\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M6);                // set State=<M6>
 | |
|         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
 | |
| 
 | |
|       nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers));      // update data
 | |
|       nvs_commit(hapNVS);                                                      // commit to NVS
 | |
| 
 | |
|       // 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)
 | |
| 
 | |
|       memcpy(tlv8.buf(kTLVType_Identifier,accessoryPairingIDLen),accessoryPairingID,accessoryPairingIDLen);   // set Identifier TLV record as accessoryPairingID
 | |
|       memcpy(tlv8.buf(kTLVType_PublicKey,accessoryLTPKLen),accessoryLTPK,accessoryLTPKLen);                   // set PublicKey TLV record as accessoryLTPK
 | |
| 
 | |
|       LOG2("------- ENCRYPTING SUB-TLVS -------\n");
 | |
| 
 | |
|       if(homeSpan.logLevel>1) tlv8.print();
 | |
| 
 | |
|       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=<M6>
 | |
|       
 | |
|       tlvRespond();                        // send response to client
 | |
| 
 | |
|       mdns_service_txt_item_set("_hap","_tcp","sf","0");           // broadcast new status
 | |
|       
 | |
|       LOG1("\n*** ACCESSORY PAIRED! ***\n");
 | |
|       homeSpan.statusLED.on();
 | |
|       
 | |
|       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
 | |
|     Serial.print("\n*** ERROR: Missing <M#> 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!
 | |
|     Serial.print("\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 <M%d>\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.buf(kTLVType_PublicKey)){            
 | |
|         Serial.print("\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=<M2>
 | |
|         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)
 | |
| 
 | |
|         int _x = 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)
 | |
| 
 | |
|         memcpy(tlv8.buf(kTLVType_Identifier,accessoryPairingIDLen),accessoryPairingID,accessoryPairingIDLen);   // set Identifier TLV record as accessoryPairingID
 | |
| 
 | |
|         LOG2("------- ENCRYPTING SUB-TLVS -------\n");
 | |
| 
 | |
|         if(homeSpan.logLevel>1) tlv8.print();
 | |
| 
 | |
|         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=<M2>
 | |
|         memcpy(tlv8.buf(kTLVType_PublicKey,32),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.buf(kTLVType_EncryptedData)){            
 | |
|         Serial.print("\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=<M4>
 | |
|         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){
 | |
|           
 | |
|         Serial.print("\n*** ERROR: Verify Authentication Failed\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M4);                // set State=<M4>
 | |
|         tlv8.val(kTLVType_Error,tagError_Authentication);    // set Error=Authentication
 | |
|         tlvRespond();                                       // send response to client
 | |
|         return(0);        
 | |
|       }
 | |
| 
 | |
|       if(!tlv8.unpack(decrypted,decryptedLen)){
 | |
|         Serial.print("\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=<M4>
 | |
|         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);
 | |
|       }
 | |
| 
 | |
|       if(homeSpan.logLevel>1) tlv8.print();             // print decrypted TLV data
 | |
|       LOG2("------- END DECRYPTED TLVS! -------\n");
 | |
| 
 | |
|       if(!tlv8.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_Signature)){            
 | |
|         Serial.print("\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=<M4>
 | |
|         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)))){
 | |
|         Serial.print("\n*** ERROR: Unrecognized Controller PairingID\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M4);                // set State=<M4>
 | |
|         tlv8.val(kTLVType_Error,tagError_Authentication);    // set Error=Authentication
 | |
|         tlvRespond();                                       // send response to client
 | |
|         return(0);
 | |
|       }
 | |
| 
 | |
|       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   
 | |
|         Serial.print("\n*** ERROR: LPTK Signature Verification Failed\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M4);                // set State=<M4>
 | |
|         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=<M4>
 | |
|       tlvRespond();                                       // send response to client (unencrypted since cPair=NULL)
 | |
| 
 | |
|       cPair=tPair;        // save Controller for this connection slot - connection is not 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 <char> jBuf(nBytes+1);
 | |
|   homeSpan.sprintfAttributes(jBuf.buf);                  // create JSON database (will need to re-cast to uint8_t* below)
 | |
| 
 | |
|   int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);      // create '200 OK' Body with Content Length = size of JSON Buf
 | |
|   char body[nChars+1];
 | |
|   sprintf(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.buf);
 | |
|   LOG2("\n");
 | |
|   
 | |
|   sendEncrypted(body,(uint8_t *)jBuf.buf,nBytes);
 | |
|        
 | |
|   return(1);
 | |
|   
 | |
| } // getAccessories
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| int HAPClient::postPairingsURL(){
 | |
| 
 | |
|   if(!cPair){                       // unverified, unencrypted session
 | |
|     unauthorizedError();
 | |
|     return(0);
 | |
|   }
 | |
| 
 | |
|   Controller *newCont;
 | |
| 
 | |
|   LOG1("In Post Pairings #");
 | |
|   LOG1(conNum);
 | |
|   LOG1(" (");  
 | |
|   LOG1(client.remoteIP());
 | |
|   LOG1(")...");
 | |
| 
 | |
|   if(tlv8.val(kTLVType_State)!=1){
 | |
|     Serial.print("\n*** ERROR: 'State' TLV record is either missing or not set to <M1> 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.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_PublicKey) || !tlv8.buf(kTLVType_Permissions)){            
 | |
|         Serial.print("\n*** ERROR: One or more of required 'Identifier,' 'PublicKey,' and 'Permissions' TLV records for this step is bad or missing\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         tlv8.val(kTLVType_Error,tagError_Unknown);           // set Error=Unknown (there is no specific error type for missing/bad TLV data)
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       if(!cPair->admin){
 | |
|         Serial.print("\n*** ERROR: Controller making request does not have admin privileges to add/update other Controllers\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         tlv8.val(kTLVType_Error,tagError_Authentication);    // set Error=Authentication
 | |
|         break;        
 | |
|       }
 | |
| 
 | |
|       if((newCont=findController(tlv8.buf(kTLVType_Identifier)))){
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         if(!memcmp(cPair->LTPK,newCont->LTPK,32)){                       // requested Controller already exists and LTPK matches
 | |
|           newCont->admin=tlv8.val(kTLVType_Permissions)==1?true:false;     // update permission of matching Controller
 | |
|         } else {
 | |
|           tlv8.val(kTLVType_Error,tagError_Unknown);         // set Error=Unknown
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       
 | |
|       if(!(newCont=getFreeController())){
 | |
|         Serial.print("\n*** ERROR: Can't pair more than ");
 | |
|         Serial.print(MAX_CONTROLLERS);
 | |
|         Serial.print(" Controllers\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         tlv8.val(kTLVType_Error,tagError_MaxPeers);           // set Error=Unknown (there is no specific error type for missing/bad TLV data)
 | |
|         break;        
 | |
|       }
 | |
| 
 | |
|       addController(tlv8.buf(kTLVType_Identifier),tlv8.buf(kTLVType_PublicKey),tlv8.val(kTLVType_Permissions)==1?true:false);
 | |
|       
 | |
|       tlv8.clear();                                         // clear TLV records
 | |
|       tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|       break;
 | |
| 
 | |
|     case 4:
 | |
|       LOG1("Remove...\n");
 | |
| 
 | |
|       if(!tlv8.buf(kTLVType_Identifier)){            
 | |
|         Serial.print("\n*** ERROR: Required 'Identifier' TLV record for this step is bad or missing\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         tlv8.val(kTLVType_Error,tagError_Unknown);           // set Error=Unknown (there is no specific error type for missing/bad TLV data)
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       if(!cPair->admin){
 | |
|         Serial.print("\n*** ERROR: Controller making request does not have admin privileges to remove Controllers\n\n");
 | |
|         tlv8.clear();                                         // clear TLV records
 | |
|         tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|         tlv8.val(kTLVType_Error,tagError_Authentication);    // set Error=Authentication
 | |
|         break;        
 | |
|       }
 | |
| 
 | |
|       removeController(tlv8.buf(kTLVType_Identifier));
 | |
|       
 | |
|       tlv8.clear();                                         // clear TLV records
 | |
|       tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|       break;
 | |
|       
 | |
|     case 5:                     
 | |
|       LOG1("List...\n");
 | |
| 
 | |
|       // NEEDS TO BE IMPLEMENTED - UNSURE IF THIS IS EVER USED BY HOMEKIT
 | |
| 
 | |
|       tlv8.clear();                                         // clear TLV records
 | |
|       tlv8.val(kTLVType_State,pairState_M2);                // set State=<M2>
 | |
|       break;
 | |
| 
 | |
|     default:
 | |
|       Serial.print("\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);
 | |
|       break;      
 | |
|   }
 | |
| 
 | |
|   nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers));      // update Controller data
 | |
|   nvs_commit(hapNVS);                                                      // commit to NVS
 | |
| 
 | |
|   tlvRespond();
 | |
| 
 | |
|   // re-check connections and close any (or all) clients as a result of controllers that were removed above
 | |
|   // must be performed AFTER sending the TLV response, since that connection itself may be terminated below
 | |
| 
 | |
|   for(int i=0;i<homeSpan.maxConnections;i++){     // loop over all connection slots
 | |
|     if(hap[i]->client){                    // if slot is connected
 | |
|       
 | |
|       if(!nAdminControllers() || (hap[i]->cPair && !hap[i]->cPair->allocated)){    // accessory unpaired, OR client connection is verified but points to a newly *unallocated* controller
 | |
|         LOG1("*** Terminating Client #");
 | |
|         LOG1(i);
 | |
|         LOG1("\n");
 | |
|         hap[i]->client.stop();
 | |
|       }
 | |
|       
 | |
|     } // if client connected
 | |
|   } // loop over all connection slots
 | |
|   
 | |
|   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<len;i++)
 | |
|     if(urlBuf[i]==',')
 | |
|       numIDs++;
 | |
|   
 | |
|   char *ids[numIDs];            // reserve space for number of IDs found
 | |
|   int flags=GET_AID;            // flags indicating which characteristic fields to include in response (HAP Table 6-13)
 | |
|   numIDs=0;                     // reset number of IDs found
 | |
| 
 | |
|   char *lastSpace=strchr(urlBuf,' ');
 | |
|   if(lastSpace)
 | |
|     lastSpace[0]='\0';
 | |
|     
 | |
|   char *p1;
 | |
|   while(char *t1=strtok_r(urlBuf,"&",&p1)){      // parse request into major tokens
 | |
|     urlBuf=NULL;
 | |
| 
 | |
|     if(!strcmp(t1,"meta=1")){
 | |
|       flags|=GET_META;
 | |
|     } else 
 | |
|     if(!strcmp(t1,"perms=1")){
 | |
|       flags|=GET_PERMS;
 | |
|     } else 
 | |
|     if(!strcmp(t1,"type=1")){
 | |
|       flags|=GET_TYPE;
 | |
|     } else 
 | |
|     if(!strcmp(t1,"ev=1")){
 | |
|       flags|=GET_EV;
 | |
|     } else
 | |
|     if(!strncmp(t1,"id=",3)){   
 | |
|       t1+=3;
 | |
|       char *p2;
 | |
|       while(char *t2=strtok_r(t1,",",&p2)){      // parse IDs
 | |
|         t1=NULL;
 | |
|         ids[numIDs++]=t2;
 | |
|       }
 | |
|     }
 | |
|   } // parse URL
 | |
| 
 | |
|   if(!numIDs)           // could not find any IDs
 | |
|     return(0);
 | |
| 
 | |
|   int nBytes=homeSpan.sprintfAttributes(ids,numIDs,flags,NULL);          // get JSON response - includes terminating null (will be recast to uint8_t* below)
 | |
|   char jsonBuf[nBytes+1];
 | |
|   homeSpan.sprintfAttributes(ids,numIDs,flags,jsonBuf);
 | |
| 
 | |
|   boolean sFlag=strstr(jsonBuf,"status");          // status attribute found?
 | |
| 
 | |
|   int nChars=snprintf(NULL,0,"HTTP/1.1 %s\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",!sFlag?"200 OK":"207 Multi-Status",nBytes);   
 | |
|   char body[nChars+1];    
 | |
|   sprintf(body,"HTTP/1.1 %s\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",!sFlag?"200 OK":"207 Multi-Status",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*
 | |
|       
 | |
|   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<n;i++)
 | |
|     if(pObj[i].status!=StatusCode::OK)
 | |
|       multiCast=1;    
 | |
| 
 | |
|   if(!multiCast){                                         // JSON object has no content
 | |
|     
 | |
|     char body[]="HTTP/1.1 204 No Content\r\n\r\n";
 | |
|     
 | |
|     LOG2("\n>>>>>>>>>> ");
 | |
|     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);
 | |
| 
 | |
|     int nChars=snprintf(NULL,0,"HTTP/1.1 207 Multi-Status\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);      // create Body with Content Length = size of JSON Buf
 | |
|     char body[nChars+1];
 | |
|     sprintf(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*
 | |
|   
 | |
|   }
 | |
| 
 | |
|   // 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);
 | |
|   int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);      // create Body with Content Length = size of JSON Buf
 | |
|   char body[nChars+1];
 | |
|   sprintf(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*
 | |
|     
 | |
|   return(1);
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::callServiceLoops(){
 | |
| 
 | |
|   homeSpan.snapTime=millis();                     // snap the current time for use in ALL loop routines
 | |
|   
 | |
|   for(int i=0;i<homeSpan.Loops.size();i++)        // loop over all services with over-ridden loop() methods
 | |
|     homeSpan.Loops[i]->loop();                    // call the loop() method
 | |
| }
 | |
| 
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::checkPushButtons(){
 | |
| 
 | |
|   for(int i=0;i<homeSpan.PushButtons.size();i++){                                // loop over all defined pushbuttons
 | |
|     SpanButton *sb=homeSpan.PushButtons[i];                                      // temporary pointer to SpanButton
 | |
|     if(sb->pushButton->triggered(sb->singleTime,sb->longTime,sb->doubleTime)){   // if the underlying PushButton is triggered
 | |
|       sb->service->button(sb->pin,sb->pushButton->type());                       // call the Service's button() routine with pin and type as parameters
 | |
|     }
 | |
|   }
 | |
|     
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| 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;cNum<homeSpan.maxConnections;cNum++){        // loop over all connection slots
 | |
|     if(hap[cNum]->client && 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);
 | |
| 
 | |
|         int nChars=snprintf(NULL,0,"EVENT/1.0 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);      // create Body with Content Length = size of JSON Buf
 | |
|         char body[nChars+1];
 | |
|         sprintf(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*
 | |
| 
 | |
|       } // if there are characteristic updates to notify client cNum
 | |
|     } // if client exists
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| void HAPClient::tlvRespond(){
 | |
| 
 | |
|   int nBytes=tlv8.pack(NULL);      // return number of bytes needed to pack TLV records into a buffer
 | |
|   uint8_t tlvData[nBytes];         // create buffer
 | |
|   tlv8.pack(tlvData);              // pack TLV records into buffer
 | |
| 
 | |
|   int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",nBytes);      // create Body with Content Length = size of TLV data
 | |
|   char body[nChars+1];
 | |
|   sprintf(body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",nBytes);
 | |
|   
 | |
|   LOG2("\n>>>>>>>>>> ");
 | |
|   LOG2(client.remoteIP());
 | |
|   LOG2(" >>>>>>>>>>\n");
 | |
|   LOG2(body);
 | |
|   if(homeSpan.logLevel>1) tlv8.print();
 | |
| 
 | |
|   if(!cPair){                       // unverified, unencrypted session
 | |
|     client.print(body);
 | |
|     client.write(tlvData,nBytes);      
 | |
|     LOG2("------------ SENT! --------------\n");
 | |
|   } else {
 | |
|     sendEncrypted(body,tlvData,nBytes);
 | |
|   }
 | |
| 
 | |
| } // tlvRespond
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| int HAPClient::receiveEncrypted(){
 | |
| 
 | |
|   uint8_t buf[1042];               // maximum size of encoded message = 2+1024+16 bytes (HAP Section 6.5.2)
 | |
|   int nBytes=0;
 | |
| 
 | |
|   while(client.read(buf,2)==2){    // read initial 2-byte AAD record
 | |
| 
 | |
|     int n=buf[0]+buf[1]*256;                // compute number of bytes expected in encoded message
 | |
| 
 | |
|     if(nBytes+n>MAX_HTTP){                  // exceeded maximum number of bytes allowed in plaintext message
 | |
|       Serial.print("\n\n*** ERROR:  Exceeded maximum HTTP message length\n\n");
 | |
|       return(0);
 | |
|       }
 | |
| 
 | |
|     if(client.read(buf+2,n+16)!=n+16){      // read expected number of total bytes = n bytes in encoded message + 16 bytes for appended authentication tag      
 | |
|       Serial.print("\n\n*** ERROR: Malformed encrypted message frame\n\n");
 | |
|       return(0);      
 | |
|     }                
 | |
| 
 | |
|     if(crypto_aead_chacha20poly1305_ietf_decrypt(httpBuf+nBytes, NULL, NULL, buf+2, n+16, buf, 2, c2aNonce.get(), c2aKey)==-1){
 | |
|       Serial.print("\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);
 | |
| 
 | |
|   int count=0;
 | |
|   unsigned long long nBytes;
 | |
| 
 | |
|   int totalBytes=2+bodyLen+16;                            // 2-byte AAD + bodyLen + 16-byte authentication tag
 | |
|   totalBytes+=(dataLen/FRAME_SIZE)*(2+FRAME_SIZE+16);     // number of full frames * size of full frame with 2-byte AAD + 16-byte authentication tag
 | |
| 
 | |
|   if(dataLen%FRAME_SIZE)                                  // if there is a residual last partial frame
 | |
|     totalBytes+=2+dataLen%FRAME_SIZE+16;                  // 2-byte AAD + residual of last partial frame + 16-byte authentication tag
 | |
| 
 | |
|   TempBuffer <uint8_t> tBuf(totalBytes);
 | |
|   
 | |
|   tBuf.buf[count]=bodyLen%256;         // store number of bytes in first frame that encrypts the Body (AAD bytes)
 | |
|   tBuf.buf[count+1]=bodyLen/256;
 | |
|   
 | |
|   crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.buf+count+2,&nBytes,(uint8_t *)body,bodyLen,tBuf.buf+count,2,NULL,a2cNonce.get(),a2cKey);   // encrypt the Body with authentication tag appended
 | |
| 
 | |
|   a2cNonce.inc();                 // increment nonce
 | |
|   
 | |
|   count+=2+bodyLen+16;            // increment count by 2-byte AAD record + length of Body + 16-byte authentication tag
 | |
| 
 | |
|   for(int i=0;i<dataLen;i+=FRAME_SIZE){      // encrypt FRAME_SIZE number of bytes in dataBuf in sequential frames
 | |
|     
 | |
|     int n=dataLen-i;           // number of bytes remaining
 | |
|     
 | |
|     if(n>FRAME_SIZE)           // maximum number of bytes to encrypt=FRAME_SIZE
 | |
|       n=FRAME_SIZE;                                     
 | |
|     
 | |
|     tBuf.buf[count]=n%256;    // store number of bytes that encrypts this frame (AAD bytes)
 | |
|     tBuf.buf[count+1]=n/256;
 | |
| 
 | |
|     crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.buf+count+2,&nBytes,dataBuf+i,n,tBuf.buf+count,2,NULL,a2cNonce.get(),a2cKey);   // encrypt the next portion of dataBuf with authentication tag appended
 | |
| 
 | |
|     a2cNonce.inc();            // increment nonce
 | |
| 
 | |
|     count+=2+n+16;             // increment count by 2-byte AAD record + length of JSON + 16-byte authentication tag
 | |
|   }
 | |
|  
 | |
|   client.write(tBuf.buf,count);   // transmit all encrypted frames to Client
 | |
| 
 | |
|   LOG2("-------- SENT ENCRYPTED! --------\n");
 | |
|       
 | |
| } // sendEncrypted
 | |
| 
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| void HAPClient::hexPrintColumn(uint8_t *buf, int n){
 | |
| 
 | |
|   char c[16];
 | |
|   
 | |
|   for(int i=0;i<n;i++){
 | |
|     sprintf(c,"%d) %02X",i,buf[i]);
 | |
|     Serial.println(c);
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::hexPrintRow(uint8_t *buf, int n){
 | |
| 
 | |
|   char c[16];
 | |
|   
 | |
|   for(int i=0;i<n;i++){
 | |
|     sprintf(c,"%02X",buf[i]);
 | |
|     Serial.print(c);
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::charPrintRow(uint8_t *buf, int n){
 | |
| 
 | |
|   char c[16];
 | |
|   
 | |
|   for(int i=0;i<n;i++){
 | |
|     sprintf(c,"%c",buf[i]);
 | |
|     Serial.print(c);
 | |
|   }
 | |
| 
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| Controller *HAPClient::findController(uint8_t *id){
 | |
|   
 | |
|   for(int i=0;i<MAX_CONTROLLERS;i++){         // loop over all controller slots
 | |
|     
 | |
|     if(controllers[i].allocated && !memcmp(controllers[i].ID,id,36)){     // found matching ID
 | |
|       LOG2("Found Controller: ");
 | |
|       if(homeSpan.logLevel>1)
 | |
|         charPrintRow(id,36);
 | |
|       LOG2(controllers[i].admin?" (admin)\n":" (regular)\n");    
 | |
|       return(controllers+i);                                              // return with pointer to matching controller
 | |
|     }
 | |
|   } // loop
 | |
| 
 | |
|   return(NULL);       // no match
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| Controller *HAPClient::getFreeController(){
 | |
|   
 | |
|   for(int i=0;i<MAX_CONTROLLERS;i++){     // loop over all controller slots
 | |
|     
 | |
|     if(!controllers[i].allocated)         // found free slot
 | |
|       return(controllers+i);              // return with pointer to free slot
 | |
|   }
 | |
| 
 | |
|   return(NULL);       // no free slots
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| Controller *HAPClient::addController(uint8_t *id, uint8_t *ltpk, boolean admin){
 | |
| 
 | |
|   Controller *slot;
 | |
| 
 | |
|   if((slot=findController(id))){
 | |
|     memcpy(slot->LTPK,ltpk,32);
 | |
|     slot->admin=admin;
 | |
|     LOG2("\n*** Updated Controller: ");
 | |
|     if(homeSpan.logLevel>1)
 | |
|       charPrintRow(id,36);
 | |
|     LOG2(slot->admin?" (admin)\n\n":" (regular)\n\n");
 | |
|     return(slot);    
 | |
|   }
 | |
| 
 | |
|   if((slot=getFreeController())){
 | |
|     slot->allocated=true;
 | |
|     memcpy(slot->ID,id,36);
 | |
|     memcpy(slot->LTPK,ltpk,32);
 | |
|     slot->admin=admin;
 | |
|     LOG2("\n*** Added Controller: ");
 | |
|     if(homeSpan.logLevel>1)
 | |
|       charPrintRow(id,36);
 | |
|     LOG2(slot->admin?" (admin)\n\n":" (regular)\n\n");
 | |
|     return(slot);       
 | |
|   }
 | |
| 
 | |
|   Serial.print("\n*** WARNING: No open slots.  Can't add Controller: ");
 | |
|   hexPrintRow(id,36);
 | |
|   Serial.print(admin?" (admin)\n\n":" (regular)\n\n\n");
 | |
|   return(NULL);
 | |
| }       
 | |
|   
 | |
| //////////////////////////////////////
 | |
| 
 | |
| int HAPClient::nAdminControllers(){
 | |
| 
 | |
|   int n=0;
 | |
|   
 | |
|   for(int i=0;i<MAX_CONTROLLERS;i++){                         // loop over all controller slots
 | |
|     n+=(controllers[i].allocated && controllers[i].admin);    // count number of allocated controllers with admin privileges
 | |
|   }
 | |
| 
 | |
|   return(n);
 | |
| }
 | |
|     
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::removeControllers(){
 | |
|   
 | |
|   for(int i=0;i<MAX_CONTROLLERS;i++)
 | |
|     controllers[i].allocated=false;
 | |
| }    
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::removeController(uint8_t *id){
 | |
| 
 | |
|   Controller *slot;
 | |
| 
 | |
|   if((slot=findController(id))){      // remove controller if found
 | |
|     LOG2("\n***Removed Controller: ");
 | |
|     if(homeSpan.logLevel>1)
 | |
|       charPrintRow(id,36);
 | |
|     LOG2(slot->admin?" (admin)\n":" (regular)\n");
 | |
|     slot->allocated=false;
 | |
| 
 | |
|     if(nAdminControllers()==0){       // if no more admins, remove all controllers
 | |
|       removeControllers();
 | |
|       LOG1("That was last Admin Controller!  Removing any remaining Regular Controllers and unpairing Accessory\n");  
 | |
|       mdns_service_txt_item_set("_hap","_tcp","sf","1");           // set Status Flag = 1 (Table 6-8)
 | |
|       homeSpan.statusLED.start(LED_PAIRING_NEEDED);
 | |
|       if(homeSpan.pairCallback)                                    // if set, invoke user-defined Pairing Callback to indicate device has been paired
 | |
|         homeSpan.pairCallback(false);
 | |
|     }
 | |
| 
 | |
|     LOG2("\n");
 | |
|   }
 | |
| 
 | |
| }    
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| void HAPClient::printControllers(){
 | |
| 
 | |
|   int n=0;
 | |
|   
 | |
|   for(int i=0;i<MAX_CONTROLLERS;i++){           // loop over all controller slots
 | |
|     if(controllers[i].allocated){
 | |
|       Serial.print("Paired Controller: ");
 | |
|       charPrintRow(controllers[i].ID,36);
 | |
|       Serial.print(controllers[i].admin?"   (admin)":" (regular)");
 | |
|       Serial.print("  LTPK: ");
 | |
|       hexPrintRow(controllers[i].LTPK,32);
 | |
|       Serial.print("\n");
 | |
|       n++;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if(n==0)
 | |
|     Serial.print("No Paired Controllers\n");
 | |
| }
 | |
| 
 | |
| //////////////////////////////////////
 | |
| 
 | |
| 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<kTLVType,10> HAPClient::tlv8;
 | |
| nvs_handle HAPClient::hapNVS;
 | |
| nvs_handle HAPClient::srpNVS;
 | |
| nvs_handle HAPClient::otaNVS;
 | |
| uint8_t HAPClient::httpBuf[MAX_HTTP+1];                 
 | |
| HKDF HAPClient::hkdf;                                   
 | |
| pairState HAPClient::pairStatus;                        
 | |
| Accessory HAPClient::accessory;                         
 | |
| Controller HAPClient::controllers[MAX_CONTROLLERS];    
 | |
| SRP6A HAPClient::srp;
 | |
| int HAPClient::conNum;
 | |
|  
 |