commit cccb61f94616a1c66b098be9a4dd1bd3a322df31 Author: Gregg Date: Sat Jul 18 21:47:39 2020 -0500 Initial commit diff --git a/.development b/.development new file mode 100644 index 0000000..e69de29 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/README.md b/README.md new file mode 100644 index 0000000..7942df6 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# HomeSpan + HomeKit for the Arduino-ESP32 diff --git a/examples/LightBulb/LightBulb.ino b/examples/LightBulb/LightBulb.ino new file mode 100644 index 0000000..e3bfa70 --- /dev/null +++ b/examples/LightBulb/LightBulb.ino @@ -0,0 +1,103 @@ + +//////////////////////////////////////////////////////////// +// // +// HomeSpan: A HomeKit implementation for the ESP32 // +// ------------------------------------------------ // +// // +// Example 1: A non-functioning on/off light bulb // +// constructed from basic HomeSpan components // +// // +//////////////////////////////////////////////////////////// + + +#include "HomeSpan.h" // Always start by including the HomeSpan library + +void setup() { + + Serial.begin(115200); // Start a serial connection - this is needed for you to type in your WiFi credentials + + // Begin a HomeSpan Session. Required parameters are Category and Name. + // These are used by HomeKit to configure the icon and name of the device shown when initially pairing. + // There are no other effects of these settings and they are ignored by HomeKit after pairing is complete. + // You can even specify a Lighting Category for a Faucet. This effects nothing but the initial incon. + // A complete list of Categories can be found in Settings.h, which is based on Section 13 of Apple's + // HomeKit Accessory Protocol (HAP) Specifications Document. + + homeSpan.begin(Category::Lighting,"HomeSpan LightBulb"); + + // Every HomeKit device consists of one or more Accessories. Each Accessory contains one or more Services. + // Every Service contains one or more Characteristics. HAP defines all allowable Services and Characteristics, + // including those that are required and those that are optional. An Accessory is typically a complete appliance, + // such as a table lamp or ceiling fan. Services are the main components of the appliance - a ceiling fan Accessory will + // typically have a fan Service and a light bulb Service. Characteristics define how each Service operates. + + // Some Characteristics are read-only and describe the name or properties of a Service. Other Characteristics + // can be both written and read by HomeKit - these are the interesting ones since they enable actions to occur, + // such as turning on or off a light, or setting its brightness. + + // HAP also requires various informational Services that describe the overall Accessory. + + // HAP calls the entirety of all Accessories, Services, and Characteristics the "Accessory Attributes Database." + // A complete list of HAP Services and Characteristics implemented in HomeSpan can be found in Services.h, which is + // based on HAP Section 8 (Services) and HAP Section 9 (Characteristics). + + // Users construct the Accessories database in HomeSpan by using a combination of new SpanAccessory, new Services (which point to underlying + // SpanServices), and new Characteristics (which point to underlying SpanCharacteristics). The database is assembled in the + // order in which components are defined. A new Service will be implemented in the last new Accessory defined, and a new + // Characteristic will be implemented in the last new Service defined. Indention helps convey this structure though is + // of course not required. + + ///////////////////////////////// + + // For this example, our Database will comprise a single Accessory containing 3 Services, each with their own required Characteristics + + new SpanAccessory(); // Begin by creating a new Accessory using SpanAccessory(), which takes no arguments + + new Service::AccessoryInformation(); // HAP requires every Accessory to implement an AccessoryInformation Service, which has 6 required Characteristics + new Characteristic::Name("My Table Lamp"); // Name of the Accessory, which shows up on the HomeKit "tiles", and should be unique across Accessories + + // The next 4 Characteristics serve no function except for being displayed in HomeKit's setting panel for each Accessory. They are nevertheless required by HAP: + + new Characteristic::Manufacturer("HomeSpan"); // Manufacturer of the Accessory (arbitrary text string, and can be the same for every Accessory) + new Characteristic::SerialNumber("123-ABC"); // Serial Number of the Accessory (arbitrary text string, and can be the same for every Accessory) + new Characteristic::Model("120-Volt Lamp"); // Model of the Accessory (arbitrary text string, and can be the same for every Accessory) + new Characteristic::FirmwareRevision("0.9"); // Firmware of the Accessory (arbitrary text string, and can be the same for every Accessory) + + // The last required Characteristic for the Accessory Information Service allows the user to identify the Characteristic and requires + // some implementation code (such as blinking an LED, or flashing the light). HomeSpan defaults to take no action if there is no + // implementation code, so we can simply create the Identify Characteristic for now and let HomeSpan default to no action. + + new Characteristic::Identify(); // Create the required Identify + + // HAP requires every Accessory (with the exception of those in Bridges) to implement the HAP Protocol Information Service. + // This Serrvice supports a single required Characteristic that defined the version number of HAP used by the device. + // HAP Release R2 requires this version to be set to "1.1.0" + + new Service::HAPProtocolInformation(); // Create the HAP Protcol Information Service + new Characteristic::Version("1.1.0"); // Set the Version Characteristicto "1.1.0" as required by HAP + + // Now that the required "informational" Services have been defined, we can finally create the Light Bulb Service + // NOTE: The order of the Services is not important - we could have created the LightBulb first. + + new Service::LightBulb(); // Create the Light Bulb Service + new Characteristic::On(); // This Service requires the "On" Characterstic to turn the light on and off + + // That's all that's needed to define a database from scratch, including all required HAP elements, to control a single lightbulb. + // Of course the database itself does not contain any code to implement the actual operation of the light - there is nothing to + // turn on and off. But you'll still see a Light Bulb tile show up in HomeKit with an ability to toggle it on and off. In the next + // example we will add the code that turns on and off an LED. For now, upload this sketch to your ESP32, pair with HomeKit, and + // verify everything works. + +} // end of setup() + +////////////////////////////////////// + +void loop(){ + + // The code in setup above implements the Accessory Attribute Database, but performs no operations. HomeSpan itself must be + // continuously polled to look for requests from Controllers, such as an iOS or MacOS device. The poll() method below is all that + // is needed to perform this continuously in each iteration of loop() + + homeSpan.poll(); // run HomeSpan! + +} // end of loop() diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..76649b3 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=HomeSpan +version=1.0.0 +author=HomeSpan +maintainer=none +sentence=HomeKit for the Espressif ESP32. +paragraph=This library provides a microcontroller-focused implementation of Apple's HomeKit Accessory Protocol (HAP - Release R2) designed specifically for the ESP32. It allows a user to pair an iOS or MacOS HomeKit-compatiable application directly to the controller via their home WiFi network without the need for any external bridge or components. The user can then use the full power of the ESP32's I/O functionality to create custom control software and/or hardware to operate external devices. +url=none +architectures=* +includes=HomeSpan.h diff --git a/src/HAP.cpp b/src/HAP.cpp new file mode 100644 index 0000000..27aef41 --- /dev/null +++ b/src/HAP.cpp @@ -0,0 +1,1518 @@ + +#include +#include + +#include "HomeSpan.h" +#include "HAP.h" + +////////////////////////////////////// + +void HAPClient::init(){ + + size_t len; // not used but required to read blobs from NVS + + Serial.print("\n"); + + nvs_open("HAP",NVS_READWRITE,&nvsHandle); // open HAP data namespace in NVS + + if(!nvs_get_blob(nvsHandle,"ACCESSORY",NULL,&len)){ // if found long-term Accessory data in NVS + nvs_get_blob(nvsHandle,"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(nvsHandle,"ACCESSORY",&accessory,sizeof(accessory)); // update data + nvs_commit(nvsHandle); // commit to NVS + } + + if(!nvs_get_blob(nvsHandle,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS + nvs_get_blob(nvsHandle,"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(nvsHandle,"CONTROLLERS",controllers,sizeof(controllers)); // update data + nvs_commit(nvsHandle); // commit to NVS + } + + Serial.print("Accessory ID: "); + charPrintRow(accessory.ID,17); + Serial.print(" LTPK: "); + hexPrintRow(accessory.LTPK,32); + Serial.print("\n"); + + printControllers(); + + tlv8.create(kTLVType_State,1,"STATE"); // define each 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(nvsHandle,"HAPHASH",NULL,&len)){ // if found HAP HASH structure + nvs_get_blob(nvsHandle,"HAPHASH",&homeSpan.hapConfig,&len); // retrieve data + } else { + Serial.print("Resetting Accessory Configuration number...\n"); + nvs_set_blob(nvsHandle,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // update data + nvs_commit(nvsHandle); // commit to NVS + } + + Serial.print("\n"); + + uint8_t tHash[48]; + TempBuffer 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(nvsHandle,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // update data + nvs_commit(nvsHandle); // commit to NVS + } else { + Serial.print("Accessory configuration number: "); + Serial.print(homeSpan.hapConfig.configNumber); + Serial.print("\n\n"); + } + +} + +////////////////////////////////////// + +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 + 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 + 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 + 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 + 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; + } + + 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 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 . Expected \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= + 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); // load server public key, B (MUST MAKE THIS A LIVE CALCULATION TO GENERATE RANDOM SET-UP CODE) + srp.loadTLV(kTLVType_Salt,&srp.s); // load salt, s (MUST MAKE THIS RANDOM AS WELL) + 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= + 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= + 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); // 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= + 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= + 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= + 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(); // 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= + 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= + 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(nvsHandle,"CONTROLLERS",controllers,sizeof(controllers)); // update data + nvs_commit(nvsHandle); // 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"); + + 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= + + tlvRespond(); // send response to client + + mdns_service_txt_item_set("_hap","_tcp","sf","0"); // broadcast new status + + LOG1("\n*** ACCESSORY PAIRED! ***\n"); + + return(1); + + break; + + } // switch + +} // 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 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 \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= + 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) + + memcpy(tlv8.buf(kTLVType_Identifier,accessoryPairingIDLen),accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID + + LOG2("------- ENCRYPTING SUB-TLVS -------\n"); + + 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= + 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= + 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= + 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= + 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(); // 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= + 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= + 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= + 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 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 + +} // 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); + 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); + + LOG2("-------- SENT ENCRYPTED! --------\n"); + + 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 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= + 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= + 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= + 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= + 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= + 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= + 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= + 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= + break; + + case 5: + LOG1("List...\n"); + + // NEEDS TO BE IMPLEMENTED + + tlv8.clear(); // clear TLV records + tlv8.val(kTLVType_State,pairState_M2); // set State= + 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(nvsHandle,"CONTROLLERS",controllers,sizeof(controllers)); // update Controller data + nvs_commit(nvsHandle); // 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;iallocated)){ // accessory unpaired, OR client connection is verified but points to an 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>>>>>>>>> "); + LOG2(client.remoteIP()); + LOG2(" >>>>>>>>>>\n"); + LOG2(body); + LOG2(jsonBuf); + LOG2("\n"); + + sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* + + LOG2("-------- SENT ENCRYPTED! --------\n"); + + 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); + + SpanPut 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); + + LOG2("-------- SENT ENCRYPTED! --------\n"); + + } 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* + + LOG2("-------- SENT ENCRYPTED! --------\n"); + } + + // Create and send Event Notifications if needed + + for(int i=0;i>>>>>>>>> "); + LOG2(hap[i].client.remoteIP()); + LOG2(" >>>>>>>>>>\n"); + LOG2(body); + LOG2(jsonBuf); + LOG2("\n"); + + hap[i].sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* + + } // if there are characteristic updates to notify + } // if client exists + } + + return(1); +} + +////////////////////////////////////// + +void HAPClient::checkNotifications(){ + + SpanPBList *pb=homeSpan.pbHead; + + int n=0; + + while(pb){ // PASS 1: loop through all characteristics registered as Push Buttons + if(!pb->characteristic->value.BOOL){ // characteristic is off + pb->start=false; // ensure timer is not started + pb->trigger=false; // turn off trigger + } + else if(!pb->start){ // else characteristic is on but timer is not started + pb->start=true; // start timer + pb->alarmTime=millis()+pb->waitTime; // set alarm time + } + else if(millis()>pb->alarmTime){ // else characteristic is on, timer is started, and timer is expired + pb->trigger=true; // set trigger + n++; // increment number of Push Buttons found that need to be turned off + } + pb=pb->next; + } + + if(!n) // nothing to do (either no Push Button characteristics, or none that need to be turned off) + return; + + SpanPut pObj[n]; // use a SpanPut object (for convenience) to load characteristics to be updated + pb=homeSpan.pbHead; // reset Push Button list + n=0; // reset number of PBs found that need to be turned off + + while(pb){ // PASS 2: loop through all characteristics registered as Push Buttons + if(pb->trigger){ // characteristic is triggered + pb->characteristic->value.BOOL=false; // turn off characteristic + pObj[n].status=SC_OK; // populate pObj + pObj[n].characteristic=pb->characteristic; + pObj[n].val=""; // dummy object needed to ensure sprintfNotify knows to consider this "update" + n++; // increment number of Push Buttons found that need to be turned off + } + pb=pb->next; + } + + for(int i=0;i>>>>>>>>> "); + LOG2(hap[i].client.remoteIP()); + LOG2(" >>>>>>>>>>\n"); + LOG2(body); + LOG2(jsonBuf); + LOG2("\n"); + + hap[i].sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t* + + } // if there are characteristic updates to notify + } // 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); + tlv8.print(); + + if(!cPair){ // unverified, unencrypted session + client.print(body); + client.write(tlvData,nBytes); + LOG2("------------ SENT! --------------\n"); + } else { + sendEncrypted(body,tlvData,nBytes); + LOG2("-------- SENT ENCRYPTED! --------\n"); + } + +} // tlvRespond + +////////////////////////////////////// + +int HAPClient::receiveEncrypted(){ + + uint8_t buf[1042]; // maximum size of encoded message = 2+1024+16 bytes (HAP Section 6.5.2) + int nFrames=0; + 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; + + httpBuf[count]=bodyLen%256; // store number of bytes in first frame that encrypts the Body (AAD bytes) + httpBuf[count+1]=bodyLen/256; + + crypto_aead_chacha20poly1305_ietf_encrypt(httpBuf+count+2,&nBytes,(uint8_t *)body,bodyLen,httpBuf+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;iFRAME_SIZE) // maximum number of bytes to encrypt=FRAME_SIZE + n=FRAME_SIZE; + + httpBuf[count]=n%256; // store number of bytes that encrypts this frame (AAD bytes) + httpBuf[count+1]=n/256; + + crypto_aead_chacha20poly1305_ietf_encrypt(httpBuf+count+2,&nBytes,dataBuf+i,n,httpBuf+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(httpBuf,count); // transmit all encrypted frames to Client + +} // sendEncrypted + +///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// + +void HAPClient::hexPrintColumn(uint8_t *buf, int n){ + + char c[16]; + + for(int i=0;i1) + 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;iLTPK,ltpk,32); + slot->admin=admin; + LOG2("\n*** Updated Controller: "); + if(DEBUG_LEVEL>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(DEBUG_LEVEL>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;i1) + charPrintRow(id,36); + LOG2(slot->admin?" (admin)\n":" (regular)\n"); + slot->allocated=false; + + if(nAdminControllers()==0){ // if no more admins, remove all controllers + removeControllers(); + LOG2("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) + } + + LOG2("\n"); + } + +} + +////////////////////////////////////// + +void HAPClient::printControllers(){ + + int n=0; + + for(int i=0;i HAPClient::tlv8; +nvs_handle HAPClient::nvsHandle; +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; + + diff --git a/src/HAP.h b/src/HAP.h new file mode 100644 index 0000000..7c3d6d9 --- /dev/null +++ b/src/HAP.h @@ -0,0 +1,144 @@ + +#include +#include + +#include "TLV.h" +#include "HAPConstants.h" +#include "HKDF.h" +#include "SRP.h" + +///////////////////////////////////////////////// +// NONCE Structure (HAP used last 64 of 96 bits) + +struct Nonce { + uint8_t x[12]; + Nonce(); + void zero(); + uint8_t *get(); + void inc(); +}; + +///////////////////////////////////////////////// +// Paired Controller Structure for Permanently-Stored Data + +struct Controller { + boolean allocated=false; // slot is allocated with Controller data + boolean admin; // Controller has admin privileges + uint8_t ID[36]; // Pairing ID + uint8_t LTPK[32]; // Long Term Ed2519 Public Key +}; + +///////////////////////////////////////////////// +// Accessory Structure for Permanently-Stored Data + +struct Accessory { + uint8_t ID[17]; // Pairing ID in form "XX:XX:XX:XX:XX:XX" + uint8_t LTSK[64]; // secret key for Ed25519 signatures + uint8_t LTPK[32]; // public key for Ed25519 signatures +}; + +///////////////////////////////////////////////// +// HAPClient Structure +// Reads and Writes from each HAP Client connection + +struct HAPClient { + + // common structures and data shared across all HAP Clients + + static const int MAX_HTTP=8095; // max number of bytes in HTTP message buffer + static const int MAX_CONTROLLERS=16; // maximum number of paired controllers (HAP requires at least 16) + + static TLV tlv8; // TLV8 structure (HAP Section 14.1) with space for 10 TLV records of type kTLVType (HAP Table 5-6) + static nvs_handle nvsHandle; // handle for non-volatile-storage of HAP data + static uint8_t httpBuf[MAX_HTTP+1]; // buffer to store HTTP messages (+1 to leave room for storing an extra 'overflow' character) + static HKDF hkdf; // generates (and stores) HKDF-SHA-512 32-byte keys derived from an inputKey of arbitrary length, a salt string, and an info string + static pairState pairStatus; // tracks pair-setup status + static SRP6A srp; // stores all SRP-6A keys used for Pair-Setup + static Accessory accessory; // Accessory ID and Ed25519 public and secret keys- permanently stored + static Controller controllers[MAX_CONTROLLERS]; // Paired Controller IDs and ED25519 long-term public keys - permanently stored + static int conNum; // connection number - used to keep track of per-connection EV notifications + + // individual structures and data defined for each Hap Client connection + + WiFiClient client=NULL; // handle to client + Controller *cPair; // pointer to info on current, session-verified Paired Controller (NULL=un-verified, and therefore un-encrypted, connection) + + // These keys are generated in the first call to pair-verify and used in the second call to pair-verify so must persist for a short period + + uint8_t publicCurveKey[32]; // public key for Curve25519 encryption + uint8_t sharedCurveKey[32]; // Pair-Verfied Shared Secret key derived from Accessory's epehmeral secretCurveKey and Controller's iosCurveKey + uint8_t sessionKey[32]; // shared Session Key (derived with various HKDF calls) + uint8_t iosCurveKey[32]; // Curve25519 public key for associated paired controller + + // CurveKey and CurveKey Nonces are created once each new session is verified in /pair-verify. Keys persist for as long as connection is open + + uint8_t a2cKey[32]; // AccessoryToControllerKey derived from HKDF-SHA-512 of sharedCurveKey (HAP Section 6.5.2) + uint8_t c2aKey[32]; // ControllerToAccessoryKey derived from HKDF-SHA-512 of sharedCurveKey (HAP Section 6.5.2) + Nonce a2cNonce; // encryption nonce (starts at zero at end of each Pair-Verify and increment every encryption - NOT DOCUMENTED) + Nonce c2aNonce; // decryption nonce (starts at zero at end of each Pair-Verify and increment every encryption - NOT DOCUMENTED) + + // define member methods + + void processRequest(); // process HAP request + int postPairSetupURL(); // POST /pair-setup (HAP Section 5.6) + int postPairVerifyURL(); // POST /pair-verify (HAP Section 5.7) + int getAccessoriesURL(); // GET /accessories (HAP Section 6.6) + int postPairingsURL(); // POST /pairings (HAP Sections 5.10-5.12) + int getCharacteristicsURL(char *urlBuf); // GET /characteristics (HAP Section 6.7.4) + int putCharacteristicsURL(char *json); // PUT /characteristics (HAP Section 6.7.2) + + void tlvRespond(); // respond to client with HTTP OK header and all defined TLV data records (those with length>0) + void sendEncrypted(char *body, uint8_t *dataBuf, int dataLen); // send client complete ChaCha20-Poly1305 encrypted HTTP mesage comprising a null-terminated 'body' and 'dataBuf' with 'dataLen' bytes + int receiveEncrypted(); // decrypt HTTP request (HAP Section 6.5) + + int notFoundError(); // return 404 error + int badRequestError(); // return 400 error + int unauthorizedError(); // return 470 error + + // define static methods + + static void init(); // initialize HAP after start-up + + static void hexPrintColumn(uint8_t *buf, int n); // prints 'n' bytes of *buf as HEX, one byte per row. For diagnostics/debugging only + static void hexPrintRow(uint8_t *buf, int n); // prints 'n' bytes of *buf as HEX, all on one row + static void charPrintRow(uint8_t *buf, int n); // prints 'n' bytes of *buf as CHAR, all on one row + + static Controller *findController(uint8_t *id); // returns pointer to controller with mathching ID (or NULL if no match) + static Controller *getFreeController(); // return pointer to next free controller slot (or NULL if no free slots) + static Controller *addController(uint8_t *id, uint8_t *ltpk, boolean admin); // stores data for new Controller with specified data. Returns pointer to Controller slot on success, else NULL + static int nAdminControllers(); // returns number of admin Controllers stored + static void removeControllers(); // removes all Controllers (sets allocated flags to false for all slots) + static void removeController(uint8_t *id); // removes specific Controller. If no remaining admin Controllers, remove all others (if any) as per HAP requirements. + static void printControllers(); // prints IDs of all allocated (paired) Controller + static void checkNotifications(); // checks for notifications and reports to controllers as needed (HAP Section 6.8) + +}; + +///////////////////////////////////////////////// +// Creates a temporary buffer that is freed after +// going out of scope + +template +struct TempBuffer { + bufType *buf; + int nBytes; + + TempBuffer(size_t len){ + nBytes=len*sizeof(bufType); + buf=(bufType *)heap_caps_malloc(nBytes,MALLOC_CAP_8BIT); + } + + ~TempBuffer(){ + heap_caps_free(buf); + } + + int len(){ + return(nBytes); + } + +}; + +///////////////////////////////////////////////// +// Extern Variables + +extern HAPClient hap[]; diff --git a/src/HAPConstants.h b/src/HAPConstants.h new file mode 100644 index 0000000..37aa57b --- /dev/null +++ b/src/HAPConstants.h @@ -0,0 +1,47 @@ + +// HAP TLV Types (HAP Table 5-6) + +typedef enum { + kTLVType_Method=0x00, + kTLVType_Identifier=0x01, + kTLVType_Salt=0x02, + kTLVType_PublicKey=0x03, + kTLVType_Proof=0x04, + kTLVType_EncryptedData=0x05, + kTLVType_State=0x06, + kTLVType_Error=0x07, + kTLVType_RetryDelay=0x08, + kTLVType_Certificate=0x09, + kTLVType_Signature=0x0A, + kTLVType_Permissions=0x0B, + kTLVType_FragmentData=0x0C, + kTLVType_FragmentLast=0x0D, + kTLVType_Flags=0x13, + kTLVType_Separator=0xFF +} kTLVType; + + +// HAP Error Codes (HAP Table 5-5) + +typedef enum { + tagError_Unknown=0x01, + tagError_Authentication=0x02, + tagError_Backoff=0x03, + tagError_MaxPeers=0x04, + tagError_MaxTries=0x05, + tagError_Unavailable=0x06, + tagError_Busy=0x07 +} tagError; + + +// Pair-Setup and Pair-Verify States + +typedef enum { + pairState_M0=0, + pairState_M1=1, + pairState_M2=2, + pairState_M3=3, + pairState_M4=4, + pairState_M5=5, + pairState_M6=6 +} pairState; diff --git a/src/HKDF.cpp b/src/HKDF.cpp new file mode 100644 index 0000000..eb08e55 --- /dev/null +++ b/src/HKDF.cpp @@ -0,0 +1,186 @@ + +#include +#include + +#include "HKDF.h" + +///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// +// Wrapper function to call mbedtls_hkdf, below, with +// HAP-specific parameters and assumptions + +int HKDF::create(uint8_t *outputKey, uint8_t *inputKey, int inputLen, char *salt, char *info){ + + return(mbedtls_hkdf( mbedtls_md_info_from_type(MBEDTLS_MD_SHA512), + (uint8_t *) salt, (size_t) strlen(salt), + inputKey, (size_t) inputLen, + (uint8_t *) info, (size_t) strlen(info), + outputKey, 32 )); + +} + +///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// +// CODE FOR HKDF COPIED FROM MBEDTLS GITHUB SINCE IT IS NOT INCLUDED +// IN STANDARD ARDUIO-ESP32 LIBRARY. + +int mbedtls_hkdf( const mbedtls_md_info_t *md, const unsigned char *salt, + size_t salt_len, const unsigned char *ikm, size_t ikm_len, + const unsigned char *info, size_t info_len, + unsigned char *okm, size_t okm_len ) +{ + int ret; + unsigned char prk[MBEDTLS_MD_MAX_SIZE]; + + ret = mbedtls_hkdf_extract( md, salt, salt_len, ikm, ikm_len, prk ); + + if( ret == 0 ) + { + ret = mbedtls_hkdf_expand( md, prk, mbedtls_md_get_size( md ), + info, info_len, okm, okm_len ); + } + + mbedtls_platform_zeroize( prk, sizeof( prk ) ); + + return( ret ); +} + +int mbedtls_hkdf_extract( const mbedtls_md_info_t *md, + const unsigned char *salt, size_t salt_len, + const unsigned char *ikm, size_t ikm_len, + unsigned char *prk ) +{ + unsigned char null_salt[MBEDTLS_MD_MAX_SIZE] = { '\0' }; + + if( salt == NULL ) + { + size_t hash_len; + + if( salt_len != 0 ) + { + return MBEDTLS_ERR_HKDF_BAD_INPUT_DATA; + } + + hash_len = mbedtls_md_get_size( md ); + + if( hash_len == 0 ) + { + return MBEDTLS_ERR_HKDF_BAD_INPUT_DATA; + } + + salt = null_salt; + salt_len = hash_len; + } + + return( mbedtls_md_hmac( md, salt, salt_len, ikm, ikm_len, prk ) ); +} + +int mbedtls_hkdf_expand( const mbedtls_md_info_t *md, const unsigned char *prk, + size_t prk_len, const unsigned char *info, + size_t info_len, unsigned char *okm, size_t okm_len ) +{ + size_t hash_len; + size_t where = 0; + size_t n; + size_t t_len = 0; + size_t i; + int ret = 0; + mbedtls_md_context_t ctx; + unsigned char t[MBEDTLS_MD_MAX_SIZE]; + + if( okm == NULL ) + { + return( MBEDTLS_ERR_HKDF_BAD_INPUT_DATA ); + } + + hash_len = mbedtls_md_get_size( md ); + + if( prk_len < hash_len || hash_len == 0 ) + { + return( MBEDTLS_ERR_HKDF_BAD_INPUT_DATA ); + } + + if( info == NULL ) + { + info = (const unsigned char *) ""; + info_len = 0; + } + + n = okm_len / hash_len; + + if( okm_len % hash_len != 0 ) + { + n++; + } + + /* + * Per RFC 5869 Section 2.3, okm_len must not exceed + * 255 times the hash length + */ + if( n > 255 ) + { + return( MBEDTLS_ERR_HKDF_BAD_INPUT_DATA ); + } + + mbedtls_md_init( &ctx ); + + if( ( ret = mbedtls_md_setup( &ctx, md, 1 ) ) != 0 ) + { + goto exit; + } + + memset( t, 0, hash_len ); + + /* + * Compute T = T(1) | T(2) | T(3) | ... | T(N) + * Where T(N) is defined in RFC 5869 Section 2.3 + */ + for( i = 1; i <= n; i++ ) + { + size_t num_to_copy; + unsigned char c = i & 0xff; + + ret = mbedtls_md_hmac_starts( &ctx, prk, prk_len ); + if( ret != 0 ) + { + goto exit; + } + + ret = mbedtls_md_hmac_update( &ctx, t, t_len ); + if( ret != 0 ) + { + goto exit; + } + + ret = mbedtls_md_hmac_update( &ctx, info, info_len ); + if( ret != 0 ) + { + goto exit; + } + + /* The constant concatenated to the end of each T(n) is a single octet. + * */ + ret = mbedtls_md_hmac_update( &ctx, &c, 1 ); + if( ret != 0 ) + { + goto exit; + } + + ret = mbedtls_md_hmac_finish( &ctx, t ); + if( ret != 0 ) + { + goto exit; + } + + num_to_copy = i != n ? hash_len : okm_len - where; + memcpy( okm + where, t, num_to_copy ); + where += hash_len; + t_len = hash_len; + } + + exit: + mbedtls_md_free( &ctx ); + mbedtls_platform_zeroize( t, sizeof( t ) ); + + return( ret ); +} diff --git a/src/HKDF.h b/src/HKDF.h new file mode 100644 index 0000000..7a5335e --- /dev/null +++ b/src/HKDF.h @@ -0,0 +1,15 @@ + +#include + +///////////////////////////////////////////////// +// HKDF-SHA-512 Structure +// +// This is a wrapper around mbedtls_hkdf, which is NOT +// included in the normal Arduino-ESP32 library. +// Code was instead downloaded from MBED GitHub directly and +// incorporated under hkdf.cpp, with a wrapper to always +// use SHA-512 with 32 bytes of output as required by HAP. + +struct HKDF { + int create(uint8_t *outputKey, uint8_t *inputKey, int inputLen, char *salt, char *info); // output of HKDF is always a 32-byte key derived from an input key, a salt string, and an info string +}; diff --git a/src/HomeSpan.cpp b/src/HomeSpan.cpp new file mode 100644 index 0000000..8a83488 --- /dev/null +++ b/src/HomeSpan.cpp @@ -0,0 +1,1057 @@ + +#include +#include +#include + +#include "Utils.h" +#include "HAP.h" +#include "HomeSpan.h" + +using namespace Utils; + +WiFiServer hapServer(80); // HTTP Server (i.e. this acccesory) running on usual port 80 (local-scoped variable to this file only) + +HAPClient hap[MAX_CONNECTIONS]; // HAP Client structure containing HTTP client connections, parsing routines, and state variables (global-scoped variable) +Span homeSpan; // HAP Attributes database and all related control functions for this Accessory (global-scoped variable) + +/////////////////////////////// +// Span // +/////////////////////////////// + +void Span::begin(Category catID, char *displayName, char *hostNameBase, char *modelName){ + + this->displayName=displayName; + this->hostNameBase=hostNameBase; + this->modelName=modelName; + sprintf(this->category,"%d",catID); + + pinMode(LED_BUILTIN,OUTPUT); + pinMode(resetPin,INPUT_PULLUP); + + delay(2000); + + Serial.print("\n************************************************************\n" + "Welcome to HomeSpan!\n" + "Apple HomeKit for the Espressif ESP-32 WROOM and Arduino IDE\n" + "************************************************************\n\n" + "** Please ensure serial monitor is set to transmit \n"); + + Serial.print("** Ground pin "); + Serial.print(resetPin); + Serial.print(" to delete all stored WiFi Network and HomeKit Pairing data (factory reset)\n\n"); + + if(!digitalRead(resetPin)){ // factory reset pin is low + nvs_flash_erase(); // erase NVS storage + Serial.print("** FACTORY RESET PIN LOW! ALL STORED DATA ERASED **\n** PROGRAM HALTED **\n"); + while(1){ + digitalWrite(LED_BUILTIN,HIGH); + delay(100); + digitalWrite(LED_BUILTIN,LOW); + delay(500); + } + } + +} // begin + +/////////////////////////////// + +void Span::poll() { + + if(!strlen(category)){ + Serial.print("\n** FATAL ERROR: Cannot run homeSpan.poll() without an initial call to homeSpan.begin()!\n** PROGRAM HALTED **\n\n"); + while(1); + + } else if(WiFi.status()!=WL_CONNECTED){ + + nvs_flash_init(); // initialize non-volatile-storage partition in flash + HAPClient::init(); // read NVS and load HAP settings + initWifi(); // initialize WiFi + + if(!HAPClient::nAdminControllers()) + Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n"); + + Serial.print(displayName); + Serial.print(" is READY!\n\n"); + } + + char cBuf[8]="?"; + + if(Serial.available()){ + readSerial(cBuf,1); + processSerialCommand(cBuf); + } + + WiFiClient newClient; + + if(newClient=hapServer.available()){ // found a new HTTP client + int freeSlot=getFreeSlot(); // get next free slot + + if(freeSlot==-1){ // no available free slots + freeSlot=randombytes_uniform(MAX_CONNECTIONS); + LOG2("=======================================\n"); + LOG1("** Freeing Client #"); + LOG1(freeSlot); + LOG1(" ("); + LOG1(millis()/1000); + LOG1(" sec) "); + LOG1(hap[freeSlot].client.remoteIP()); + LOG1("\n"); + hap[freeSlot].client.stop(); // disconnect client from first slot and re-use + } + + hap[freeSlot].client=newClient; // copy new client handle into free slot + + LOG2("=======================================\n"); + LOG1("** Client #"); + LOG1(freeSlot); + LOG1(" Connected: ("); + LOG1(millis()/1000); + LOG1(" sec) "); + LOG1(hap[freeSlot].client.remoteIP()); + LOG1("\n"); + LOG2("\n"); + + hap[freeSlot].cPair=NULL; // reset pointer to verified ID + homeSpan.clearNotify(freeSlot); // clear all notification requests for this connection + HAPClient::pairStatus=pairState_M1; // reset starting PAIR STATE (which may be needed if Accessory failed in middle of pair-setup) + } + + for(int i=0;i>> WiFi SSID ("); + Serial.print(wifiData.ssid); + Serial.print("): "); + readSerial(wifiData.ssid,MAX_SSID); + Serial.print(wifiData.ssid); + + Serial.print("\n>>> WiFi Password ("); + Serial.print(wifiData.pwd); + Serial.print("): "); + readSerial(wifiData.pwd,MAX_PWD); + Serial.print(mask(wifiData.pwd,2)); + Serial.print("\n\n"); + + nvs_set_blob(wifiHandle,"WIFIDATA",&wifiData,sizeof(wifiData)); // update data + nvs_commit(wifiHandle); // commit to NVS + } + + char id[18]; // create string version of Accessory ID for MDNS broadcast + memcpy(id,HAPClient::accessory.ID,17); // copy ID bytes + id[17]='\0'; // add terminating null + + // create broadcaset name from server base name plus accessory ID (with ':' replaced by '_') + + int nChars=snprintf(NULL,0,"%s-%.2s_%.2s_%.2s_%.2s_%.2s_%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15); + char hostName[nChars+1]; + sprintf(hostName,"%s-%.2s_%.2s_%.2s_%.2s_%.2s_%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15); + + int nTries=0; + + while(WiFi.status()!=WL_CONNECTED){ + Serial.print("Connecting to: "); + Serial.print(wifiData.ssid); + Serial.print("... "); + nTries++; + + if(WiFi.begin(wifiData.ssid,wifiData.pwd)!=WL_CONNECTED){ + int delayTime=nTries%6?5000:60000; + int blinkTime=nTries%6?500:1000; + char buf[8]=""; + Serial.print("Can't connect. Re-trying in "); + Serial.print(delayTime/1000); + Serial.print(" seconds (or type 'W ' to reset WiFi data)...\n"); + long sTime=millis(); + while(millis()-sTime qBuf(sprintfAttributes(NULL)+1); + sprintfAttributes(qBuf.buf); + + Serial.print("\n*** Attributes Database: size="); + Serial.print(qBuf.len()-1); + Serial.print(" configuration="); + Serial.print(hapConfig.configNumber); + Serial.print(" ***\n\n"); + prettyPrint(qBuf.buf); + Serial.print("\n*** End Database ***\n\n"); + } + break; + + case 'W': { + nvs_handle wifiHandle; + nvs_open("WIFI",NVS_READWRITE,&wifiHandle); // open WIFI data namespace in NVS + nvs_erase_all(wifiHandle); + nvs_commit(wifiHandle); + Serial.print("\n** WIFI Network Data DELETED **\n** Restarting...\n\n"); + delay(2000); + ESP.restart(); + } + break; + + case 'H': { + nvs_erase_all(HAPClient::nvsHandle); + nvs_commit(HAPClient::nvsHandle); + Serial.print("\n** HomeKit Pairing Data DELETED **\n** Restarting...\n\n"); + delay(2000); + ESP.restart(); + } + break; + + case 'F': { + nvs_flash_erase(); + Serial.print("\n** FACTORY RESET **\n** Restarting...\n\n"); + delay(2000); + ESP.restart(); + } + break; + + case '?': { + Serial.print("\n*** HomeSpan Commands ***\n\n"); + Serial.print(" s - print connection status\n"); + Serial.print(" d - print attributes database\n"); + Serial.print(" W - delete stored WiFi data and restart\n"); + Serial.print(" H - delete stored HomeKit Pairing data and restart\n"); + Serial.print(" F - delete all stored WiFi Network and HomeKit Pairing data and restart\n"); + Serial.print(" ? - print this list of commands\n"); + Serial.print("\n*** End Commands ***\n\n"); + } + break; + + default: + Serial.print("** Unknown command: '"); + Serial.print(c); + Serial.print("' - type '?' for list of commands.\n"); + + break; + + } // switch +} + +/////////////////////////////// + +int Span::sprintfAttributes(char *cBuf){ + + int nBytes=0; + + nBytes+=snprintf(cBuf,cBuf?64:0,"{\"accessories\":["); + + for(int i=0;isprintfAttributes(cBuf?(cBuf+nBytes):NULL); + if(i+1Accessories.size()) // aid out of range + return(NULL); + + aid--; // convert from aid to array index number + + for(int i=0;iServices.size();i++){ // loop over all Services in this Accessory + for(int j=0;jServices[i]->Characteristics.size();j++){ // loop over all Characteristics in this Service + + if(iid == Accessories[aid]->Services[i]->Characteristics[j]->iid) // if matching iid + return(Accessories[aid]->Services[i]->Characteristics[j]); // return pointer to Characteristic + } + } + + return(NULL); +} + +/////////////////////////////// + +int Span::countCharacteristics(char *buf){ + + int nObj=0; + + const char tag[]="\"aid\""; + while(buf=strstr(buf,tag)){ // count number of characteristic objects in PUT JSON request + nObj++; + buf+=strlen(tag); + } + + return(nObj); +} + +/////////////////////////////// + +int Span::updateCharacteristics(char *buf, SpanPut *pObj){ + + int nObj=0; + char *p1; + int cFound=0; + + while(char *t1=strtok_r(buf,"{",&p1)){ // parse 'buf' and extract objects into 'pObj' unless NULL + buf=NULL; + char *p2; + int okay=0; + + while(char *t2=strtok_r(t1,"}[]:, \"\t\n\r",&p2)){ + + if(!cFound){ // first token found + if(strcmp(t2,"characteristics")){ + Serial.print("\n*** ERROR: Problems parsing JSON - initial \"characteristics\" tag not found\n\n"); + return(0); + } + cFound=1; + break; + } + + t1=NULL; + char *t3; + if(!strcmp(t2,"aid") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){ + pObj[nObj].aid=atoi(t3); + okay|=1; + } else + if(!strcmp(t2,"iid") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){ + pObj[nObj].iid=atoi(t3); + okay|=2; + } else + if(!strcmp(t2,"value") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){ + pObj[nObj].val=t3; + okay|=4; + } else + if(!strcmp(t2,"ev") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){ + pObj[nObj].ev=t3; + okay|=8; + } else { + Serial.print("\n*** ERROR: Problems parsing JSON characteristics object - unexpected property \""); + Serial.print(t2); + Serial.print("\"\n\n"); + return(0); + } + } // parse property tokens + + if(!t1){ // at least one token was found that was not initial "characteristics" + if(okay==7 || okay==11 || okay==15){ // all required properties found + nObj++; // increment number of characteristic objects found + } else { + Serial.print("\n*** ERROR: Problems parsing JSON characteristics object - missing required properties\n\n"); + return(0); + } + } + + } // parse objects + + for(int i=0;iloadUpdate(pObj[i].val,pObj[i].ev); // save status code, which is either an error, or SC_TBD (in which case isUpdated for the characteristic has been set to true) + else + pObj[i].status=SC_UnknownResource; // if not found, set HAP error + + } // first pass + + for(int i=0;iservice; // set service containing the characteristic underlying the object + statusCode status=svc->update(); // update service and save returned statusCode + + for(int j=i;jservice==svc){ // if service matches + pObj[j].status=status; // save statusCode for this object + LOG1("Updating aid="); + LOG1(svc->Characteristics[j]->aid); + LOG1(" iid="); + LOG1(svc->Characteristics[j]->iid); + if(status==SC_OK){ // if status is okay + pObj[j].characteristic->value + =pObj[j].characteristic->newValue; // update characteristic value with new value + LOG1(" (okay)\n"); + } else { // if status not okay + pObj[j].characteristic->newValue + =pObj[j].characteristic->value; // replace characteristic new value with original value + LOG1(" (failed)\n"); + } + pObj[j].characteristic->isUpdated=false; // reset isUpdated flag for characteristic + } + } + + } // object had SC_TBD status + } // loop over all objects + + return(1); +} + +/////////////////////////////// + +void Span::clearNotify(int slotNum){ + + for(int i=0;iServices.size();j++){ + for(int k=0;kServices[j]->Characteristics.size();k++){ + Accessories[i]->Services[j]->Characteristics[k]->ev[slotNum]=false; + } + } + } +} + +/////////////////////////////// + +int Span::sprintfNotify(SpanPut *pObj, int nObj, char *cBuf, int conNum, int &numNotify){ + + int nChars=0; + + nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":["); + + for(int i=0;iev[conNum]){ // if notifications requested for this characteristic by specified connection number + + if(numNotify>0) // already printed at least one other characteristic + nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,","); + + nChars+=pObj[i].characteristic->sprintfAttributes(cBuf?(cBuf+nChars):NULL,GET_AID); // get JSON attributes for characteristic + numNotify++; + + } // notification requested + } // characteristic updated + } // loop over all objects + + nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"]}"); + + return(nChars); +} + +/////////////////////////////// + +int Span::sprintfAttributes(SpanPut *pObj, int nObj, char *cBuf){ + + int nChars=0; + + nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":["); + + for(int i=0;iperms&SpanCharacteristic::PR){ // if permissions allow reading + status[i]=0; + } else { + Characteristics[i]=NULL; + status[i]=SC_WriteOnly; + sFlag=true; + } + } else { + status[i]=SC_UnknownResource; + sFlag=true; + } + } + + nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":["); + + for(int i=0;isprintfAttributes(cBuf?(cBuf+nChars):NULL,flags); // get JSON attributes for characteristic + else{ + sscanf(ids[i],"%d.%d",&aid,&iid); // parse aid and iid + nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"{\"iid\":%d,\"aid\":%d}",iid,aid); // else create JSON attributes based on requested aid/iid + } + + if(sFlag){ // status flag is needed - overlay at end + nChars--; + nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,",\"status\":%d}",status[i]); + } + + if(i+1sprintfAttributes(cBuf?(cBuf+nBytes):NULL); + if(i+1type=type; + hidden=(mod==ServiceType::Hidden); + primary=(mod==ServiceType::Primary); + + if(homeSpan.Accessories.empty()){ + Serial.print("*** FATAL ERROR: Can't create new Service without a defined Accessory. Program halted!\n\n"); + while(1); + } + + homeSpan.Accessories.back()->Services.push_back(this); + iid=++(homeSpan.Accessories.back()->iidCount); + +} + +/////////////////////////////// + +int SpanService::sprintfAttributes(char *cBuf){ + int nBytes=0; + + nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d,\"type\":\"%s\",",iid,type); + + if(hidden) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"hidden\":true,"); + + if(primary) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"primary\":true,"); + + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"characteristics\":["); + + for(int i=0;isprintfAttributes(cBuf?(cBuf+nBytes):NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC); + if(i+1type=type; + this->perms=perms; + + if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){ + Serial.print("*** FATAL ERROR: Can't create new Characteristic without a defined Service. Program halted!\n\n"); + while(1); + } + + homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this); + iid=++(homeSpan.Accessories.back()->iidCount); + service=homeSpan.Accessories.back()->Services.back(); + aid=homeSpan.Accessories.back()->aid; +} + + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, boolean value) : SpanCharacteristic(type, perms) { + this->format=BOOL; + this->value.BOOL=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, int32_t value) : SpanCharacteristic(type, perms) { + this->format=INT; + this->value.INT=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint8_t value) : SpanCharacteristic(type, perms) { + this->format=UINT8; + this->value.UINT8=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint16_t value) : SpanCharacteristic(type, perms) { + this->format=UINT16; + this->value.UINT16=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint32_t value) : SpanCharacteristic(type, perms) { + this->format=UINT32; + this->value.UINT32=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint64_t value) : SpanCharacteristic(type, perms) { + this->format=UINT64; + this->value.UINT64=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, double value) : SpanCharacteristic(type, perms) { + this->format=FLOAT; + this->value.FLOAT=value; +} + +/////////////////////////////// + +SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, const char* value) : SpanCharacteristic(type, perms) { + this->format=STRING; + this->value.STRING=value; +} + +/////////////////////////////// + +int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){ + int nBytes=0; + + const char permCodes[][7]={"pr","pw","ev","aa","tw","hd","wr"}; + + nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d",iid); + + if(flags&GET_TYPE) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"type\":\"%s\"",type); + + switch(format){ + + case BOOL: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%s",value.BOOL?"true":"false"); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"bool\""); + break; + + case INT: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%d",value.INT); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"int\""); + break; + + case UINT8: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%u",value.UINT8); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint8\""); + break; + + case UINT16: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%u",value.UINT16); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint16\""); + break; + + case UINT32: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%lu",value.UINT32); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint32\""); + break; + + case UINT64: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%llu",value.UINT64); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint64\""); + break; + + case FLOAT: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%lg",value.FLOAT); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"float\""); + break; + + case STRING: + if(perms&PR) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":\"%s\"",value.STRING); + if(flags&GET_META) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"string\""); + break; + + } // switch + + if(range && (flags&GET_META)){ + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"minValue\":%d,\"maxValue\":%d,\"minStep\":%d",range->min,range->max,range->step); + } + + if(desc && (flags&GET_DESC)){ + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"description\":\"%s\"",desc); + } + + if(flags&GET_PERMS){ + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"perms\":["); + for(int i=0;i<7;i++){ + if(perms&(1<=(1<<(i+1))) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,","); + } + } + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"]"); + } + + if(flags&GET_AID) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"aid\":%d",aid); + + if(flags&GET_EV) + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"ev\":%s",ev[HAPClient::conNum]?"true":"false"); + + nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"}"); + + return(nBytes); +} + +/////////////////////////////// + +statusCode SpanCharacteristic::loadUpdate(char *val, char *ev){ + + if(ev){ // request for notification + boolean evFlag; + + if(!strcmp(ev,"0") || !strcmp(ev,"false")) + evFlag=false; + else if(!strcmp(ev,"1") || !strcmp(ev,"true")) + evFlag=true; + else + return(SC_InvalidValue); + + if(evFlag && !(perms&EV)) // notification is not supported for characteristic + return(SC_NotifyNotAllowed); + + LOG1("Notification Request for aid="); + LOG1(aid); + LOG1(" iid="); + LOG1(iid); + LOG1(": "); + LOG1(evFlag?"true":"false"); + LOG1("\n"); + this->ev[HAPClient::conNum]=evFlag; + } + + if(!val) // no request to update value + return(SC_OK); + + if(!(perms&PW)) // cannot write to read only characteristic + return(SC_ReadOnly); + + switch(format){ + + case BOOL: + if(!strcmp(val,"0") || !strcmp(val,"false")) + newValue.BOOL=false; + else if(!strcmp(val,"1") || !strcmp(val,"true")) + newValue.BOOL=true; + else + return(SC_InvalidValue); + break; + + case INT: + if(!sscanf(val,"%d",&newValue.INT)) + return(SC_InvalidValue); + break; + + case UINT8: + if(!sscanf(val,"%u",&newValue.UINT8)) + return(SC_InvalidValue); + break; + + case UINT16: + if(!sscanf(val,"%u",&newValue.UINT16)) + return(SC_InvalidValue); + break; + + case UINT32: + if(!sscanf(val,"%llu",&newValue.UINT32)) + return(SC_InvalidValue); + break; + + case UINT64: + if(!sscanf(val,"%llu",&newValue.UINT64)) + return(SC_InvalidValue); + break; + + case FLOAT: + if(!sscanf(val,"%lg",&newValue.FLOAT)) + return(SC_InvalidValue); + break; + + } // switch + + isUpdated=true; + return(SC_TBD); +} + +/////////////////////////////// + +void SpanCharacteristic::autoOff(int waitTime){ + + SpanPBList **pb=&homeSpan.pbHead; + + while(*pb) // traverse list until end + pb=&((*pb)->next); + + *pb=new SpanPBList; + (*pb)->characteristic=this; + (*pb)->waitTime=waitTime; +} + +////////////////////////////////////// diff --git a/src/HomeSpan.h b/src/HomeSpan.h new file mode 100644 index 0000000..d49e112 --- /dev/null +++ b/src/HomeSpan.h @@ -0,0 +1,222 @@ + +#include + +#include "Settings.h" + +using std::vector; + +enum statusCode { // HAP Table 6-11 + SC_OK=0, + SC_Unable=-70402, + SC_Busy=-70403, + SC_ReadOnly=-70404, + SC_WriteOnly=-70405, + SC_NotifyNotAllowed=-70406, + SC_UnknownResource=-70409, + SC_InvalidValue=-70410, + SC_TBD=-1 // status To-Be-Determined (TBD) once service.update() called +}; + +enum { + GET_AID=1, + GET_META=2, + GET_PERMS=4, + GET_TYPE=8, + GET_EV=16, + GET_DESC=32, + GET_ALL=255 +}; + +// Forward-Declarations + +struct Span; +struct SpanAccessory; +struct SpanService; +struct SpanCharacteristic; +struct SpanRange; +struct SpanPut; +struct SpanPBList; + +/////////////////////////////// + +struct SpanConfig { + int configNumber=0; // configuration number - broadcast as Bonjour "c#" (computed automatically) + uint8_t hashCode[48]={0}; // SHA-384 hash of Span Database stored as a form of unique "signature" to know when to update the config number upon changes +}; + +/////////////////////////////// + +struct Span{ + + char *displayName; // display name for this device - broadcast as part of Bonjour MDNS + char *hostNameBase; // base of host name of this device - full host name broadcast by Bonjour MDNS will have 6-byte accessoryID as well as '.local' automatically appended + char *modelName; // model name of this device - broadcast as Bonjour field "md" + char category[3]=""; // category ID of primary accessory - broadcast as Bonjour field "ci" (HAP Section 13) + + int resetPin=21; // drive this pin low to "factory" reset NVS data on start-up + + SpanPBList *pbHead=NULL; // head of linked-list of characteristics to auto-turnoff after they are turned on (to emulate a single-shot PushButton) + SpanConfig hapConfig; // track configuration changes to the HAP Accessory database; used to increment the configuration number (c#) when changes found + vector Accessories; // vector of pointers to all Accessories + + void begin(Category catID, + char *displayName="HomeSpan Server", + char *hostNameBase="homespan", + char *modelName="HS-ESP32"); + + void poll(); // poll HAP Clients and process any new HAP requests + int getFreeSlot(); // returns free HAPClient slot number. HAPClients slot keep track of each active HAPClient connection + void initWifi(); // initialize and connect to WiFi network + void processSerialCommand(char *c); // process command 'c' (typically from readSerial, though can be called with any 'c') + + int sprintfAttributes(char *cBuf); // prints Attributes JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + void prettyPrint(char *buf, int nsp=2); // print arbitrary JSON from buf to serial monitor, formatted with indentions of 'nsp' spaces + SpanCharacteristic *find(int aid, int iid); // return Characteristic with matching aid and iid (else NULL if not found) + + int countCharacteristics(char *buf); // return number of characteristic objects referenced in PUT /characteristics JSON request + int updateCharacteristics(char *buf, SpanPut *pObj); // parses PUT /characteristics JSON request 'buf into 'pObj' and updates referenced characteristics; returns 1 on success, 0 on fail + int sprintfAttributes(SpanPut *pObj, int nObj, char *cBuf); // prints SpanPut object into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + int sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf); // prints accessory.characteristic ids into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + + void clearNotify(int slotNum); // set ev notification flags for connection 'slotNum' to false across all characteristics + int sprintfNotify(SpanPut *pObj, int nObj, char *cBuf, int conNum, int &numNotify); // prints notification JSON into buf based on SpanPut objects and specified connection number + + void setResetPin(int pin){resetPin=pin;} // sets new pin to be used for factory reset + +}; + +/////////////////////////////// + +struct SpanAccessory{ + + int aid=0; // Accessory Instance ID (HAP Table 6-1) + int iidCount=0; // running count of iid to use for Services and Characteristics associated with this Accessory + vector Services; // vector of pointers to all Services in this Accessory + + SpanAccessory(); + + int sprintfAttributes(char *cBuf); // prints Accessory JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL +}; + +/////////////////////////////// + +struct SpanService{ + + int iid=0; // Instance ID (HAP Table 6-2) + const char *type; // Service Type + boolean hidden=false; // optional property indicating service is hidden + boolean primary=false; // optional property indicating service is primary + vector Characteristics; // vector of pointers to all Characteristics in this Service + + SpanService(const char *type, ServiceType mod=ServiceType::Regular); + + int sprintfAttributes(char *cBuf); // prints Service JSON records into buf; return number of characters printed, excluding null terminator + virtual statusCode update() {return(SC_OK);} // update Service and return final statusCode based on updated Characteristics - should be overridden by DEVICE-SPECIFIC Services +}; + +/////////////////////////////// + +struct SpanCharacteristic{ + + enum { // create bitflags based on HAP Table 6-4 + PR=1, + PW=2, + EV=4, + AA=8, + TW=16, + HD=32, + WR=64 + }; + + enum FORMAT { // HAP Table 6-5 + BOOL, + UINT8, + UINT16, + UINT32, + UINT64, + INT, + FLOAT, + STRING + }; + + union UVal { + boolean BOOL; + uint8_t UINT8; + uint16_t UINT16; + uint32_t UINT32; + uint64_t UINT64; + int32_t INT; + double FLOAT; + const char *STRING; + }; + + int iid=0; // Instance ID (HAP Table 6-3) + char *type; // Characteristic Type + UVal value; // Characteristic Value + uint8_t perms; // Characteristic Permissions + FORMAT format; // Characteristic Format + char *desc=NULL; // Characteristic Description (optional) + SpanRange *range=NULL; // Characteristic min/max/step; NULL = default values (optional) + boolean ev[MAX_CONNECTIONS]={false}; // Characteristic Event Notify Enable (per-connection) + + int aid=0; // Accessory ID - passed through from Service containing this Characteristic + boolean isUpdated=false; // set to true when new value has been requested by PUT /characteristic + UVal newValue; // the updated value requested by PUT /characteristic + SpanService *service=NULL; // pointer to Service containing this Characteristic + + SpanCharacteristic(char *type, uint8_t perms); + SpanCharacteristic(char *type, uint8_t perms, boolean value); + SpanCharacteristic(char *type, uint8_t perms, uint8_t value); + SpanCharacteristic(char *type, uint8_t perms, uint16_t value); + SpanCharacteristic(char *type, uint8_t perms, uint32_t value); + SpanCharacteristic(char *type, uint8_t perms, uint64_t value); + SpanCharacteristic(char *type, uint8_t perms, int32_t value); + SpanCharacteristic(char *type, uint8_t perms, double value); + SpanCharacteristic(char *type, uint8_t perms, const char* value); + + int sprintfAttributes(char *cBuf, int flags); // prints Characteristic JSON records into buf, according to flags mask; return number of characters printed, excluding null terminator + statusCode loadUpdate(char *val, char *ev); // load updated val/ev from PUT /characteristic JSON request. Return intiial HAP status code (checks to see if characteristic is found, is writable, etc.) + void autoOff(int waitTime=250); // turns Characteristic off (false) automatically after waitTime milliseconds; only applicable to BOOL characteristics +}; + +/////////////////////////////// + +struct SpanRange{ + int min; + int max; + int step; + + SpanRange(int _min, int _max, int _step) : min{_min}, max{_max}, step{_step} {}; +}; + +/////////////////////////////// + +struct SpanPut{ // storage to process PUT /characteristics request + int aid; // aid to update + int iid; // iid to update + char *val=NULL; // updated value (optional, though either at least 'val' or 'ev' must be specified) + char *ev=NULL; // updated event notification flag (optional, though either at least 'val' or 'ev' must be specified) + statusCode status; // return status (HAP Table 6-11) + SpanCharacteristic *characteristic=NULL; // Characteristic to update (NULL if not found) +}; + +/////////////////////////////// + +struct SpanPBList{ + SpanPBList *next=NULL; // next item in linked-list + SpanCharacteristic *characteristic; // characteristic to auto-turnoff whenever activated + int waitTime; // time to wait until auto-turnoff (in milliseconds) + unsigned long alarmTime; // alarm time for trigger to auto-turnoff + boolean start=false; // alarm timer started + boolean trigger=false; // alarm timer triggered +}; + + +///////////////////////////////////////////////// +// Extern Variables + +extern Span homeSpan; + +///////////////////////////////////////////////// + +#include "Services.h" diff --git a/src/RFControl.cpp b/src/RFControl.cpp new file mode 100644 index 0000000..77f5453 --- /dev/null +++ b/src/RFControl.cpp @@ -0,0 +1,83 @@ + +#include +#include +#include + +#include "RFControl.h" + +/////////////////// + +RFControl::RFControl(int pin){ + if(!configured){ // configure RMT peripheral + + DPORT_REG_SET_BIT(DPORT_PERIP_CLK_EN_REG,1<<9); // enable RMT clock by setting bit 9 + DPORT_REG_CLR_BIT(DPORT_PERIP_RST_EN_REG,1<<9); // set RMT to normal ("un-reset") mode by clearing bit 9 + REG_SET_BIT(RMT_APB_CONF_REG,3); // enables access to RMT memory and enables wraparound mode (though the latter does not seem to be needed to set continuous TX) + REG_WRITE(RMT_INT_ENA_REG,1<pin=pin; + + pinMode(pin,OUTPUT); + REG_WRITE(GPIO_FUNC0_OUT_SEL_CFG_REG+4*pin,87); // set GPIO OUTPUT of pin in GPIO_MATRIX to use RMT Channel-0 Output (=signal 87) + REG_SET_FIELD(GPIO_FUNC0_OUT_SEL_CFG_REG+4*pin,GPIO_FUNC0_OEN_SEL,1); // use GPIO_ENABLE_REG of pin (not RMT) to enable this channel + REG_WRITE(GPIO_ENABLE_W1TC_REG,1<32767 || onTime>32767){ + Serial.println("\n*** ERROR: Request to add RF Control pulse with ON or OFF time exceeds 32767 maximum allowed number of ticks\n\n"); + } else { + + pRMT[pCount++]=(offTime<<16)+onTime+(1<<15); // load pulse information into RMT memory and increment pointer + } +} + +/////////////////// + +void RFControl::eot_int(void *arg){ + numCycles--; + REG_WRITE(RMT_INT_CLR_REG,~0); // interrupt MUST be cleared first; transmission re-started after (clearing after restart crestes havoc) + if(numCycles) + REG_WRITE(RMT_CH0CONF1_REG,0x0000000D); // use REF_TICK clock; reset xmit and receive memory address to start of channel; re-start transmission +} + +/////////////////// + +boolean RFControl::configured=false; +volatile int RFControl::numCycles; +uint32_t *RFControl::pRMT=(uint32_t *)RMT_CHANNEL_MEM(0); +int RFControl::pCount=0; + +RFControl RF433(RF433_PIN); +RFControl RF315(RF315_PIN); diff --git a/src/RFControl.h b/src/RFControl.h new file mode 100644 index 0000000..3449277 --- /dev/null +++ b/src/RFControl.h @@ -0,0 +1,29 @@ + +//////////////////////////////////// +// RF Control Module // +//////////////////////////////////// + +#define RF433_PIN 22 // pin used for 433MHz transmitter +#define RF315_PIN 23 // pin used for 315MHz transmitter + +class RFControl { + private: + int pin; + static volatile int numCycles; + static boolean configured; + static uint32_t *pRMT; + static int pCount; + static void eot_int(void *arg); + + public: + RFControl(int pin); // creates transmitter on pin + static void clear(); // clears transmitter memory + static void add(uint16_t onTime, uint16_t offTime); // adds pulse of onTime ticks HIGH followed by offTime ticks LOW + void start(int _numCycles, int tickTime=1); // starts transmission of pulses, repeated for numCycles, where each tick in pulse is tickTime microseconds long +}; + +// Two transmitters are defined + +extern RFControl RF433; +extern RFControl RF315; + diff --git a/src/SRP.cpp b/src/SRP.cpp new file mode 100644 index 0000000..605aacd --- /dev/null +++ b/src/SRP.cpp @@ -0,0 +1,266 @@ + +#include + +#include "HAP.h" + +///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// + +SRP6A::SRP6A(){ + + uint8_t tBuf[768]; // temporary buffer for staging + uint8_t tHash[64]; // temporary buffer for storing SHA-512 results + + char N3072[]="FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74" + "020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437" + "4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05" + "98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB" + "9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33" + "A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864" + "D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2" + "08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"; + + // initialize MPI structures + + mbedtls_mpi_init(&N); + mbedtls_mpi_init(&g); + mbedtls_mpi_init(&s); + mbedtls_mpi_init(&x); + mbedtls_mpi_init(&v); + mbedtls_mpi_init(&A); + mbedtls_mpi_init(&b); + mbedtls_mpi_init(&B); + mbedtls_mpi_init(&S); + mbedtls_mpi_init(&k); + mbedtls_mpi_init(&u); + mbedtls_mpi_init(&K); + mbedtls_mpi_init(&M1); + mbedtls_mpi_init(&M1V); + mbedtls_mpi_init(&M2); + mbedtls_mpi_init(&_rr); + mbedtls_mpi_init(&t1); + mbedtls_mpi_init(&t2); + mbedtls_mpi_init(&t3); + + // load N and g into mpi structures + + mbedtls_mpi_read_string(&N,16,N3072); + mbedtls_mpi_lset(&g,5); + + // compute k = SHA512( N | PAD(g) ) + + mbedtls_mpi_write_binary(&N,tBuf,384); // write N into first half of staging buffer + mbedtls_mpi_write_binary(&g,tBuf+384,384); // write g into second half of staging buffer (fully padded with leading zeros) + mbedtls_sha512_ret(tBuf,768,tHash,0); // create hash of data + mbedtls_mpi_read_binary(&k,tHash,64); // load hash result into mpi structure k + +} + +////////////////////////////////////// + +void SRP6A::createPublicKey(){ + + uint8_t tBuf[80]; // temporary buffer for staging + uint8_t tHash[64]; // temporary buffer for storing SHA-512 results + char icp[22]; // storage for I:P + + getSalt(); // create and load s (random 16 bytes) + getPrivateKey(); // create and load b (random 32 bytes) + getSetupCode(icp); // I="Pair-Setup" and P=Pair-Setup Code (in form XXX-XX-XXX) + + // compute x = SHA512( s | SHA512( I | ":" | P ) ) + + mbedtls_mpi_write_binary(&s,tBuf,16); // write s into first 16 bytes of staging buffer + mbedtls_sha512_ret((uint8_t *)icp,strlen(icp),tBuf+16,0); // create hash of username:password and write into last 64 bytes of staging buffer + mbedtls_sha512_ret(tBuf,80,tHash,0); // create second hash of salted, hashed username:password + mbedtls_mpi_read_binary(&x,tHash,64); // load hash result into mpi structure x + + // compute v = g^x % N + + mbedtls_mpi_exp_mod(&v,&g,&x,&N,&_rr); // create verifier, v (_rr is an internal "helper" structure that mbedtls uses to speed up subsequent exponential calculations) + + // compute B = kv + g^b %N + + mbedtls_mpi_mul_mpi(&t1,&k,&v); // t1 = k*v + mbedtls_mpi_exp_mod(&t2,&g,&b,&N,&_rr); // t2 = g^b %N + mbedtls_mpi_add_mpi(&t3,&t1,&t2); // t3 = t1 + t2 + mbedtls_mpi_mod_mpi(&B,&t3,&N); // B = t3 %N = ACCESSORY PUBLIC KEY + +} + +////////////////////////////////////// + +void SRP6A::getSalt(){ + + uint8_t salt[16]; + randombytes_buf(salt,16); // generate 16 random bytes using libsodium (which uses the ESP32 hardware-based random number generator) + + mbedtls_mpi_read_binary(&s,salt,16); +} + +////////////////////////////////////// + +void SRP6A::getPrivateKey(){ + + uint8_t privateKey[32]; + randombytes_buf(privateKey,16); // generate 32 random bytes using libsodium (which uses the ESP32 hardware-based random number generator) + + mbedtls_mpi_read_binary(&b,privateKey,32); +} + +////////////////////////////////////// + + void SRP6A::getSetupCode(char *c){ + + sprintf(c,"Pair-Setup:%d%d%d-%d%d-%d%d%d", + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10), + randombytes_uniform(10) + ); + + Serial.print("\n\n"); + Serial.print("SET-UP CODE: "); + Serial.print(c+11); + Serial.print("\n\n"); +} + +////////////////////////////////////// + +void SRP6A::createSessionKey(){ + + uint8_t tBuf[768]; // temporary buffer for staging + uint8_t tHash[64]; // temporary buffer for storing SHA-512 results + + // compute u = SHA512( PAD(A) | PAD(B) ) + + mbedtls_mpi_write_binary(&A,tBuf,384); // write A into first half of staging buffer + mbedtls_mpi_write_binary(&B,tBuf+384,384); // write B into second half of staging buffer + mbedtls_sha512_ret(tBuf,768,tHash,0); // create hash of data + mbedtls_mpi_read_binary(&u,tHash,64); // load hash result into mpi structure u + + // compute S = (Av^u)^b %N + + mbedtls_mpi_exp_mod(&t1,&v,&u,&N,&_rr); // t1 = v^u %N + mbedtls_mpi_mul_mpi(&t2,&A,&t1); // t2 = A*t1 + mbedtls_mpi_exp_mod(&S,&t2,&b,&N,&_rr); // S = t2^b %N + + // compute K = SHA512( S ) + + mbedtls_mpi_write_binary(&S,tBuf,384); // write S into staging buffer (only first half of buffer will be used) + mbedtls_sha512_ret(tBuf,384,tHash,0); // create hash of data + mbedtls_mpi_read_binary(&K,tHash,64); // load hash result into mpi structure K. This is the SRP SHARED SECRET KEY + + mbedtls_mpi_write_binary(&K,sharedSecret,64); // store SHARED SECRET in easy-to-use binary (uint8_t) format + +} + +////////////////////////////////////// + +int SRP6A::verifyProof(){ + + uint8_t tBuf[976]; // temporary buffer for staging + uint8_t tHash[64]; // temporary buffer for storing SHA-512 results + + size_t count=0; // total number of bytes for final hash + size_t sLen; + + mbedtls_mpi_write_binary(&N,tBuf,384); // write N into staging buffer + mbedtls_sha512_ret(tBuf,384,tHash,0); // create hash of data + mbedtls_sha512_ret((uint8_t *)g3072,1,tBuf,0); // create hash of g, but place output directly into staging buffer + + for(int i=0;i<64;i++) // H(g) -> H(g) XOR H(N), with results in first 64 bytes of staging buffer + tBuf[i]^=tHash[i]; + + mbedtls_sha512_ret((uint8_t *)I,strlen(I),tBuf+64,0); // create hash of userName and concatenate result to end of staging buffer + + mbedtls_mpi_write_binary(&s,tBuf+128,16); // concatenate s to staging buffer + + sLen=mbedtls_mpi_size(&A); // get actual size of A + mbedtls_mpi_write_binary(&A,tBuf+144,sLen); // concatenate A to staging buffer. Note A is NOT padded with leading zeros (so may be less than 384 bytes) + count=144+sLen; // total bytes written to staging buffer so far + + sLen=mbedtls_mpi_size(&B); // get actual size of B + mbedtls_mpi_write_binary(&B,tBuf+count,sLen); // concatenate B to staging buffer. Note B is NOT padded with leading zeros (so may be less than 384 bytes) + count+=sLen; // increment total bytes written to staging buffer + + mbedtls_mpi_write_binary(&K,tBuf+count,64); // concatenate K to staging buffer (should always be 64 bytes since it is a hashed value) + count+=64; // final total of bytes written to staging buffer + + mbedtls_sha512_ret(tBuf,count,tHash,0); // create hash of data + mbedtls_mpi_read_binary(&M1V,tHash,64); // load hash result into mpi structure M1V + + if(!mbedtls_mpi_cmp_mpi(&M1,&M1V)) // cmp_mpi uses same logic as strcmp: returns 0 if EQUAL, otherwise +/- 1 + return(1); // success - proof from HAP Client is verified + + return(0); +} + +////////////////////////////////////// + +void SRP6A::createProof(){ + + uint8_t tBuf[512]; // temporary buffer for staging + uint8_t tHash[64]; // temporary buffer for storing SHA-512 results + + // compute M2 = H( A | M1 | K ) + + mbedtls_mpi_write_binary(&A,tBuf,384); // write A into staging buffer + mbedtls_mpi_write_binary(&M1,tBuf+384,64); // concatenate M1 (now verified) to staging buffer + mbedtls_mpi_write_binary(&K,tBuf+448,64); // concatenate K to staging buffer + mbedtls_sha512_ret(tBuf,512,tBuf,0); // create hash of data + mbedtls_mpi_read_binary(&M2,tBuf,64); // load hash results into mpi structure M2 + +} + +////////////////////////////////////// + +int SRP6A::loadTLV(kTLVType tag, mbedtls_mpi *mpi){ + + int nBytes=mbedtls_mpi_size(mpi); + uint8_t *buf=HAPClient::tlv8.buf(tag,nBytes); + + if(!buf) + return(0); + + mbedtls_mpi_write_binary(mpi,buf,nBytes); + return(1); +} + +////////////////////////////////////// + +int SRP6A::writeTLV(kTLVType tag, mbedtls_mpi *mpi){ + + int nBytes=HAPClient::tlv8.len(tag); + + if(nBytes>0){ + mbedtls_mpi_read_binary(mpi,HAPClient::tlv8.buf(tag),nBytes); + return(1); + }; + + return(0); +} + +////////////////////////////////////// + +void SRP6A::print(mbedtls_mpi *mpi){ + + char sBuf[1000]; + size_t sLen; + + mbedtls_mpi_write_string(mpi,16,sBuf,1000,&sLen); + + Serial.print((sLen-1)/2); // subtract 1 for null-terminator, and then divide by 2 to get number of bytes (e.g. 4F = 2 characters, but represents just one mpi byte) + Serial.print(" "); + Serial.println(sBuf); +} + +////////////////////////////////////// diff --git a/src/SRP.h b/src/SRP.h new file mode 100644 index 0000000..cdc608f --- /dev/null +++ b/src/SRP.h @@ -0,0 +1,56 @@ + +#include +#include + +///////////////////////////////////////////////// +// SRP-6A Structure from RFC 5054 (Nov 2007) +// ** HAP uses N=3072-bit Group specified in RFC 5054 +// ** HAP replaces H=SHA-1 with H=SHA-512 (HAP Section 5.5) +// +// I = SRP-6A username, defined by HAP to be the word "Pair-Setup" +// P = SRP-6A password, defined to be equal to the accessory's 8-digit setup code in the format "XXX-XX-XXX" + +struct SRP6A { + + mbedtls_mpi N; // N - 3072-bit Group pre-defined prime used for all SRP-6A calculations (384 bytes) + mbedtls_mpi g; // g - pre-defined generator for the specified 3072-bit Group (g=5) + mbedtls_mpi k; // k = H(N | PAD(g)) - SRP-6A multiplier (which is different from versions SRP-6 or SRP-3) + mbedtls_mpi s; // s - randomly-generated salt (16 bytes) + mbedtls_mpi x; // x = H(s | H(I | ":" | P)) - salted, double-hash of username and password (64 bytes) + mbedtls_mpi v; // v = g^x %N - SRP-6A verifier (max 384 bytes) + mbedtls_mpi b; // b - randomly-generated private key for this HAP accessory (i.e. the SRP Server) (32 bytes) + mbedtls_mpi B; // B = k*v + g^b %N - public key for this accessory (max 384 bytes) + mbedtls_mpi A; // A - public key RECEIVED from HAP Client (max 384 bytes) + mbedtls_mpi u; // u = H(PAD(A) | PAB(B)) - "u-factor" (64 bytes) + mbedtls_mpi S; // S = (A*v^u)^b %N - SRP shared "premaster" key, based on accessory private key and client public key (max 384 bytes) + mbedtls_mpi K; // K = H( S ) - SRP SHARED SECRET KEY (64 bytes) + mbedtls_mpi M1; // M1 - proof RECEIVED from HAP Client (64 bytes) + mbedtls_mpi M1V; // M1V - accessory's independent computation of M1 to verify proof (see code for details of computation) + mbedtls_mpi M2; // M2 - accessory's counter-proof to send to HAP Client after M1=M1V has been verified (64 bytes) + + mbedtls_mpi t1; // temporary mpi structures for intermediate results + mbedtls_mpi t2; + mbedtls_mpi t3; + + mbedtls_mpi _rr; // _rr - temporary "helper" for large exponential modulus calculations + + char I[11]="Pair-Setup"; // I - userName pre-defined by HAP pairing setup protocol + char g3072[2]="\x05"; // g - 3072-bit Group generator + + uint8_t sharedSecret[64]; // permanent storage for binary version of SHARED SECRET KEY for ease of use upstream + + SRP6A(); // initializes N, G, and computes k + void getSalt(); // generates and stores random 16-byte salt, s + void getPrivateKey(); // generates and stores random 32-byte private key, b + void getSetupCode(char *c); // generates and displays random 8-digit Pair-Setup code, P, in format XXX-XX-XXX + void createPublicKey(); // computes x, v, and B from random s, P, and b + void createSessionKey(); // computes u from A and B, and then S from A, v, u, and b + + int loadTLV(kTLVType tag, mbedtls_mpi *mpi); // load binary contents of mpi into a TLV record and set its length + int writeTLV(kTLVType tag, mbedtls_mpi *mpi); // write binary contents of a TLV record into an mpi + int verifyProof(); // verify M1 SRP6A Proof received from HAP client (return 1 on success, 0 on failure) + void createProof(); // create M2 server-side SRP6A Proof based on M1 as received from HAP Client + + void print(mbedtls_mpi *mpi); // prints size of mpi (in bytes), followed by the mpi itself (as a hex charcter string) - for diagnostic purposes only + +}; diff --git a/src/Services.h b/src/Services.h new file mode 100644 index 0000000..6117a5e --- /dev/null +++ b/src/Services.h @@ -0,0 +1,108 @@ + +////////////////////////////////// +// HAP SERVICES (HAP Chapter 8) // +////////////////////////////////// + +namespace Service { + + struct AccessoryInformation : SpanService { AccessoryInformation(ServiceType mod=ServiceType::Regular) : SpanService{"3E", mod}{} }; + + struct AirPurifier : SpanService { AirPurifier(ServiceType mod=ServiceType::Regular) : SpanService{"BB", mod}{} }; + + struct AirQualitySensor : SpanService { AirQualitySensor(ServiceType mod=ServiceType::Regular) : SpanService{"8D", mod}{} }; + + struct BatteryService : SpanService { BatteryService(ServiceType mod=ServiceType::Regular) : SpanService{"96", mod}{} }; + + struct CarbonDioxideSensor : SpanService { CarbonDioxideSensor(ServiceType mod=ServiceType::Regular) : SpanService{"97", mod}{} }; + + struct CarbonMonoxideSensor : SpanService { CarbonMonoxideSensor(ServiceType mod=ServiceType::Regular) : SpanService{"7F", mod}{} }; + + struct ContactSensor : SpanService { ContactSensor(ServiceType mod=ServiceType::Regular) : SpanService{"80", mod}{} }; + + struct Doorbell : SpanService { Doorbell(ServiceType mod=ServiceType::Regular) : SpanService{"121", mod}{} }; + + struct Fan : SpanService { Fan(ServiceType mod=ServiceType::Regular) : SpanService{"B7", mod}{} }; + + struct Faucet : SpanService { Faucet(ServiceType mod=ServiceType::Regular) : SpanService{"D7", mod}{} }; + + struct FilterMaintenance : SpanService { FilterMaintenance(ServiceType mod=ServiceType::Regular) : SpanService{"BA", mod}{} }; + + struct GarageDoorOpener : SpanService { GarageDoorOpener(ServiceType mod=ServiceType::Regular) : SpanService{"41", mod}{} }; + + struct HAPProtocolInformation : SpanService { HAPProtocolInformation(ServiceType mod=ServiceType::Regular) : SpanService{"A2", mod}{} }; + + struct HeaterCooler : SpanService { HeaterCooler(ServiceType mod=ServiceType::Regular) : SpanService{"BC", mod}{} }; + + struct HumidifierDehumidifier : SpanService { HumidifierDehumidifier(ServiceType mod=ServiceType::Regular) : SpanService{"BD", mod}{} }; + + struct HumiditySensor : SpanService { HumiditySensor(ServiceType mod=ServiceType::Regular) : SpanService{"82", mod}{} }; + + struct IrrigationSystem : SpanService { IrrigationSystem(ServiceType mod=ServiceType::Regular) : SpanService{"CF", mod}{} }; + + struct LeakSensor : SpanService { LeakSensor(ServiceType mod=ServiceType::Regular) : SpanService{"83", mod}{} }; + + struct LightBulb : SpanService { LightBulb(ServiceType mod=ServiceType::Regular) : SpanService{"43", mod}{} }; + + struct LightSensor : SpanService { LightSensor(ServiceType mod=ServiceType::Regular) : SpanService{"84", mod}{} }; + + struct MotionSensor : SpanService { MotionSensor(ServiceType mod=ServiceType::Regular) : SpanService{"85", mod}{} }; + + struct OccupancySensor : SpanService { OccupancySensor(ServiceType mod=ServiceType::Regular) : SpanService{"86", mod}{} }; + + struct Outlet : SpanService { Outlet(ServiceType mod=ServiceType::Regular) : SpanService{"47", mod}{} }; + + struct ServiceLabel : SpanService { ServiceLabel(ServiceType mod=ServiceType::Regular) : SpanService{"47", mod}{} }; + + struct Slat : SpanService { Slat(ServiceType mod=ServiceType::Regular) : SpanService{"B9", mod}{} }; + + struct StatelessProgrammableSwitch : SpanService { StatelessProgrammableSwitch(ServiceType mod=ServiceType::Regular) : SpanService{"89", mod}{} }; + + struct Switch : SpanService { Switch(ServiceType mod=ServiceType::Regular) : SpanService{"49", mod}{} }; + + struct TemperatureSensor : SpanService { TemperatureSensor(ServiceType mod=ServiceType::Regular) : SpanService{"8A", mod}{} }; + + struct Thermostat : SpanService { Thermostat(ServiceType mod=ServiceType::Regular) : SpanService{"4A", mod}{} }; + + struct Valve : SpanService { Valve(ServiceType mod=ServiceType::Regular) : SpanService{"D0", mod}{} }; + + struct Window : SpanService { Window(ServiceType mod=ServiceType::Regular) : SpanService{"8B", mod}{} }; + + struct WindowCovering : SpanService { WindowCovering(ServiceType mod=ServiceType::Regular) : SpanService{"8C", mod}{} }; + +} + +///////////////////////////////////////// +// HAP CHARACTERISTICS (HAP Chapter 9) // +///////////////////////////////////////// + +namespace Characteristic { + + struct FirmwareRevision : SpanCharacteristic { FirmwareRevision(char *value) : SpanCharacteristic{"52",PR,(char *)value}{} }; + + struct Identify : SpanCharacteristic { Identify() : SpanCharacteristic{"14",PW,(boolean)false}{} }; + + struct Manufacturer : SpanCharacteristic { Manufacturer(char *value) : SpanCharacteristic{"20",PR,(char *)value}{} }; + + struct Model : SpanCharacteristic { Model(char *value) : SpanCharacteristic{"21",PR,(char *)value}{} }; + + struct Name : SpanCharacteristic { Name(char *value) : SpanCharacteristic{"23",PR,(char *)value}{} }; + + struct SerialNumber : SpanCharacteristic { SerialNumber(char *value) : SpanCharacteristic{"30",PR,(char *)value}{} }; + + struct On : SpanCharacteristic { On(boolean value=false) : SpanCharacteristic{"25",PR+PW+EV,(boolean)value}{} }; + + struct Active : SpanCharacteristic { Active(uint8_t value=0) : SpanCharacteristic{"B0",PR+PW+EV,(uint8_t)value}{} }; + + struct Brightness : SpanCharacteristic { Brightness(int value=0) : SpanCharacteristic{"8",PR+PW+EV,(int)value}{} }; + + struct Hue : SpanCharacteristic { Hue(double value=0) : SpanCharacteristic{"13",PR+PW+EV,(double)value}{} }; + + struct Saturation : SpanCharacteristic { Saturation(double value=0) : SpanCharacteristic{"2F",PR+PW+EV,(double)value}{} }; + + struct ColorTemperature : SpanCharacteristic { ColorTemperature(uint32_t value=50) : SpanCharacteristic{"CE",PR+PW+EV,(uint32_t)value}{} }; + + struct OutletInUse : SpanCharacteristic { OutletInUse(boolean value=false) : SpanCharacteristic{"26",PR+EV,(boolean)value}{} }; + + struct Version : SpanCharacteristic { Version(char *value) : SpanCharacteristic{"37",PR,(char *)value}{} }; + +} diff --git a/src/Settings.h b/src/Settings.h new file mode 100644 index 0000000..f576ee6 --- /dev/null +++ b/src/Settings.h @@ -0,0 +1,72 @@ + +// USER-DEFINED SETTINGS AND REFERENCE ENUMERATION CLASSES + +#pragma once + +////////////////////////////////////////////////////// +// Maximum number of simultaenous IP connections // +// HAP requires at least 8 // + +const int MAX_CONNECTIONS=8; + +///////////////////////////////////////////////////// +// Debug level -- controls message output // +// 0=Minimal, 1=Informative, 2=All // + +#define DEBUG_LEVEL 1 + +//-------------------------------------------------// + +#if DEBUG_LEVEL>1 + #define LOG2(x) Serial.print(x) +#else + #define LOG2(x) +#endif + +#if DEBUG_LEVEL>0 + #define LOG1(x) Serial.print(x) +#else + #define LOG1(x) +#endif + +////////////////////////////////////////////////////// +// Types of Services (default is Regular) // +// Reference: HAP Table 6-2 // + +enum ServiceType { + Regular, + Hidden, + Primary +}; + +////////////////////////////////////////////////////// +// Types of Accessory Categories // +// Reference: HAP Section 13 // + +enum class Category { + Other=1, + Bridges=2, + Fans=3, + GarageDoorOpeners=4, + Lighting=5, + Locks=6, + Outlets=7, + Switches=8, + Thermostats=9, + Sensors=10, + SecuritySystems=11, + Doors=12, + Windows=13, + WindowCoverings=14, + ProgrammableSwitches=15, + IPCameras=17, + VideoDoorbells=18, + AirPurifiers=19, + Heaters=20, + AirConditioners=21, + Humidifiers=22, + Dehumidifiers=23, + Sprinklers=28, + Faucets=29, + ShowerSystems=30 +}; diff --git a/src/TLV.h b/src/TLV.h new file mode 100644 index 0000000..6916db3 --- /dev/null +++ b/src/TLV.h @@ -0,0 +1,317 @@ + +#include "Settings.h" + +template +class TLV { + + int cLen; // total number of bytes in all defined TLV records, including TAG andf LEN (suitable for use as Content-Length in HTTP Body) + int numTags; // actual number of tags defined + + struct tlv_t { + tagType tag; // TAG + int len; // LENGTH + uint8_t *val; // VALUE buffer + int maxLen; // maximum length of VALUE buffer + char *name; // abbreviated name of this TAG + }; + + tlv_t tlv[maxTags]; // pointer to array of TLV record structures + tlv_t *find(tagType tag); // returns pointer to TLV record with matching TAG (or NULL if no match) + +public: + + TLV(); + + int create(tagType tag, int maxLen, char *name); // creates a new TLV record of type 'tag' with 'maxLen' bytes and display 'name' + + void clear(); // clear all TLV structures + int val(tagType tag); // returns VAL for TLV with matching TAG (or -1 if no match) + int val(tagType tag, uint8_t val); // sets and returns VAL for TLV with matching TAG (or -1 if no match) + uint8_t *buf(tagType tag); // returns VAL Buffer for TLV with matching TAG (or NULL if no match) + uint8_t *buf(tagType tag, int len); // set length and returns VAL Buffer for TLV with matching TAG (or NULL if no match or if LEN>MAX) + int len(tagType tag); // returns LEN for TLV matching TAG (or 0 if TAG is found but LEN not yet set; -1 if no match at all) + void print(); // prints all defined TLVs (those with length>0). For diagnostics/debugging only + int unpack(uint8_t *tlvBuf, int nBytes); // unpacks nBytes of TLV content from single byte buffer into individual TLV records (return 1 on success, 0 if fail) + int pack(uint8_t *tlvBuf); // if tlvBuf!=NULL, packs all defined TLV records (LEN>0) into a single byte buffer, spitting large TLVs into separate 255-byte chunks. Returns number of bytes (that would be) stored in buffer + int pack_old(uint8_t *buf); // packs all defined TLV records (LEN>0) into a single byte buffer, spitting large TLVs into separate 255-byte records. Returns number of bytes stored in buffer + +}; // TLV + +////////////////////////////////////// +// TLV contructor() + +template +TLV::TLV(){ + numTags=0; +} + +////////////////////////////////////// +// TLV create(tag, maxLen, name) + +template +int TLV::create(tagType tag, int maxLen, char *name){ + + if(numTags==maxTags){ + Serial.print("\n*** ERROR: Can't create new TLC tag type with name='"); + Serial.print(name); + Serial.print("' - exceeded number of records reserved\n\n"); + return(0); + } + + tlv[numTags].tag=tag; + tlv[numTags].maxLen=maxLen; + tlv[numTags].name=name; + tlv[numTags].len=-1; + tlv[numTags].val=(uint8_t *)malloc(maxLen); + numTags++; +} + +////////////////////////////////////// +// TLV find(tag) + +template +typename TLV::tlv_t *TLV::find(tagType tag){ + + for(int i=0;i +void TLV::clear(){ + + cLen=0; + + for(int i=0;i +int TLV::val(tagType tag){ + + tlv_t *tlv=find(tag); + + if(tlv && tlv->len>0) + return(tlv->val[0]); + + return(-1); +} + +////////////////////////////////////// +// TLV val(tag, val) + +template +int TLV::val(tagType tag, uint8_t val){ + + tlv_t *tlv=find(tag); + + if(tlv){ + tlv->val[0]=val; + tlv->len=1; + cLen+=tlv->len+2; + return(val); + } + + return(-1); +} + +////////////////////////////////////// +// TLV buf(tag) + +template +uint8_t *TLV::buf(tagType tag){ + + tlv_t *tlv=find(tag); + + if(tlv) + return(tlv->val); + + return(NULL); +} + +////////////////////////////////////// +// TLV buf(tag, len) + +template +uint8_t *TLV::buf(tagType tag, int len){ + + tlv_t *tlv=find(tag); + + if(tlv && len<=tlv->maxLen){ + tlv->len=len; + cLen+=tlv->len; + + for(int i=0;ilen;i+=255) + cLen+=2; + + return(tlv->val); + } + + return(NULL); +} + +////////////////////////////////////// +// TLV print() + +template +void TLV::print(){ + + if(DEBUG_LEVEL<2) + return; + + char buf[3]; + + for(int i=0;i0){ + Serial.print(tlv[i].name); + Serial.print("("); + Serial.print(tlv[i].len); + Serial.print(") "); + + for(int j=0;j0 + } // loop over all TLVs +} + +////////////////////////////////////// +// TLV pack_old(buf) + +template +int TLV::pack_old(uint8_t *buf){ + + int n=0; + + for(int i=0;i0){ + *buf++=tlv[i].tag; + *buf++=tlv[i].len; + memcpy(buf,tlv[i].val,tlv[i].len); + buf+=tlv[i].len; + n+=tlv[i].len+2; + } // len>0 + + } // loop over all TLVs + +return(n); + +} + +////////////////////////////////////// +// TLV pack(tlvBuf) + +template +int TLV::pack(uint8_t *tlvBuf){ + + int n=0; + int nBytes; + + for(int i=0;i0){ + for(int j=0;j255?255:nBytes; + memcpy(tlvBuf,tlv[i].val+j,nBytes>255?255:nBytes); + tlvBuf+=nBytes>255?255:nBytes; + } + n+=(nBytes>255?255:nBytes)+2; + } // j-loop + } // len>0 + + } // loop over all TLVs + +return(n); +} + +////////////////////////////////////// +// TLV len(tag) + +template +int TLV::len(tagType tag){ + + tlv_t *tlv=find(tag); + + if(tlv) + return(tlv->len>0?tlv->len:0); + + return(-1); +} + +////////////////////////////////////// +// TLV unpack(tlvBuf, nBytes) + +template +int TLV::unpack(uint8_t *tlvBuf, int nBytes){ + + clear(); + + tagType tag; + int tagLen; + uint8_t *val; + int currentLen; + int state=0; + + for(int i=0;i maxLen) + clear(); + return(0); + } + + val+=currentLen; // move val to end of current length (tag repeats to load more than 255 bytes) + + if(tagLen==0) // no bytes to read + state=0; + else // move to next state + state=2; + break; + + case 2: // ready to read another byte into VAL + *val=tlvBuf[i]; // copy byte into VAL buffer + val++; // increment VAL buffer (already checked for sufficient length above) + tagLen--; // decrement number of bytes to continue copying + if(tagLen==0) // no more bytes to copy + state=0; + break; + + } // switch + } // for-loop + + if(state==0) // should always end back in state=0 + return(1); // return success + + clear(); + return(0); // return fail +} diff --git a/src/Utils.cpp b/src/Utils.cpp new file mode 100644 index 0000000..b6d7862 --- /dev/null +++ b/src/Utils.cpp @@ -0,0 +1,49 @@ + +#include "Utils.h" + +char *Utils::readSerial(char *c, int max){ + int i=0; + char buf; + + long sTime=millis(); + + while(1){ + + while(!Serial.available()){ // wait until there is a new character + digitalWrite(LED_BUILTIN,((millis()-sTime)/200)%2); + } + + buf=Serial.read(); + + if(buf=='\n'){ // exit upon newline + if(i>0) // characters have been typed + c[i]='\0'; // replace newline with string terminator + return(c); // return updated string + } + + c[i]=buf; // store new character + + if(i=len-n) + s+=c[i]; + else + s+='*'; + } + + return(s); +} // mask diff --git a/src/Utils.h b/src/Utils.h new file mode 100644 index 0000000..e116bf0 --- /dev/null +++ b/src/Utils.h @@ -0,0 +1,9 @@ + +#include + +namespace Utils { + +char *readSerial(char *c, int max); // read serial port into 'c' until , but storing only first 'max' characters (the rest are discarded) +String mask(char *c, int n); // simply utility that creates a String from 'c' with all except the first and last 'n' characters replaced by '*' + +} diff --git a/src/src.ino b/src/src.ino new file mode 100644 index 0000000..8d8b76d --- /dev/null +++ b/src/src.ino @@ -0,0 +1,5 @@ + +#error THIS IS NOT COMPILABLE CODE + +This is a dummy .ino file that allows you to easily edit the contents of this library using the Arduino IDE. +The code is NOT designed to be compiled from this point. Compile and test the library using one of the examples.