diff --git a/platformio.ini b/platformio.ini index 88c1fbb..f949eb7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,6 +32,7 @@ board_build.embed_files = lib/generated/index.css.gz lib/generated/config.json.gz lib/generated/xdrconfig.json.gz + lib/generated/md5.js.gz board_build.partitions = partitions_custom.csv extra_scripts = pre:extra_script.py diff --git a/src/main.cpp b/src/main.cpp index c55d24d..6731cbc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -53,6 +53,7 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting #include #include #include "esp_heap_caps.h" +#include #include "GwJsonDocument.h" #include "N2kDataToNMEA0183.h" @@ -127,6 +128,47 @@ GwCounter countTCPOut("countTCPout"); GwCounter countSerialIn("countSerialIn"); GwCounter countSerialOut("countSerialOut"); +unsigned long saltBase=esp_random(); + +char hv(uint8_t nibble){ + nibble=nibble&0xf; + if (nibble < 10) return (char)('0'+nibble); + return (char)('A'+nibble-10); +} +void toHex(unsigned long v,char *buffer,size_t bsize){ + uint8_t *bp=(uint8_t *)&v; + size_t i=0; + for (;i> 4); + buffer[2*i+1]=hv(*bp); + bp++; + } + if ((2*i) < bsize) buffer[2*i]=0; +} + +bool checkPass(String hash){ + if (! config.getBool(config.useAdminPass)) return true; + String pass=config.getString(config.adminPassword); + unsigned long now=millis()/1000UL & ~0x7UL; + MD5Builder builder; + char buffer[2*sizeof(now)+1]; + for (int i=0;i< 5 ;i++){ + unsigned long base=saltBase+now; + toHex(base,buffer,2*sizeof(now)+1); + builder.begin(); + builder.add(buffer); + builder.add(pass); + builder.calculate(); + String md5=builder.toString(); + bool rt=hash == md5; + logger.logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d", + hash.c_str(),base,md5.c_str(),(int)rt); + if (rt) return true; + now -= 8; + } + return false; +} + void updateNMEACounter(int id,const char *msg,bool incoming,bool fail=false){ //we rely on the msg being long enough char key[6]; @@ -417,6 +459,11 @@ protected: status["clientIP"] = WiFi.localIP().toString(); status["numClients"] = socketServer.numClients(); status["apIp"] = gwWifi.apIP(); + size_t bsize=2*sizeof(unsigned long)+1; + unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL); + char buffer[bsize]; + toHex(base,buffer,bsize); + status["salt"] = buffer; //nmea0183Converter->toJson(status); countNMEA2KIn.toJson(status); countNMEA2KOut.toJson(status); @@ -430,6 +477,20 @@ protected: } }; +class CheckPassRequest : public GwRequestMessage{ + String hash; + public: + CheckPassRequest(String h): GwRequestMessage(F("application/json"),F("checkPass")){ + this->hash=h; + }; + protected: + virtual void processRequest(){ + bool res=checkPass(hash); + if (res) result=JSON_OK; + else result=F("{\"status\":\"ERROR\"}"); + } + +}; class CapabilitiesRequest : public GwRequestMessage{ public: CapabilitiesRequest() : GwRequestMessage(F("application/json"),F("capabilities")){}; @@ -709,6 +770,11 @@ void setup() { }); webserver.registerMainHandler("/api/xdrUnmapped", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new XdrUnMappedRequest(); }); + webserver.registerMainHandler("/api/checkPass", [](AsyncWebServerRequest *request)->GwRequestMessage * + { + String hash=request->arg("hash"); + return new CheckPassRequest(hash); + }); webserver.begin(); xdrMappings.begin(); diff --git a/web/config.json b/web/config.json index 6353bee..24ac547 100644 --- a/web/config.json +++ b/web/config.json @@ -123,6 +123,26 @@ ] } }, + { + "name": "useAdminPass", + "type": "boolean", + "default": "true", + "description": "use a password for config modifications", + "category": "system" + }, + { + "name": "adminPassword", + "type": "password", + "default": "esp32admin", + "check": "checkAdminPass", + "description": "set the password for config modifications", + "category": "system", + "capabilities": { + "hardwareReset": [ + "true" + ] + } + }, { "name": "showInvalidData", "label": "show all data", diff --git a/web/index.html b/web/index.html index 7097914..25f0065 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,8 @@ NMEA 2000 Gateway - + + diff --git a/web/index.js b/web/index.js index cb65ac2..0a36bf2 100644 --- a/web/index.js +++ b/web/index.js @@ -1,6 +1,8 @@ let self = this; let lastUpdate = (new Date()).getTime(); let reloadConfig = false; +let adminPass="ABCDE"; +let lastSalt=""; function addEl(type, clazz, parent, text) { let el = document.createElement(type); if (clazz) { @@ -60,6 +62,9 @@ function update() { getJson('/api/status') .then(function (jsonData) { for (let k in jsonData) { + if (k == "salt"){ + lastSalt=jsonData[k]; + } if (typeof (jsonData[k]) === 'object') { for (let sk in jsonData[k]) { let key = k + "." + sk; @@ -986,6 +991,24 @@ function loadConfigDefinitions() { }) .catch(function (err) { alert("unable to load config: " + err) }) } +function verifyPass(){ + return new Promise(function(resolve,reject){ + let hash=lastSalt+adminPass; + let md5hash=MD5(hash); + getJson('api/checkPass?hash='+encodeURIComponent(md5hash)) + .then(function(jsonData){ + if (jsonData.status == 'OK') resolve('ok'); + else reject(jsonData.status); + return; + }) + .catch(function(error){reject(error);}) + }); +} +function testPass(){ + verifyPass() + .then(function(r){console.log("password ok");}) + .catch(function(e){alert("check failed: "+e);}); +} function converterInfo() { getJson("api/converterInfo").then(function (json) { let text = "

