summaryrefslogtreecommitdiff
path: root/apps/plugins/otp.c
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 /apps/plugins/otp.c
parent59ae562a3280105595e690ebff772ea4f7790970 (diff)
downloadrockbox-30d7ead6af5c28ff72d6e47bab7e044657be7ce6.zip
rockbox-30d7ead6af5c28ff72d6e47bab7e044657be7ce6.tar.gz
rockbox-30d7ead6af5c28ff72d6e47bab7e044657be7ce6.tar.bz2
rockbox-30d7ead6af5c28ff72d6e47bab7e044657be7ce6.tar.xz
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
Diffstat (limited to 'apps/plugins/otp.c')
-rw-r--r--apps/plugins/otp.c1095
1 files changed, 1095 insertions, 0 deletions
diff --git a/apps/plugins/otp.c b/apps/plugins/otp.c
new file mode 100644
index 0000000..69cb8b7
--- /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;
+}