diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d91bebe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.nds +*.elf \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..95395e2 --- /dev/null +++ b/Makefile @@ -0,0 +1,122 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +include $(DEVKITARM)/ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# INCLUDES is a list of directories containing extra header files +#--------------------------------------------------------------------------------- +TARGET := SmolOTP +BUILD := build +SOURCES := gfx source data +INCLUDES := include build + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -mthumb -mthumb-interwork + +CFLAGS := -g -Wall -O2\ + -march=armv5te -mtune=arm946e-s -fomit-frame-pointer\ + -ffast-math \ + $(ARCH) + +CFLAGS += $(INCLUDE) -DARM9 # -DDEBUG +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=ds_arm9.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +#--------------------------------------------------------------------------------- +# any extra libraries we wish to link with the project +#--------------------------------------------------------------------------------- +LIBS := -lfat -lnds9 -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(LIBNDS) + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.bin))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES := $(BINFILES:.bin=.o) \ + $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +.PHONY: $(BUILD) clean + +#--------------------------------------------------------------------------------- +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).elf $(TARGET).nds $(TARGET).ds.gba + +#--------------------------------------------------------------------------------- +else + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +$(OUTPUT).nds : $(OUTPUT).elf +$(OUTPUT).elf : $(OFILES) + +#--------------------------------------------------------------------------------- +%.o : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + $(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/README.md b/README.md new file mode 100644 index 0000000..c32ddea --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# SmolOTP + +Barebones OTP application designed for immediate usage on the simplest on devices. + +Download binary releases: + +## Current Support + +Platforms: +* Nintendo DS + +OTP protocols: +* TOTP + +## Planned Features + +* More platforms (via LibMultiSpacc) +* Encryption of secrets, unlock via fast-usable key combos +* Changing options (time configuration, adding secrets, ...) in-app +* Better gathering of time/date with less configuration needed by users +* Handling unlimited secrets (currently there is a 256 hard limit + much lower soft limit because of no screen scrolling lol) + +## Credits + +This app integrates the following third-party libraries: + +* TOTP protocol implementation: +* HMAC+SHA1 protocol implementation: +* INI configuration parser: diff --git a/source/hmac.c b/source/hmac.c new file mode 100644 index 0000000..60ccc27 --- /dev/null +++ b/source/hmac.c @@ -0,0 +1,33 @@ +#include "hmac.h" + +/* function doing the HMAC-SHA-1 calculation */ +void hmac_sha1(const uint8_t* key, const uint32_t keysize, const uint8_t* msg, const uint32_t msgsize, uint8_t* output) +{ + struct sha1 outer, inner; + uint8_t tmp; + + sha1_reset(&outer); + sha1_reset(&inner); + + uint32_t i; + for (i = 0; i < keysize; ++i) + { + tmp = key[i] ^ 0x5C; + sha1_input(&outer, &tmp, 1); + tmp = key[i] ^ 0x36; + sha1_input(&inner, &tmp, 1); + } + for (; i < 64; ++i) + { + tmp = 0x5C; + sha1_input(&outer, &tmp, 1); + tmp = 0x36; + sha1_input(&inner, &tmp, 1); + } + + sha1_input(&inner, msg, msgsize); + sha1_result(&inner, output); + + sha1_input(&outer, output, HMAC_SHA1_HASH_SIZE); + sha1_result(&outer, output); +} diff --git a/source/hmac.h b/source/hmac.h new file mode 100644 index 0000000..27e7275 --- /dev/null +++ b/source/hmac.h @@ -0,0 +1,19 @@ +#ifndef __HMAC_H__ +#define __HMAC_H__ + +#include +#include "sha1.h" + +#define HMAC_SHA1_HASH_SIZE 20 + +/***********************************************************************' + * HMAC(K,m) : HMAC SHA1 + * @param key : secret key + * @param keysize : key-length ín bytes + * @param msg : msg to calculate HMAC over + * @param msgsize : msg-length in bytes + * @param output : writeable buffer with at least 20 bytes available + */ +void hmac_sha1(const uint8_t* key, const uint32_t keysize, const uint8_t* msg, const uint32_t msgsize, uint8_t* output); + +#endif /* __HMAC_H__ */ diff --git a/source/ini.c b/source/ini.c new file mode 100644 index 0000000..509952d --- /dev/null +++ b/source/ini.c @@ -0,0 +1,304 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#if INI_CUSTOM_ALLOCATOR +#include +void* ini_malloc(size_t size); +void ini_free(void* ptr); +void* ini_realloc(void* ptr, size_t size); +#else +#include +#define ini_malloc malloc +#define ini_free free +#define ini_realloc realloc +#endif +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Used by ini_parse_string() to keep track of string parsing state. */ +typedef struct { + const char* ptr; + size_t num_left; +} ini_parse_string_ctx; + +/* Strip whitespace chars off end of given string, in place. Return s. */ +static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to NUL at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Similar to strncpy, but ensures dest (size bytes) is + NUL-terminated, and doesn't pad with NULs. */ +static char* strncpy0(char* dest, const char* src, size_t size) +{ + /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ + size_t i; + for (i = 0; i < size - 1 && src[i]; i++) + dest[i] = src[i]; + dest[i] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; + size_t max_line = INI_MAX_LINE; +#else + char* line; + size_t max_line = INI_INITIAL_ALLOC; +#endif +#if INI_ALLOW_REALLOC && !INI_USE_STACK + char* new_line; + size_t offset; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)ini_malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } +#endif + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, (int)max_line, stream) != NULL) { +#if INI_ALLOW_REALLOC && !INI_USE_STACK + offset = strlen(line); + while (offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) + max_line = INI_MAX_LINE; + new_line = ini_realloc(line, max_line); + if (!new_line) { + ini_free(line); + return -2; + } + line = new_line; + if (reader(line + offset, (int)(max_line - offset), stream) == NULL) + break; + if (max_line >= INI_MAX_LINE) + break; + offset += strlen(line + offset); + } +#endif + + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(start, NULL); + if (*end) + *end = '\0'; + rstrip(start); +#endif + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!HANDLER(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; +#if INI_CALL_HANDLER_ON_NEW_SECTION + if (!HANDLER(user, section, NULL, NULL) && !error) + error = lineno; +#endif + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = end + 1; +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + value = lskip(value); + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ +#if INI_ALLOW_NO_VALUE + *end = '\0'; + name = rstrip(start); + if (!HANDLER(user, section, name, NULL) && !error) + error = lineno; +#else + error = lineno; +#endif + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + ini_free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +/* An ini_reader function to read the next line from a string buffer. This + is the fgets() equivalent used by ini_parse_string(). */ +static char* ini_reader_string(char* str, int num, void* stream) { + ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; + const char* ctx_ptr = ctx->ptr; + size_t ctx_num_left = ctx->num_left; + char* strp = str; + char c; + + if (ctx_num_left == 0 || num < 2) + return NULL; + + while (num > 1 && ctx_num_left != 0) { + c = *ctx_ptr++; + ctx_num_left--; + *strp++ = c; + if (c == '\n') + break; + num--; + } + + *strp = '\0'; + ctx->ptr = ctx_ptr; + ctx->num_left = ctx_num_left; + return str; +} + +/* See documentation in header file. */ +int ini_parse_string(const char* string, ini_handler handler, void* user) { + ini_parse_string_ctx ctx; + + ctx.ptr = string; + ctx.num_left = strlen(string); + return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, + user); +} diff --git a/source/ini.h b/source/ini.h new file mode 100644 index 0000000..d1a2ba8 --- /dev/null +++ b/source/ini.h @@ -0,0 +1,178 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2020, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef INI_H +#define INI_H + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Nonzero if ini_handler callback should accept lineno parameter. */ +#ifndef INI_HANDLER_LINENO +#define INI_HANDLER_LINENO 0 +#endif + +/* Visibility symbols, required for Windows DLLs */ +#ifndef INI_API +#if defined _WIN32 || defined __CYGWIN__ +# ifdef INI_SHARED_LIB +# ifdef INI_SHARED_LIB_BUILDING +# define INI_API __declspec(dllexport) +# else +# define INI_API __declspec(dllimport) +# endif +# else +# define INI_API +# endif +#else +# if defined(__GNUC__) && __GNUC__ >= 4 +# define INI_API __attribute__ ((visibility ("default"))) +# else +# define INI_API +# endif +#endif +#endif + +/* Typedef for prototype of handler function. */ +#if INI_HANDLER_LINENO +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value, + int lineno); +#else +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); +#endif + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +INI_API int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O (see also + ini_parse_string). */ +INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Same as ini_parse(), but takes a zero-terminated string with the INI data +instead of a file. Useful for parsing INI data from a network socket or +already in memory. */ +INI_API int ini_parse_string(const char* string, ini_handler handler, void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See https://github.com/benhoyt/inih/issues/21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Chars that begin a start-of-line comment. Per Python configparser, allow + both ; and # comments at the start of a line by default. */ +#ifndef INI_START_COMMENT_PREFIXES +#define INI_START_COMMENT_PREFIXES ";#" +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file (stack or heap). Note that + this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +/* Nonzero to allow heap line buffer to grow via realloc(), zero for a + fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is + zero. */ +#ifndef INI_ALLOW_REALLOC +#define INI_ALLOW_REALLOC 0 +#endif + +/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK + is zero. */ +#ifndef INI_INITIAL_ALLOC +#define INI_INITIAL_ALLOC 200 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Nonzero to call the handler at the start of each new section (with + name and value NULL). Default is to only call the handler on + each name=value pair. */ +#ifndef INI_CALL_HANDLER_ON_NEW_SECTION +#define INI_CALL_HANDLER_ON_NEW_SECTION 0 +#endif + +/* Nonzero to allow a name without a value (no '=' or ':' on the line) and + call the handler with value NULL in this case. Default is to treat + no-value lines as an error. */ +#ifndef INI_ALLOW_NO_VALUE +#define INI_ALLOW_NO_VALUE 0 +#endif + +/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory + allocation functions (INI_USE_STACK must also be 0). These functions must + have the same signatures as malloc/free/realloc and behave in a similar + way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ +#ifndef INI_CUSTOM_ALLOCATOR +#define INI_CUSTOM_ALLOCATOR 0 +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* INI_H */ diff --git a/source/main.c b/source/main.c new file mode 100644 index 0000000..ee5b8ae --- /dev/null +++ b/source/main.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "rfc4226.h" +#include "rfc6238.h" +#include "utils.h" +#include "parser.h" +#include "ini.h" + +#define T0 0 +#define DIGITS 6 +#define VALIDITY 30 +#define TIME 2 +#define VERSION 1.0 + +#define AppConfigFile "/.AppData/SmolOTP/Config.ini" +#define MaxSecretLength 32 +#define MaxSecretsCount 256 + +extern NODE *provider_list = NULL; + +typedef struct AppConfigType { + int timezone; + bool useDaylightSavings; + int totpSecretsCount; + char totpNames[MaxSecretsCount][MaxSecretLength]; + char totpSecrets[MaxSecretsCount][MaxSecretLength]; + uint32_t totpResults[MaxSecretsCount]; +} AppConfigType; + +char currentTotpSecret[MaxSecretLength]; + +uint32_t getTotp(char secret[], int timezone) +{ + size_t pos; + size_t len = strlen(secret); + size_t keylen; + time_t curtime; + uint8_t *keysec; + uint32_t result; + + if (validate_b32key(secret, len, pos) == 1) { + // invalid secret + return UINT32_MAX; + } else { + keysec = (uint8_t *)secret; + keylen = decode_b32key(&keysec, len); + curtime = getTotpTime(T0, VALIDITY, timezone); + result = TOTP(keysec, keylen, curtime, DIGITS); + return result; + } +} + +void waitUserExit(int code) +{ + int gamepadKeys; + while(1) { + swiWaitForVBlank(); + scanKeys(); + gamepadKeys = keysDown(); + if (gamepadKeys & KEY_START || gamepadKeys & KEY_SELECT) { + exit(code); + } + } +} + +void calcOtps(AppConfigType *AppConfig) +{ + for (int i=0; itotpSecretsCount; i++) + { + // we copy the secret in a temporary variable because the calculation corrupts it (?) + strcpy(currentTotpSecret, AppConfig->totpSecrets[i]); + AppConfig->totpResults[i] = getTotp(currentTotpSecret, AppConfig->timezone); + } +} + +void printOtps(AppConfigType *AppConfig) +{ + if (AppConfig->totpSecretsCount == 0) + { + iprintf("\x1b[5;0H No Secrets Defined in \n \"%s\"\n", AppConfigFile); + } + for (int i=0; itotpSecretsCount; i++) + { + iprintf("\x1b[%d;0H %s\n", 5+(i*2), AppConfig->totpNames[i]); + + if (AppConfig->totpResults[i] == UINT32_MAX) { + iprintf("\x1b[%d;23H INVALID\n", 5+(i*2)); + } else { + iprintf("\x1b[%d;24H %06u\n", 5+(i*2), AppConfig->totpResults[i]); + } + } +} + +static int HandleAppConfig(AppConfigType *AppConfig, const char* section, const char* name, const char* value) +{ + #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0 + if (MATCH("Options", "TimezoneOffset")) + { + AppConfig->timezone = atoi(value); + } + //else if (MATCH("Options", "UseDaylightSavingTime")) + //{ + // AppConfig->useDaylightSavings = (strcmp(strlwr(value), "true") == 0 ? true : false); + //} + else if (strcmp(section, "Secrets") == 0) + { + strcpy(AppConfig->totpNames[AppConfig->totpSecretsCount], name); + strcpy(AppConfig->totpSecrets[AppConfig->totpSecretsCount], value); + AppConfig->totpSecretsCount++; + } + return 1; +} + +void RewriteAppConfig(AppConfigType *AppConfig) +{ + FILE *file = fopen(AppConfigFile, "w"); + fprintf(file, + "\n[Options]\n" + "# Note: For now Daylight Saving Time is not implemented, so set your UTC offset to account for that" + "TimezoneOffset = %d\n" + "\n[Secrets]\n", + AppConfig->timezone); + for (int i=0; itotpSecretsCount; i++) + { + fprintf(file, "%s = %s\n", AppConfig->totpNames[i], AppConfig->totpSecrets[i]); + } + fclose(file); +} + +// TODO +/*bool isinDaylightSavings(int month, int day, int weekday, int hours, int minutes) +{ + // TODO: handle daylight savings start/end dates with alternatives to the european standard + // https://en.m.wikipedia.org/wiki/Daylight_saving_time_by_country + //bool isSavingsMonth = (month >= 3 && month <= 10); + //return (isSavingsMonth && isSwitchDay weekday == && ); + return false; +}*/ + +int main(void) +{ + int totpViewStart = 0; + int totpViewEnd = 5; + int gamepadKeys; + time_t unixTime; + struct tm *timeStruct; + int year, month, day, weekday, hours, minutes, seconds; + bool inDaylightSavings; + char totpNames[MaxSecretsCount][MaxSecretLength]; + char totpSecrets[MaxSecretsCount][MaxSecretLength]; + uint32_t totpResults[MaxSecretsCount]; + + AppConfigType AppConfig = { + .timezone = 0, + .useDaylightSavings = false, + .totpSecretsCount = 0, + .totpNames = totpNames, + .totpSecrets = totpSecrets, + .totpResults = totpResults, + }; + + consoleDemoInit(); + + if (!fatInitDefault()) + { + iprintf("fatInitDefault failure\nPress START to exit...\n"); + waitUserExit(-1); + } + + mkdir("/.AppData"); + mkdir("/.AppData/SmolOTP"); + + if (ini_parse(AppConfigFile, HandleAppConfig, &AppConfig) < 0) + { + RewriteAppConfig(&AppConfig); + } + + /*if (AppConfig.useDaylightSavings && isinDaylightSavings(month, day, weekday, hours, minutes)) + { + inDaylightSavings = true; + AppConfig.timezone++; + }*/ + + calcOtps(&AppConfig); + printOtps(&AppConfig); + + while(1) + { + unixTime = getUnixTime(AppConfig.timezone); + timeStruct = gmtime((const time_t *)&unixTime); + + year = timeStruct->tm_year + 1900; + month = timeStruct->tm_mon; + day = timeStruct->tm_mday; + //weekday = timeStruct->tm_wday; + hours = timeStruct->tm_hour; + minutes = timeStruct->tm_min; + seconds = timeStruct->tm_sec; + + iprintf("\x1b[0;0H UTC Time: %04d-%02d-%02d %02d:%02d:%02d\n (Timezone Offset: %s%d)\n\n--------------------------------", + year, month, day, hours, minutes, seconds, + (AppConfig.timezone >= 0 ? "+" : ""), AppConfig.timezone//, + //(AppConfig.useDaylightSavings ? "" : "DST Adjustments are Disabled.") + ); + + if (seconds % 30 == 0) + { + calcOtps(&AppConfig); + printOtps(&AppConfig); + } + + swiWaitForVBlank(); + scanKeys(); + gamepadKeys = keysDown(); + + // TODO: add secrets encryption, and decryption via keypad combos + if (gamepadKeys & KEY_START || gamepadKeys & KEY_SELECT) + { + exit(0); + } + else if (totpViewStart > 0 && gamepadKeys & KEY_UP) + { + // scroll list up ... + } + else if (totpViewEnd < AppConfig.totpSecretsCount && gamepadKeys & KEY_DOWN) + { + // scroll list down ... + } + } + + return 0; +} diff --git a/source/parser.c b/source/parser.c new file mode 100644 index 0000000..1820193 --- /dev/null +++ b/source/parser.c @@ -0,0 +1,123 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#include "plist.h" +#include "parser.h" + +PROVIDER split_str(char *spl, char delim) +{ + char *tmp_name; + char *tmp_secret; + PROVIDER p; + size_t count = 0; + + size_t totlen = strlen(spl) - 2; + + //Get break point + do { + count++; + } while (spl[count] != delim); + + tmp_name = (char *) malloc(count * sizeof(char) + 1); + tmp_secret = (char *) malloc((totlen-count) * sizeof(char) + 1); + + /* + * Get first part of the string + */ + memcpy(tmp_name, spl, count); + tmp_name[count] = '\0'; + /* + * Get second part of the string + */ + memcpy(tmp_secret, spl+(count+1), (totlen-count)); + tmp_secret[(totlen-count)] = '\0'; + +#ifdef DEBUG + printf("[GOT LEN]: %ld\n", strlen(spl)); + printf("[PROVIDER SECTION]: %ld characters\n", count); + printf("[GOT NAME]: %s\n", tmp_name); + printf("[SECRET SECTION]: %ld\n", (strlen(spl)-count+1)); + printf("[GOT SECRET]: %s\n", tmp_secret); +#endif + + p = (PROVIDER) { + .pname = tmp_name, + .psecret = tmp_secret, + .otpvalue = NULL + }; + + return p; +} + +void process_provider(NODE **plist, char *line) +{ + PROVIDER p; + p = split_str(line, ':'); + push(plist, p.pname, p.psecret, p.otpvalue); +} + +// reimplementation of getline() to stop the compiler from crying () +ssize_t getline(char **restrict buffer, size_t *restrict size, FILE *restrict fp) { + register int c; + register char *cs = NULL; + + if (cs == NULL) { + register int length = 0; + while ((c = getc(fp)) != EOF) { + cs = (char *)realloc(cs, ++length+1); + if ((*(cs + length - 1) = c) == '\n') { + *(cs + length) = '\0'; + *buffer = cs; + break; + } + } + return (ssize_t)(*size = length); + } else { + while (--(*size) > 0 && (c = getc(fp)) != EOF) { + if ((*cs++ = c) == '\n') + break; + } + *cs = '\0'; + } + return (ssize_t)(*size=strlen(*buffer)); +} + +void load_providers(char *fname) +{ + FILE *f; + size_t len = 1024; + + if (fname == NULL) + exit(ENOENT); + f = fopen(fname, "r"); + if (f == NULL) + exit(ENOENT); + char *line = NULL; + + while (getline(&line, &len, f) != -1) { + if (line[0] != '#') + process_provider(&provider_list, line); + } + + free(line); + fclose(f); +} diff --git a/source/parser.h b/source/parser.h new file mode 100644 index 0000000..6a4ef0d --- /dev/null +++ b/source/parser.h @@ -0,0 +1,27 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#include +#include +#include +#include +#include + +#include "plist.h" + +extern NODE *provider_list; + +PROVIDER split_str(char *spl, char delim); +void process_provider(NODE **plist, char *line); +void load_providers(char *fname); diff --git a/source/plist.c b/source/plist.c new file mode 100644 index 0000000..9d559a0 --- /dev/null +++ b/source/plist.c @@ -0,0 +1,142 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#include +#include +#include + +#include "plist.h" + +size_t get_len(NODE *head) +{ + NODE *cur = NULL; + cur = head; + size_t length = 0; + + while (cur != NULL) { + cur = cur->next; + length++; + } + return length; +} + +bool exists(NODE *head, NODE *target) +{ + printf("Check if the target node exists in list\n"); + + NODE *cur = NULL; + cur = head; + while (cur != NULL) { + if ((cur->p)->pname == (target->p)->pname) + return 1; + cur = cur->next; + } + return 0; +} + +NODE *get_node(NODE *head, char *pname) +{ + NODE *cur = NULL; + cur = head; + while (cur != NULL) { + if ((cur->p)->pname == pname) { + return cur; + } + cur = cur->next; + } + return NULL; +} + +int update_value(NODE **head, char *pname, uint32_t optvalue) +{ + NODE *cur; + cur = *head; + uint32_t *x = &optvalue; + while (cur != NULL) { + if ((cur->p)->pname == pname) { + (cur->p)->otpvalue = *x; + return 0; + } + cur = cur->next; + } + return -1; +} + +void push(NODE **head, char *pname, char *psecret, uint32_t *otpvalue) +{ + NODE *cur = (NODE *) malloc(sizeof(NODE)); + PROVIDER *p = (PROVIDER *) malloc(sizeof(PROVIDER)); + + p->pname = pname; + p->psecret = psecret; + p->otpvalue = otpvalue; + + cur->p = p; + + cur->next = *head; + *head = cur; +} + +NODE *pop(NODE **head) +{ + NODE *tmp = *head; + *head = (*head)->next; + return tmp; +} + +void del(char *del, NODE *head) +{ + if(head == NULL) + fprintf(stderr, "No valid list, no head found\n"); + + NODE *cur = NULL; + NODE *prev = NULL; + + cur = prev = head; + + while(cur != NULL && (strcmp((cur->p)->pname, del) != 0)) { + prev = cur; + cur = cur->next; + } + // Reached the end, should return .. + if(cur == NULL) + return; + /* Found the pname in the list, free the node and + * modify the pointer to next + */ + prev->next = cur->next; + free(cur); +} + +void freeProvider(PROVIDER *p) +{ + free(p->pname); + free(p->psecret); + free((PROVIDER *)p); +} + +void freeList(NODE *head) +{ + NODE *tmp; + + while (head != NULL) { + tmp = head; + #ifdef DEBUG + printf("Deleting Provider %s\n", (tmp->p)->pname); + #endif + freeProvider(tmp->p); + head = head->next; + free(tmp); + } +} diff --git a/source/plist.h b/source/plist.h new file mode 100644 index 0000000..ad2072a --- /dev/null +++ b/source/plist.h @@ -0,0 +1,44 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#ifndef PLIST_H +#define PLIST_H + +#include +#include +#include +#include + +typedef struct { + char *pname; + char *psecret; + uint32_t *otpvalue; +} PROVIDER; + +typedef struct Node { + PROVIDER *p; + struct Node *next; +} NODE; + +void freeList(NODE *head); +void freeProvider(PROVIDER *p); +size_t get_len(NODE *head); +int update_value(NODE **head, char *pname, uint32_t optvalue); +void push(NODE **head, char *pname, char *psecret, uint32_t *otpvalue); +void del(char *del, NODE *head); +bool exists(NODE *head, NODE *target); +NODE *pop(NODE **head); +NODE *get_node(NODE *head, char *pname); + +#endif diff --git a/source/rfc4226.c b/source/rfc4226.c new file mode 100644 index 0000000..b3a6f4b --- /dev/null +++ b/source/rfc4226.c @@ -0,0 +1,96 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#include +#include +#include +#include +#include + +#include "hmac.h" + +char currentHmacResult[HMAC_SHA1_HASH_SIZE]; + +uint8_t *hmac(unsigned char *key, int kl, uint64_t interval) +{ + hmac_sha1(key, kl, &interval, sizeof(interval), ¤tHmacResult); + return currentHmacResult; +} + +uint32_t DT(uint8_t *digest) +{ + uint64_t offset; + uint32_t bin_code; + +#ifdef DEBUG + char mdString[40]; + for (int i = 0; i < 20; i++) + sprintf(&mdString[i*2], "%02x", (unsigned int)digest[i]); + printf("HMAC digest: %s\n", mdString); +#endif + + // dynamically truncates hash + offset = digest[19] & 0x0f; + + bin_code = (digest[offset] & 0x7f) << 24 + | (digest[offset+1] & 0xff) << 16 + | (digest[offset+2] & 0xff) << 8 + | (digest[offset+3] & 0xff); + + // truncates code to 6 digits +#ifdef DEBUG + printf("OFFSET: %d\n", offset); + printf("\nDBC1: %d\n", bin_code); +#endif + + return bin_code; +} + +uint32_t mod_hotp(uint32_t bin_code, int digits) +{ + int power = pow(10, digits); + uint32_t otp = bin_code % power; + return otp; +} + +uint32_t HOTP(uint8_t *key, size_t kl, uint64_t interval, int digits) +{ + uint8_t *digest; + uint32_t result; + uint32_t endianness; + +#ifdef DEBUG + printf("KEY IS: %s\n", key); + printf("KEY LEN IS: %d\n", kl); + printf("COUNTER IS: %d\n", interval); +#endif + + endianness = 0xdeadbeef; + if ((*(const uint8_t *)&endianness) == 0xef) { + interval = ((interval & 0x00000000ffffffff) << 32) | ((interval & 0xffffffff00000000) >> 32); + interval = ((interval & 0x0000ffff0000ffff) << 16) | ((interval & 0xffff0000ffff0000) >> 16); + interval = ((interval & 0x00ff00ff00ff00ff) << 8) | ((interval & 0xff00ff00ff00ff00) >> 8); + }; + + //First Phase, get the digest of the message using the provided key ... + digest = (uint8_t *)hmac(key, kl, interval); + + //Second Phase, get the dbc from the algorithm + uint32_t dbc = DT(digest); + + //Third Phase: calculate the mod_k of the dbc to get the correct number + result = mod_hotp(dbc, digits); + + return result; +} diff --git a/source/rfc4226.h b/source/rfc4226.h new file mode 100644 index 0000000..7d9f449 --- /dev/null +++ b/source/rfc4226.h @@ -0,0 +1,30 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#ifndef RFC4226_H +#define RFC4226_H + +#include +#include + +//MAIN HOTP function +uint32_t HOTP(uint8_t *key, size_t kl, uint64_t interval, int digits); + +//First step +uint8_t *hmac(unsigned char *key, int kl, uint64_t interval); + +//Second step +uint32_t DT(uint8_t *digest); + +#endif diff --git a/source/rfc6238.c b/source/rfc6238.c new file mode 100644 index 0000000..1294473 --- /dev/null +++ b/source/rfc6238.c @@ -0,0 +1,34 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#include "rfc6238.h" + +time_t getUnixTime(int timezoneOffset) +{ + // note: subtract, not add, the TZ offset from the system time to get the UTC time based on TZ + // eg. if localTime = 12:00, then utcTime = localTime - (+02) = 10:00 + return (time(NULL) - (timezoneOffset * 60 * 60)); +} + +time_t getTotpTime(time_t t0, int timestep, int timezoneOffset) +{ + return (floor((getUnixTime(timezoneOffset) - t0) / timestep)); +} + +uint32_t TOTP(uint8_t *key, size_t kl, uint64_t time, int digits) +{ + uint32_t totp; + totp = HOTP(key, kl, time, digits); + return totp; +} diff --git a/source/rfc6238.h b/source/rfc6238.h new file mode 100644 index 0000000..4e5aeb6 --- /dev/null +++ b/source/rfc6238.h @@ -0,0 +1,40 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#ifndef RFC6238_H +#define RFC6238_H + +#include +#include +#include +#include + +#include "rfc4226.h" + +#define TS 30 // time step in seconds, default value + +/******** RFC6238 ********** + * + * TOTP = HOTP(k,T) where + * K = the supersecret key + * T = ( Current Unix time - T0) / X + * where X is the Time Step + * + * *************************/ + +uint32_t TOTP(uint8_t *key, size_t kl, uint64_t time, int digits); +time_t getUnixTime(int timezoneOffset); +time_t getTotpTime(time_t t0, int timestep, int timezoneOffset); + +#endif diff --git a/source/sha1.c b/source/sha1.c new file mode 100644 index 0000000..ce280a6 --- /dev/null +++ b/source/sha1.c @@ -0,0 +1,512 @@ +/* + * sha1.c + * + * Description: + * This file implements the Secure Hashing Algorithm 1 as + * defined in FIPS PUB 180-1 published April 17, 1995. + * + * The SHA-1, produces a 160-bit message digest for a given + * data stream. It should take about 2**n steps to find a + * message with the same digest as a given message and + * 2**(n/2) to find any two messages with the same digest, + * when n is the digest size in bits. Therefore, this + * algorithm can serve as a means of providing a + * "fingerprint" for a message. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits + * long. Although SHA-1 allows a message digest to be generated + * for messages of any number of bits less than 2^64, this + * implementation only works with messages with a length that is + * a multiple of the size of an 8-bit character. + * + */ + +#include "sha1.h" + +/* Local Function Prototyptes */ +static void _pad_block(struct sha1*); +static void _process_block(struct sha1*); + +/* SHA1 circular left shift */ +static uint32_t _circular_shift(const uint32_t nbits, const uint32_t word) +{ + return ((word << nbits) | (word >> (32 - nbits))); +} + +/* + * sha1_reset + * + * Description: + * This function will initialize the SHA1-context in preparation + * for computing a new SHA1 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + * + */ +int sha1_reset(struct sha1* context) +{ + if (context == 0) + { + return shaNull; + } + + context->Length_Low = 0; + context->Length_High = 0; + context->Message_Block_Index = 0; + + context->Intermediate_Hash[0] = 0x67452301; + context->Intermediate_Hash[1] = 0xEFCDAB89; + context->Intermediate_Hash[2] = 0x98BADCFE; + context->Intermediate_Hash[3] = 0x10325476; + context->Intermediate_Hash[4] = 0xC3D2E1F0; + + context->flags = 0; + + return shaSuccess; +} + +/* + * sha1_result + * + * Description: + * This function will return the 160-bit message digest into the + * Message_Digest array provided by the caller. + * NOTE: The first octet of hash is stored in the 0th element, + * the last octet of hash in the 19th element. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA-1 hash. + * Message_Digest: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + * + */ +int sha1_result(struct sha1* context, uint8_t Message_Digest[SHA1HashSize]) +{ + int i; + + if ( (context == 0) + || (Message_Digest == 0)) + { + return shaNull; + } + + if ((context->flags & FLAG_CORRUPTED) != 0) + { + return shaStateError; + } + + if ((context->flags & FLAG_COMPUTED) == 0) + { + _pad_block(context); + + for (i = 0; i < 64; ++i) + { + /* message may be sensitive, clear it out */ + context->Message_Block[i] = 0; + } + context->Length_Low = 0; /* and clear length */ + context->Length_High = 0; + context->flags |= FLAG_COMPUTED; + } + + for (i = 0; i < SHA1HashSize; ++i) + { + Message_Digest[i] = (context->Intermediate_Hash[i >> 2] >> (8 * (3 - (i & 0x03)))); + } + + return shaSuccess; +} + +/* + * sha1_input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update + * message_array: [in] + * An array of characters representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array + * + * Returns: + * sha Error Code. + * + */ +int sha1_input(struct sha1* context, const uint8_t* message_array, unsigned length) +{ + if (length == 0) + { + return shaSuccess; + } + + if ( (context == 0) + || (message_array == 0)) + { + return shaNull; + } + + if ((context->flags & FLAG_COMPUTED) != 0) + { + context->flags |= FLAG_CORRUPTED; + return shaStateError; + } + + if ((context->flags & FLAG_CORRUPTED) != 0) + { + return shaStateError; + } + + while ( (length != 0) + && (context->flags == 0)) + { + context->Message_Block[context->Message_Block_Index] = (*message_array); + + context->Message_Block_Index += 1; + context->Length_Low += 8; + + if (context->Length_Low == 0) + { + context->Length_High += 1; + + if (context->Length_High == 0) + { + /* Message is too long */ + context->flags |= FLAG_CORRUPTED; + } + } + + if (context->Message_Block_Index == 64) + { + _process_block(context); + } + + message_array += 1; + length -= 1; + } + + return shaSuccess; +} + +/* + * _process_block + * + * Description: + * This function will process the next 512 bits of the message + * stored in the Message_Block array. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the publication. + * + * + */ +#if 0 // original code +static void _process_block(struct sha1 *context) +{ + const uint32_t K[] = /* Constants defined in SHA-1 */ + { + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + uint32_t t; /* Loop counter */ + uint32_t temp; /* Temporary word value */ + uint32_t W[80]; /* Word sequence */ + uint32_t A, B, C, D, E; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = 0; t < 16; ++t) + { + W[t] = context->Message_Block[(t * 4) + 0] << 24; + W[t] |= context->Message_Block[(t * 4) + 1] << 16; + W[t] |= context->Message_Block[(t * 4) + 2] << 8; + W[t] |= context->Message_Block[(t * 4) + 3] << 0; + } + + for (t = 16; t < 80; ++t) + { + W[t] = _circular_shift(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]); + } + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + + for (t = 0; t < 20; ++t) + { + temp = _circular_shift(5, A) + + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (; t < 40; ++t) + { + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[t] + K[1]; + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (; t < 60; ++t) + { + temp = _circular_shift(5, A) + + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (; t < 80; ++t) + { + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[t] + K[3]; + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + + context->Message_Block_Index = 0; +} + +#else + +//#define METHOD2 + void _process_block(struct sha1 *context) + { + const uint32_t K[] = /* Constants defined in SHA-1 */ + { + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + uint8_t t; /* Loop counter */ + uint32_t temp; /* Temporary word value */ +#ifdef METHOD2 + uint8_t s; + uint32_t W[16]; +#else + uint32_t W[80]; /* Word sequence */ +#endif + uint32_t A, B, C, D, E; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = 0; t < 16; ++t) + { + W[t] = ((uint32_t)context->Message_Block[t * 4 + 0]) << 24; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 1]) << 16; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 2]) << 8; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 3]) << 0; + } + +#ifndef METHOD2 + for (t = 16; t < 80; ++t) + { + W[t] = _circular_shift(1, (W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16])); + } +#endif + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + + for (t = 0; t < 20; ++t) + { +#ifdef METHOD2 + s = t & 0x0f; + if (t >= 16) + { + W[s] = _circular_shift(1, (W[(s + 13) & 0x0f] ^ W[(s + 8) & 0x0f] ^ W[(s + 2) & 0x0f] ^ W[s])); + } + temp = _circular_shift(5, A) + ((B & C) | ((~B) & D)) + E + W[s] + K[0]; +#else + temp = _circular_shift(5, A) + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; +#endif + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (t = 20; t < 40; ++t) + { +#ifdef METHOD2 + s = (t & 0x0f); + W[s] = _circular_shift(1, (W[(s + 13) & 0x0f] ^ W[(s + 8) & 0x0f] ^ W[(s + 2) & 0x0f] ^ W[s])); + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[s] + K[1]; +#else + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[t] + K[1]; +#endif + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (t = 40; t < 60; ++t) + { +#ifdef METHOD2 + s = (t & 0x0f); + W[s] = _circular_shift(1, (W[(s + 13) & 0x0f] ^ W[(s + 8) & 0x0f] ^ W[(s + 2) & 0x0f] ^ W[s])); + temp = _circular_shift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[s] + K[2]; +#else + temp = _circular_shift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; +#endif + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + for (t = 60; t < 80; ++t) + { +#ifdef METHOD2 + s = (t & 0x0f); + W[s] = _circular_shift(1, (W[(s + 13) & 0x0f] ^ W[(s + 8) & 0x0f] ^ W[(s + 2) & 0x0f] ^ W[s])); + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[s] + K[3]; +#else + temp = _circular_shift(5, A) + (B ^ C ^ D) + E + W[t] + K[3]; +#endif + E = D; + D = C; + C = _circular_shift(30, B); + B = A; + A = temp; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + + context->Message_Block_Index = 0; + } + +#endif + +/* + * _pad_block + * + * Description: + * According to the standard, the message must be padded to an even + * 512 bits. The first padding bit must be a '1'. The last 64 + * bits represent the length of the original message. All bits in + * between should be 0. This function will pad the message + * according to those rules by filling the Message_Block array + * accordingly. It will also call the ProcessMessageBlock function + * provided appropriately. When it returns, it can be assumed that + * the message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad + * ProcessMessageBlock: [in] + * The appropriate SHA*ProcessMessageBlock function + * Returns: + * Nothing. + * + */ +static void _pad_block(struct sha1* context) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index > 55) + { + context->Message_Block[context->Message_Block_Index] = 0x80; + context->Message_Block_Index += 1; + + while (context->Message_Block_Index < 64) + { + context->Message_Block[context->Message_Block_Index] = 0; + context->Message_Block_Index += 1; + } + + _process_block(context); + + while (context->Message_Block_Index < 56) + { + context->Message_Block[context->Message_Block_Index] = 0; + context->Message_Block_Index += 1; + } + } + else + { + context->Message_Block[context->Message_Block_Index] = 0x80; + context->Message_Block_Index += 1; + + while (context->Message_Block_Index < 56) + { + context->Message_Block[context->Message_Block_Index] = 0; + context->Message_Block_Index += 1; + } + } + + /* + * Store the message length as the last 8 bytes + */ + context->Message_Block[56] = context->Length_High >> 24; + context->Message_Block[57] = context->Length_High >> 16; + context->Message_Block[58] = context->Length_High >> 8; + context->Message_Block[59] = context->Length_High >> 0; + context->Message_Block[60] = context->Length_Low >> 24; + context->Message_Block[61] = context->Length_Low >> 16; + context->Message_Block[62] = context->Length_Low >> 8; + context->Message_Block[63] = context->Length_Low >> 0; + + _process_block(context); +} diff --git a/source/sha1.h b/source/sha1.h new file mode 100644 index 0000000..1f57abb --- /dev/null +++ b/source/sha1.h @@ -0,0 +1,55 @@ +/* + * sha1.h + * + * Description: + * This is the header file for code which implements the Secure + * Hashing Algorithm 1 as defined in FIPS PUB 180-1 published + * April 17, 1995. + * + * Many of the variable names in this code, especially the + * single character names, were used because those were the names + * used in the publication. + * + * Please read the file sha1.c for more information. + * + */ + +#ifndef _SHA1_H_ +#define _SHA1_H_ + +#include + +#define SHA1HashSize 20 + +enum +{ + shaSuccess = 0, + shaNull, /* Null pointer parameter */ + shaInputTooLong, /* input data too long */ + shaStateError /* called Input after Result */ +}; + +#define FLAG_COMPUTED 1 +#define FLAG_CORRUPTED 2 + +/* + * Data structure holding contextual information about the SHA-1 hash + */ +struct sha1 +{ + uint8_t Message_Block[64]; /* 512-bit message blocks */ + uint32_t Intermediate_Hash[5]; /* Message Digest */ + uint32_t Length_Low; /* Message length in bits */ + uint32_t Length_High; /* Message length in bits */ + uint16_t Message_Block_Index; /* Index into message block array */ + uint8_t flags; +}; + +/* + * Public API + */ +int sha1_reset (struct sha1* context); +int sha1_input (struct sha1* context, const uint8_t* message_array, unsigned length); +int sha1_result(struct sha1* context, uint8_t Message_Digest[SHA1HashSize]); + +#endif /* #ifndef _SHA1_H_ */ diff --git a/source/utils.c b/source/utils.c new file mode 100644 index 0000000..d3e18e1 --- /dev/null +++ b/source/utils.c @@ -0,0 +1,125 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#include "utils.h" + +static const int8_t base32_vals[256] = { + // This map cheats and interprets: + // - the numeral zero as the letter "O" as in oscar + // - the numeral one as the letter "L" as in lima + // - the numeral eight as the letter "B" as in bravo + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x20 + 14, 11, 26, 27, 28, 29, 30, 31, 1, -1, -1, -1, -1, 0, -1, -1, // 0x30 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 0x40 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 0x50 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 0x60 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, -1, -1, -1, -1, // 0x70 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x80 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x90 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xA0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xB0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xC0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xD0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xE0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0xF0 +}; + +int validate_b32key(char *k, size_t len, size_t pos) +{ + // validates base32 key + if (((len & 0xF) != 0) && ((len & 0xF) != 8)) + return 1; + for (pos = 0; (pos < len); pos++) { + if (base32_vals[k[pos]] == -1) + return 1; + if (k[pos] == '=') { + if (((pos & 0xF) == 0) || ((pos & 0xF) == 8)) + return(1); + if ((len - pos) > 6) + return 1; + switch (pos % 8) { + case 2: + case 4: + case 5: + case 7: + break; + default: + return 1; + } + for ( ; (pos < len); pos++) { + if (k[pos] != '=') + return 1; + } + } + } + return 0; +} + +size_t decode_b32key(uint8_t **k, size_t len) +{ + size_t keylen; + size_t pos; + // decodes base32 secret key + keylen = 0; + for (pos = 0; pos <= (len - 8); pos += 8) { + // MSB is Most Significant Bits (0x80 == 10000000 ~= MSB) + // MB is middle bits (0x7E == 01111110 ~= MB) + // LSB is Least Significant Bits (0x01 == 00000001 ~= LSB) + + // byte 0 + (*k)[keylen+0] = (base32_vals[(*k)[pos+0]] << 3) & 0xF8; // 5 MSB + (*k)[keylen+0] |= (base32_vals[(*k)[pos+1]] >> 2) & 0x07; // 3 LSB + if ((*k)[pos+2] == '=') { + keylen += 1; + break; + } + + // byte 1 + (*k)[keylen+1] = (base32_vals[(*k)[pos+1]] << 6) & 0xC0; // 2 MSB + (*k)[keylen+1] |= (base32_vals[(*k)[pos+2]] << 1) & 0x3E; // 5 MB + (*k)[keylen+1] |= (base32_vals[(*k)[pos+3]] >> 4) & 0x01; // 1 LSB + if ((*k)[pos+4] == '=') { + keylen += 2; + break; + } + + // byte 2 + (*k)[keylen+2] = (base32_vals[(*k)[pos+3]] << 4) & 0xF0; // 4 MSB + (*k)[keylen+2] |= (base32_vals[(*k)[pos+4]] >> 1) & 0x0F; // 4 LSB + if ((*k)[pos+5] == '=') { + keylen += 3; + break; + } + + // byte 3 + (*k)[keylen+3] = (base32_vals[(*k)[pos+4]] << 7) & 0x80; // 1 MSB + (*k)[keylen+3] |= (base32_vals[(*k)[pos+5]] << 2) & 0x7C; // 5 MB + (*k)[keylen+3] |= (base32_vals[(*k)[pos+6]] >> 3) & 0x03; // 2 LSB + if ((*k)[pos+7] == '=') { + keylen += 4; + break; + } + + // byte 4 + (*k)[keylen+4] = (base32_vals[(*k)[pos+6]] << 5) & 0xE0; // 3 MSB + (*k)[keylen+4] |= (base32_vals[(*k)[pos+7]] >> 0) & 0x1F; // 5 LSB + keylen += 5; + } + (*k)[keylen] = 0; + + return keylen; +} diff --git a/source/utils.h b/source/utils.h new file mode 100644 index 0000000..9c6355d --- /dev/null +++ b/source/utils.h @@ -0,0 +1,26 @@ +/* + * + * TOTP: Time-Based One-Time Password Algorithm + * Copyright (c) 2017, fmount + * + * This software is distributed under MIT License + * + * Compute the hmac using openssl library. + * SHA-1 engine is used by default, but you can pass another one, + * + * e.g EVP_md5(), EVP_sha224, EVP_sha512, etc + * + */ + +#ifndef _UTILS_H +#define _UTILS_H + +#include +#include +#include +#include + +int validate_b32key(char *k, size_t len, size_t pos); +size_t decode_b32key(uint8_t **k, size_t len); + +#endif