diff --git a/CMakeLists.txt b/CMakeLists.txt index e121aaf3e3c..802a8f87d2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,12 +50,14 @@ set(CORE_SRCS cores/esp32/Esp.cpp cores/esp32/FunctionalInterrupt.cpp cores/esp32/HardwareSerial.cpp + cores/esp32/HEXBuilder.cpp cores/esp32/IPAddress.cpp cores/esp32/libb64/cdecode.c cores/esp32/libb64/cencode.c cores/esp32/main.cpp cores/esp32/MD5Builder.cpp cores/esp32/Print.cpp + cores/esp32/SHA1Builder.cpp cores/esp32/stdlib_noniso.c cores/esp32/Stream.cpp cores/esp32/StreamString.cpp diff --git a/cores/esp32/HEXBuilder.cpp b/cores/esp32/HEXBuilder.cpp new file mode 100644 index 00000000000..f3b0f7a9b3e --- /dev/null +++ b/cores/esp32/HEXBuilder.cpp @@ -0,0 +1,71 @@ +/* + Copyright (c) 2015 Hristo Gochkov. All rights reserved. + This file is part of the esp32 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include + +static uint8_t hex_char_to_byte(uint8_t c) +{ + return (c >= 'a' && c <= 'f') ? (c - ((uint8_t)'a' - 0xa)) : + (c >= 'A' && c <= 'F') ? (c - ((uint8_t)'A' - 0xA)) : + (c >= '0' && c<= '9') ? (c - (uint8_t)'0') : 0x10; // unknown char is 16 +} + +size_t HEXBuilder::hex2bytes(unsigned char * out, size_t maxlen, String &in) { + return hex2bytes(out, maxlen, in.c_str()); +} + +size_t HEXBuilder::hex2bytes(unsigned char * out, size_t maxlen, const char * in) { + size_t len = 0; + for(;*in;in++) { + uint8_t c = hex_char_to_byte(*in); + // Silently skip anything unknown. + if (c > 15) + continue; + + if (len & 1) { + if (len/2 < maxlen) + out[len/2] |= c; + } else { + if (len/2 < maxlen) + out[len/2] = c<<4; + } + len++; + } + return (len + 1)/2; +} + +size_t HEXBuilder::bytes2hex(char * out, size_t maxlen, const unsigned char * in, size_t len) { + for(size_t i = 0; i < len; i++) { + if (i*2 + 1 < maxlen) { + sprintf(out + (i * 2), "%02x", in[i]); + } + } + return len * 2 + 1; +} + +String HEXBuilder::bytes2hex(const unsigned char * in, size_t len) { + size_t maxlen = len * 2 + 1; + char * out = (char *) malloc(maxlen); + if (!out) return String(); + bytes2hex(out, maxlen, in, len); + String ret = String(out); + free(out); + return ret; +} diff --git a/cores/esp32/HEXBuilder.h b/cores/esp32/HEXBuilder.h new file mode 100644 index 00000000000..b3ec02ae267 --- /dev/null +++ b/cores/esp32/HEXBuilder.h @@ -0,0 +1,34 @@ +/* + Copyright (c) 2015 Hristo Gochkov. All rights reserved. + This file is part of the esp32 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef HEXBuilder_h +#define HEXBuilder_h + +#include +#include + +class HEXBuilder { +public: + static size_t hex2bytes(unsigned char * out, size_t maxlen, String & in); + static size_t hex2bytes(unsigned char * out, size_t maxlen, const char * in); + + static String bytes2hex(const unsigned char * in, size_t len); + static size_t bytes2hex(char * out, size_t maxlen, const unsigned char * in, size_t len); +}; +#endif diff --git a/cores/esp32/HashBuilder.h b/cores/esp32/HashBuilder.h new file mode 100644 index 00000000000..ce6f1f1af42 --- /dev/null +++ b/cores/esp32/HashBuilder.h @@ -0,0 +1,60 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef HashBuilder_h +#define HashBuilder_h + +#include +#include + +#include "HEXBuilder.h" + +class HashBuilder : public HEXBuilder +{ +public: + virtual ~HashBuilder() {} + virtual void begin() = 0; + + virtual void add(uint8_t* data, size_t len) = 0; + virtual void add(const char* data) + { + add((uint8_t*)data, strlen(data)); + } + virtual void add(char* data) + { + add((const char*)data); + } + virtual void add(String data) + { + add(data.c_str()); + } + + virtual void addHexString(const char* data) = 0; + virtual void addHexString(char* data) + { + addHexString((const char*)data); + } + virtual void addHexString(String data) + { + addHexString(data.c_str()); + } + + virtual bool addStream(Stream& stream, const size_t maxLen) = 0; + virtual void calculate() = 0; + virtual void getBytes(uint8_t* output) = 0; + virtual void getChars(char* output) = 0; + virtual String toString() = 0; +}; + +#endif diff --git a/cores/esp32/MD5Builder.cpp b/cores/esp32/MD5Builder.cpp index e242c3763b9..2198d06a27e 100644 --- a/cores/esp32/MD5Builder.cpp +++ b/cores/esp32/MD5Builder.cpp @@ -16,39 +16,30 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + #include +#include #include -static uint8_t hex_char_to_byte(uint8_t c) -{ - return (c >= 'a' && c <= 'f') ? (c - ((uint8_t)'a' - 0xa)) : - (c >= 'A' && c <= 'F') ? (c - ((uint8_t)'A' - 0xA)) : - (c >= '0' && c<= '9') ? (c - (uint8_t)'0') : 0; -} - void MD5Builder::begin(void) { memset(_buf, 0x00, ESP_ROM_MD5_DIGEST_LEN); esp_rom_md5_init(&_ctx); } -void MD5Builder::add(uint8_t * data, uint16_t len) +void MD5Builder::add(uint8_t * data, size_t len) { esp_rom_md5_update(&_ctx, data, len); } void MD5Builder::addHexString(const char * data) { - uint16_t i, len = strlen(data); + size_t len = strlen(data); uint8_t * tmp = (uint8_t*)malloc(len/2); if(tmp == NULL) { return; } - for(i=0; i #include @@ -25,41 +27,27 @@ #include "esp_system.h" #include "esp_rom_md5.h" -class MD5Builder +#include "HashBuilder.h" + +class MD5Builder : public HashBuilder { private: md5_context_t _ctx; uint8_t _buf[ESP_ROM_MD5_DIGEST_LEN]; public: - void begin(void); - void add(uint8_t * data, uint16_t len); - void add(const char * data) - { - add((uint8_t*)data, strlen(data)); - } - void add(char * data) - { - add((const char*)data); - } - void add(String data) - { - add(data.c_str()); - } - void addHexString(const char * data); - void addHexString(char * data) - { - addHexString((const char*)data); - } - void addHexString(String data) - { - addHexString(data.c_str()); - } - bool addStream(Stream & stream, const size_t maxLen); - void calculate(void); - void getBytes(uint8_t * output); - void getChars(char * output); - String toString(void); -}; + void begin(void) override; + using HashBuilder::add; + void add(uint8_t * data, size_t len) override; + + using HashBuilder::addHexString; + void addHexString(const char * data) override; + + bool addStream(Stream & stream, const size_t maxLen) override; + void calculate(void) override; + void getBytes(uint8_t * output) override; + void getChars(char * output) override; + String toString(void) override; +}; #endif diff --git a/cores/esp32/SHA1Builder.cpp b/cores/esp32/SHA1Builder.cpp new file mode 100644 index 00000000000..34f93271321 --- /dev/null +++ b/cores/esp32/SHA1Builder.cpp @@ -0,0 +1,367 @@ +/* + * FIPS-180-1 compliant SHA-1 implementation + * + * Copyright (C) 2006-2015, ARM Limited, All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is part of mbed TLS (https://tls.mbed.org) + * Modified for esp32 by Lucas Saavedra Vaz on 11 Jan 2024 + */ + +#include +#include + +// 32-bit integer manipulation macros (big endian) + +#ifndef GET_UINT32_BE +#define GET_UINT32_BE(n,b,i) \ +{ \ + (n) = ((uint32_t) (b)[(i) ] << 24) \ + | ((uint32_t) (b)[(i) + 1] << 16) \ + | ((uint32_t) (b)[(i) + 2] << 8) \ + | ((uint32_t) (b)[(i) + 3] ); \ +} +#endif + +#ifndef PUT_UINT32_BE +#define PUT_UINT32_BE(n,b,i) \ +{ \ + (b)[(i) ] = (uint8_t) ((n) >> 24); \ + (b)[(i) + 1] = (uint8_t) ((n) >> 16); \ + (b)[(i) + 2] = (uint8_t) ((n) >> 8); \ + (b)[(i) + 3] = (uint8_t) ((n) ); \ +} +#endif + +// Constants + +static const uint8_t sha1_padding[64] = +{ + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// Private methods + +void SHA1Builder::process(const uint8_t* data) +{ + uint32_t temp, W[16], A, B, C, D, E; + + GET_UINT32_BE(W[ 0], data, 0); + GET_UINT32_BE(W[ 1], data, 4); + GET_UINT32_BE(W[ 2], data, 8); + GET_UINT32_BE(W[ 3], data, 12); + GET_UINT32_BE(W[ 4], data, 16); + GET_UINT32_BE(W[ 5], data, 20); + GET_UINT32_BE(W[ 6], data, 24); + GET_UINT32_BE(W[ 7], data, 28); + GET_UINT32_BE(W[ 8], data, 32); + GET_UINT32_BE(W[ 9], data, 36); + GET_UINT32_BE(W[10], data, 40); + GET_UINT32_BE(W[11], data, 44); + GET_UINT32_BE(W[12], data, 48); + GET_UINT32_BE(W[13], data, 52); + GET_UINT32_BE(W[14], data, 56); + GET_UINT32_BE(W[15], data, 60); + +#define sha1_S(x,n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n))) + +#define sha1_R(t) \ +( \ + temp = W[(t - 3) & 0x0F] ^ W[(t - 8) & 0x0F] ^ \ + W[(t - 14) & 0x0F] ^ W[ t & 0x0F], \ + (W[t & 0x0F] = sha1_S(temp,1)) \ +) + +#define sha1_P(a,b,c,d,e,x) \ +{ \ + e += sha1_S(a,5) + sha1_F(b,c,d) + sha1_K + x; b = sha1_S(b,30); \ +} + + A = state[0]; + B = state[1]; + C = state[2]; + D = state[3]; + E = state[4]; + +#define sha1_F(x,y,z) (z ^ (x & (y ^ z))) +#define sha1_K 0x5A827999 + + sha1_P(A, B, C, D, E, W[0]); + sha1_P(E, A, B, C, D, W[1]); + sha1_P(D, E, A, B, C, W[2]); + sha1_P(C, D, E, A, B, W[3]); + sha1_P(B, C, D, E, A, W[4]); + sha1_P(A, B, C, D, E, W[5]); + sha1_P(E, A, B, C, D, W[6]); + sha1_P(D, E, A, B, C, W[7]); + sha1_P(C, D, E, A, B, W[8]); + sha1_P(B, C, D, E, A, W[9]); + sha1_P(A, B, C, D, E, W[10]); + sha1_P(E, A, B, C, D, W[11]); + sha1_P(D, E, A, B, C, W[12]); + sha1_P(C, D, E, A, B, W[13]); + sha1_P(B, C, D, E, A, W[14]); + sha1_P(A, B, C, D, E, W[15]); + sha1_P(E, A, B, C, D, sha1_R(16)); + sha1_P(D, E, A, B, C, sha1_R(17)); + sha1_P(C, D, E, A, B, sha1_R(18)); + sha1_P(B, C, D, E, A, sha1_R(19)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x,y,z) (x ^ y ^ z) +#define sha1_K 0x6ED9EBA1 + + sha1_P(A, B, C, D, E, sha1_R(20)); + sha1_P(E, A, B, C, D, sha1_R(21)); + sha1_P(D, E, A, B, C, sha1_R(22)); + sha1_P(C, D, E, A, B, sha1_R(23)); + sha1_P(B, C, D, E, A, sha1_R(24)); + sha1_P(A, B, C, D, E, sha1_R(25)); + sha1_P(E, A, B, C, D, sha1_R(26)); + sha1_P(D, E, A, B, C, sha1_R(27)); + sha1_P(C, D, E, A, B, sha1_R(28)); + sha1_P(B, C, D, E, A, sha1_R(29)); + sha1_P(A, B, C, D, E, sha1_R(30)); + sha1_P(E, A, B, C, D, sha1_R(31)); + sha1_P(D, E, A, B, C, sha1_R(32)); + sha1_P(C, D, E, A, B, sha1_R(33)); + sha1_P(B, C, D, E, A, sha1_R(34)); + sha1_P(A, B, C, D, E, sha1_R(35)); + sha1_P(E, A, B, C, D, sha1_R(36)); + sha1_P(D, E, A, B, C, sha1_R(37)); + sha1_P(C, D, E, A, B, sha1_R(38)); + sha1_P(B, C, D, E, A, sha1_R(39)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x,y,z) ((x & y) | (z & (x | y))) +#define sha1_K 0x8F1BBCDC + + sha1_P(A, B, C, D, E, sha1_R(40)); + sha1_P(E, A, B, C, D, sha1_R(41)); + sha1_P(D, E, A, B, C, sha1_R(42)); + sha1_P(C, D, E, A, B, sha1_R(43)); + sha1_P(B, C, D, E, A, sha1_R(44)); + sha1_P(A, B, C, D, E, sha1_R(45)); + sha1_P(E, A, B, C, D, sha1_R(46)); + sha1_P(D, E, A, B, C, sha1_R(47)); + sha1_P(C, D, E, A, B, sha1_R(48)); + sha1_P(B, C, D, E, A, sha1_R(49)); + sha1_P(A, B, C, D, E, sha1_R(50)); + sha1_P(E, A, B, C, D, sha1_R(51)); + sha1_P(D, E, A, B, C, sha1_R(52)); + sha1_P(C, D, E, A, B, sha1_R(53)); + sha1_P(B, C, D, E, A, sha1_R(54)); + sha1_P(A, B, C, D, E, sha1_R(55)); + sha1_P(E, A, B, C, D, sha1_R(56)); + sha1_P(D, E, A, B, C, sha1_R(57)); + sha1_P(C, D, E, A, B, sha1_R(58)); + sha1_P(B, C, D, E, A, sha1_R(59)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x,y,z) (x ^ y ^ z) +#define sha1_K 0xCA62C1D6 + + sha1_P(A, B, C, D, E, sha1_R(60)); + sha1_P(E, A, B, C, D, sha1_R(61)); + sha1_P(D, E, A, B, C, sha1_R(62)); + sha1_P(C, D, E, A, B, sha1_R(63)); + sha1_P(B, C, D, E, A, sha1_R(64)); + sha1_P(A, B, C, D, E, sha1_R(65)); + sha1_P(E, A, B, C, D, sha1_R(66)); + sha1_P(D, E, A, B, C, sha1_R(67)); + sha1_P(C, D, E, A, B, sha1_R(68)); + sha1_P(B, C, D, E, A, sha1_R(69)); + sha1_P(A, B, C, D, E, sha1_R(70)); + sha1_P(E, A, B, C, D, sha1_R(71)); + sha1_P(D, E, A, B, C, sha1_R(72)); + sha1_P(C, D, E, A, B, sha1_R(73)); + sha1_P(B, C, D, E, A, sha1_R(74)); + sha1_P(A, B, C, D, E, sha1_R(75)); + sha1_P(E, A, B, C, D, sha1_R(76)); + sha1_P(D, E, A, B, C, sha1_R(77)); + sha1_P(C, D, E, A, B, sha1_R(78)); + sha1_P(B, C, D, E, A, sha1_R(79)); + +#undef sha1_K +#undef sha1_F + + state[0] += A; + state[1] += B; + state[2] += C; + state[3] += D; + state[4] += E; +} + +// Public methods + +void SHA1Builder::begin(void) +{ + total[0] = 0; + total[1] = 0; + + state[0] = 0x67452301; + state[1] = 0xEFCDAB89; + state[2] = 0x98BADCFE; + state[3] = 0x10325476; + state[4] = 0xC3D2E1F0; + + memset(buffer, 0x00, sizeof(buffer)); + memset(hash, 0x00, sizeof(hash)); +} + +void SHA1Builder::add(uint8_t* data, size_t len) +{ + size_t fill; + uint32_t left; + + if(len == 0) + { + return; + } + + left = total[0] & 0x3F; + fill = 64 - left; + + total[0] += (uint32_t) len; + total[0] &= 0xFFFFFFFF; + + if(total[0] < (uint32_t) len) + { + total[1]++; + } + + if(left && len >= fill) + { + memcpy((void *) (buffer + left), data, fill); + process(buffer); + data += fill; + len -= fill; + left = 0; + } + + while(len >= 64) + { + process(data); + data += 64; + len -= 64; + } + + if(len > 0) { + memcpy((void *) (buffer + left), data, len); + } +} + +void SHA1Builder::addHexString(const char * data) +{ + uint16_t len = strlen(data); + uint8_t * tmp = (uint8_t*)malloc(len/2); + if(tmp == NULL) { + return; + } + hex2bytes(tmp, len/2, data); + add(tmp, len/2); + free(tmp); +} + +bool SHA1Builder::addStream(Stream & stream, const size_t maxLen) +{ + const int buf_size = 512; + int maxLengthLeft = maxLen; + uint8_t * buf = (uint8_t*) malloc(buf_size); + + if(!buf) { + return false; + } + + int bytesAvailable = stream.available(); + while((bytesAvailable > 0) && (maxLengthLeft > 0)) { + + // determine number of bytes to read + int readBytes = bytesAvailable; + if(readBytes > maxLengthLeft) { + readBytes = maxLengthLeft ; // read only until max_len + } + if(readBytes > buf_size) { + readBytes = buf_size; // not read more the buffer can handle + } + + // read data and check if we got something + int numBytesRead = stream.readBytes(buf, readBytes); + if(numBytesRead< 1) { + free(buf); + return false; + } + + // Update SHA1 with buffer payload + add(buf, numBytesRead); + + // update available number of bytes + maxLengthLeft -= numBytesRead; + bytesAvailable = stream.available(); + } + free(buf); + return true; +} + +void SHA1Builder::calculate(void) +{ + uint32_t last, padn; + uint32_t high, low; + uint8_t msglen[8]; + + high = (total[0] >> 29) | (total[1] << 3); + low = (total[0] << 3); + + PUT_UINT32_BE(high, msglen, 0); + PUT_UINT32_BE(low, msglen, 4); + + last = total[0] & 0x3F; + padn = (last < 56) ? (56 - last) : (120 - last); + + add((uint8_t*)sha1_padding, padn); + add(msglen, 8); + + PUT_UINT32_BE(state[0], hash, 0); + PUT_UINT32_BE(state[1], hash, 4); + PUT_UINT32_BE(state[2], hash, 8); + PUT_UINT32_BE(state[3], hash, 12); + PUT_UINT32_BE(state[4], hash, 16); +} + +void SHA1Builder::getBytes(uint8_t * output) +{ + memcpy(output, hash, SHA1_HASH_SIZE); +} + +void SHA1Builder::getChars(char * output) +{ + bytes2hex(output, SHA1_HASH_SIZE*2+1, hash, SHA1_HASH_SIZE); +} + +String SHA1Builder::toString(void) +{ + char out[(SHA1_HASH_SIZE * 2) + 1]; + getChars(out); + return String(out); +} diff --git a/cores/esp32/SHA1Builder.h b/cores/esp32/SHA1Builder.h new file mode 100644 index 00000000000..4a0dfe0c100 --- /dev/null +++ b/cores/esp32/SHA1Builder.h @@ -0,0 +1,51 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SHA1Builder_h +#define SHA1Builder_h + +#include +#include + +#include "HashBuilder.h" + +#define SHA1_HASH_SIZE 20 + +class SHA1Builder : public HashBuilder +{ +private: + uint32_t total[2]; /* number of bytes processed */ + uint32_t state[5]; /* intermediate digest state */ + unsigned char buffer[64]; /* data block being processed */ + uint8_t hash[SHA1_HASH_SIZE]; /* SHA-1 result */ + + void process(const uint8_t* data); + +public: + void begin() override; + + using HashBuilder::add; + void add(uint8_t* data, size_t len) override; + + using HashBuilder::addHexString; + void addHexString(const char* data) override; + + bool addStream(Stream& stream, const size_t maxLen) override; + void calculate() override; + void getBytes(uint8_t* output) override; + void getChars(char* output) override; + String toString() override; +}; + +#endif diff --git a/libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino b/libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino new file mode 100644 index 00000000000..3e9ae5bfd53 --- /dev/null +++ b/libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino @@ -0,0 +1,71 @@ +#include + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + Serial.println("\n\n\nStart."); + + // Convert a HEX string like 6c6c6f20576f726c64 to a binary buffer + { + const char * out = "Hello World"; + const char * hexin = "48656c6c6f20576f726c6400"; // As the string above is \0 terminated too + + unsigned char buff[256]; + size_t len = HEXBuilder::hex2bytes(buff, sizeof(buff), hexin); + + if (len != 1 + strlen(out)) + Serial.println("Odd - length 1 is wrong"); + + if (memcmp(buff, out, len) != 0) + Serial.println("Odd - decode 1 went wrong"); + + // Safe to print this binary buffer -- as we've included a \0 in the hex sequence. + Serial.printf("IN: <%s>\nOUT <%s\\0>\n", hexin, buff); + }; + + { + String helloHEX = "48656c6c6f20576f726c64"; + const char hello[] = "Hello World"; + + unsigned char buff[256]; + size_t len = HEXBuilder::hex2bytes(buff, sizeof(buff), helloHEX); + + if (len != strlen(hello)) + Serial.println("Odd - length 2 is wrong"); + + if (strcmp((char *) buff, hello) != 0) + Serial.println("Odd - decode 2 went wrong"); + } + + { + const unsigned char helloBytes[] = { 0x48, 0x56, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 }; + String helloHEX = "48566c6c6f20576f726c64"; + + + String out = HEXBuilder::bytes2hex(helloBytes, sizeof(helloBytes)); + if (out.length() != 2 * sizeof(helloBytes)) + Serial.println("Odd - length 3 is wrong"); + + // we need to ignore case - as a hex string can be spelled in uppercase and lowercase + if (!out.equalsIgnoreCase(helloHEX)) { + Serial.println("Odd - decode 3 went wrong"); + } + } + + { + const unsigned char helloBytes[] = { 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 }; + const char helloHex[] = "6c6c6f20576f726c64"; + + char buff[256]; + size_t len = HEXBuilder::bytes2hex(buff, sizeof(buff), helloBytes, sizeof(helloBytes)); + if (len != 1 + 2 * sizeof(helloBytes)) + Serial.println("Odd - length 4 is wrong"); + + // we need to ignore case - as a hex string can be spelled in uppercase and lowercase + if (strcasecmp(buff, helloHex)) + Serial.println("Odd - decode 4 went wrong"); + } + Serial.println("Done."); +} + +void loop() {} diff --git a/libraries/ESP32/examples/Utilities/MD5Builder/MD5Builder.ino b/libraries/ESP32/examples/Utilities/MD5Builder/MD5Builder.ino new file mode 100644 index 00000000000..9b439587000 --- /dev/null +++ b/libraries/ESP32/examples/Utilities/MD5Builder/MD5Builder.ino @@ -0,0 +1,96 @@ +#include + +// Occasionally it is useful to compare a password that the user +// has entered to a build in string. However this means that the +// password ends up `in the clear' in the firmware and in your +// source code. +// +// MD5Builder helps you obfuscate this (it is not terribly secure, MD5 +// has been phased out as insecure eons ago) by letting you create an +// MD5 of the data the user entered; and then compare this to an MD5 +// string that you have put in your code. + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + Serial.println("\n\n\nStart."); + + // Check if a password obfuscated in an MD5 actually + // matches the original string. + // + // echo -n "Hello World" | openssl md5 + { + String md5 = "b10a8db164e0754105b7a99be72e3fe5"; + String password = "Hello World"; + + MD5Builder md; + + md.begin(); + md.add(password); + md.calculate(); + + String result = md.toString(); + + if (!md5.equalsIgnoreCase(result)) + Serial.println("Odd - failing MD5 on String"); + else + Serial.println("OK!"); + } + + // Check that this also work if we add the password not as + // a normal string - but as a string with the HEX values. + { + String passwordAsHex = "48656c6c6f20576f726c64"; + String md5 = "b10a8db164e0754105b7a99be72e3fe5"; + + MD5Builder md; + + md.begin(); + md.addHexString(passwordAsHex); + md.calculate(); + + String result = md.toString(); + + if (!md5.equalsIgnoreCase(result)) { + Serial.println("Odd - failing MD5 on hex string"); + Serial.println(md5); + Serial.println(result); + } + else + Serial.println("OK!"); + + } + + // Check that this also work if we add the password as + // an unsigned byte array. + { + uint8_t password[] = { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 }; + String md5 = "b10a8db164e0754105b7a99be72e3fe5"; + MD5Builder md; + + md.begin(); + md.add(password, sizeof(password)); + md.calculate(); + + String result = md.toString(); + + if (!md5.equalsIgnoreCase(result)) + Serial.println("Odd - failing MD5 on byte array"); + else + Serial.println("OK!"); + + // And also check that we can compare this as pure, raw, bytes + uint8_t raw[16] = { 0xb1, 0x0a, 0x8d, 0xb1, 0x64, 0xe0, 0x75, 0x41, + 0x05, 0xb7, 0xa9, 0x9b, 0xe7, 0x2e, 0x3f, 0xe5 + }; + uint8_t res[16]; + md.getBytes(res); + if (memcmp(raw, res, 16)) + Serial.println("Odd - failing MD5 on byte array when compared as bytes"); + else + Serial.println("OK!"); + + } +} + +void loop() {} diff --git a/libraries/ESP32/examples/Utilities/SHA1Builder/SHA1Builder.ino b/libraries/ESP32/examples/Utilities/SHA1Builder/SHA1Builder.ino new file mode 100644 index 00000000000..40270953473 --- /dev/null +++ b/libraries/ESP32/examples/Utilities/SHA1Builder/SHA1Builder.ino @@ -0,0 +1,97 @@ +#include + +// Occasionally it is useful to compare a password that the user +// has entered to a build in string. However this means that the +// password ends up `in the clear' in the firmware and in your +// source code. +// +// SHA1Builder helps you obfuscate this (This is not much more secure. +// SHA1 is past its retirement age and long obsolte/insecure, but it helps +// a little) by letting you create an (unsalted!) SHA1 of the data the +// user entered; and then compare this to an SHA1 string that you have put +// in your code. + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + Serial.println("\n\n\nStart."); + + // Check if a password obfuscated in an SHA1 actually + // matches the original string. + // + // echo -n "Hello World" | openssl sha1 + { + String sha1_str = "0a4d55a8d778e5022fab701977c5d840bbc486d0"; + String password = "Hello World"; + + SHA1Builder sha; + + sha.begin(); + sha.add(password); + sha.calculate(); + + String result = sha.toString(); + + if (!sha1_str.equalsIgnoreCase(result)) + Serial.println("Odd - failing SHA1 on String"); + else + Serial.println("OK!"); + } + + // Check that this also work if we add the password not as + // a normal string - but as a string with the HEX values. + { + String passwordAsHex = "48656c6c6f20576f726c64"; + String sha1_str = "0a4d55a8d778e5022fab701977c5d840bbc486d0"; + + SHA1Builder sha; + + sha.begin(); + sha.addHexString(passwordAsHex); + sha.calculate(); + + String result = sha.toString(); + + if (!sha1_str.equalsIgnoreCase(result)) { + Serial.println("Odd - failing SHA1 on hex string"); + Serial.println(sha1_str); + Serial.println(result); + } + else + Serial.println("OK!"); + + } + + // Check that this also work if we add the password as + // an unsigned byte array. + { + uint8_t password[] = { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 }; + String sha1_str = "0a4d55a8d778e5022fab701977c5d840bbc486d0"; + SHA1Builder sha; + + sha.begin(); + sha.add(password, sizeof(password)); + sha.calculate(); + + String result = sha.toString(); + + if (!sha1_str.equalsIgnoreCase(result)) + Serial.println("Odd - failing SHA1 on byte array"); + else + Serial.println("OK!"); + + // And also check that we can compare this as pure, raw, bytes + uint8_t raw[20] = { 0x0a, 0x4d, 0x55, 0xa8, 0xd7, 0x78, 0xe5, 0x02, 0x2f, 0xab, + 0x70, 0x19, 0x77, 0xc5, 0xd8, 0x40, 0xbb, 0xc4, 0x86, 0xd0 + }; + uint8_t res[20]; + sha.getBytes(res); + if (memcmp(raw, res, 20)) + Serial.println("Odd - failing SHA1 on byte array when compared as bytes"); + else + Serial.println("OK!"); + + } +} + +void loop() {} diff --git a/libraries/WebServer/examples/HttpAuthCallback/.skip.esp32h2 b/libraries/WebServer/examples/HttpAuthCallback/.skip.esp32h2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/WebServer/examples/HttpAuthCallback/HttpAuthCallback.ino b/libraries/WebServer/examples/HttpAuthCallback/HttpAuthCallback.ino new file mode 100644 index 00000000000..8cdded55a1c --- /dev/null +++ b/libraries/WebServer/examples/HttpAuthCallback/HttpAuthCallback.ino @@ -0,0 +1,64 @@ +#include +#include +#include +#include + +const char* ssid = "........"; +const char* password = "........"; + +WebServer server(80); + +typedef struct credentials_t { + String username; + String password; +} credentials_t; + +credentials_t passwdfile[] = { + { "admin", "esp32" }, + { "fred", "41234123" }, + { "charlie", "sdfsd" }, + { "alice", "vambdnkuhj" }, + { "bob", "svcdbjhws12" }, +}; +const size_t N_CREDENTIALS = sizeof(passwdfile) / sizeof(credentials_t); + +String * credentialsHandler(HTTPAuthMethod mode, String username, String params[]) +{ + for (int i = 0; i < N_CREDENTIALS; i++) { + if (username == passwdfile[i].username) + return new String(passwdfile[i].password); + } + return NULL; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("WiFi Connect Failed! Rebooting..."); + delay(1000); + ESP.restart(); + } + ArduinoOTA.begin(); + + server.on("/", []() { + if (!server.authenticate(&credentialsHandler)) { + server.requestAuthentication(); + return; + } + server.send(200, "text/plain", "Login OK"); + }); + server.begin(); + + Serial.print("Open http://"); + Serial.print(WiFi.localIP()); + Serial.println("/ in your browser to see it working"); +} + +void loop() { + ArduinoOTA.handle(); + server.handleClient(); + delay(2);//allow the cpu to switch to other tasks +} diff --git a/libraries/WebServer/examples/HttpAuthCallbackInline/.skip.esp32h2 b/libraries/WebServer/examples/HttpAuthCallbackInline/.skip.esp32h2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/WebServer/examples/HttpAuthCallbackInline/HttpAuthCallbackInline.ino b/libraries/WebServer/examples/HttpAuthCallbackInline/HttpAuthCallbackInline.ino new file mode 100644 index 00000000000..48f0d3f8127 --- /dev/null +++ b/libraries/WebServer/examples/HttpAuthCallbackInline/HttpAuthCallbackInline.ino @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +const char* ssid = "........"; +const char* password = "........"; + +WebServer server(80); + +typedef struct credentials_t { + char * username; + char * password; +} credentials_t; + +credentials_t passwdfile[] = { + { "admin", "esp32" }, + { "fred", "41234123" }, + { "charlie", "sdfsd" }, + { "alice", "vambdnkuhj" }, + { "bob", "svcdbjhws12" }, + { NULL, NULL } +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("WiFi Connect Failed! Rebooting..."); + delay(1000); + ESP.restart(); + } + ArduinoOTA.begin(); + + server.on("/", []() { + if (!server.authenticate([](HTTPAuthMethod mode, String username, String params[]) -> String * { + // Scan the password list for the username and return the password if + // we find the username. + // + for (credentials_t * entry = passwdfile; entry->username; entry++) { + if (username == entry->username) { + return new String(entry->password); + }; + }; + // we've not found the user in the list. + return NULL; + })) + { + server.requestAuthentication(); + return; + } + server.send(200, "text/plain", "Login OK"); + }); + server.begin(); + + Serial.print("Open http://"); + Serial.print(WiFi.localIP()); + Serial.println("/ in your browser to see it working"); +} + +void loop() { + ArduinoOTA.handle(); + server.handleClient(); + delay(2);//allow the cpu to switch to other tasks +} diff --git a/libraries/WebServer/examples/HttpBasicAuthSHA1/.skip.esp32h2 b/libraries/WebServer/examples/HttpBasicAuthSHA1/.skip.esp32h2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/WebServer/examples/HttpBasicAuthSHA1/HttpBasicAuthSHA1.ino b/libraries/WebServer/examples/HttpBasicAuthSHA1/HttpBasicAuthSHA1.ino new file mode 100644 index 00000000000..bfc2df6d037 --- /dev/null +++ b/libraries/WebServer/examples/HttpBasicAuthSHA1/HttpBasicAuthSHA1.ino @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +// Rather than specify the password as plaintext; we +// provide it as an (unsalted!) SHA1 hash. This is not +// much more secure (SHA1 is past its retirement age, +// and long obsolte/insecure) - but it helps a little. + +const char* ssid = "........"; +const char* password = "........"; + +WebServer server(80); + +// Passwords as plaintext - human readable and easily visible in +// the sourcecode and in the firmware/binary. +const char* www_username = "admin"; +const char* www_password = "esp32"; + +// The sha1 of 'esp32' (without the trailing \0) expressed as 20 +// bytes of hex. Created by for example 'echo -n esp32 | openssl sha1' +// or http://www.sha1-online.com. +const char* www_username_hex = "hexadmin"; +const char* www_password_hex = "8cb124f8c277c16ec0b2ee00569fd151a08e342b"; + +// The same; but now expressed as a base64 string (e.g. as commonly used +// by webservers). Created by ` echo -n esp32 | openssl sha1 -binary | openssl base64` +const char* www_username_base64 = "base64admin"; +const char* www_password_base64 = "jLEk+MJ3wW7Asu4AVp/RUaCONCs="; + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("WiFi Connect Failed! Rebooting..."); + delay(1000); + ESP.restart(); + } + ArduinoOTA.begin(); + + server.on("/", []() { + if (server.authenticate(www_username, www_password)) { + server.send(200, "text/plain", "Login against cleartext password OK"); + return; + } + if (server.authenticateBasicSHA1(www_username_hex, www_password_hex)) { + server.send(200, "text/plain", "Login against HEX of the SHA1 of the password OK"); + return; + } + if (server.authenticateBasicSHA1(www_username_base64, www_password_base64)) { + server.send(200, "text/plain", "Login against Base64 of the SHA1 of the password OK"); + return; + } + Serial.println("No/failed authentication"); + return server.requestAuthentication(); + }); + + server.begin(); + + Serial.print("Open http://"); + Serial.print(WiFi.localIP()); + Serial.println("/ in your browser to see it working"); +} + +void loop() { + ArduinoOTA.handle(); + server.handleClient(); + delay(2);//allow the cpu to switch to other tasks +} diff --git a/libraries/WebServer/examples/HttpBasicAuthSHA1orBearerToken/.skip.esp32h2 b/libraries/WebServer/examples/HttpBasicAuthSHA1orBearerToken/.skip.esp32h2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/WebServer/examples/HttpBasicAuthSHA1orBearerToken/HttpBasicAuthSHA1orBearerToken.ino b/libraries/WebServer/examples/HttpBasicAuthSHA1orBearerToken/HttpBasicAuthSHA1orBearerToken.ino new file mode 100644 index 00000000000..ecf04031181 --- /dev/null +++ b/libraries/WebServer/examples/HttpBasicAuthSHA1orBearerToken/HttpBasicAuthSHA1orBearerToken.ino @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +// We have two options - we either come in with a bearer +// token - i.e. a special header or API token; or we +// get a normal HTTP style basic auth prompt. +// +// To do a bearer fetch - use something like Swagger or with curl: +// +// curl https://myesp.com/ -H "Authorization: Bearer SecritToken" +// +// We avoid hardcoding this "SecritToken" into the code by +// using a SHA1 instead (which is not paricularly secure). + +// Create the secret token SHA1 with: +// echo -n SecritToken | openssl sha1 + +String secret_token_hex = "d2cce6b472959484a21c3194080c609b8a2c910b"; + +// Wifi credentials + +const char* ssid = "........"; +const char* password = "........"; + +WebServer server(80); + +// Rather than specify the admin password as plaintext; we +// provide it as an (unsalted!) SHA1 hash. This is not +// much more secure (SHA1 is past its retirement age, +// and long obsolte/insecure) - but it helps a little. + +// The sha1 of 'esp32' (without the trailing \0) expressed as 20 +// bytes of hex. Created by for example 'echo -n esp32 | openssl sha1' +// or http://www.sha1-online.com. +const char* www_username_hex = "admin"; +const char* www_password_hex = "8cb124f8c277c16ec0b2ee00569fd151a08e342b"; + +static unsigned char _bearer[20]; + +String* check_bearer_or_auth(HTTPAuthMethod mode, String authReq, String params[]) { + // we expect authReq to be "bearer some-secret" + String lcAuthReq = authReq; + lcAuthReq.toLowerCase(); + if (mode == OTHER_AUTH && (lcAuthReq.startsWith("bearer "))) { + String secret = authReq.substring(7); + secret.trim(); + + uint8_t sha1[20]; + SHA1Builder sha_builder; + + sha_builder.begin(); + sha_builder.add((uint8_t*) secret.c_str(), secret.length()); + sha_builder.calculate(); + sha_builder.getBytes(sha1); + + if (memcmp(_bearer, sha1, sizeof(_bearer)) == 0) { + Serial.println("Bearer token matches"); + return new String("anything non null"); + } else { + Serial.println("Bearer token does not match"); + } + } else if (mode == BASIC_AUTH) { + bool ret = server.authenticateBasicSHA1(www_username_hex, www_password_hex); + if (ret) { + Serial.println("Basic auth succeeded"); + return new String(params[0]); + } else { + Serial.println("Basic auth failed"); + } + } + + // No auth found + return NULL; +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("WiFi Connect Failed! Rebooting..."); + delay(1000); + ESP.restart(); + } + ArduinoOTA.begin(); + + // Convert token to a convenient binary representation. + size_t len = HEXBuilder::hex2bytes(_bearer, sizeof(_bearer), secret_token_hex); + if (len != 20) + Serial.println("Bearer token does not look like a valid SHA1 hex string ?!"); + + server.on("/", []() { + if (!server.authenticate(&check_bearer_or_auth)) { + Serial.println("No/failed authentication"); + return server.requestAuthentication(); + } + Serial.println("Authentication succeeded"); + server.send(200, "text/plain", "Login OK"); + return; + }); + + server.begin(); + + Serial.print("Open http://"); + Serial.print(WiFi.localIP()); + Serial.println("/ in your browser to see it working"); +} + +void loop() { + ArduinoOTA.handle(); + server.handleClient(); + delay(2);//allow the cpu to switch to other tasks +} diff --git a/libraries/WebServer/src/WebServer.cpp b/libraries/WebServer/src/WebServer.cpp index ea9b4d5692e..dd443dbd8e8 100644 --- a/libraries/WebServer/src/WebServer.cpp +++ b/libraries/WebServer/src/WebServer.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include "esp_random.h" #include "WiFiServer.h" @@ -31,7 +32,8 @@ #include "FS.h" #include "detail/RequestHandlersImpl.h" #include "MD5Builder.h" - +#include "SHA1Builder.h" +#include "base64.h" static const char AUTHORIZATION_HEADER[] = "Authorization"; static const char qop_auth[] PROGMEM = "qop=auth"; @@ -40,7 +42,6 @@ static const char WWW_Authenticate[] = "WWW-Authenticate"; static const char Content_Length[] = "Content-Length"; static const char ETAG_HEADER[] = "If-None-Match"; - WebServer::WebServer(IPAddress addr, int port) : _corsEnabled(false) , _server(addr, port) @@ -128,91 +129,185 @@ static String md5str(String &in){ return md5.toString(); } -bool WebServer::authenticate(const char * username, const char * password){ - if(hasHeader(FPSTR(AUTHORIZATION_HEADER))) { - String authReq = header(FPSTR(AUTHORIZATION_HEADER)); - if(authReq.startsWith(F("Basic"))){ - authReq = authReq.substring(6); - authReq.trim(); - char toencodeLen = strlen(username)+strlen(password)+1; - char *toencode = (char *)malloc(toencodeLen + 1); - if(toencode == NULL){ - authReq = ""; - return false; - } - char *encoded = (char *)malloc(base64_encode_expected_len(toencodeLen)+1); - if(encoded == NULL){ - authReq = ""; - free(toencode); - return false; - } - sprintf(toencode, "%s:%s", username, password); - if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) { - authReq = ""; - free(toencode); - free(encoded); - return true; - } - free(toencode); - free(encoded);; - } else if(authReq.startsWith(F("Digest"))) { - authReq = authReq.substring(7); - log_v("%s", authReq.c_str()); - String _username = _extractParam(authReq,F("username=\""),'\"'); - if(!_username.length() || _username != String(username)) { - authReq = ""; - return false; - } - // extracting required parameters for RFC 2069 simpler Digest - String _realm = _extractParam(authReq, F("realm=\""),'\"'); - String _nonce = _extractParam(authReq, F("nonce=\""),'\"'); - String _uri = _extractParam(authReq, F("uri=\""),'\"'); - String _response = _extractParam(authReq, F("response=\""),'\"'); - String _opaque = _extractParam(authReq, F("opaque=\""),'\"'); - - if((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_response.length()) || (!_opaque.length())) { - authReq = ""; - return false; - } - if((_opaque != _sopaque) || (_nonce != _snonce) || (_realm != _srealm)) { - authReq = ""; - return false; - } - // parameters for the RFC 2617 newer Digest - String _nc,_cnonce; - if(authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { - _nc = _extractParam(authReq, F("nc="), ','); - _cnonce = _extractParam(authReq, F("cnonce=\""),'\"'); - } - String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password)); - log_v("Hash of user:realm:pass=%s", _H1.c_str()); - String _H2 = ""; - if(_currentMethod == HTTP_GET){ - _H2 = md5str(String(F("GET:")) + _uri); - }else if(_currentMethod == HTTP_POST){ - _H2 = md5str(String(F("POST:")) + _uri); - }else if(_currentMethod == HTTP_PUT){ - _H2 = md5str(String(F("PUT:")) + _uri); - }else if(_currentMethod == HTTP_DELETE){ - _H2 = md5str(String(F("DELETE:")) + _uri); - }else{ - _H2 = md5str(String(F("GET:")) + _uri); - } - log_v("Hash of GET:uri=%s", _H2.c_str()); - String _responsecheck = ""; - if(authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { - _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); - } else { - _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); - } - log_v("The Proper response=%s", _responsecheck.c_str()); - if(_response == _responsecheck){ +bool WebServer::authenticateBasicSHA1(const char * _username, const char * _sha1Base64orHex) { + return WebServer::authenticate([_username,_sha1Base64orHex](HTTPAuthMethod mode, String username, String params[])->String * { + // rather than work on a password to compare with; we take the sha1 of the + // password received over the wire and compare that to the base64 encoded + // sha1 passed as _sha1base64. That way there is no need to have a + // plaintext password in the code/binary (though note that SHA1 is well + // past its retirement age). When that matches - we `cheat' by returning + // the password we got in the first place; so the normal BasicAuth + // can be completed. Note that this cannot work for a digest auth - + // as there the password in the clear is part of the calculation. + + if (params == nullptr) { + log_e("Something went wrong. params is NULL"); + return NULL; + } + + uint8_t sha1[20]; + char sha1calc[48]; // large enough for base64 and Hex represenation + String ret; + SHA1Builder sha_builder; + base64 b64; + + log_v("Trying to authenticate user %s using SHA1.", username.c_str()); + sha_builder.begin(); + sha_builder.add((uint8_t*) params[0].c_str(), params[0].length()); + sha_builder.calculate(); + sha_builder.getBytes(sha1); + + // we can either decode _sha1base64orHex and then compare the 20 bytes; + // or encode the sha we calculated. We pick the latter as encoding of a + // fixed array of 20 bytes is safer than operating on something external. + if (strlen(_sha1Base64orHex) == 20 * 2) { // 2 chars per byte + sha_builder.bytes2hex(sha1calc, sizeof(sha1calc), sha1, sizeof(sha1)); + log_v("Calculated SHA1 in hex: %s", sha1calc); + } else { + ret = b64.encode(sha1, sizeof(sha1)); + ret.toCharArray(sha1calc, sizeof(sha1calc)); + log_v("Calculated SHA1 in base64: %s", sha1calc); + } + + return ((username.equalsConstantTime(_username)) && + (String((char*)sha1calc).equalsConstantTime(_sha1Base64orHex)) && + (mode == BASIC_AUTH) /* to keep things somewhat time constant. */ + ) ? new String(params[0]) : NULL; + }); +} + +bool WebServer::authenticate(const char * _username, const char * _password) { + return WebServer::authenticate([_username,_password](HTTPAuthMethod mode, String username, String params[])->String * { + return username.equalsConstantTime(_username) ? new String(_password) : NULL; + }); +} + +bool WebServer::authenticate(THandlerFunctionAuthCheck fn) { + if (!hasHeader(FPSTR(AUTHORIZATION_HEADER))) { + return false; + } + + String authReq = header(FPSTR(AUTHORIZATION_HEADER)); + if (authReq.startsWith(AuthTypeBasic)) { + log_v("Trying to authenticate using Basic Auth"); + bool ret = false; + + authReq = authReq.substring(6); // length of AuthTypeBasic including the space at the end. + authReq.trim(); + + /* base64 encoded string is always shorter (or equal) in length */ + char* decoded = (authReq.length() < HTTP_MAX_BASIC_AUTH_LEN) ? new char[authReq.length()] : NULL; + if (decoded) { + char* p; + if (base64_decode_chars(authReq.c_str(), authReq.length(), decoded) && (p = index(decoded, ':')) && p) { authReq = ""; - return true; + /* Note: rfc7617 guarantees that there will not be an escaped colon in the username itself. + * Note: base64_decode_chars() guarantees a terminating \0 + */ + *p = '\0'; + char * _username = decoded, * _password = p + 1; + String params[] = { + _password, + _srealm + }; + String* password = fn(BASIC_AUTH, _username, params); + + if (password) { + ret = password->equalsConstantTime(_password); + // we're more concerned about the password; as the attacker already + // knows the _pasword. Arduino's string handling is simple; it reallocs + // even when smaller; so a memset is enough (no capacity/size). + memset((void *)password->c_str(), 0, password->length()); + delete password; + } } + delete[] decoded; + } + authReq = ""; + log_v("Authentication %s", ret ? "Success" : "Failed"); + return ret; + } else if (authReq.startsWith(AuthTypeDigest)) { + log_v("Trying to authenticate using Digest Auth"); + authReq = authReq.substring(7); + log_v("%s", authReq.c_str()); + + // extracting required parameters for RFC 2069 simpler Digest + String _username = _extractParam(authReq, F("username=\""), '\"'); + String _realm = _extractParam(authReq, F("realm=\""), '\"'); + String _uri = _extractParam(authReq, F("uri=\""), '\"'); + if (!_username.length()) + goto exf; + + String params[] = { + _realm, + _uri + }; + String* password = fn(DIGEST_AUTH, _username, params); + if (!password) + goto exf; + + String _H1 = md5str(String(_username) + ':' + _realm + ':' + * password); + // we're extra concerned; as digest request us to know the password + // in the clear. + memset((void *) password->c_str(), 0, password->length()); + delete password; + _username = ""; + + String _nonce = _extractParam(authReq, F("nonce=\""), '\"'); + String _response = _extractParam(authReq, F("response=\""), '\"'); + String _opaque = _extractParam(authReq, F("opaque=\""), '\"'); + + if ((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_response.length()) || (!_opaque.length())) + goto exf; + + if ((_opaque != _sopaque) || (_nonce != _snonce) || (_realm != _srealm)) + goto exf; + + // parameters for the RFC 2617 newer Digest + String _nc, _cnonce; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _nc = _extractParam(authReq, F("nc="), ','); + _cnonce = _extractParam(authReq, F("cnonce=\""), '\"'); + } + + log_v("Hash of user:realm:pass=%s", _H1.c_str()); + String _H2 = ""; + if (_currentMethod == HTTP_GET) { + _H2 = md5str(String(F("GET:")) + _uri); + } else if (_currentMethod == HTTP_POST) { + _H2 = md5str(String(F("POST:")) + _uri); + } else if (_currentMethod == HTTP_PUT) { + _H2 = md5str(String(F("PUT:")) + _uri); + } else if (_currentMethod == HTTP_DELETE) { + _H2 = md5str(String(F("DELETE:")) + _uri); + } else { + _H2 = md5str(String(F("GET:")) + _uri); + } + log_v("Hash of GET:uri=%s", _H2.c_str()); + String _responsecheck = ""; + if (authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); + } else { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); } authReq = ""; + + log_v("The Proper response=%s", _responsecheck.c_str()); + bool ret = _response == _responsecheck; + log_v("Authentication %s", ret ? "Success" : "Failed"); + return ret; + } else if (authReq.length()) { + // OTHER_AUTH + log_v("Trying to authenticate using Other Auth, authReq=%s", authReq.c_str()); + String* ret = fn(OTHER_AUTH, authReq, {}); + if (ret) { + log_v("Authentication Success"); + return true; + } } +exf: + authReq = ""; + log_v("Authentication Failed"); return false; } @@ -232,11 +327,11 @@ void WebServer::requestAuthentication(HTTPAuthMethod mode, const char* realm, co _srealm = String(realm); } if(mode == BASIC_AUTH) { - sendHeader(String(FPSTR(WWW_Authenticate)), String(F("Basic realm=\"")) + _srealm + String(F("\""))); + sendHeader(String(FPSTR(WWW_Authenticate)), AuthTypeBasic + String(F(" realm=\"")) + _srealm + String(F("\""))); } else { _snonce=_getRandomHexString(); _sopaque=_getRandomHexString(); - sendHeader(String(FPSTR(WWW_Authenticate)), String(F("Digest realm=\"")) +_srealm + String(F("\", qop=\"auth\", nonce=\"")) + _snonce + String(F("\", opaque=\"")) + _sopaque + String(F("\""))); + sendHeader(String(FPSTR(WWW_Authenticate)), AuthTypeDigest + String(F(" realm=\"")) +_srealm + String(F("\", qop=\"auth\", nonce=\"")) + _snonce + String(F("\", opaque=\"")) + _sopaque + String(F("\""))); } using namespace mime; send(401, String(FPSTR(mimeTable[html].mimeType)), authFailMsg); @@ -411,8 +506,8 @@ void WebServer::_prepareHeader(String& response, int code, const char* content_t } if (_corsEnabled) { sendHeader(String(FPSTR("Access-Control-Allow-Origin")), String("*")); - sendHeader(String(FPSTR("Access-Control-Allow-Methods")), String("*")); - sendHeader(String(FPSTR("Access-Control-Allow-Headers")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Methods")), String("*")); + sendHeader(String(FPSTR("Access-Control-Allow-Headers")), String("*")); } sendHeader(String(F("Connection")), String(F("close"))); @@ -543,9 +638,9 @@ String WebServer::pathArg(unsigned int i) { String WebServer::arg(String name) { for (int j = 0; j < _postArgsLen; ++j) { - if ( _postArgs[j].key == name ) - return _postArgs[j].value; - } + if ( _postArgs[j].key == name ) + return _postArgs[j].value; + } for (int i = 0; i < _currentArgCount; ++i) { if ( _currentArgs[i].key == name ) return _currentArgs[i].value; @@ -571,9 +666,9 @@ int WebServer::args() { bool WebServer::hasArg(String name) { for (int j = 0; j < _postArgsLen; ++j) { - if (_postArgs[j].key == name) - return true; - } + if (_postArgs[j].key == name) + return true; + } for (int i = 0; i < _currentArgCount; ++i) { if (_currentArgs[i].key == name) return true; diff --git a/libraries/WebServer/src/WebServer.h b/libraries/WebServer/src/WebServer.h index aa703059d13..d668c4e70e7 100644 --- a/libraries/WebServer/src/WebServer.h +++ b/libraries/WebServer/src/WebServer.h @@ -34,7 +34,7 @@ enum HTTPUploadStatus { UPLOAD_FILE_START, UPLOAD_FILE_WRITE, UPLOAD_FILE_END, UPLOAD_FILE_ABORTED }; enum HTTPClientStatus { HC_NONE, HC_WAIT_READ, HC_WAIT_CLOSE }; -enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; +enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH, OTHER_AUTH }; #define HTTP_DOWNLOAD_UNIT_SIZE 1436 @@ -46,6 +46,7 @@ enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; #define HTTP_MAX_POST_WAIT 5000 //ms to wait for POST data to arrive #define HTTP_MAX_SEND_WAIT 5000 //ms to wait for data chunk to be ACKed #define HTTP_MAX_CLOSE_WAIT 2000 //ms to wait for the client to close the connection +#define HTTP_MAX_BASIC_AUTH_LEN 256 // maximum length of a basic Auth base64 encoded username:password string #define CONTENT_LENGTH_UNKNOWN ((size_t) -1) #define CONTENT_LENGTH_NOT_SET ((size_t) -2) @@ -82,12 +83,41 @@ class WebServer virtual void close(); void stop(); + const String AuthTypeDigest = F("Digest"); + const String AuthTypeBasic = F("Basic"); + + /* Callbackhandler for authentication. The extra parameters depend on the + * HTTPAuthMethod mode: + * + * BASIC_AUTH enteredUsernameOrReq contains the username entered by the user + * param[0] password entered (in the clear) + * param[1] authentication realm. + * + * To return - the password the user entered password is compared to. Or Null on fail. + * + * DIGEST_AUTH enteredUsernameOrReq contains the username entered by the user + * param[0] autenticaiton realm + * param[1] authentication URI + * + * To return - the password of which the digest will be based on for comparison. Or NULL + * to fail. + * + * OTHER_AUTH enteredUsernameOrReq rest of the auth line. + * params empty array + * + * To return - NULL to fail; or any string. + */ + typedef std::function THandlerFunctionAuthCheck; + + bool authenticate(THandlerFunctionAuthCheck fn); bool authenticate(const char * username, const char * password); + bool authenticateBasicSHA1(const char * _username, const char * _sha1AsBase64orHex); + void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String("") ); typedef std::function THandlerFunction; void on(const Uri &uri, THandlerFunction fn); - void on(const Uri &uri, HTTPMethod method, THandlerFunction fn); + void on(const Uri &uri, HTTPMethod method, THandlerFunction fn); void on(const Uri &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn); //ufn handles file uploads void addHandler(RequestHandler* handler); void serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_header = NULL );