summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFranklin Wei <frankhwei536@gmail.com>2016-05-25 21:43:32 -0400
committerFranklin Wei <frankhwei536@gmail.com>2016-06-05 14:25:09 -0400
commit30d7ead6af5c28ff72d6e47bab7e044657be7ce6 (patch)
tree57b38d872bb37cf361fb2777ba5da48d5fc14a3c
parent59ae562a3280105595e690ebff772ea4f7790970 (diff)
downloadrockbox-30d7ead.tar.gz
rockbox-30d7ead.zip
One-Time Password client (HOTP and TOTP)
* Implements RFC 4226 (HOTP) and RFC 6238 (TOTP) * Adds sha1.c to apps/plugins/lib (orignally tools/hmac-sha1.c) * See manual entry for more information Change-Id: Ia4a4031b29f97361b541e71438aa7f3ea82212f2
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES1
-rw-r--r--apps/plugins/lib/SOURCES1
-rw-r--r--apps/plugins/lib/sha1.c434
-rw-r--r--apps/plugins/lib/sha1.h116
-rw-r--r--apps/plugins/otp.c1095
-rw-r--r--manual/configure_rockbox/time_and_date.tex2
-rw-r--r--manual/plugins/main.tex2
-rw-r--r--manual/plugins/otp.tex72
9 files changed, 1724 insertions, 0 deletions
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 <robert@ilse.nl> -- 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 <stddef.h>
+
+/* 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. }