├── library.properties ├── LICENSE.txt ├── README.md ├── examples └── RFC6238-generator │ └── RFC6238-generator.ino └── src ├── TOTP-RC6236-generator.hpp └── TOTP-generator.hpp /library.properties: -------------------------------------------------------------------------------- 1 | name=TOTP-generator 2 | version=1.0.1 3 | author=Dirk-Willem van Gulik 4 | license=ASLv2 5 | maintainer=Dirk-Willem van Gulik 6 | sentence=Time based one time password generator; complies with RFC 6238 7 | paragraph=RFC 6238 time based one time password generator. It will accept the base32 encoded seeds (and all the other parameters typically found in the Qr codes). 8 | category=Communication 9 | url=https://github.com/dirkx/Arduino-TOTP-RFC6238-generator 10 | architectures=* 11 | depends=Base32-Decode 12 | includes=TOTP-generator.hpp 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino libary for TOTP generation 2 | 3 | Example use: 4 | 5 | // Seed value - as per the QR code; which is in fact a base32 encoded 6 | // byte array (i.e. it is binary). 7 | // 8 | const char * seed = "ORUGKU3FMNZGK5CTMVSWI==="; 9 | 10 | // Example of the same thing - but as usually formatted when shown 11 | // as the 'alternative text to enter' 12 | // 13 | // const char * seed = "ORU GKU 3FM NZG K5C TMV SWI"; 14 | 15 | String * otp = TOTP::currentOTP(seed); 16 | 17 | Serial.print(ctime(&t)); 18 | Serial.print(" TOTP at this time is: "); 19 | Serial.println(*otp); 20 | Serial.println(); 21 | 22 | This assumes a normal RFC compliant TOTP. It is possible that the Qr code provides 23 | different values for the interval (default is 30 seconds), epoch or the hash (sha1). 24 | These can be passwd as optional arguments. 25 | -------------------------------------------------------------------------------- /examples/RFC6238-generator/RFC6238-generator.ino: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Dirk-Willem van Gulik, All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #ifndef WIFI_NETWORK 26 | #define WIFI_NETWORK "mySecretWiFiPassword" 27 | #warning "You propably want to change this line!" 28 | #endif 29 | 30 | #ifndef WIFI_PASSWD 31 | #define WIFI_PASSWD "mySecretWiFiPassword" 32 | #warning "You propably want to change this line!" 33 | #endif 34 | 35 | #ifndef NTP_SERVER 36 | #define NTP_SERVER "nl.pool.ntp.org" 37 | #warning "You MUST set an appropriate ntp pool - see http://ntp.org" 38 | #endif 39 | 40 | #ifndef NTP_DEFAULT_TZ 41 | #define NTP_DEFAULT_TZ "CET-1CEST,M3.5.0,M10.5.0/3" 42 | #endif 43 | 44 | const char* ssid = WIFI_NETWORK; 45 | const char* password = WIFI_PASSWD; 46 | 47 | void setup() { 48 | Serial.begin(115200); 49 | while (!Serial) delay(10); 50 | 51 | Serial.println("\n\n" __FILE__ "Started"); 52 | 53 | WiFi.mode(WIFI_STA); 54 | WiFi.begin(ssid, password); 55 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { 56 | Serial.println("WiFi Connect Failed! Rebooting..."); 57 | delay(1000); 58 | ESP.restart(); 59 | } 60 | 61 | // we need a reasonable accurate time for TOTP to work. 62 | // 63 | configTzTime(NTP_DEFAULT_TZ, NTP_SERVER); 64 | } 65 | 66 | 67 | void loop() { 68 | // Print the one time passcode every seconds; 69 | // 70 | static unsigned long lst = millis(); 71 | if (millis() - lst < 1000) 72 | return; 73 | lst = millis(); 74 | 75 | time_t t = time(NULL); 76 | if (t < 1000000) { 77 | Serial.println("Not having a stable time yet.. TOTP is not going to work."); 78 | return; 79 | }; 80 | 81 | // Seed value - as per the QR code; which is in fact a base32 encoded 82 | // byte array (i.e. it is binary). 83 | // 84 | const char * seed = "ORUGKU3FMNZGK5CTMVSWI==="; 85 | 86 | // Example of the same thing - but as usually formatted when shown 87 | // as the 'alternative text to enter' 88 | // 89 | // const char * seed = "ORU GKU 3FM NZG K5C TMV SWI"; 90 | 91 | String * otp = TOTP::currentOTP(seed); 92 | 93 | Serial.print(ctime(&t)); 94 | Serial.print(" TOTP at this time is: "); 95 | Serial.println(*otp); 96 | Serial.println(); 97 | 98 | delete otp; 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/TOTP-RC6236-generator.hpp: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Dirk-Willem van Gulik, All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | #ifndef _TOTP_RFC6238_H 19 | #define _TOTP_RFC6238_H 20 | 21 | // Needed for the SHA1 22 | // 23 | #include 24 | 25 | // Needed for base32 decode - origin 26 | // https://github.com/dirkx/Arduino-Base32-Decode/releases 27 | // 28 | #include 29 | 30 | class TOTP { 31 | public: 32 | 33 | // Defaults from RFC 6238 34 | // Seed assumed in base64 format; and to be a multiple of 8 bits. 35 | // once decoded. 36 | static const time_t RFC6238_DEFAULT_interval = 30; // seconds (default) 37 | static const time_t RFC6238_DEFAULT_epoch = 0; // epoch relative to the unix epoch (jan 1970 is the default) 38 | static const int RFC6238_DEFAULT_digits = 6; // length (default is 6) 39 | 40 | static String * currentOTP(String seed, 41 | time_t interval = RFC6238_DEFAULT_interval, 42 | int digits = RFC6238_DEFAULT_digits, 43 | time_t epoch = RFC6238_DEFAULT_epoch 44 | ) 45 | { 46 | return currentOTP(time(NULL), seed, interval, digits, epoch); 47 | } 48 | 49 | static String * currentOTP(time_t t, 50 | String seed, 51 | time_t interval = RFC6238_DEFAULT_interval, 52 | int digits = RFC6238_DEFAULT_digits, 53 | time_t epoch = RFC6238_DEFAULT_epoch 54 | ) 55 | { 56 | uint64_t v = t; 57 | v = (v - epoch) / interval; 58 | 59 | // HMAC is calculated in big-endian (network) order. 60 | // v = htonll(v); 61 | 62 | // Unfortunately htonll is not exposed 63 | uint32_t endianness = 0xdeadbeef; 64 | if ((*(const uint8_t *)&endianness) == 0xef) { 65 | v = ((v & 0x00000000ffffffff) << 32) | ((v & 0xffffffff00000000) >> 32); 66 | v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); 67 | v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); 68 | }; 69 | 70 | unsigned char buff[ seed.length() ]; 71 | bzero(buff, sizeof(buff)); 72 | int n = base32decode(seed.c_str(), buff, sizeof(buff)); 73 | if (n < 0) { 74 | Serial.println("Could not decode base32 seed"); 75 | return NULL; 76 | } 77 | 78 | #ifdef RFC6238_DEBUG 79 | Serial.print("Key: "); 80 | Serial.print(seed); 81 | Serial.print(" --> "); 82 | for (int i = 0; i < n; i++) { 83 | Serial.printf("%02x", buff[i]); 84 | } 85 | Serial.printf(" -- bits=%d -- check this against https://cryptotools.net/otp\n",n * 8); 86 | #endif 87 | 88 | unsigned char digest[20]; 89 | if (mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), 90 | buff, n, // key 91 | (unsigned char*) &v, sizeof(v), // input 92 | digest)) return NULL; 93 | 94 | uint8_t offst = digest[19] & 0x0f; 95 | uint32_t bin_code = (digest[offst + 0] & 0x7f) << 24 96 | | (digest[offst + 1] & 0xff) << 16 97 | | (digest[offst + 2] & 0xff) << 8 98 | | (digest[offst + 3] & 0xff); 99 | int power = pow(10, digits); 100 | 101 | #if RFC6238_DEBUG 102 | // To check against https://cryptotools.net/otp 103 | // 104 | for (int i = 0; i < 20; i++) { 105 | if (offst == i) Serial.print("|"); 106 | Serial.printf("%02x", digest[i]); 107 | if (offst == i) Serial.print("|"); 108 | } 109 | Serial.println(); 110 | #endif 111 | 112 | // prefix with zero's - as needed & cut off to right number of digits. 113 | // 114 | char outbuff[32]; 115 | snprintf(outbuff, sizeof(outbuff), "%06u", bin_code % power); 116 | String * otp = new String(outbuff); 117 | 118 | return (otp); 119 | } 120 | }; 121 | #endif 122 | -------------------------------------------------------------------------------- /src/TOTP-generator.hpp: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Dirk-Willem van Gulik, All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | #ifndef _TOTP_RFC6238_H 19 | #define _TOTP_RFC6238_H 20 | 21 | // Needed for the SHA1 22 | // 23 | #include 24 | 25 | // Needed for base32 decode - origin 26 | // https://github.com/dirkx/Arduino-Base32-Decode/releases 27 | // 28 | #include 29 | 30 | class TOTP { 31 | public: 32 | 33 | // Defaults from RFC 6238 34 | // Seed assumed in base64 format; and to be a multiple of 8 bits. 35 | // once decoded. 36 | static const time_t RFC6238_DEFAULT_interval = 30; // seconds (default) 37 | static const time_t RFC6238_DEFAULT_epoch = 0; // epoch relative to the unix epoch (jan 1970 is the default) 38 | static const int RFC6238_DEFAULT_digits = 6; // length (default is 6) 39 | 40 | static String * currentOTP(String seed, 41 | time_t interval = RFC6238_DEFAULT_interval, 42 | int digits = RFC6238_DEFAULT_digits, 43 | time_t epoch = RFC6238_DEFAULT_epoch 44 | ) 45 | { 46 | return currentOTP(seed, time(NULL), interval, digits, epoch); 47 | } 48 | 49 | static String * currentOTP(String seed, 50 | time_t t, 51 | time_t interval = RFC6238_DEFAULT_interval, 52 | int digits = RFC6238_DEFAULT_digits, 53 | time_t epoch = RFC6238_DEFAULT_epoch 54 | ) 55 | { 56 | uint64_t v = t; 57 | v = (v - epoch) / interval; 58 | 59 | // HMAC is calculated in big-endian (network) order. 60 | // v = htonll(v); 61 | 62 | // Unfortunately htonll is not exposed 63 | uint32_t endianness = 0xdeadbeef; 64 | if ((*(const uint8_t *)&endianness) == 0xef) { 65 | v = ((v & 0x00000000ffffffff) << 32) | ((v & 0xffffffff00000000) >> 32); 66 | v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); 67 | v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); 68 | }; 69 | 70 | unsigned char buff[ seed.length() ]; 71 | bzero(buff, sizeof(buff)); 72 | int n = base32decode(seed.c_str(), buff, sizeof(buff)); 73 | if (n < 0) { 74 | Serial.println("Could not decode base32 seed"); 75 | return NULL; 76 | } 77 | 78 | #ifdef RFC6238_DEBUG 79 | Serial.print("Key: "); 80 | Serial.print(seed); 81 | Serial.print(" --> "); 82 | for (int i = 0; i < n; i++) { 83 | Serial.printf("%02x", buff[i]); 84 | } 85 | Serial.printf(" -- bits=%d -- check this against https://cryptotools.net/otp\n",n * 8); 86 | #endif 87 | 88 | unsigned char digest[20]; 89 | if (mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), 90 | buff, n, // key 91 | (unsigned char*) &v, sizeof(v), // input 92 | digest)) return NULL; 93 | 94 | uint8_t offst = digest[19] & 0x0f; 95 | uint32_t bin_code = (digest[offst + 0] & 0x7f) << 24 96 | | (digest[offst + 1] & 0xff) << 16 97 | | (digest[offst + 2] & 0xff) << 8 98 | | (digest[offst + 3] & 0xff); 99 | int power = pow(10, digits); 100 | 101 | #if RFC6238_DEBUG 102 | // To check against https://cryptotools.net/otp 103 | // 104 | for (int i = 0; i < 20; i++) { 105 | if (offst == i) Serial.print("|"); 106 | Serial.printf("%02x", digest[i]); 107 | if (offst == i) Serial.print("|"); 108 | } 109 | Serial.println(); 110 | #endif 111 | 112 | // prefix with zero's - as needed & cut off to right number of digits. 113 | // 114 | char outbuff[32]; 115 | snprintf(outbuff, sizeof(outbuff), "%06u", bin_code % power); 116 | String * otp = new String(outbuff); 117 | 118 | return (otp); 119 | } 120 | }; 121 | #endif 122 | --------------------------------------------------------------------------------