summaryrefslogtreecommitdiff
path: root/apps/plugins/otp.c
diff options
context:
space:
mode:
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 0000000000..69cb8b7982
--- /dev/null
+++ b/apps/plugins/otp.c
@@ -0,0 +1,1095 @@
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2016 Franklin Wei
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ****************************************************************************/
21
22/* simple OTP plugin */
23
24/* see RFCs 4226, 6238 for more information about the algorithms used */
25
26#include "plugin.h"
27
28#include "lib/display_text.h"
29#include "lib/pluginlib_actions.h"
30#include "lib/pluginlib_exit.h"
31#include "lib/sha1.h"
32
33#define MAX_NAME 50
34#define SECRET_MAX 256
35#define URI_MAX 256
36#define ACCT_FILE PLUGIN_APPS_DATA_DIR "/otp.dat"
37
38#define MAX(a, b) (((a)>(b))?(a):(b))
39
40struct account_t {
41 char name[MAX_NAME];
42
43 bool is_totp; // hotp otherwise
44
45 union {
46 uint64_t hotp_counter;
47 int totp_period;
48 };
49
50 int digits;
51
52 unsigned char secret[SECRET_MAX];
53 int sec_len;
54};
55
56static int max_accts = 0;
57
58/* in plugin buffer */
59static struct account_t *accounts = NULL;
60
61static int next_slot = 0;
62
63/* in SECONDS, asked for on first run */
64static int time_offs = 0;
65
66static int HOTP(unsigned char *secret, size_t sec_len, uint64_t ctr, int digits)
67{
68 ctr = htobe64(ctr);
69 unsigned char hash[20];
70 if(hmac_sha1(secret, sec_len, &ctr, 8, hash))
71 {
72 return -1;
73 }
74
75 int offs = hash[19] & 0xF;
76 uint32_t code = (hash[offs] & 0x7F) << 24 |
77 hash[offs + 1] << 16 |
78 hash[offs + 2] << 8 |
79 hash[offs + 3];
80
81 int mod = 1;
82 for(int i = 0; i < digits; ++i)
83 mod *= 10;
84
85 // debug
86 // rb->splashf(HZ * 5, "HOTP %*s, %llu, %d: %d", sec_len, secret, htobe64(ctr), digits, code % mod);
87
88 return code % mod;
89}
90
91#if CONFIG_RTC
92static time_t get_utc(void)
93{
94 return rb->mktime(rb->get_time()) - time_offs;
95}
96
97static int TOTP(unsigned char *secret, size_t sec_len, uint64_t step, int digits)
98{
99 uint64_t tm = get_utc() / step;
100 return HOTP(secret, sec_len, tm, digits);
101}
102#endif
103
104/* search the accounts for a duplicate */
105static bool acct_exists(const char *name)
106{
107 for(int i = 0; i < next_slot; ++i)
108 if(!rb->strcmp(accounts[i].name, name))
109 return true;
110 return false;
111}
112
113// Base32 implementation
114//
115// Copyright 2010 Google Inc.
116// Author: Markus Gutschke
117//
118// Licensed under the Apache License, Version 2.0 (the "License");
119// you may not use this file except in compliance with the License.
120// You may obtain a copy of the License at
121//
122// http://www.apache.org/licenses/LICENSE-2.0
123//
124// Unless required by applicable law or agreed to in writing, software
125// distributed under the License is distributed on an "AS IS" BASIS,
126// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
127// See the License for the specific language governing permissions and
128// limitations under the License.
129
130static int base32_decode(uint8_t *result, int bufSize, const uint8_t *encoded) {
131 int buffer = 0;
132 int bitsLeft = 0;
133 int count = 0;
134 for (const uint8_t *ptr = encoded; count < bufSize && *ptr; ++ptr) {
135 uint8_t ch = *ptr;
136 if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') {
137 continue;
138 }
139 buffer <<= 5;
140
141 // Deal with commonly mistyped characters
142 if (ch == '0') {
143 ch = 'O';
144 } else if (ch == '1') {
145 ch = 'L';
146 } else if (ch == '8') {
147 ch = 'B';
148 }
149
150 // Look up one base32 digit
151 if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
152 ch = (ch & 0x1F) - 1;
153 } else if (ch >= '2' && ch <= '7') {
154 ch -= '2' - 26;
155 } else {
156 return -1;
157 }
158
159 buffer |= ch;
160 bitsLeft += 5;
161 if (bitsLeft >= 8) {
162 result[count++] = buffer >> (bitsLeft - 8);
163 bitsLeft -= 8;
164 }
165 }
166 if (count < bufSize) {
167 result[count] = '\000';
168 }
169 return count;
170}
171
172static int base32_encode(const uint8_t *data, int length, uint8_t *result,
173 int bufSize) {
174 if (length < 0 || length > (1 << 28)) {
175 return -1;
176 }
177 int count = 0;
178 if (length > 0) {
179 int buffer = data[0];
180 int next = 1;
181 int bitsLeft = 8;
182 while (count < bufSize && (bitsLeft > 0 || next < length)) {
183 if (bitsLeft < 5) {
184 if (next < length) {
185 buffer <<= 8;
186 buffer |= data[next++] & 0xFF;
187 bitsLeft += 8;
188 } else {
189 int pad = 5 - bitsLeft;
190 buffer <<= pad;
191 bitsLeft += pad;
192 }
193 }
194 int index = 0x1F & (buffer >> (bitsLeft - 5));
195 bitsLeft -= 5;
196 result[count++] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[index];
197 }
198 }
199 if (count < bufSize) {
200 result[count] = '\000';
201 }
202 return count;
203}
204
205/***********************************************************************
206 * File browser (from rockpaint)
207 ***********************************************************************/
208
209static bool browse( char *dst, int dst_size, const char *start )
210{
211 struct browse_context browse;
212
213 rb->browse_context_init(&browse, SHOW_ALL,
214 BROWSE_SELECTONLY|BROWSE_NO_CONTEXT_MENU,
215 NULL, NOICON, start, NULL);
216
217 browse.buf = dst;
218 browse.bufsize = dst_size;
219
220 rb->rockbox_browse(&browse);
221
222 return (browse.flags & BROWSE_SELECTED);
223}
224
225static bool read_accts(void)
226{
227 int fd = rb->open(ACCT_FILE, O_RDONLY);
228 if(fd < 0)
229 return false;
230
231 char buf[4];
232 char magic[4] = { 'O', 'T', 'P', '1' };
233 rb->read(fd, buf, 4);
234 if(memcmp(magic, buf, 4))
235 {
236 rb->splash(HZ * 2, "Corrupt save data!");
237 rb->close(fd);
238 return false;
239 }
240
241 rb->read(fd, &time_offs, sizeof(time_offs));
242
243 while(next_slot < max_accts)
244 {
245 if(rb->read(fd, accounts + next_slot, sizeof(struct account_t)) != sizeof(struct account_t))
246 break;
247 ++next_slot;
248 }
249
250 rb->close(fd);
251 return true;
252}
253
254static void save_accts(void)
255{
256 int fd = rb->open(ACCT_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0600);
257 rb->fdprintf(fd, "OTP1");
258
259 rb->write(fd, &time_offs, sizeof(time_offs));
260
261 for(int i = 0; i < next_slot; ++i)
262 rb->write(fd, accounts + i, sizeof(struct account_t));
263 rb->close(fd);
264}
265
266static void add_acct_file(void)
267{
268 char fname[MAX_PATH];
269 rb->splash(HZ * 2, "Please choose file containing URI(s).");
270 int before = next_slot;
271 if(browse(fname, sizeof(fname), "/"))
272 {
273 int fd = rb->open(fname, O_RDONLY);
274 do {
275 memset(accounts + next_slot, 0, sizeof(struct account_t));
276
277 accounts[next_slot].digits = 6;
278
279 char uri_buf[URI_MAX];
280 if(!rb->read_line(fd, uri_buf, sizeof(uri_buf)))
281 break;
282
283 if(next_slot >= max_accts)
284 {
285 rb->splash(HZ * 2, "Account limit reached: some accounts not added.");
286 break;
287 }
288
289 /* check for URI prefix */
290 if(rb->strncmp(uri_buf, "otpauth://", 10))
291 continue;
292
293 char *save;
294 char *tok = rb->strtok_r(uri_buf + 10, "/", &save);
295 if(!rb->strcmp(tok, "totp"))
296 {
297 accounts[next_slot].is_totp = true;
298 accounts[next_slot].totp_period = 30;
299#if !CONFIG_RTC
300 rb->splash(2 * HZ, "TOTP not supported!");
301 continue;
302#endif
303 }
304 else if(!rb->strcmp(tok, "hotp"))
305 {
306 accounts[next_slot].is_totp = false;
307 accounts[next_slot].hotp_counter = 0;
308 }
309
310 tok = rb->strtok_r(NULL, "?", &save);
311 if(!tok)
312 continue;
313
314 if(acct_exists(tok))
315 {
316 rb->splashf(HZ * 2, "Not adding account with duplicate name `%s'!", tok);
317 continue;
318 }
319
320 if(!rb->strlen(tok))
321 {
322 rb->splashf(HZ * 2, "Skipping account with empty name.");
323 continue;
324 }
325
326 rb->strlcpy(accounts[next_slot].name, tok, sizeof(accounts[next_slot].name));
327
328 bool have_secret = false;
329
330 do {
331 tok = rb->strtok_r(NULL, "=", &save);
332 if(!tok)
333 continue;
334
335 if(!rb->strcmp(tok, "secret"))
336 {
337 if(have_secret)
338 {
339 rb->splashf(HZ * 2, "URI with multiple `secret' parameters found, skipping!");
340 goto fail;
341 }
342 have_secret = true;
343 tok = rb->strtok_r(NULL, "&", &save);
344 if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, tok)) <= 0)
345 goto fail;
346 }
347 else if(!rb->strcmp(tok, "counter"))
348 {
349 if(accounts[next_slot].is_totp)
350 {
351 rb->splash(HZ * 2, "Counter parameter specified for TOTP!? Skipping...");
352 goto fail;
353 }
354 tok = rb->strtok_r(NULL, "&", &save);
355 accounts[next_slot].hotp_counter = rb->atoi(tok);
356 }
357 else if(!rb->strcmp(tok, "period"))
358 {
359 if(!accounts[next_slot].is_totp)
360 {
361 rb->splash(HZ * 2, "Period parameter specified for HOTP!? Skipping...");
362 goto fail;
363 }
364 tok = rb->strtok_r(NULL, "&", &save);
365 accounts[next_slot].totp_period = rb->atoi(tok);
366 }
367 else if(!rb->strcmp(tok, "digits"))
368 {
369 tok = rb->strtok_r(NULL, "&", &save);
370 accounts[next_slot].digits = rb->atoi(tok);
371 if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9)
372 {
373 rb->splashf(HZ * 2, "Digits parameter not in acceptable range, skipping.");
374 goto fail;
375 }
376 }
377 else
378 rb->splashf(HZ, "Unnown parameter `%s' ignored.", tok);
379 } while(tok);
380
381 if(!have_secret)
382 {
383 rb->splashf(HZ * 2, "URI with NO `secret' parameter found, skipping!");
384 goto fail;
385 }
386
387 ++next_slot;
388
389 fail:
390
391 ;
392 } while(1);
393 rb->close(fd);
394 }
395 if(before == next_slot)
396 rb->splash(HZ * 2, "No accounts added.");
397 else
398 {
399 rb->splashf(HZ * 2, "Added %d account(s).", next_slot - before);
400 save_accts();
401 }
402}
403
404static void add_acct_manual(void)
405{
406 if(next_slot >= max_accts)
407 {
408 rb->splashf(HZ * 2, "Account limit reached!");
409 return;
410 }
411 memset(accounts + next_slot, 0, sizeof(struct account_t));
412
413 rb->splash(HZ * 1, "Enter account name.");
414 if(rb->kbd_input(accounts[next_slot].name, sizeof(accounts[next_slot].name)) < 0)
415 return;
416
417 if(acct_exists(accounts[next_slot].name))
418 {
419 rb->splash(HZ * 2, "Duplicate account name!");
420 return;
421 }
422
423 rb->splash(HZ * 2, "Enter base32-encoded secret.");
424
425 char temp_buf[SECRET_MAX * 2];
426 memset(temp_buf, 0, sizeof(temp_buf));
427
428 if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0)
429 return;
430
431 if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, temp_buf)) <= 0)
432 {
433 rb->splash(HZ * 2, "Invalid Base32 secret!");
434 return;
435 }
436
437#if CONFIG_RTC
438 const struct text_message prompt = { (const char*[]) {"Is this a TOTP account?", "The protocol can be determined from the URI."}, 2};
439 enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL);
440 if(response == YESNO_NO)
441 accounts[next_slot].is_totp = false;
442 else
443 accounts[next_slot].is_totp = true;
444#endif
445
446 memset(temp_buf, 0, sizeof(temp_buf));
447
448 if(!accounts[next_slot].is_totp)
449 {
450 rb->splash(HZ * 2, "Enter counter (0 is normal).");
451 temp_buf[0] = '0';
452 }
453 else
454 {
455 rb->splash(HZ * 2, "Enter time step (30 is normal).");
456 temp_buf[0] = '3';
457 temp_buf[1] = '0';
458 }
459
460 if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0)
461 return;
462
463 if(!accounts[next_slot].is_totp)
464 accounts[next_slot].hotp_counter = rb->atoi(temp_buf);
465 else
466 accounts[next_slot].totp_period = rb->atoi(temp_buf);
467
468 rb->splash(HZ * 2, "Enter code length (6 is normal).");
469
470 memset(temp_buf, 0, sizeof(temp_buf));
471 temp_buf[0] = '6';
472
473 if(rb->kbd_input(temp_buf, sizeof(temp_buf)) < 0)
474 return;
475
476 accounts[next_slot].digits = rb->atoi(temp_buf);
477
478 if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9)
479 {
480 rb->splash(HZ, "Invalid length!");
481 return;
482 }
483
484 ++next_slot;
485
486 save_accts();
487
488 rb->splashf(HZ * 2, "Success.");
489}
490
491static void add_acct(void)
492{
493 MENUITEM_STRINGLIST(menu, "Add Account", NULL,
494 "From URI on disk",
495 "Manual Entry",
496 "Back");
497 int sel = 0;
498 bool quit = false;
499 while(!quit)
500 {
501 switch(rb->do_menu(&menu, &sel, NULL, false))
502 {
503 case 0:
504 add_acct_file();
505 break;
506 case 1:
507 add_acct_manual();
508 break;
509 case 2:
510 default:
511 quit = true;
512 break;
513 }
514 }
515}
516
517static void show_code(int acct)
518{
519 /* rockbox's printf doesn't support a variable field width afaik */
520 char format_buf[64];
521 if(!accounts[acct].is_totp)
522 {
523 rb->snprintf(format_buf, sizeof(format_buf), "%%0%dd", accounts[acct].digits);
524 rb->splashf(0, format_buf, HOTP(accounts[acct].secret,
525 accounts[acct].sec_len,
526 accounts[acct].hotp_counter,
527 accounts[acct].digits));
528 ++accounts[acct].hotp_counter;
529 }
530#if CONFIG_RTC
531 else
532 {
533 rb->snprintf(format_buf, sizeof(format_buf), "%%0%dd (%%ld second(s) left)", accounts[acct].digits);
534 rb->splashf(0, format_buf, TOTP(accounts[acct].secret,
535 accounts[acct].sec_len,
536 accounts[acct].totp_period,
537 accounts[acct].digits),
538 accounts[acct].totp_period - get_utc() % accounts[acct].totp_period);
539 }
540#else
541 else
542 {
543 rb->splash(0, "TOTP not supported on this device!");
544 }
545#endif
546 rb->sleep(HZ * 2);
547 while(1)
548 {
549 int button = rb->button_get(true);
550 if(button && !(button & BUTTON_REL))
551 break;
552 rb->yield();
553 }
554
555 save_accts();
556 rb->lcd_clear_display();
557}
558
559static void gen_codes(void)
560{
561 rb->lcd_clear_display();
562 /* native menus don't seem to support dynamic names easily, so we
563 * roll our own */
564 static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
565 int idx = 0;
566 if(next_slot > 0)
567 {
568 rb->lcd_putsf(0, 0, "Generate Code");
569 rb->lcd_putsf(0, 1, "%s", accounts[0].name);
570 rb->lcd_update();
571 }
572 else
573 {
574 rb->splash(HZ * 2, "No accounts configured!");
575 return;
576 }
577 while(1)
578 {
579 int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
580 switch(button)
581 {
582 case PLA_LEFT:
583 --idx;
584 if(idx < 0)
585 idx = next_slot - 1;
586 break;
587 case PLA_RIGHT:
588 ++idx;
589 if(idx >= next_slot)
590 idx = 0;
591 break;
592 case PLA_SELECT:
593 show_code(idx);
594 break;
595 case PLA_CANCEL:
596 case PLA_EXIT:
597 exit_on_usb(button);
598 return;
599 default:
600 break;
601 }
602 rb->lcd_clear_display();
603 rb->lcd_putsf(0, 0, "Generate Code");
604 rb->lcd_putsf(0, 1, "%s", accounts[idx].name);
605 rb->lcd_update();
606 }
607}
608
609static bool danger_confirm(void)
610{
611 int sel = 0;
612 MENUITEM_STRINGLIST(menu, "Are you REALLY SURE?", NULL,
613 "No",
614 "No",
615 "No",
616 "No",
617 "No",
618 "No",
619 "No",
620 "Yes, DO IT", // 7
621 "No",
622 "No",
623 "No",
624 "No");
625
626 switch(rb->do_menu(&menu, &sel, NULL, false))
627 {
628 case 7:
629 return true;
630 default:
631 return false;
632 }
633}
634
635char data_buf[MAX(MAX_NAME, SECRET_MAX * 2)];
636char temp_sec[SECRET_MAX];
637size_t old_len;
638
639static void edit_menu(int acct)
640{
641 rb->splashf(HZ, "Editing account `%s'.", accounts[acct].name);
642
643 /* HACK ALERT */
644 /* two different menus, one handling logic */
645 MENUITEM_STRINGLIST(menu_1, "Edit Account", NULL,
646 "Rename",
647 "Delete",
648 "Change HOTP Counter",
649 "Change Digit Count",
650 "Change Shared Secret",
651 "Back");
652
653 MENUITEM_STRINGLIST(menu_2, "Edit Account", NULL,
654 "Rename", // 0
655 "Delete", // 1
656 "Change TOTP Period", // 2
657 "Change Digit Count", // 3
658 "Change Shared Secret", // 4
659 "Back"); // 5
660
661 const struct menu_item_ex *menu = (accounts[acct].is_totp) ? &menu_2 : &menu_1;
662
663 bool quit = false;
664 int sel = 0;
665 while(!quit)
666 {
667 switch(rb->do_menu(menu, &sel, NULL, false))
668 {
669 case 0: // rename
670 rb->splash(HZ, "Enter new name.");
671 rb->strlcpy(data_buf, accounts[acct].name, sizeof(data_buf));
672 if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0)
673 break;
674 if(acct_exists(data_buf))
675 {
676 rb->splash(HZ * 2, "Duplicate account name!");
677 break;
678 }
679 rb->strlcpy(accounts[acct].name, data_buf, sizeof(accounts[acct].name));
680 save_accts();
681 break;
682 case 1: // delete
683 if(danger_confirm())
684 {
685 rb->memmove(accounts + acct, accounts + acct + 1, (next_slot - acct - 1) * sizeof(struct account_t));
686 --next_slot;
687 save_accts();
688 rb->splashf(HZ, "Deleted.");
689 return;
690 }
691 else
692 rb->splash(HZ, "Not confirmed.");
693 break;
694 case 2: // HOTP counter OR TOTP period
695 if(accounts[acct].is_totp)
696 rb->snprintf(data_buf, sizeof(data_buf), "%d", (int)accounts[acct].hotp_counter);
697 else
698 rb->snprintf(data_buf, sizeof(data_buf), "%d", accounts[acct].totp_period);
699
700 if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0)
701 break;
702
703 if(accounts[acct].is_totp)
704 accounts[acct].totp_period = rb->atoi(data_buf);
705 else
706 accounts[acct].hotp_counter = rb->atoi(data_buf);
707
708 save_accts();
709
710 rb->splash(HZ, "Success.");
711 break;
712 case 3: // digits
713 rb->snprintf(data_buf, sizeof(data_buf), "%d", accounts[acct].digits);
714 if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0)
715 break;
716
717 accounts[acct].digits = rb->atoi(data_buf);
718 save_accts();
719
720 rb->splash(HZ, "Success.");
721 break;
722 case 4: // secret
723 old_len = accounts[acct].sec_len;
724 memcpy(temp_sec, accounts[acct].secret, accounts[acct].sec_len);
725 base32_encode(accounts[acct].secret, accounts[acct].sec_len, data_buf, sizeof(data_buf));
726
727 if(rb->kbd_input(data_buf, sizeof(data_buf)) < 0)
728 break;
729
730 int ret = base32_decode(accounts[acct].secret, sizeof(accounts[acct].secret), data_buf);
731 if(ret <= 0)
732 {
733 memcpy(accounts[acct].secret, temp_sec, SECRET_MAX);
734 accounts[acct].sec_len = old_len;
735 rb->splash(HZ * 2, "Invalid Base32 secret!");
736 break;
737 }
738 accounts[acct].sec_len = ret;
739
740 save_accts();
741 rb->splash(HZ, "Success.");
742 break;
743 case 5:
744 quit = true;
745 break;
746 default:
747 break;
748 }
749 }
750}
751
752static void edit_accts(void)
753{
754 rb->lcd_clear_display();
755 /* native menus don't seem to support dynamic names easily, so we
756 * roll our own */
757 static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
758 int idx = 0;
759 if(next_slot > 0)
760 {
761 rb->lcd_putsf(0, 0, "Edit Account");
762 rb->lcd_putsf(0, 1, "%s", accounts[0].name);
763 rb->lcd_update();
764 }
765 else
766 {
767 rb->splash(HZ * 2, "No accounts configured!");
768 return;
769 }
770 while(1)
771 {
772 int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
773 switch(button)
774 {
775 case PLA_LEFT:
776 --idx;
777 if(idx < 0)
778 idx = next_slot - 1;
779 break;
780 case PLA_RIGHT:
781 ++idx;
782 if(idx >= next_slot)
783 idx = 0;
784 break;
785 case PLA_SELECT:
786 edit_menu(idx);
787 if(!next_slot)
788 return;
789 if(idx == next_slot)
790 idx = 0;
791 break;
792 case PLA_CANCEL:
793 case PLA_EXIT:
794 return;
795 default:
796 exit_on_usb(button);
797 break;
798 }
799 rb->lcd_clear_display();
800 rb->lcd_putsf(0, 0, "Edit Account");
801 rb->lcd_putsf(0, 1, "%s", accounts[idx].name);
802 rb->lcd_update();
803 }
804}
805
806#if CONFIG_RTC
807
808/* label is like this: [+/-]HH:MM ... */
809static int get_time_seconds(const char *label)
810{
811 if(!rb->strcmp(label, "UTC"))
812 return 0;
813
814 char buf[32];
815
816 /* copy the part after "UTC" */
817 rb->strlcpy(buf, label + 3, sizeof(buf));
818
819 char *save, *tok;
820
821 tok = rb->strtok_r(buf, ":", &save);
822 /* positive or negative: sign left */
823 int hr = rb->atoi(tok);
824
825 tok = rb->strtok_r(NULL, ": ", &save);
826 int min = rb->atoi(tok);
827
828 return 3600 * hr + 60 * min;
829}
830
831/* returns the offset in seconds associated with a time zone */
832static int get_time_offs(void)
833{
834 MENUITEM_STRINGLIST(menu, "Select Time Offset", NULL,
835 "UTC-12:00", // 0
836 "UTC-11:00", // 1
837 "UTC-10:00 (HAST)", // 2
838 "UTC-9:30", // 3
839 "UTC-9:00 (AKST, HADT)", // 4
840 "UTC-8:00 (PST, AKDT)", // 5
841 "UTC-7:00 (MST, PDT)", // 6
842 "UTC-6:00 (CST, MDT)", // 7
843 "UTC-5:00 (EST, CDT)", // 8
844 "UTC-4:00 (AST, EDT)", // 9
845 "UTC-3:30 (NST)", // 10
846 "UTC-3:00 (ADT)", // 11
847 "UTC-2:30 (NDT)", // 12
848 "UTC-2:00", // 13
849 "UTC-1:00", // 14
850 "UTC", // 15
851 "UTC+1:00", // 16
852 "UTC+2:00", // 17
853 "UTC+3:00", // 18
854 "UTC+3:30", // 19
855 "UTC+4:00", // 20
856 "UTC+4:30", // 21
857 "UTC+5:00", // 22
858 "UTC+5:30", // 23
859 "UTC+5:45", // 24
860 "UTC+6:00", // 25
861 "UTC+6:30", // 26
862 "UTC+7:00", // 27
863 "UTC+8:00", // 28
864 "UTC+8:30", // 29
865 "UTC+8:45", // 30
866 "UTC+9:00", // 31
867 "UTC+9:30", // 32
868 "UTC+10:00", // 33
869 "UTC+10:30", // 34
870 "UTC+11:00", // 35
871 "UTC+12:00", // 36
872 "UTC+12:45", // 37
873 "UTC+13:00", // 38
874 "UTC+14:00", // 39
875 );
876
877 int sel = 0;
878 for(unsigned int i = 0; i < ARRAYLEN(menu_); ++i)
879 if(time_offs == get_time_seconds(menu_[i]))
880 {
881 sel = i;
882 break;
883 }
884
885 /* relies on menu internals */
886 rb->do_menu(&menu, &sel, NULL, false);
887
888 /* see apps/menu.h */
889 const char *label = menu_[sel];
890
891 return get_time_seconds(label);
892
893#if 0
894 /* provided in case menu internals change */
895 switch(rb->do_menu(&menu, &sel, NULL, false))
896 {
897 case 0: case 1: case 2:
898 return (sel - 12) * 3600;
899 case 3:
900 return -9 * 3600 - 30 * 60;
901 case 4: case 5: case 6: case 7: case 8: case 9:
902 return (sel - 13) * 3600;
903 case 10:
904 return -3 * 3600 - 30 * 60;
905 case 11:
906 return -3 * 3600;
907 case 12:
908 return -3 * 3600 - 30 * 60;
909 case 13: case 14: case 15: case 16: case 17: case 18:
910 return (sel - 15) * 3600;
911
912 case 19:
913 return 3 * 3600 + 30 * 60;
914 case 20:
915 return 4 * 3600;
916 case 21:
917 return 4 * 3600 + 30 * 60;
918 case 22:
919 return 5 * 3600;
920 case 23:
921 return 5 * 3600 + 30 * 60;
922 case 24:
923 return 5 * 3600 + 45 * 60;
924 case 25:
925 return 6 * 3600;
926 case 26:
927 return 6 * 3600 + 30 * 60;
928 case 27: case 28:
929 return (sel - 20) * 3600;
930 case 29:
931 return 8 * 3600 + 30 * 60;
932 case 30:
933 return 8 * 3600 + 45 * 60;
934 case 31:
935 return 9 * 3600;
936 case 32:
937 return 9 * 3600 + 30 * 60;
938 case 33:
939 return 10 * 3600;
940 case 34:
941 return 10 * 3600 + 30 * 60;
942 case 35: case 36:
943 return (sel - 24) * 3600;
944 case 37:
945 return 12 * 3600 + 45 * 60;
946 case 38: case 39:
947 return (sel - 25) * 3600;
948 default:
949 rb->splash(0, "BUG: time zone fall-through: REPORT ME!!!");
950 break;
951 }
952 return 0;
953#endif
954}
955#endif
956
957static void adv_menu(void)
958{
959 MENUITEM_STRINGLIST(menu, "Advanced", NULL,
960 "Edit Account",
961 "Delete ALL accounts",
962#if CONFIG_RTC
963 "Change Time Offset",
964#endif
965 "Back");
966
967 bool quit = false;
968 int sel = 0;
969 while(!quit)
970 {
971 switch(rb->do_menu(&menu, &sel, NULL, false))
972 {
973 case 0:
974 edit_accts();
975 break;
976 case 1:
977 if(danger_confirm())
978 {
979 next_slot = 0;
980 save_accts();
981 rb->splash(HZ, "It is done, my master.");
982 }
983 else
984 rb->splash(HZ, "Not confirmed.");
985 break;
986#if CONFIG_RTC
987 case 2:
988 time_offs = get_time_offs();
989 break;
990 case 3:
991#else
992 case 2:
993#endif
994 quit = 1;
995 break;
996 default:
997 break;
998 }
999 }
1000}
1001
1002/* displays the help text */
1003static void show_help(void)
1004{
1005
1006#ifdef HAVE_LCD_COLOR
1007 rb->lcd_set_foreground(LCD_WHITE);
1008 rb->lcd_set_background(LCD_BLACK);
1009#endif
1010
1011#ifdef HAVE_LCD_BITMAP
1012 rb->lcd_setfont(FONT_UI);
1013#endif
1014
1015 static char *help_text[] = { "One-Time Password Manager", "",
1016 "Introduction", "",
1017 "This", "plugin", "allows", "you", "to", "generate", "one-time", "passwords", "to", "provide", "a", "second", "factor", "of", "authentication", "for", "services", "that", "support", "it.",
1018 "It", "suppports", "both", "event-based", "(HOTP),", "and", "time-based", "(TOTP)", "password", "schemes.",
1019 "In", "order", "to", "ensure", "proper", "functioning", "of", "time-based", "passwords", "ensure", "that", "the", "clock", "is", "accurate", "to", "within", "30", "seconds", "of", "actual", "time."
1020 "Note", "that", "some", "devices", "lack", "a", "real-time", "clock,", "so", "time-based", "passwords", "are", "not", "supported", "on", "those", "targets." };
1021
1022 struct style_text style[] = {
1023 {0, TEXT_CENTER | TEXT_UNDERLINE},
1024 {2, C_RED},
1025 LAST_STYLE_ITEM
1026 };
1027
1028 display_text(ARRAYLEN(help_text), help_text, style, NULL, true);
1029}
1030
1031/* this is the plugin entry point */
1032enum plugin_status plugin_start(const void* parameter)
1033{
1034 (void)parameter;
1035
1036 /* self-test with RFC 4226 values */
1037 if(HOTP("12345678901234567890", rb->strlen("12345678901234567890"), 1, 6) != 287082)
1038 {
1039 return PLUGIN_ERROR;
1040 }
1041
1042 size_t bufsz;
1043 accounts = rb->plugin_get_buffer(&bufsz);
1044 max_accts = bufsz / sizeof(struct account_t);
1045
1046 if(!read_accts())
1047#if CONFIG_RTC
1048 {
1049 time_offs = get_time_offs();
1050 }
1051#else
1052 {
1053 ;
1054 }
1055#endif
1056
1057 MENUITEM_STRINGLIST(menu, "One-Time Password Manager", NULL,
1058 "Add Account",
1059 "Generate Code",
1060 "Help",
1061 "Advanced",
1062 "Quit");
1063
1064 bool quit = false;
1065 int sel = 0;
1066 while(!quit)
1067 {
1068 switch(rb->do_menu(&menu, &sel, NULL, false))
1069 {
1070 case 0:
1071 add_acct();
1072 break;
1073 case 1:
1074 gen_codes();
1075 break;
1076 case 2:
1077 show_help();
1078 break;
1079 case 3:
1080 adv_menu();
1081 break;
1082 case 4:
1083 quit = 1;
1084 break;
1085 default:
1086 break;
1087 }
1088 }
1089
1090 /* save to disk */
1091 save_accts();
1092
1093 /* tell Rockbox that we have completed successfully */
1094 return PLUGIN_OK;
1095}