diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index fd7a49af8f..28248802b7 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -65,6 +65,7 @@ mp3_encoder,apps mpegplayer,viewers nim,games oscilloscope,demos +otp,apps pacbox,games pdbox,viewers pegbox,games diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 3865fbf85e..c7a8cb69f7 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -33,6 +33,7 @@ disktidy.c flipit.c shopper.c resistor.c +otp.c #ifdef USB_ENABLE_HID remote_control.c diff --git a/apps/plugins/lib/SOURCES b/apps/plugins/lib/SOURCES index 0a125dbbe4..747355b44b 100644 --- a/apps/plugins/lib/SOURCES +++ b/apps/plugins/lib/SOURCES @@ -1,3 +1,4 @@ +sha1.c gcc-support.c pluginlib_actions.c helper.c diff --git a/apps/plugins/lib/sha1.c b/apps/plugins/lib/sha1.c new file mode 100644 index 0000000000..107c50256b --- /dev/null +++ b/apps/plugins/lib/sha1.c @@ -0,0 +1,434 @@ +/* sha1.c - Functions to compute SHA1 message digest of files or + memory blocks according to the NIST specification FIPS-180-1. + + Copyright (C) 2000, 2001, 2003, 2004, 2005, 2006 Free Software + Foundation, Inc. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Written by Scott G. Miller + Credits: + Robert Klep -- Expansion function fix +*/ + +#include "plugin.h" +#include "sha1.h" + +#ifdef WORDS_BIGENDIAN +# define SWAP(n) (n) +#else +# define SWAP(n) \ + (((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24)) +#endif + +#define BLOCKSIZE 4096 +#if BLOCKSIZE % 64 != 0 +# error "invalid BLOCKSIZE" +#endif + +/* This array contains the bytes used to pad the buffer to the next + 64-byte boundary. (RFC 1321, 3.1: Step 1) */ +static const unsigned char fillbuf[64] = { 0x80, 0 /* , 0, 0, ... */ }; + + +/* Take a pointer to a 160 bit block of data (five 32 bit ints) and + initialize it to the start constants of the SHA1 algorithm. This + must be called before using hash in the call to sha1_hash. */ +void +sha1_init_ctx (struct sha1_ctx *ctx) +{ + ctx->A = 0x67452301; + ctx->B = 0xefcdab89; + ctx->C = 0x98badcfe; + ctx->D = 0x10325476; + ctx->E = 0xc3d2e1f0; + + ctx->total[0] = ctx->total[1] = 0; + ctx->buflen = 0; +} + +/* Put result from CTX in first 20 bytes following RESBUF. The result + must be in little endian byte order. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32-bit value. */ +void * +sha1_read_ctx (const struct sha1_ctx *ctx, void *resbuf) +{ + ((uint32_t *) resbuf)[0] = SWAP (ctx->A); + ((uint32_t *) resbuf)[1] = SWAP (ctx->B); + ((uint32_t *) resbuf)[2] = SWAP (ctx->C); + ((uint32_t *) resbuf)[3] = SWAP (ctx->D); + ((uint32_t *) resbuf)[4] = SWAP (ctx->E); + + return resbuf; +} + +/* Process the remaining bytes in the internal buffer and the usual + prolog according to the standard and write the result to RESBUF. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32-bit value. */ +void * +sha1_finish_ctx (struct sha1_ctx *ctx, void *resbuf) +{ + /* Take yet unprocessed bytes into account. */ + uint32_t bytes = ctx->buflen; + size_t size = (bytes < 56) ? 64 / 4 : 64 * 2 / 4; + + /* Now count remaining bytes. */ + ctx->total[0] += bytes; + if (ctx->total[0] < bytes) + ++ctx->total[1]; + + /* Put the 64-bit file length in *bits* at the end of the buffer. */ + ctx->buffer[size - 2] = SWAP ((ctx->total[1] << 3) | (ctx->total[0] >> 29)); + ctx->buffer[size - 1] = SWAP (ctx->total[0] << 3); + + memcpy (&((char *) ctx->buffer)[bytes], fillbuf, (size - 2) * 4 - bytes); + + /* Process last bytes. */ + sha1_process_block (ctx->buffer, size * 4, ctx); + + return sha1_read_ctx (ctx, resbuf); +} + +/* Compute SHA1 message digest for LEN bytes beginning at BUFFER. The + result is always in little endian byte order, so that a byte-wise + output yields to the wanted ASCII representation of the message + digest. */ +void *sha1_buffer (const char *buffer, size_t len, void *resblock) +{ + struct sha1_ctx ctx; + + /* Initialize the computation context. */ + sha1_init_ctx (&ctx); + + /* Process whole buffer but last len % 64 bytes. */ + sha1_process_bytes (buffer, len, &ctx); + + /* Put result in desired memory area. */ + return sha1_finish_ctx (&ctx, resblock); +} + +void +sha1_process_bytes (const void *buffer, size_t len, struct sha1_ctx *ctx) +{ + /* When we already have some bits in our internal buffer concatenate + both inputs first. */ + if (ctx->buflen != 0) + { + size_t left_over = ctx->buflen; + size_t add = 128 - left_over > len ? len : 128 - left_over; + + memcpy (&((char *) ctx->buffer)[left_over], buffer, add); + ctx->buflen += add; + + if (ctx->buflen > 64) + { + sha1_process_block (ctx->buffer, ctx->buflen & ~63, ctx); + + ctx->buflen &= 63; + /* The regions in the following copy operation cannot overlap. */ + memcpy (ctx->buffer, + &((char *) ctx->buffer)[(left_over + add) & ~63], + ctx->buflen); + } + + buffer = (const char *) buffer + add; + len -= add; + } + + /* Process available complete blocks. */ + if (len >= 64) + { + { + sha1_process_block (buffer, len & ~63, ctx); + buffer = (const char *) buffer + (len & ~63); + len &= 63; + } + } + + /* Move remaining bytes in internal buffer. */ + if (len > 0) + { + size_t left_over = ctx->buflen; + + memcpy (&((char *) ctx->buffer)[left_over], buffer, len); + left_over += len; + if (left_over >= 64) + { + sha1_process_block (ctx->buffer, 64, ctx); + left_over -= 64; + memcpy (ctx->buffer, &ctx->buffer[16], left_over); + } + ctx->buflen = left_over; + } +} + +/* --- Code below is the primary difference between md5.c and sha1.c --- */ + +/* SHA1 round constants */ +#define K1 0x5a827999 +#define K2 0x6ed9eba1 +#define K3 0x8f1bbcdc +#define K4 0xca62c1d6 + +/* Round functions. Note that F2 is the same as F4. */ +#define F1(B,C,D) ( D ^ ( B & ( C ^ D ) ) ) +#define F2(B,C,D) (B ^ C ^ D) +#define F3(B,C,D) ( ( B & C ) | ( D & ( B | C ) ) ) +#define F4(B,C,D) (B ^ C ^ D) + +/* Process LEN bytes of BUFFER, accumulating context into CTX. + It is assumed that LEN % 64 == 0. + Most of this code comes from GnuPG's cipher/sha1.c. */ + +void +sha1_process_block (const void *buffer, size_t len, struct sha1_ctx *ctx) +{ + const uint32_t *words = buffer; + size_t nwords = len / sizeof (uint32_t); + const uint32_t *endp = words + nwords; + uint32_t x[16]; + uint32_t a = ctx->A; + uint32_t b = ctx->B; + uint32_t c = ctx->C; + uint32_t d = ctx->D; + uint32_t e = ctx->E; + + /* First increment the byte count. RFC 1321 specifies the possible + length of the file up to 2^64 bits. Here we only compute the + number of bytes. Do a double word increment. */ + ctx->total[0] += len; + if (ctx->total[0] < len) + ++ctx->total[1]; + +#define rol(x, n) (((x) << (n)) | ((uint32_t) (x) >> (32 - (n)))) + +#define M(I) ( tm = x[I&0x0f] ^ x[(I-14)&0x0f] \ + ^ x[(I-8)&0x0f] ^ x[(I-3)&0x0f] \ + , (x[I&0x0f] = rol(tm, 1)) ) + +#define R(A,B,C,D,E,F,K,M) do { E += rol( A, 5 ) \ + + F( B, C, D ) \ + + K \ + + M; \ + B = rol( B, 30 ); \ + } while(0) + + while (words < endp) + { + uint32_t tm; + int t; + for (t = 0; t < 16; t++) + { + x[t] = SWAP (*words); + words++; + } + + R( a, b, c, d, e, F1, K1, x[ 0] ); + R( e, a, b, c, d, F1, K1, x[ 1] ); + R( d, e, a, b, c, F1, K1, x[ 2] ); + R( c, d, e, a, b, F1, K1, x[ 3] ); + R( b, c, d, e, a, F1, K1, x[ 4] ); + R( a, b, c, d, e, F1, K1, x[ 5] ); + R( e, a, b, c, d, F1, K1, x[ 6] ); + R( d, e, a, b, c, F1, K1, x[ 7] ); + R( c, d, e, a, b, F1, K1, x[ 8] ); + R( b, c, d, e, a, F1, K1, x[ 9] ); + R( a, b, c, d, e, F1, K1, x[10] ); + R( e, a, b, c, d, F1, K1, x[11] ); + R( d, e, a, b, c, F1, K1, x[12] ); + R( c, d, e, a, b, F1, K1, x[13] ); + R( b, c, d, e, a, F1, K1, x[14] ); + R( a, b, c, d, e, F1, K1, x[15] ); + R( e, a, b, c, d, F1, K1, M(16) ); + R( d, e, a, b, c, F1, K1, M(17) ); + R( c, d, e, a, b, F1, K1, M(18) ); + R( b, c, d, e, a, F1, K1, M(19) ); + R( a, b, c, d, e, F2, K2, M(20) ); + R( e, a, b, c, d, F2, K2, M(21) ); + R( d, e, a, b, c, F2, K2, M(22) ); + R( c, d, e, a, b, F2, K2, M(23) ); + R( b, c, d, e, a, F2, K2, M(24) ); + R( a, b, c, d, e, F2, K2, M(25) ); + R( e, a, b, c, d, F2, K2, M(26) ); + R( d, e, a, b, c, F2, K2, M(27) ); + R( c, d, e, a, b, F2, K2, M(28) ); + R( b, c, d, e, a, F2, K2, M(29) ); + R( a, b, c, d, e, F2, K2, M(30) ); + R( e, a, b, c, d, F2, K2, M(31) ); + R( d, e, a, b, c, F2, K2, M(32) ); + R( c, d, e, a, b, F2, K2, M(33) ); + R( b, c, d, e, a, F2, K2, M(34) ); + R( a, b, c, d, e, F2, K2, M(35) ); + R( e, a, b, c, d, F2, K2, M(36) ); + R( d, e, a, b, c, F2, K2, M(37) ); + R( c, d, e, a, b, F2, K2, M(38) ); + R( b, c, d, e, a, F2, K2, M(39) ); + R( a, b, c, d, e, F3, K3, M(40) ); + R( e, a, b, c, d, F3, K3, M(41) ); + R( d, e, a, b, c, F3, K3, M(42) ); + R( c, d, e, a, b, F3, K3, M(43) ); + R( b, c, d, e, a, F3, K3, M(44) ); + R( a, b, c, d, e, F3, K3, M(45) ); + R( e, a, b, c, d, F3, K3, M(46) ); + R( d, e, a, b, c, F3, K3, M(47) ); + R( c, d, e, a, b, F3, K3, M(48) ); + R( b, c, d, e, a, F3, K3, M(49) ); + R( a, b, c, d, e, F3, K3, M(50) ); + R( e, a, b, c, d, F3, K3, M(51) ); + R( d, e, a, b, c, F3, K3, M(52) ); + R( c, d, e, a, b, F3, K3, M(53) ); + R( b, c, d, e, a, F3, K3, M(54) ); + R( a, b, c, d, e, F3, K3, M(55) ); + R( e, a, b, c, d, F3, K3, M(56) ); + R( d, e, a, b, c, F3, K3, M(57) ); + R( c, d, e, a, b, F3, K3, M(58) ); + R( b, c, d, e, a, F3, K3, M(59) ); + R( a, b, c, d, e, F4, K4, M(60) ); + R( e, a, b, c, d, F4, K4, M(61) ); + R( d, e, a, b, c, F4, K4, M(62) ); + R( c, d, e, a, b, F4, K4, M(63) ); + R( b, c, d, e, a, F4, K4, M(64) ); + R( a, b, c, d, e, F4, K4, M(65) ); + R( e, a, b, c, d, F4, K4, M(66) ); + R( d, e, a, b, c, F4, K4, M(67) ); + R( c, d, e, a, b, F4, K4, M(68) ); + R( b, c, d, e, a, F4, K4, M(69) ); + R( a, b, c, d, e, F4, K4, M(70) ); + R( e, a, b, c, d, F4, K4, M(71) ); + R( d, e, a, b, c, F4, K4, M(72) ); + R( c, d, e, a, b, F4, K4, M(73) ); + R( b, c, d, e, a, F4, K4, M(74) ); + R( a, b, c, d, e, F4, K4, M(75) ); + R( e, a, b, c, d, F4, K4, M(76) ); + R( d, e, a, b, c, F4, K4, M(77) ); + R( c, d, e, a, b, F4, K4, M(78) ); + R( b, c, d, e, a, F4, K4, M(79) ); + + a = ctx->A += a; + b = ctx->B += b; + c = ctx->C += c; + d = ctx->D += d; + e = ctx->E += e; + } +} + +/* memxor.c -- perform binary exclusive OR operation of two memory blocks. + Copyright (C) 2005, 2006 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Written by Simon Josefsson. The interface was inspired by memxor + in Niels Möller's Nettle. */ + +void * +memxor (void * dest, const void * src, size_t n) +{ + char const *s = src; + char *d = dest; + + for (; n > 0; n--) + *d++ ^= *s++; + + return dest; +} + +/* hmac-sha1.c -- hashed message authentication codes + Copyright (C) 2005, 2006 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Written by Simon Josefsson. */ + +#define IPAD 0x36 +#define OPAD 0x5c + +int +hmac_sha1 (const void *key, size_t keylen, + const void *in, size_t inlen, void *resbuf) +{ + struct sha1_ctx inner; + struct sha1_ctx outer; + char optkeybuf[20]; + char block[64]; + char innerhash[20]; + + /* Reduce the key's size, so that it becomes <= 64 bytes large. */ + + if (keylen > 64) + { + struct sha1_ctx keyhash; + + sha1_init_ctx (&keyhash); + sha1_process_bytes (key, keylen, &keyhash); + sha1_finish_ctx (&keyhash, optkeybuf); + + key = optkeybuf; + keylen = 20; + } + + /* Compute INNERHASH from KEY and IN. */ + + sha1_init_ctx (&inner); + + memset (block, IPAD, sizeof (block)); + memxor (block, key, keylen); + + sha1_process_block (block, 64, &inner); + sha1_process_bytes (in, inlen, &inner); + + sha1_finish_ctx (&inner, innerhash); + + /* Compute result from KEY and INNERHASH. */ + + sha1_init_ctx (&outer); + + memset (block, OPAD, sizeof (block)); + memxor (block, key, keylen); + + sha1_process_block (block, 64, &outer); + sha1_process_bytes (innerhash, 20, &outer); + + sha1_finish_ctx (&outer, resbuf); + + return 0; +} diff --git a/apps/plugins/lib/sha1.h b/apps/plugins/lib/sha1.h new file mode 100644 index 0000000000..6358d046d3 --- /dev/null +++ b/apps/plugins/lib/sha1.h @@ -0,0 +1,116 @@ +/* Taken from gnulib (http://savannah.gnu.org/projects/gnulib/) */ +/* Declarations of functions and data types used for SHA1 sum + library functions. + Copyright (C) 2000, 2001, 2003, 2005, 2006 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +#ifndef SHA1_H +#define SHA1_H 1 + +#include "plugin.h" + +/* Structure to save state of computation between the single steps. */ +struct sha1_ctx +{ + uint32_t A; + uint32_t B; + uint32_t C; + uint32_t D; + uint32_t E; + + uint32_t total[2]; + uint32_t buflen; + uint32_t buffer[32]; +}; + + +/* Initialize structure containing state of computation. */ +void sha1_init_ctx (struct sha1_ctx *ctx); + +/* Starting with the result of former calls of this function (or the + initialization function update the context for the next LEN bytes + starting at BUFFER. + It is necessary that LEN is a multiple of 64!!! */ +void sha1_process_block (const void *buffer, size_t len, + struct sha1_ctx *ctx); + +/* Starting with the result of former calls of this function (or the + initialization function update the context for the next LEN bytes + starting at BUFFER. + It is NOT required that LEN is a multiple of 64. */ +void sha1_process_bytes (const void *buffer, size_t len, + struct sha1_ctx *ctx); + +/* Process the remaining bytes in the buffer and put result from CTX + in first 20 bytes following RESBUF. The result is always in little + endian byte order, so that a byte-wise output yields to the wanted + ASCII representation of the message digest. + + IMPORTANT: On some systems it is required that RESBUF be correctly + aligned for a 32 bits value. */ +void *sha1_finish_ctx (struct sha1_ctx *ctx, void *resbuf); + + +/* Put result from CTX in first 20 bytes following RESBUF. The result is + always in little endian byte order, so that a byte-wise output yields + to the wanted ASCII representation of the message digest. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32 bits value. */ +void *sha1_read_ctx (const struct sha1_ctx *ctx, void *resbuf); + +/* Compute SHA1 message digest for LEN bytes beginning at BUFFER. The + result is always in little endian byte order, so that a byte-wise + output yields to the wanted ASCII representation of the message + digest. */ +void *sha1_buffer (const char *buffer, size_t len, void *resblock); + +#endif + + +/* hmac.h -- hashed message authentication codes + Copyright (C) 2005 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Written by Simon Josefsson. */ + +#ifndef HMAC_H +#define HMAC_H 1 + +#include + +/* Compute Hashed Message Authentication Code with SHA-1, over BUFFER + data of BUFLEN bytes using the KEY of KEYLEN bytes, writing the + output to pre-allocated 20 byte minimum RESBUF buffer. Return 0 on + success. */ +int +hmac_sha1 (const void *key, size_t keylen, + const void *in, size_t inlen, void *resbuf); + +#endif /* HMAC_H */ diff --git a/apps/plugins/otp.c b/apps/plugins/otp.c new file mode 100644 index 0000000000..69cb8b7982 --- /dev/null +++ b/apps/plugins/otp.c @@ -0,0 +1,1095 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2016 Franklin Wei + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* simple OTP plugin */ + +/* see RFCs 4226, 6238 for more information about the algorithms used */ + +#include "plugin.h" + +#include "lib/display_text.h" +#include "lib/pluginlib_actions.h" +#include "lib/pluginlib_exit.h" +#include "lib/sha1.h" + +#define MAX_NAME 50 +#define SECRET_MAX 256 +#define URI_MAX 256 +#define ACCT_FILE PLUGIN_APPS_DATA_DIR "/otp.dat" + +#define MAX(a, b) (((a)>(b))?(a):(b)) + +struct account_t { + char name[MAX_NAME]; + + bool is_totp; // hotp otherwise + + union { + uint64_t hotp_counter; + int totp_period; + }; + + int digits; + + unsigned char secret[SECRET_MAX]; + int sec_len; +}; + +static int max_accts = 0; + +/* in plugin buffer */ +static struct account_t *accounts = NULL; + +static int next_slot = 0; + +/* in SECONDS, asked for on first run */ +static int time_offs = 0; + +static int HOTP(unsigned char *secret, size_t sec_len, uint64_t ctr, int digits) +{ + ctr = htobe64(ctr); + unsigned char hash[20]; + if(hmac_sha1(secret, sec_len, &ctr, 8, hash)) + { + return -1; + } + + int offs = hash[19] & 0xF; + uint32_t code = (hash[offs] & 0x7F) << 24 | + hash[offs + 1] << 16 | + hash[offs + 2] << 8 | + hash[offs + 3]; + + int mod = 1; + for(int i = 0; i < digits; ++i) + mod *= 10; + + // debug + // rb->splashf(HZ * 5, "HOTP %*s, %llu, %d: %d", sec_len, secret, htobe64(ctr), digits, code % mod); + + return code % mod; +} + +#if CONFIG_RTC +static time_t get_utc(void) +{ + return rb->mktime(rb->get_time()) - time_offs; +} + +static int TOTP(unsigned char *secret, size_t sec_len, uint64_t step, int digits) +{ + uint64_t tm = get_utc() / step; + return HOTP(secret, sec_len, tm, digits); +} +#endif + +/* search the accounts for a duplicate */ +static bool acct_exists(const char *name) +{ + for(int i = 0; i < next_slot; ++i) + if(!rb->strcmp(accounts[i].name, name)) + return true; + return false; +} + +// Base32 implementation +// +// Copyright 2010 Google Inc. +// Author: Markus Gutschke +// +// 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. + +static int base32_decode(uint8_t *result, int bufSize, const uint8_t *encoded) { + int buffer = 0; + int bitsLeft = 0; + int count = 0; + for (const uint8_t *ptr = encoded; count < bufSize && *ptr; ++ptr) { + uint8_t ch = *ptr; + if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') { + continue; + } + buffer <<= 5; + + // Deal with commonly mistyped characters + if (ch == '0') { + ch = 'O'; + } else if (ch == '1') { + ch = 'L'; + } else if (ch == '8') { + ch = 'B'; + } + + // Look up one base32 digit + if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) { + ch = (ch & 0x1F) - 1; + } else if (ch >= '2' && ch <= '7') { + ch -= '2' - 26; + } else { + return -1; + } + + buffer |= ch; + bitsLeft += 5; + if (bitsLeft >= 8) { + result[count++] = buffer >> (bitsLeft - 8); + bitsLeft -= 8; + } + } + if (count < bufSize) { + result[count] = '\000'; + } + return count; +} + +static int base32_encode(const uint8_t *data, int length, uint8_t *result, + int bufSize) { + if (length < 0 || length > (1 << 28)) { + return -1; + } + int count = 0; + if (length > 0) { + int buffer = data[0]; + int next = 1; + int bitsLeft = 8; + while (count < bufSize && (bitsLeft > 0 || next < length)) { + if (bitsLeft < 5) { + if (next < length) { + buffer <<= 8; + buffer |= data[next++] & 0xFF; + bitsLeft += 8; + } else { + int pad = 5 - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + int index = 0x1F & (buffer >> (bitsLeft - 5)); + bitsLeft -= 5; + result[count++] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[index]; + } + } + if (count < bufSize) { + result[count] = '\000'; + } + return count; +} + +/*********************************************************************** + * File browser (from rockpaint) + ***********************************************************************/ + +static bool browse( char *dst, int dst_size, const char *start ) +{ + struct browse_context browse; + + rb->browse_context_init(&browse, SHOW_ALL, + BROWSE_SELECTONLY|BROWSE_NO_CONTEXT_MENU, + NULL, NOICON, start, NULL); + + browse.buf = dst; + browse.bufsize = dst_size; + + rb->rockbox_browse(&browse); + + return (browse.flags & BROWSE_SELECTED); +} + +static bool read_accts(void) +{ + int fd = rb->open(ACCT_FILE, O_RDONLY); + if(fd < 0) + return false; + + char buf[4]; + char magic[4] = { 'O', 'T', 'P', '1' }; + rb->read(fd, buf, 4); + if(memcmp(magic, buf, 4)) + { + rb->splash(HZ * 2, "Corrupt save data!"); + rb->close(fd); + return false; + } + + rb->read(fd, &time_offs, sizeof(time_offs)); + + while(next_slot < max_accts) + { + if(rb->read(fd, accounts + next_slot, sizeof(struct account_t)) != sizeof(struct account_t)) + break; + ++next_slot; + } + + rb->close(fd); + return true; +} + +static void save_accts(void) +{ + int fd = rb->open(ACCT_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0600); + rb->fdprintf(fd, "OTP1"); + + rb->write(fd, &time_offs, sizeof(time_offs)); + + for(int i = 0; i < next_slot; ++i) + rb->write(fd, accounts + i, sizeof(struct account_t)); + rb->close(fd); +} + +static void add_acct_file(void) +{ + char fname[MAX_PATH]; + rb->splash(HZ * 2, "Please choose file containing URI(s)."); + int before = next_slot; + if(browse(fname, sizeof(fname), "/")) + { + int fd = rb->open(fname, O_RDONLY); + do { + memset(accounts + next_slot, 0, sizeof(struct account_t)); + + accounts[next_slot].digits = 6; + + char uri_buf[URI_MAX]; + if(!rb->read_line(fd, uri_buf, sizeof(uri_buf))) + break; + + if(next_slot >= max_accts) + { + rb->splash(HZ * 2, "Account limit reached: some accounts not added."); + break; + } + + /* check for URI prefix */ + if(rb->strncmp(uri_buf, "otpauth://", 10)) + continue; + + char *save; + char *tok = rb->strtok_r(uri_buf + 10, "/", &save); + if(!rb->strcmp(tok, "totp")) + { + accounts[next_slot].is_totp = true; + accounts[next_slot].totp_period = 30; +#if !CONFIG_RTC + rb->splash(2 * HZ, "TOTP not supported!"); + continue; +#endif + } + else if(!rb->strcmp(tok, "hotp")) + { + accounts[next_slot].is_totp = false; + accounts[next_slot].hotp_counter = 0; + } + + tok = rb->strtok_r(NULL, "?", &save); + if(!tok) + continue; + + if(acct_exists(tok)) + { + rb->splashf(HZ * 2, "Not adding account with duplicate name `%s'!", tok); + continue; + } + + if(!rb->strlen(tok)) + { + rb->splashf(HZ * 2, "Skipping account with empty name."); + continue; + } + + rb->strlcpy(accounts[next_slot].name, tok, sizeof(accounts[next_slot].name)); + + bool have_secret = false; + + do { + tok = rb->strtok_r(NULL, "=", &save); + if(!tok) + continue; + + if(!rb->strcmp(tok, "secret")) + { + if(have_secret) + { + rb->splashf(HZ * 2, "URI with multiple `secret' parameters found, skipping!"); + goto fail; + } + have_secret = true; + tok = rb->strtok_r(NULL, "&", &save); + if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, tok)) <= 0) + goto fail; + } + else if(!rb->strcmp(tok, "counter")) + { + if(accounts[next_slot].is_totp) + { + rb->splash(HZ * 2, "Counter parameter specified for TOTP!? Skipping..."); + goto fail; + } + tok = rb->strtok_r(NULL, "&", &save); + accounts[next_slot].hotp_counter = rb->atoi(tok); + } + else if(!rb->strcmp(tok, "period")) + { + if(!accounts[next_slot].is_totp) + { + rb->splash(HZ * 2, "Period parameter specified for HOTP!? Skipping..."); + goto fail; + } + tok = rb->strtok_r(NULL, "&", &save); + accounts[next_slot].totp_period = rb->atoi(tok); + } + else if(!rb->strcmp(tok, "digits")) + { + tok = rb->strtok_r(NULL, "&", &save); + accounts[next_slot].digits = rb->atoi(tok); + if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9) + { + rb->splashf(HZ * 2, "Digits parameter not in acceptable range, skipping."); + goto fail; + } + } + else + rb->splashf(HZ, "Unnown parameter `%s' ignored.", tok); + } while(tok); + + if(!have_secret) + { + rb->splashf(HZ * 2, "URI with NO `secret' parameter found, skipping!"); + goto fail; + } + + ++next_slot; + + fail: + + ; + } while(1); + rb->close(fd); + } + if(before == next_slot) + rb->splash(HZ * 2, "No accounts added."); + else + { + rb->splashf(HZ * 2, "Added %d account(s).", next_slot - before); + save_accts(); + } +} + +static void add_acct_manual(void) +{ + if(next_slot >= max_accts) + { + rb->splashf(HZ * 2, "Account limit reached!"); + return; + } + memset(accounts + next_slot, 0, sizeof(struct account_t)); + + rb->splash(HZ * 1, "Enter account name."); + if(rb->kbd_input(accounts[next_slot].name, sizeof(accounts[next_slot].name)) < 0) + return; + + if(acct_exists(accounts[next_slot].name)) + { + rb->splash(HZ * 2, "Duplicate account name!"); + return; + } + + rb->splash(HZ * 2, "Enter base32-encoded secret."); + + char temp_buf[SECRET_MAX * 2]; + memset(temp_buf, 0, sizeof(temp_buf)); + + if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0) + return; + + if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, temp_buf)) <= 0) + { + rb->splash(HZ * 2, "Invalid Base32 secret!"); + return; + } + +#if CONFIG_RTC + const struct text_message prompt = { (const char*[]) {"Is this a TOTP account?", "The protocol can be determined from the URI."}, 2}; + enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL); + if(response == YESNO_NO) + accounts[next_slot].is_totp = false; + else + accounts[next_slot].is_totp = true; +#endif + + memset(temp_buf, 0, sizeof(temp_buf)); + + if(!accounts[next_slot].is_totp) + { + rb->splash(HZ * 2, "Enter counter (0 is normal)."); + temp_buf[0] = '0'; + } + else + { + rb->splash(HZ * 2, "Enter time step (30 is normal)."); + temp_buf[0] = '3'; + temp_buf[1] = '0'; + } + + if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0) + return; + + if(!accounts[next_slot].is_totp) + accounts[next_slot].hotp_counter = rb->atoi(temp_buf); + else + accounts[next_slot].totp_period = rb->atoi(temp_buf); + + rb->splash(HZ * 2, "Enter code length (6 is normal)."); + + memset(temp_buf, 0, sizeof(temp_buf)); + temp_buf[0] = '6'; + + if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0) + return; + + accounts[next_slot].digits = rb->atoi(temp_buf); + + if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9) + { + rb->splash(HZ, "Invalid length!"); + return; + } + + ++next_slot; + + save_accts(); + + rb->splashf(HZ * 2, "Success."); +} + +static void add_acct(void) +{ + MENUITEM_STRINGLIST(menu, "Add Account", NULL, + "From URI on disk", + "Manual Entry", + "Back"); + int sel = 0; + bool quit = false; + while(!quit) + { + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 0: + add_acct_file(); + break; + case 1: + add_acct_manual(); + break; + case 2: + default: + quit = true; + break; + } + } +} + +static void show_code(int acct) +{ + /* rockbox's printf doesn't support a variable field width afaik */ + char format_buf[64]; + if(!accounts[acct].is_totp) + { + rb->snprintf(format_buf, sizeof(format_buf), "%%0%dd", accounts[acct].digits); + rb->splashf(0, format_buf, HOTP(accounts[acct].secret, + accounts[acct].sec_len, + accounts[acct].hotp_counter, + accounts[acct].digits)); + ++accounts[acct].hotp_counter; + } +#if CONFIG_RTC + else + { + rb->snprintf(format_buf, sizeof(format_buf), "%%0%dd (%%ld second(s) left)", accounts[acct].digits); + rb->splashf(0, format_buf, TOTP(accounts[acct].secret, + accounts[acct].sec_len, + accounts[acct].totp_period, + accounts[acct].digits), + accounts[acct].totp_period - get_utc() % accounts[acct].totp_period); + } +#else + else + { + rb->splash(0, "TOTP not supported on this device!"); + } +#endif + rb->sleep(HZ * 2); + while(1) + { + int button = rb->button_get(true); + if(button && !(button & BUTTON_REL)) + break; + rb->yield(); + } + + save_accts(); + rb->lcd_clear_display(); +} + +static void gen_codes(void) +{ + rb->lcd_clear_display(); + /* native menus don't seem to support dynamic names easily, so we + * roll our own */ + static const struct button_mapping *plugin_contexts[] = { pla_main_ctx }; + int idx = 0; + if(next_slot > 0) + { + rb->lcd_putsf(0, 0, "Generate Code"); + rb->lcd_putsf(0, 1, "%s", accounts[0].name); + rb->lcd_update(); + } + else + { + rb->splash(HZ * 2, "No accounts configured!"); + return; + } + while(1) + { + int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts)); + switch(button) + { + case PLA_LEFT: + --idx; + if(idx < 0) + idx = next_slot - 1; + break; + case PLA_RIGHT: + ++idx; + if(idx >= next_slot) + idx = 0; + break; + case PLA_SELECT: + show_code(idx); + break; + case PLA_CANCEL: + case PLA_EXIT: + exit_on_usb(button); + return; + default: + break; + } + rb->lcd_clear_display(); + rb->lcd_putsf(0, 0, "Generate Code"); + rb->lcd_putsf(0, 1, "%s", accounts[idx].name); + rb->lcd_update(); + } +} + +static bool danger_confirm(void) +{ + int sel = 0; + MENUITEM_STRINGLIST(menu, "Are you REALLY SURE?", NULL, + "No", + "No", + "No", + "No", + "No", + "No", + "No", + "Yes, DO IT", // 7 + "No", + "No", + "No", + "No"); + + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 7: + return true; + default: + return false; + } +} + +char data_buf[MAX(MAX_NAME, SECRET_MAX * 2)]; +char temp_sec[SECRET_MAX]; +size_t old_len; + +static void edit_menu(int acct) +{ + rb->splashf(HZ, "Editing account `%s'.", accounts[acct].name); + + /* HACK ALERT */ + /* two different menus, one handling logic */ + MENUITEM_STRINGLIST(menu_1, "Edit Account", NULL, + "Rename", + "Delete", + "Change HOTP Counter", + "Change Digit Count", + "Change Shared Secret", + "Back"); + + MENUITEM_STRINGLIST(menu_2, "Edit Account", NULL, + "Rename", // 0 + "Delete", // 1 + "Change TOTP Period", // 2 + "Change Digit Count", // 3 + "Change Shared Secret", // 4 + "Back"); // 5 + + const struct menu_item_ex *menu = (accounts[acct].is_totp) ? &menu_2 : &menu_1; + + bool quit = false; + int sel = 0; + while(!quit) + { + switch(rb->do_menu(menu, &sel, NULL, false)) + { + case 0: // rename + rb->splash(HZ, "Enter new name."); + rb->strlcpy(data_buf, accounts[acct].name, sizeof(data_buf)); + if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0) + break; + if(acct_exists(data_buf)) + { + rb->splash(HZ * 2, "Duplicate account name!"); + break; + } + rb->strlcpy(accounts[acct].name, data_buf, sizeof(accounts[acct].name)); + save_accts(); + break; + case 1: // delete + if(danger_confirm()) + { + rb->memmove(accounts + acct, accounts + acct + 1, (next_slot - acct - 1) * sizeof(struct account_t)); + --next_slot; + save_accts(); + rb->splashf(HZ, "Deleted."); + return; + } + else + rb->splash(HZ, "Not confirmed."); + break; + case 2: // HOTP counter OR TOTP period + if(accounts[acct].is_totp) + rb->snprintf(data_buf, sizeof(data_buf), "%d", (int)accounts[acct].hotp_counter); + else + rb->snprintf(data_buf, sizeof(data_buf), "%d", accounts[acct].totp_period); + + if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0) + break; + + if(accounts[acct].is_totp) + accounts[acct].totp_period = rb->atoi(data_buf); + else + accounts[acct].hotp_counter = rb->atoi(data_buf); + + save_accts(); + + rb->splash(HZ, "Success."); + break; + case 3: // digits + rb->snprintf(data_buf, sizeof(data_buf), "%d", accounts[acct].digits); + if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0) + break; + + accounts[acct].digits = rb->atoi(data_buf); + save_accts(); + + rb->splash(HZ, "Success."); + break; + case 4: // secret + old_len = accounts[acct].sec_len; + memcpy(temp_sec, accounts[acct].secret, accounts[acct].sec_len); + base32_encode(accounts[acct].secret, accounts[acct].sec_len, data_buf, sizeof(data_buf)); + + if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0) + break; + + int ret = base32_decode(accounts[acct].secret, sizeof(accounts[acct].secret), data_buf); + if(ret <= 0) + { + memcpy(accounts[acct].secret, temp_sec, SECRET_MAX); + accounts[acct].sec_len = old_len; + rb->splash(HZ * 2, "Invalid Base32 secret!"); + break; + } + accounts[acct].sec_len = ret; + + save_accts(); + rb->splash(HZ, "Success."); + break; + case 5: + quit = true; + break; + default: + break; + } + } +} + +static void edit_accts(void) +{ + rb->lcd_clear_display(); + /* native menus don't seem to support dynamic names easily, so we + * roll our own */ + static const struct button_mapping *plugin_contexts[] = { pla_main_ctx }; + int idx = 0; + if(next_slot > 0) + { + rb->lcd_putsf(0, 0, "Edit Account"); + rb->lcd_putsf(0, 1, "%s", accounts[0].name); + rb->lcd_update(); + } + else + { + rb->splash(HZ * 2, "No accounts configured!"); + return; + } + while(1) + { + int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts)); + switch(button) + { + case PLA_LEFT: + --idx; + if(idx < 0) + idx = next_slot - 1; + break; + case PLA_RIGHT: + ++idx; + if(idx >= next_slot) + idx = 0; + break; + case PLA_SELECT: + edit_menu(idx); + if(!next_slot) + return; + if(idx == next_slot) + idx = 0; + break; + case PLA_CANCEL: + case PLA_EXIT: + return; + default: + exit_on_usb(button); + break; + } + rb->lcd_clear_display(); + rb->lcd_putsf(0, 0, "Edit Account"); + rb->lcd_putsf(0, 1, "%s", accounts[idx].name); + rb->lcd_update(); + } +} + +#if CONFIG_RTC + +/* label is like this: [+/-]HH:MM ... */ +static int get_time_seconds(const char *label) +{ + if(!rb->strcmp(label, "UTC")) + return 0; + + char buf[32]; + + /* copy the part after "UTC" */ + rb->strlcpy(buf, label + 3, sizeof(buf)); + + char *save, *tok; + + tok = rb->strtok_r(buf, ":", &save); + /* positive or negative: sign left */ + int hr = rb->atoi(tok); + + tok = rb->strtok_r(NULL, ": ", &save); + int min = rb->atoi(tok); + + return 3600 * hr + 60 * min; +} + +/* returns the offset in seconds associated with a time zone */ +static int get_time_offs(void) +{ + MENUITEM_STRINGLIST(menu, "Select Time Offset", NULL, + "UTC-12:00", // 0 + "UTC-11:00", // 1 + "UTC-10:00 (HAST)", // 2 + "UTC-9:30", // 3 + "UTC-9:00 (AKST, HADT)", // 4 + "UTC-8:00 (PST, AKDT)", // 5 + "UTC-7:00 (MST, PDT)", // 6 + "UTC-6:00 (CST, MDT)", // 7 + "UTC-5:00 (EST, CDT)", // 8 + "UTC-4:00 (AST, EDT)", // 9 + "UTC-3:30 (NST)", // 10 + "UTC-3:00 (ADT)", // 11 + "UTC-2:30 (NDT)", // 12 + "UTC-2:00", // 13 + "UTC-1:00", // 14 + "UTC", // 15 + "UTC+1:00", // 16 + "UTC+2:00", // 17 + "UTC+3:00", // 18 + "UTC+3:30", // 19 + "UTC+4:00", // 20 + "UTC+4:30", // 21 + "UTC+5:00", // 22 + "UTC+5:30", // 23 + "UTC+5:45", // 24 + "UTC+6:00", // 25 + "UTC+6:30", // 26 + "UTC+7:00", // 27 + "UTC+8:00", // 28 + "UTC+8:30", // 29 + "UTC+8:45", // 30 + "UTC+9:00", // 31 + "UTC+9:30", // 32 + "UTC+10:00", // 33 + "UTC+10:30", // 34 + "UTC+11:00", // 35 + "UTC+12:00", // 36 + "UTC+12:45", // 37 + "UTC+13:00", // 38 + "UTC+14:00", // 39 + ); + + int sel = 0; + for(unsigned int i = 0; i < ARRAYLEN(menu_); ++i) + if(time_offs == get_time_seconds(menu_[i])) + { + sel = i; + break; + } + + /* relies on menu internals */ + rb->do_menu(&menu, &sel, NULL, false); + + /* see apps/menu.h */ + const char *label = menu_[sel]; + + return get_time_seconds(label); + +#if 0 + /* provided in case menu internals change */ + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 0: case 1: case 2: + return (sel - 12) * 3600; + case 3: + return -9 * 3600 - 30 * 60; + case 4: case 5: case 6: case 7: case 8: case 9: + return (sel - 13) * 3600; + case 10: + return -3 * 3600 - 30 * 60; + case 11: + return -3 * 3600; + case 12: + return -3 * 3600 - 30 * 60; + case 13: case 14: case 15: case 16: case 17: case 18: + return (sel - 15) * 3600; + + case 19: + return 3 * 3600 + 30 * 60; + case 20: + return 4 * 3600; + case 21: + return 4 * 3600 + 30 * 60; + case 22: + return 5 * 3600; + case 23: + return 5 * 3600 + 30 * 60; + case 24: + return 5 * 3600 + 45 * 60; + case 25: + return 6 * 3600; + case 26: + return 6 * 3600 + 30 * 60; + case 27: case 28: + return (sel - 20) * 3600; + case 29: + return 8 * 3600 + 30 * 60; + case 30: + return 8 * 3600 + 45 * 60; + case 31: + return 9 * 3600; + case 32: + return 9 * 3600 + 30 * 60; + case 33: + return 10 * 3600; + case 34: + return 10 * 3600 + 30 * 60; + case 35: case 36: + return (sel - 24) * 3600; + case 37: + return 12 * 3600 + 45 * 60; + case 38: case 39: + return (sel - 25) * 3600; + default: + rb->splash(0, "BUG: time zone fall-through: REPORT ME!!!"); + break; + } + return 0; +#endif +} +#endif + +static void adv_menu(void) +{ + MENUITEM_STRINGLIST(menu, "Advanced", NULL, + "Edit Account", + "Delete ALL accounts", +#if CONFIG_RTC + "Change Time Offset", +#endif + "Back"); + + bool quit = false; + int sel = 0; + while(!quit) + { + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 0: + edit_accts(); + break; + case 1: + if(danger_confirm()) + { + next_slot = 0; + save_accts(); + rb->splash(HZ, "It is done, my master."); + } + else + rb->splash(HZ, "Not confirmed."); + break; +#if CONFIG_RTC + case 2: + time_offs = get_time_offs(); + break; + case 3: +#else + case 2: +#endif + quit = 1; + break; + default: + break; + } + } +} + +/* displays the help text */ +static void show_help(void) +{ + +#ifdef HAVE_LCD_COLOR + rb->lcd_set_foreground(LCD_WHITE); + rb->lcd_set_background(LCD_BLACK); +#endif + +#ifdef HAVE_LCD_BITMAP + rb->lcd_setfont(FONT_UI); +#endif + + static char *help_text[] = { "One-Time Password Manager", "", + "Introduction", "", + "This", "plugin", "allows", "you", "to", "generate", "one-time", "passwords", "to", "provide", "a", "second", "factor", "of", "authentication", "for", "services", "that", "support", "it.", + "It", "suppports", "both", "event-based", "(HOTP),", "and", "time-based", "(TOTP)", "password", "schemes.", + "In", "order", "to", "ensure", "proper", "functioning", "of", "time-based", "passwords", "ensure", "that", "the", "clock", "is", "accurate", "to", "within", "30", "seconds", "of", "actual", "time." + "Note", "that", "some", "devices", "lack", "a", "real-time", "clock,", "so", "time-based", "passwords", "are", "not", "supported", "on", "those", "targets." }; + + struct style_text style[] = { + {0, TEXT_CENTER | TEXT_UNDERLINE}, + {2, C_RED}, + LAST_STYLE_ITEM + }; + + display_text(ARRAYLEN(help_text), help_text, style, NULL, true); +} + +/* this is the plugin entry point */ +enum plugin_status plugin_start(const void* parameter) +{ + (void)parameter; + + /* self-test with RFC 4226 values */ + if(HOTP("12345678901234567890", rb->strlen("12345678901234567890"), 1, 6) != 287082) + { + return PLUGIN_ERROR; + } + + size_t bufsz; + accounts = rb->plugin_get_buffer(&bufsz); + max_accts = bufsz / sizeof(struct account_t); + + if(!read_accts()) +#if CONFIG_RTC + { + time_offs = get_time_offs(); + } +#else + { + ; + } +#endif + + MENUITEM_STRINGLIST(menu, "One-Time Password Manager", NULL, + "Add Account", + "Generate Code", + "Help", + "Advanced", + "Quit"); + + bool quit = false; + int sel = 0; + while(!quit) + { + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 0: + add_acct(); + break; + case 1: + gen_codes(); + break; + case 2: + show_help(); + break; + case 3: + adv_menu(); + break; + case 4: + quit = 1; + break; + default: + break; + } + } + + /* save to disk */ + save_accts(); + + /* tell Rockbox that we have completed successfully */ + return PLUGIN_OK; +} diff --git a/manual/configure_rockbox/time_and_date.tex b/manual/configure_rockbox/time_and_date.tex index fe1624211b..aa3f563eed 100644 --- a/manual/configure_rockbox/time_and_date.tex +++ b/manual/configure_rockbox/time_and_date.tex @@ -1,5 +1,7 @@ % $Id:$ % +\label{ref:Timeanddateactual} + Time related menu options. Pressing \ActionStdContext{} will voice the current time if voice support is enabled. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index e5f5deb140..2cd49035ff 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -272,6 +272,8 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} {\input{plugins/metronome.tex}} +{\input{plugins/otp.tex}} + \opt{lcd_bitmap}{\input{plugins/periodic_table.tex}} \opt{swcodec}{\opt{recording_mic}{\input{plugins/pitch_detector.tex}}} diff --git a/manual/plugins/otp.tex b/manual/plugins/otp.tex new file mode 100644 index 0000000000..5b1a29f8c2 --- /dev/null +++ b/manual/plugins/otp.tex @@ -0,0 +1,72 @@ +% $Id$ % +\subsection{One-Time Password Client} +This plugin provides the ability to generate one-time passwords (OTPs) +for authentication purposes. It implements an HMAC-based One-Time +Password Algorithm (RFC 4226), and on targets which support it, a +Time-based One-Time Password Algorithm (RFC 6238). + +\subsubsection{Adding Accounts} +The plugin supports two methods of adding accounts: URI import, and +manual entry. + +\opt{rtc}{ It is important to note that for TOTP (time-based) accounts + to work properly, the clock on your device MUST be accurate to no + less than 30 seconds from the time on the authentication server, and + the correct time zone must be configured in the plugin. See + \reference{ref:Timeanddateactual} for more information. } + +\subsubsection{URI Import} +This method of adding an account reads a list of URIs from a file. It +expects each URI to be on a line by itself in the following format: + +\begin{verbatim} +otpauth://[hotp OR totp]/[account name]?secret=[Base32 secret][&counter=X][&period=X][&digits=X] +\end{verbatim} + +An example is shown below, provisioning a TOTP key for an account called ``bob'': + +\begin{verbatim} +otpauth://totp/bob?secret=JBSWY3DPEHPK3PXP +\end{verbatim} + +Any other URI options are not supported and will be ignored. + +Most services will provide a scannable QR code that encodes a OTP +URI. In order to use those, first scan the QR code separately and save +the URI to a file on your device. If necessary, rewrite the URI so it +is in the format shown above. For example, GitHub's URI has a slash +after the provider. In order for this URI to be properly parsed, you +must rewrite the account name so that it does not contain a slash. + +\subsubsection{Manual Import} +If direct URI import is not possible, the plugin supports the manual +entry of data associated with an account. After you select the +``Manual Entry'' option, it will prompt you for an account name. You +may type anything you wish, but it should be memorable. It will then +prompt you for the Base32-encoded secret. Most services will provide +this to you directly, but some may only provide you with a QR code. In +these cases, you must scan the QR code separately, and then enter the +string following the ``secret='' parameter on your Rockbox device +manually. + +On devices with a real-time clock, \opt{rtc}{like yours,} the plugin +will ask whether the account is a time-based account +(TOTP). \opt{rtc}{If you answer ``yes'' to this question, it will ask + for further information regarding the account. Usually it is safe to + accept the defaults here. } However, if your device lacks a +real-time clock, the plugin's functionality will be restricted to +HMAC-based (HOTP) accounts only. If this is the case, the plugin will +prompt you for information regarding the HOTP setup. + +\opt{rtc} { + \subsection{Advanced Settings} + \subsubsection{Time Zone Configuration} + In order for TOTP accounts to work properly, the plugin must be able + to determine the current UTC time. This means that, first, your + device's clock must be synchronized with UTC time, and second, that + the plugin knows what time zone the clock is using. The plugin will + prompt you on its first run for this piece of information. However, + should this setting need changing at a later time, possibly due to + Daylight Saving Time adjustment, it is located under the + ``Advanced'' submenu. NOTE: in the UI simulator, use the ``UTC'' + setting no matter what the clock may read. }