Converted entities

"; diff --git a/web/md5.js b/web/md5.js new file mode 100644 index 0000000..3a4d825 --- /dev/null +++ b/web/md5.js @@ -0,0 +1,184 @@ +/** +* +* MD5 (Message-Digest Algorithm) +* http://www.webtoolkit.info/ +* +**/ +var MD5 = function (string) { + function RotateLeft(lValue, iShiftBits) { + return (lValue<>>(32-iShiftBits)); + } + function AddUnsigned(lX,lY) { + var lX4,lY4,lX8,lY8,lResult; + lX8 = (lX & 0x80000000); + lY8 = (lY & 0x80000000); + lX4 = (lX & 0x40000000); + lY4 = (lY & 0x40000000); + lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF); + if (lX4 & lY4) { + return (lResult ^ 0x80000000 ^ lX8 ^ lY8); + } + if (lX4 | lY4) { + if (lResult & 0x40000000) { + return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); + } else { + return (lResult ^ 0x40000000 ^ lX8 ^ lY8); + } + } else { + return (lResult ^ lX8 ^ lY8); + } + } + function F(x,y,z) { return (x & y) | ((~x) & z); } + function G(x,y,z) { return (x & z) | (y & (~z)); } + function H(x,y,z) { return (x ^ y ^ z); } + function I(x,y,z) { return (y ^ (x | (~z))); } + function FF(a,b,c,d,x,s,ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + }; + function GG(a,b,c,d,x,s,ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + }; + function HH(a,b,c,d,x,s,ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + }; + function II(a,b,c,d,x,s,ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + }; + function ConvertToWordArray(string) { + var lWordCount; + var lMessageLength = string.length; + var lNumberOfWords_temp1=lMessageLength + 8; + var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64; + var lNumberOfWords = (lNumberOfWords_temp2+1)*16; + var lWordArray=Array(lNumberOfWords-1); + var lBytePosition = 0; + var lByteCount = 0; + while ( lByteCount < lMessageLength ) { + lWordCount = (lByteCount-(lByteCount % 4))/4; + lBytePosition = (lByteCount % 4)*8; + lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29; + return lWordArray; + }; + function WordToHex(lValue) { + var WordToHexValue="",WordToHexValue_temp="",lByte,lCount; + for (lCount = 0;lCount<=3;lCount++) { + lByte = (lValue>>>(lCount*8)) & 255; + WordToHexValue_temp = "0" + lByte.toString(16); + WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2); + } + return WordToHexValue; + }; + function Utf8Encode(string) { + string = string.replace(/\r\n/g,"\n"); + var utftext = ""; + for (var n = 0; n < string.length; n++) { + var c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } + else if((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + }; + var x=Array(); + var k,AA,BB,CC,DD,a,b,c,d; + var S11=7, S12=12, S13=17, S14=22; + var S21=5, S22=9 , S23=14, S24=20; + var S31=4, S32=11, S33=16, S34=23; + var S41=6, S42=10, S43=15, S44=21; + string = Utf8Encode(string); + x = ConvertToWordArray(string); + a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476; + for (k=0;k