From da55b9b6b1c350729987157c980167f78c2025f9 Mon Sep 17 00:00:00 2001 From: Gregg Date: Wed, 27 Dec 2023 14:31:22 -0600 Subject: [PATCH] Start re-working SRP6A's use in Pair-Setup to make it local instead of global --- src/HAP.cpp | 71 ++++++++++++++++++------------------------------ src/HAP.h | 10 ++++++- src/HomeSpan.cpp | 59 ++++++++++++++++++++++++---------------- src/HomeSpan.h | 2 +- src/SRP.cpp | 50 ++++++++++++++++++++++------------ src/SRP.h | 28 ++++++++++++++++++- src/Utils.h | 2 +- src/src.ino | 2 +- 8 files changed, 133 insertions(+), 91 deletions(-) diff --git a/src/HAP.cpp b/src/HAP.cpp index 7825485..7a683c0 100644 --- a/src/HAP.cpp +++ b/src/HAP.cpp @@ -36,7 +36,7 @@ void HAPClient::init(){ - size_t len; // not used but required to read blobs from NVS + size_t len; // not used but required to read blobs from NVS nvs_open("SRP",NVS_READWRITE,&srpNVS); // open SRP data namespace in NVS nvs_open("HAP",NVS_READWRITE,&hapNVS); // open HAP data namespace in NVS @@ -48,37 +48,20 @@ void HAPClient::init(){ homeSpan.spanOTA.setPassword(DEFAULT_OTA_PASSWORD); // ...use default password } } + + if(nvs_get_blob(srpNVS,"VERIFYDATA",NULL,&len)) // if Pair-Setup verification code data not found in NVS + homeSpan.setPairingCode(DEFAULT_SETUP_CODE); // create and save verification from using Pairing Setup Code - if(strlen(homeSpan.pairingCodeCommand)){ // load verification setup code if provided - homeSpan.processSerialCommand(homeSpan.pairingCodeCommand); // if load failed due to invalid code, the logic below still runs and will pick up previous code or use the default one - } - - struct { // temporary structure to hold SRP verification code and salt stored in NVS - uint8_t salt[16]; - uint8_t verifyCode[384]; - } verifyData; - - if(!nvs_get_blob(srpNVS,"VERIFYDATA",NULL,&len)){ // if found verification code data in NVS - nvs_get_blob(srpNVS,"VERIFYDATA",&verifyData,&len); // retrieve data - srp.loadVerifyCode(verifyData.verifyCode,verifyData.salt); // load verification code and salt into SRP structure - } else { - LOG0("Generating SRP verification data for default Setup Code: %.3s-%.2s-%.3s\n",homeSpan.defaultSetupCode,homeSpan.defaultSetupCode+3,homeSpan.defaultSetupCode+5); - srp.createVerifyCode(homeSpan.defaultSetupCode,verifyData.verifyCode,verifyData.salt); // create verification code from default Setup Code and random salt - nvs_set_blob(srpNVS,"VERIFYDATA",&verifyData,sizeof(verifyData)); // update data - nvs_commit(srpNVS); // commit to NVS - LOG0("Setup Payload for Optional QR Code: %s\n\n",homeSpan.qrCode.get(atoi(homeSpan.defaultSetupCode),homeSpan.qrID,atoi(homeSpan.category))); - } - - if(!strlen(homeSpan.qrID)){ // Setup ID has not been specified in sketch - if(!nvs_get_str(hapNVS,"SETUPID",NULL,&len)){ // check for saved value - nvs_get_str(hapNVS,"SETUPID",homeSpan.qrID,&len); // retrieve data + if(!strlen(homeSpan.qrID)){ // is Setup ID has not been specified in sketch + if(!nvs_get_str(hapNVS,"SETUPID",NULL,&len)){ // check for saved value + nvs_get_str(hapNVS,"SETUPID",homeSpan.qrID,&len); // retrieve data } else { - sprintf(homeSpan.qrID,"%s",DEFAULT_QR_ID); // use default + sprintf(homeSpan.qrID,"%s",DEFAULT_QR_ID); // use default } } - if(!nvs_get_blob(hapNVS,"ACCESSORY",NULL,&len)){ // if found long-term Accessory data in NVS - nvs_get_blob(hapNVS,"ACCESSORY",&accessory,&len); // retrieve data + if(!nvs_get_blob(hapNVS,"ACCESSORY",NULL,&len)){ // if found long-term Accessory data in NVS + nvs_get_blob(hapNVS,"ACCESSORY",&accessory,&len); // retrieve data } else { LOG0("Generating new random Accessory ID and Long-Term Ed25519 Signature Keys...\n\n"); uint8_t buf[6]; @@ -91,13 +74,13 @@ void HAPClient::init(){ memcpy(accessory.ID,cBuf,17); // copy into Accessory ID for permanent storage crypto_sign_keypair(accessory.LTPK,accessory.LTSK); // generate new random set of keys using libsodium public-key signature - nvs_set_blob(hapNVS,"ACCESSORY",&accessory,sizeof(accessory)); // update data - nvs_commit(hapNVS); // commit to NVS + nvs_set_blob(hapNVS,"ACCESSORY",&accessory,sizeof(accessory)); // update data + nvs_commit(hapNVS); // commit to NVS } - if(!nvs_get_blob(hapNVS,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS + if(!nvs_get_blob(hapNVS,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS TempBuffer tBuf(len/sizeof(Controller)); - nvs_get_blob(hapNVS,"CONTROLLERS",tBuf,&len); // retrieve data + nvs_get_blob(hapNVS,"CONTROLLERS",tBuf,&len); // retrieve data for(int i=0;i - srp.createPublicKey(); // create accessory Public Key from Pair-Setup code (displayed to user) - mbedtls_mpi_write_binary(&srp.B,*itPublicKey,(*itPublicKey).len); // load server PublicKey, B, into TLV - mbedtls_mpi_write_binary(&srp.s,*itSalt,(*itSalt).len); // load Salt, s, into TLV + srp->createPublicKey(); // create accessory Public Key from Pair-Setup code (displayed to user) + mbedtls_mpi_write_binary(&srp->B,*itPublicKey,(*itPublicKey).len); // load server PublicKey, B, into TLV + mbedtls_mpi_write_binary(&srp->s,*itSalt,(*itSalt).len); // load Salt, s, into TLV tlvRespond(responseTLV); // send response to client pairStatus=pairState_M3; // set next expected pair-state request from client return(1); @@ -426,12 +409,12 @@ int HAPClient::postPairSetupURL(uint8_t *content, size_t len){ return(0); }; - mbedtls_mpi_read_binary(&srp.A,*itPublicKey,(*itPublicKey).len); // load client PublicKey TLV into A - mbedtls_mpi_read_binary(&srp.M1,*itClientProof,(*itClientProof).len); // load client Proof TLV into M1 + mbedtls_mpi_read_binary(&srp->A,*itPublicKey,(*itPublicKey).len); // load client PublicKey TLV into A + mbedtls_mpi_read_binary(&srp->M1,*itClientProof,(*itClientProof).len); // load client Proof TLV into M1 - srp.createSessionKey(); // create session key, K, from receipt of client Public Key, A + srp->createSessionKey(); // create session key, K, from receipt of client Public Key, A - if(!srp.verifyProof()){ // verify client Proof, M1 + if(!srp->verifyProof()){ // verify client Proof, M1 LOG0("\n*** ERROR: SRP Proof Verification Failed\n\n"); responseTLV.add(kTLVType_State,pairState_M4); // set State= responseTLV.add(kTLVType_Error,tagError_Authentication); // set Error=Authentication @@ -443,8 +426,8 @@ int HAPClient::postPairSetupURL(uint8_t *content, size_t len){ auto itAccProof=responseTLV.add(kTLVType_Proof,64,NULL); // create blank accessory Proof TLV with space for 64 bytes responseTLV.add(kTLVType_State,pairState_M4); // set State= - srp.createProof(); // M1 has been successully verified; now create accessory proof M2 - mbedtls_mpi_write_binary(&srp.M2,*itAccProof,(*itAccProof).len); // load accessory Proof, M2, into TLV + srp->createProof(); // M1 has been successully verified; now create accessory proof M2 + mbedtls_mpi_write_binary(&srp->M2,*itAccProof,(*itAccProof).len); // load accessory Proof, M2, into TLV tlvRespond(responseTLV); // send response to client pairStatus=pairState_M5; // set next expected pair-state request from client return(1); @@ -473,7 +456,7 @@ int HAPClient::postPairSetupURL(uint8_t *content, size_t len){ // The iosDeviceX HKDF calculations are separate and will be performed further below with the SALT and INFO as specified in the HAP docs. TempBuffer srpSessionKey(crypto_box_PUBLICKEYBYTES); // temporary space - used only in this block - hkdf.create(srpSessionKey,srp.sharedSecret,64,"Pair-Setup-Encrypt-Salt","Pair-Setup-Encrypt-Info"); // create SessionKey + hkdf.create(srpSessionKey,srp->sharedSecret,64,"Pair-Setup-Encrypt-Salt","Pair-Setup-Encrypt-Info"); // create SessionKey LOG2("------- DECRYPTING SUB-TLVS -------\n"); @@ -514,7 +497,7 @@ int HAPClient::postPairSetupURL(uint8_t *content, size_t len){ // Note that the SALT and INFO text fields now match those in HAP Section 5.6.6.1 TempBuffer iosDeviceX(32); - hkdf.create(iosDeviceX,srp.sharedSecret,64,"Pair-Setup-Controller-Sign-Salt","Pair-Setup-Controller-Sign-Info"); // derive iosDeviceX (32 bytes) from SRP Shared Secret using HKDF + hkdf.create(iosDeviceX,srp->sharedSecret,64,"Pair-Setup-Controller-Sign-Salt","Pair-Setup-Controller-Sign-Info"); // derive iosDeviceX (32 bytes) from SRP Shared Secret using HKDF // Concatenate iosDeviceX, IOS ID, and IOS PublicKey into iosDeviceInfo @@ -534,7 +517,7 @@ int HAPClient::postPairSetupURL(uint8_t *content, size_t len){ // Now perform the above steps in reverse to securely transmit the AccessoryLTPK to the Controller (HAP Section 5.6.6.2) TempBuffer 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 + hkdf.create(accessoryX,srp->sharedSecret,64,"Pair-Setup-Accessory-Sign-Salt","Pair-Setup-Accessory-Sign-Info"); // derive accessoryX from SRP Shared Secret using HKDF // Concatenate accessoryX, Accessory ID, and Accessory PublicKey into accessoryInfo @@ -1672,6 +1655,6 @@ HKDF HAPClient::hkdf; pairState HAPClient::pairStatus; Accessory HAPClient::accessory; list> HAPClient::controllerList; -SRP6A HAPClient::srp; +SRP6A *HAPClient::srp; int HAPClient::conNum; diff --git a/src/HAP.h b/src/HAP.h index 797747c..4256c82 100644 --- a/src/HAP.h +++ b/src/HAP.h @@ -52,6 +52,14 @@ const TLV8_names HAP_Names[] = { #define hap_controller_IDBYTES 36 #define hap_accessory_IDBYTES 17 +///////////////////////////////////////////////// +// Pair-Setup Code Verification Data and Salt + +struct Verification { + uint8_t salt[16]; + uint8_t verifyCode[384]; +}; + ///////////////////////////////////////////////// // NONCE Structure (HAP used last 64 of 96 bits) @@ -108,7 +116,7 @@ struct HAPClient { static nvs_handle srpNVS; // handle for non-volatile-storage of SRP data 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 SRP6A *srp; // stores all SRP-6A keys used for Pair-Setup (must persist through multiple calls to Pair-Setup) static Accessory accessory; // Accessory ID and Ed25519 public and secret keys- permanently stored static list> controllerList; // linked-list of 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 diff --git a/src/HomeSpan.cpp b/src/HomeSpan.cpp index daae9fe..febca1b 100644 --- a/src/HomeSpan.cpp +++ b/src/HomeSpan.cpp @@ -674,31 +674,8 @@ void Span::processSerialCommand(const char *c){ break; case 'S': { - - char setupCode[10]; - struct { // temporary structure to hold SRP verification code and salt stored in NVS - uint8_t salt[16]; - uint8_t verifyCode[384]; - } verifyData; - - sscanf(c+1," %9[0-9]",setupCode); - - if(strlen(setupCode)!=8){ - LOG0("\n*** Invalid request to change Setup Code. Code must be exactly 8 digits.\n\n"); - } else - - if(!network.allowedCode(setupCode)){ - LOG0("\n*** Invalid request to change Setup Code. Code too simple.\n\n"); - } else { - - LOG0("\nGenerating SRP verification data for new Setup Code: %.3s-%.2s-%.3s ... ",setupCode,setupCode+3,setupCode+5); - HAPClient::srp.createVerifyCode(setupCode,verifyData.verifyCode,verifyData.salt); // create verification code from default Setup Code and random salt - nvs_set_blob(HAPClient::srpNVS,"VERIFYDATA",&verifyData,sizeof(verifyData)); // update data - nvs_commit(HAPClient::srpNVS); // commit to NVS - LOG0("New Code Saved!\n"); - LOG0("Setup Payload for Optional QR Code: %s\n\n",qrCode.get(atoi(setupCode),qrID,atoi(category))); - } + setPairingCode(c+1); } break; @@ -1207,6 +1184,40 @@ const char* Span::statusString(HS_STATUS s){ /////////////////////////////// +Span& Span::setPairingCode(const char *s){ + + char setupCode[10]; + + sscanf(s," %9[0-9]",setupCode); + + if(strlen(setupCode)!=8){ + LOG0("\n*** Invalid request to change Setup Code. Code must be exactly 8 digits.\n\n"); + return(*this); + } + + if(!network.allowedCode(setupCode)){ + LOG0("\n*** Invalid request to change Setup Code. Code too simple.\n\n"); + return(*this); + } + + TempBuffer verifyData; // temporary storage for verification data + SRP6A *srp=new SRP6A; // create instance of SRP + + LOG0("\nGenerating SRP verification data for new Setup Code: %.3s-%.2s-%.3s ... ",setupCode,setupCode+3,setupCode+5); + + srp->createVerifyCode(setupCode,verifyData.get()->verifyCode,verifyData.get()->salt); // create verification code with random salt from specified Setup Code + nvs_set_blob(HAPClient::srpNVS,"VERIFYDATA",&verifyData,sizeof(verifyData)); // update data + nvs_commit(HAPClient::srpNVS); // commit to NVS + + LOG0("New Code Saved!\n"); + LOG0("Setup Payload for Optional QR Code: %s\n\n",qrCode.get(atoi(setupCode),qrID,atoi(category))); + + delete srp; + return(*this); +} + +/////////////////////////////// + Span& Span::setWifiCredentials(const char *ssid, const char *pwd){ sprintf(network.wifiData.ssid,"%.*s",MAX_SSID,ssid); sprintf(network.wifiData.pwd,"%.*s",MAX_PWD,pwd); diff --git a/src/HomeSpan.h b/src/HomeSpan.h index 557a73a..0a789b3 100644 --- a/src/HomeSpan.h +++ b/src/HomeSpan.h @@ -364,7 +364,7 @@ class Span{ Span& setStatusCallback(void (*f)(HS_STATUS status)){statusCallback=f;return(*this);} // sets an optional user-defined function to call when HomeSpan status changes const char* statusString(HS_STATUS s); // returns char string for HomeSpan status change messages - Span& setPairingCode(const char *s){sprintf(pairingCodeCommand,"S %9s",s);return(*this);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead + Span& setPairingCode(const char *s); // {sprintf(pairingCodeCommand,"S %9s",s);return(*this);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead void deleteStoredValues(){processSerialCommand("V");} // deletes stored Characteristic values from NVS int enableOTA(boolean auth=true, boolean safeLoad=true){return(spanOTA.init(auth, safeLoad, NULL));} // enables Over-the-Air updates, with (auth=true) or without (auth=false) authorization password diff --git a/src/SRP.cpp b/src/SRP.cpp index e399ed8..d1ba6da 100644 --- a/src/SRP.cpp +++ b/src/SRP.cpp @@ -39,19 +39,6 @@ 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); @@ -85,21 +72,47 @@ SRP6A::SRP6A(){ 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 + +} +////////////////////////////////////// + +SRP6A::~SRP6A(){ + + mbedtls_mpi_free(&N); + mbedtls_mpi_free(&g); + mbedtls_mpi_free(&s); + mbedtls_mpi_free(&x); + mbedtls_mpi_free(&v); + mbedtls_mpi_free(&A); + mbedtls_mpi_free(&b); + mbedtls_mpi_free(&B); + mbedtls_mpi_free(&S); + mbedtls_mpi_free(&k); + mbedtls_mpi_free(&u); + mbedtls_mpi_free(&K); + mbedtls_mpi_free(&M1); + mbedtls_mpi_free(&M1V); + mbedtls_mpi_free(&M2); + mbedtls_mpi_free(&_rr); + mbedtls_mpi_free(&t1); + mbedtls_mpi_free(&t2); + mbedtls_mpi_free(&t3); + } ////////////////////////////////////// void SRP6A::createVerifyCode(const char *setupCode, uint8_t *verifyCode, uint8_t *salt){ - 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 + TempBuffer tBuf(80); // temporary buffer for staging + TempBuffer tHash(64); // temporary buffer for storing SHA-512 results + char *icp; // storage for I:P 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); - sprintf(icp,"Pair-Setup:%.3s-%.2s-%.3s",setupCode,setupCode+3,setupCode+5); + asprintf(&icp,"Pair-Setup:%.3s-%.2s-%.3s",setupCode,setupCode+3,setupCode+5); // compute x = SHA512( s | SHA512( I | ":" | P ) ) @@ -112,7 +125,8 @@ void SRP6A::createVerifyCode(const char *setupCode, uint8_t *verifyCode, uint8_t 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) mbedtls_mpi_write_binary(&v,verifyCode,384); // write v into verifyCode - + + free(icp); } ////////////////////////////////////// diff --git a/src/SRP.h b/src/SRP.h index 00068ac..d356287 100644 --- a/src/SRP.h +++ b/src/SRP.h @@ -31,7 +31,17 @@ #include #include -#include "HAPConstants.h" +#if defined(BOARD_HAS_PSRAM) +#define HS_MALLOC ps_malloc +#define HS_CALLOC ps_calloc +#define HS_REALLOC ps_realloc +#define ps_new(X) new(ps_malloc(sizeof(X)))X +#else +#define HS_MALLOC malloc +#define HS_CALLOC calloc +#define HS_REALLOC realloc +#define ps_new(X) new X +#endif ///////////////////////////////////////////////// // SRP-6A Structure from RFC 5054 (Nov 2007) @@ -41,6 +51,19 @@ // 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" +const char N3072[]="FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74" + "020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437" + "4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05" + "98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB" + "9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33" + "A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864" + "D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2" + "08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"; + struct SRP6A { mbedtls_mpi N; // N - 3072-bit Group pre-defined prime used for all SRP-6A calculations (384 bytes) @@ -71,6 +94,9 @@ struct SRP6A { 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 + ~SRP6A(); + + void *operator new(size_t size){return(HS_MALLOC(size));} // override new operator to use PSRAM when available void createVerifyCode(const char *setupCode, uint8_t *verifyCode, uint8_t *salt); void loadVerifyCode(uint8_t *verifyCode, uint8_t *salt); diff --git a/src/Utils.h b/src/Utils.h index 5fe89af..9d64db0 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -59,7 +59,7 @@ class TempBuffer { public: - TempBuffer(size_t _nElements) : nElements(_nElements) { + TempBuffer(size_t _nElements=1) : nElements(_nElements) { buf=(bufType *)HS_MALLOC(nElements*sizeof(bufType)); if(buf==NULL){ Serial.printf("\n\n*** FATAL ERROR: Requested allocation of %d bytes failed. Program Halting.\n\n",nElements*sizeof(bufType)); diff --git a/src/src.ino b/src/src.ino index 090e7c4..7ed3389 100644 --- a/src/src.ino +++ b/src/src.ino @@ -33,7 +33,7 @@ void setup() { Serial.begin(115200); - homeSpan.setLogLevel(2); + homeSpan.setLogLevel(2); // .setPairingCode("33344456"); homeSpan.begin(Category::Lighting,"HomeSpan Max");