aced667f48
The new vuprintf makes unnecessary workarounds due to formatting limitations. I checked grep output for whatever appeared to fit but it's possible I missed some instances because they weren't so obvious. Also, this means sound settings can dynamically work with any number of decimals rather than the current assumption of one or two. Add an ipow() function to help and take advantage of dynamic field width and precision. Consolidate string formatting of sound settings. Change-Id: I46caf534859dfd1916cd440cd25e5206b192fcd8
1093 lines
30 KiB
C
1093 lines
30 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* 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)
|
|
{
|
|
if(!accounts[acct].is_totp)
|
|
{
|
|
rb->splashf(0, "%0*d", accounts[acct].digits,
|
|
HOTP(accounts[acct].secret,
|
|
accounts[acct].sec_len,
|
|
accounts[acct].hotp_counter,
|
|
accounts[acct].digits));
|
|
++accounts[acct].hotp_counter;
|
|
}
|
|
#if CONFIG_RTC
|
|
else
|
|
{
|
|
rb->splashf(0, "%0*d (%ld seconds(s) left)", accounts[acct].digits,
|
|
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;
|
|
}
|