summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/plugin.c2
-rw-r--r--apps/plugin.h8
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES1
-rw-r--r--apps/plugins/keybox.c666
5 files changed, 677 insertions, 1 deletions
diff --git a/apps/plugin.c b/apps/plugin.c
index 08beddf8fb..b864170218 100644
--- a/apps/plugin.c
+++ b/apps/plugin.c
@@ -605,6 +605,8 @@ static const struct plugin_api rockbox_api = {
605#ifdef HAVE_TAGCACHE 605#ifdef HAVE_TAGCACHE
606 tagcache_get_numeric, 606 tagcache_get_numeric,
607#endif 607#endif
608
609 gui_syncyesno_run,
608}; 610};
609 611
610int plugin_load(const char* plugin, const void* parameter) 612int plugin_load(const char* plugin, const void* parameter)
diff --git a/apps/plugin.h b/apps/plugin.h
index 28c3d5a5a5..38d96f3751 100644
--- a/apps/plugin.h
+++ b/apps/plugin.h
@@ -96,6 +96,8 @@ void* plugin_get_buffer(size_t *buffer_size);
96#include "lcd-remote.h" 96#include "lcd-remote.h"
97#endif 97#endif
98 98
99#include "yesno.h"
100
99#ifdef PLUGIN 101#ifdef PLUGIN
100 102
101#if defined(DEBUG) || defined(SIMULATOR) 103#if defined(DEBUG) || defined(SIMULATOR)
@@ -126,7 +128,7 @@ void* plugin_get_buffer(size_t *buffer_size);
126#define PLUGIN_MAGIC 0x526F634B /* RocK */ 128#define PLUGIN_MAGIC 0x526F634B /* RocK */
127 129
128/* increase this every time the api struct changes */ 130/* increase this every time the api struct changes */
129#define PLUGIN_API_VERSION 118 131#define PLUGIN_API_VERSION 119
130 132
131/* update this to latest version if a change to the api struct breaks 133/* update this to latest version if a change to the api struct breaks
132 backwards compatibility (and please take the opportunity to sort in any 134 backwards compatibility (and please take the opportunity to sort in any
@@ -757,6 +759,10 @@ struct plugin_api {
757 long (*tagcache_get_numeric)(const struct tagcache_search *tcs, int tag); 759 long (*tagcache_get_numeric)(const struct tagcache_search *tcs, int tag);
758#endif 760#endif
759 761
762 enum yesno_res (*gui_syncyesno_run)(const struct text_message * main_message,
763 const struct text_message * yes_message,
764 const struct text_message * no_message);
765
760}; 766};
761 767
762/* plugin header */ 768/* plugin header */
diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES
index 029bcfd164..a8110593f0 100644
--- a/apps/plugins/CATEGORIES
+++ b/apps/plugins/CATEGORIES
@@ -32,6 +32,7 @@ iriverify,viewers
32jackpot,games 32jackpot,games
33jewels,games 33jewels,games
34jpeg,viewers 34jpeg,viewers
35keybox,apps
35lamp,apps 36lamp,apps
36logo,demos 37logo,demos
37mandelbrot,demos 38mandelbrot,demos
diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES
index 8b491f4c1c..4b0efa9ea4 100644
--- a/apps/plugins/SOURCES
+++ b/apps/plugins/SOURCES
@@ -6,6 +6,7 @@ cube.c
6dict.c 6dict.c
7firmware_flash.c 7firmware_flash.c
8jackpot.c 8jackpot.c
9keybox.c
9logo.c 10logo.c
10mosaique.c 11mosaique.c
11properties.c 12properties.c
diff --git a/apps/plugins/keybox.c b/apps/plugins/keybox.c
new file mode 100644
index 0000000000..d3b03f28a7
--- /dev/null
+++ b/apps/plugins/keybox.c
@@ -0,0 +1,666 @@
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id:$
9 *
10 * Copyright (C) 2008 Nils Wallménius
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#include "plugin.h"
22#include "md5.h"
23PLUGIN_HEADER
24
25#define KEYBOX_FILE PLUGIN_DIR "/apps/keybox.dat"
26#define BLOCK_SIZE 8
27#define MAX_ENTRIES 12*BLOCK_SIZE /* keep this a multiple of BLOCK_SIZE */
28#define FIELD_LEN 32 /* should be enough for anyone ;) */
29
30/* salt 4 bytes (needed for decryption) not encrypted padded with 4 bytes of zeroes
31 pwhash 16 bytes (to check for the right password) encrypted
32 encrypted data. */
33
34#define HEADER_LEN 24
35
36enum
37{
38 FILE_OPEN_ERROR = -1
39};
40
41struct pw_entry
42{
43 bool used;
44 char title[FIELD_LEN];
45 char name[FIELD_LEN];
46 char password[FIELD_LEN];
47 struct pw_entry *next;
48};
49
50struct pw_list
51{
52 struct pw_entry first; /* always points to the first element in the list */
53 struct pw_entry entries[MAX_ENTRIES];
54 int num_entries;
55} pw_list;
56
57/* use this to access hashes in different ways, not byte order
58 independent but does it matter? */
59union hash
60{
61 uint8_t bytes[16];
62 uint32_t words[4];
63};
64
65static const struct plugin_api* rb;
66MEM_FUNCTION_WRAPPERS(rb);
67static char buffer[sizeof(struct pw_entry)*MAX_ENTRIES];
68static int bytes_read = 0; /* bytes read into the buffer */
69static struct gui_synclist kb_list;
70static union hash key;
71static char master_pw[FIELD_LEN];
72static uint32_t salt;
73static union hash pwhash;
74static bool data_changed = false;
75
76static int context_item_cb(int action, const struct menu_item_ex *this_item);
77static void encrypt_buffer(char *buf, size_t size, uint32_t *key);
78static void decrypt_buffer(char *buf, size_t size, uint32_t *key);
79
80/* the following two functions are the reference TEA implementation by
81 David Wheeler and Roger Needham taken from
82 http://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm */
83
84static void encrypt(uint32_t* v, uint32_t* k)
85{
86 uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */
87 static const uint32_t delta=0x9e3779b9; /* a key schedule constant */
88 uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
89 for (i=0; i < 32; i++) { /* basic cycle start */
90 sum += delta;
91 v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
92 v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); /* end cycle */
93 }
94 v[0]=v0; v[1]=v1;
95}
96
97static void decrypt(uint32_t* v, uint32_t* k)
98{
99 uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
100 static const uint32_t delta=0x9e3779b9; /* a key schedule constant */
101 uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
102 for (i=0; i<32; i++) { /* basic cycle start */
103 v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
104 v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
105 sum -= delta; /* end cycle */
106 }
107 v[0]=v0; v[1]=v1;
108}
109
110MENUITEM_RETURNVALUE(context_add_entry, "Add entry", 0,
111 NULL, Icon_NOICON);
112MENUITEM_RETURNVALUE(context_edit_title, "Edit title", 1,
113 &context_item_cb, Icon_NOICON);
114MENUITEM_RETURNVALUE(context_edit_name, "Edit user name", 1,
115 &context_item_cb, Icon_NOICON);
116MENUITEM_RETURNVALUE(context_edit_password, "Edit password", 2,
117 &context_item_cb, Icon_NOICON);
118MENUITEM_RETURNVALUE(context_delete_entry, "Delete entry", 3,
119 &context_item_cb, Icon_NOICON);
120MENUITEM_RETURNVALUE(context_debug, "debug", 4,
121 &context_item_cb, Icon_NOICON);
122
123MAKE_MENU(context_m, "Context menu",
124 context_item_cb, Icon_NOICON,
125 &context_add_entry, &context_edit_title, &context_edit_name,
126 &context_edit_password, &context_delete_entry);
127
128static int context_item_cb(int action, const struct menu_item_ex *this_item)
129{
130 if (action == ACTION_REQUEST_MENUITEM
131 && pw_list.num_entries == 0
132 && this_item != &context_add_entry)
133 {
134 return ACTION_EXIT_MENUITEM;
135 }
136 return action;
137}
138
139static char * kb_list_cb(int selected_item, void *data,
140 char *buffer, size_t buffer_len)
141{
142 (void)data;
143 int i;
144 struct pw_entry *entry = pw_list.first.next;
145 for (i = 0; i < selected_item; i++)
146 {
147 if (entry->next)
148 entry = entry->next;
149 }
150 rb->snprintf(buffer, buffer_len, "%s", entry->title);
151
152 return buffer;
153}
154
155static void init_ll(void)
156{
157 pw_list.first.next = &pw_list.entries[0];
158 pw_list.entries[0].next = NULL;
159 pw_list.num_entries = 0;
160}
161
162static void delete_entry(int selected_item)
163{
164 int i;
165 struct pw_entry *entry = pw_list.first.next;
166 struct pw_entry *entry2;
167
168 /* find the entry before the one to delete */
169 for (i = 0; i < selected_item - 1; i++)
170 {
171 if (entry->next)
172 entry = entry->next;
173 }
174 if (entry->next)
175 entry2 = entry->next;
176 else
177 return;
178 if (entry2->next)
179 entry->next = entry2->next;
180
181 entry2->used = false;
182 entry2->name[0] = '\0';
183 entry2->password[0] = '\0';
184 entry2->next = NULL;
185
186 rb->gui_synclist_set_nb_items(&kb_list, --pw_list.num_entries);
187 data_changed = true;
188}
189
190static void add_entry(int selected_item)
191{
192 int i, j;
193 struct pw_entry *entry = pw_list.first.next;
194 for (i = 0; i < MAX_ENTRIES && pw_list.entries[i].used; i++)
195 ;
196
197 if (pw_list.entries[i].used)
198 {
199 rb->splash(HZ, "Password list full");
200 return;
201 }
202
203 rb->splash(HZ, "Enter title");
204 rb->kbd_input(pw_list.entries[i].title, FIELD_LEN);
205 rb->splash(HZ, "Enter name");
206 rb->kbd_input(pw_list.entries[i].name, FIELD_LEN);
207 rb->splash(HZ, "Enter password");
208 rb->kbd_input(pw_list.entries[i].password, FIELD_LEN);
209
210 for (j = 0; j < selected_item; j++)
211 {
212 if (entry->next)
213 entry = entry->next;
214 }
215
216 rb->gui_synclist_set_nb_items(&kb_list, ++pw_list.num_entries);
217
218 pw_list.entries[i].used = true;
219 pw_list.entries[i].next = entry->next;
220
221 entry->next = &pw_list.entries[i];
222
223 if (entry->next == entry)
224 entry->next = NULL;
225
226 data_changed = true;
227}
228
229static void edit_title(int selected_item)
230{
231 int i;
232 struct pw_entry *entry = pw_list.first.next;
233 for (i = 0; i < selected_item; i++)
234 {
235 if (entry->next)
236 entry = entry->next;
237 }
238 if (rb->kbd_input(entry->title, FIELD_LEN) == 0)
239 data_changed = true;
240}
241
242static void edit_name(int selected_item)
243{
244 int i;
245 struct pw_entry *entry = pw_list.first.next;
246 for (i = 0; i < selected_item; i++)
247 {
248 if (entry->next)
249 entry = entry->next;
250 }
251 if (rb->kbd_input(entry->name, FIELD_LEN) == 0)
252 data_changed = true;
253}
254
255static void edit_pw(int selected_item)
256{
257 int i;
258 struct pw_entry *entry = pw_list.first.next;
259 for (i = 0; i < selected_item; i++)
260 {
261 if (entry->next)
262 entry = entry->next;
263 }
264 if (rb->kbd_input(entry->password, FIELD_LEN) == 0)
265 data_changed = true;
266}
267
268static void context_menu(int selected_item)
269{
270 int selection, result;
271 bool exit = false;
272
273 do {
274 result = rb->do_menu(&context_m, &selection, NULL, false);
275 switch (result) {
276 case 0:
277 add_entry(selected_item);
278 return;
279 case 1:
280 edit_title(selected_item);
281 return;
282 case 2:
283 edit_name(selected_item);
284 return;
285 case 3:
286 edit_pw(selected_item);
287 return;
288 case 4:
289 delete_entry(selected_item);
290 return;
291 default:
292 exit = true;
293 break;
294 }
295 rb->yield();
296 } while (!exit);
297}
298
299static void splash_pw(int selected_item)
300{
301 int i;
302 struct pw_entry *entry = pw_list.first.next;
303
304 for (i = 0; i < selected_item; i++)
305 {
306 if (entry->next)
307 entry = entry->next;
308 }
309 if (entry->name != '\0')
310 rb->splash(0, "%s %s", entry->name, entry->password);
311 else
312 rb->splash(0, "%s", entry->password);
313 rb->get_action(CONTEXT_STD, TIMEOUT_BLOCK);
314}
315
316static void hash_pw(union hash *out)
317{
318 int i;
319 struct md5_s pw_md5;
320
321 InitMD5(&pw_md5);
322 AddMD5(&pw_md5, master_pw, rb->strlen(master_pw));
323 EndMD5(&pw_md5);
324
325 for (i = 0; i < 4; i++)
326 out->words[i] = htole32(pw_md5.p_digest[i]);
327}
328
329static void make_key(void)
330{
331 int i;
332 char buf[sizeof(master_pw) + sizeof(salt) + 1];
333 struct md5_s key_md5;
334 size_t len = rb->strlen(master_pw);
335
336 rb->strncpy(buf, master_pw, sizeof(buf));
337
338 rb->memcpy(&buf[len], &salt, sizeof(salt));
339
340 InitMD5(&key_md5);
341 AddMD5(&key_md5, buf, rb->strlen(buf));
342 EndMD5(&key_md5);
343
344 for (i = 0; i < 4; i++)
345 key.words[i] = key_md5.p_digest[i];
346}
347
348static void decrypt_buffer(char *buf, size_t size, uint32_t *key)
349{
350 unsigned int i;
351 uint32_t block[2];
352
353 for (i = 0; i < size/BLOCK_SIZE; i++)
354 {
355 rb->memcpy(&block[0], &buf[i*BLOCK_SIZE], sizeof(block));
356
357 block[0] = letoh32(block[0]);
358 block[1] = letoh32(block[1]);
359
360 decrypt(&block[0], key);
361
362 /* byte swap one block */
363 block[0] = letoh32(block[0]);
364 block[1] = letoh32(block[1]);
365
366 rb->memcpy(&buf[i*BLOCK_SIZE], &block[0], sizeof(block));
367 }
368}
369
370static void encrypt_buffer(char *buf, size_t size, uint32_t *key)
371{
372 unsigned int i;
373 uint32_t block[2];
374
375 for (i = 0; i < size/BLOCK_SIZE; i++)
376 {
377 rb->memcpy(&block[0], &buf[i*BLOCK_SIZE], sizeof(block));
378
379 /* byte swap one block */
380 block[0] = htole32(block[0]);
381 block[1] = htole32(block[1]);
382
383 encrypt(&block[0], key);
384
385 block[0] = htole32(block[0]);
386 block[1] = htole32(block[1]);
387
388 rb->memcpy(&buf[i*BLOCK_SIZE], &block[0], sizeof(block));
389 }
390}
391
392static int parse_buffer(void)
393{
394 int i;
395 intptr_t len;
396 struct pw_entry *entry = pw_list.first.next;
397 char *start, *end;
398 start = &buffer[HEADER_LEN];
399
400 rb->memcpy(&salt, &buffer[0], sizeof(salt));
401 make_key();
402
403 decrypt_buffer(&buffer[8], bytes_read - 8, &key.words[0]);
404
405 if (rb->memcmp(&buffer[8], &pwhash, sizeof(union hash)))
406 {
407 rb->splash(HZ*2, "Wrong password");
408 return -1;
409 }
410
411 for (i=0; i < MAX_ENTRIES; i++)
412 {
413 end = rb->strchr(start, '\0'); /* find eol */
414 len = (intptr_t)end - (intptr_t)&buffer[HEADER_LEN];
415 if ((len > bytes_read + HEADER_LEN)
416 || (intptr_t)start == (intptr_t)end)
417 {
418 break;
419 }
420
421 rb->strncpy(entry->title, start,
422 MIN((intptr_t)end - (intptr_t)start, FIELD_LEN));
423 start = end +1;
424
425 end = rb->strchr(start, '\0'); /* find eol */
426 len = (intptr_t)end - (intptr_t)&buffer[HEADER_LEN];
427 if (len > bytes_read + HEADER_LEN)
428 {
429 break;
430 }
431
432 rb->strncpy(entry->name, start,
433 MIN((intptr_t)end - (intptr_t)start, FIELD_LEN));
434 start = end +1;
435
436 end = rb->strchr(start, '\0'); /* find eol */
437 len = (intptr_t)end - (intptr_t)&buffer[HEADER_LEN];
438 if (len > bytes_read + HEADER_LEN)
439 {
440 break;
441 }
442 rb->strncpy(entry->password, start,
443 MIN((intptr_t)end - (intptr_t)start, FIELD_LEN));
444 start = end +1;
445 entry->used = true;
446 if (i + 1 < MAX_ENTRIES - 1)
447 {
448 entry->next = &pw_list.entries[i+1];
449 entry = entry->next;
450 }
451 else
452 {
453 break;
454 }
455 }
456 entry->next = NULL;
457 pw_list.num_entries = i;
458 rb->gui_synclist_set_nb_items(&kb_list, pw_list.num_entries);
459 return 0;
460}
461
462static void write_output(int fd)
463{
464 size_t bytes_written;
465 int i;
466 size_t len, size;
467 char *p = &buffer[HEADER_LEN]; /* reserve space for salt + hash */
468
469 rb->memcpy(&buffer[8], &pwhash, sizeof(union hash));
470 struct pw_entry *entry = pw_list.first.next;
471
472 for (i = 0; i < pw_list.num_entries; i++)
473 {
474 len = rb->strlen(entry->title);
475 rb->strncpy(p, entry->title, len+1);
476 p += len+1;
477 len = rb->strlen(entry->name);
478 rb->strncpy(p, entry->name, len+1);
479 p += len+1;
480 len = rb->strlen(entry->password);
481 rb->strncpy(p, entry->password, len+1);
482 p += len+1;
483 if (entry->next)
484 entry = entry->next;
485 }
486
487 /* round up to a number divisible by BLOCK_SIZE */
488 size = (((intptr_t)p - (intptr_t)&buffer) / BLOCK_SIZE + 1) * BLOCK_SIZE;
489
490 salt = rb->rand();
491 make_key();
492
493 encrypt_buffer(&buffer[8], size, &key.words[0]);
494 rb->memcpy(&buffer[0], &salt, sizeof(salt));
495
496 bytes_written = rb->write(fd, &buffer, size);
497}
498
499static int enter_pw(char *pw_buf, size_t buflen, bool new_pw)
500{
501 char buf[2][sizeof(master_pw)];
502 rb->memset(buf, 0, sizeof(buf));
503 rb->memset(master_pw, 0, sizeof(master_pw));
504
505 if (new_pw)
506 {
507 rb->splash(HZ, "Enter new master password");
508 rb->kbd_input(buf[0], sizeof(buf[0]));
509 rb->splash(HZ, "Confirm master password");
510 rb->kbd_input(buf[1], sizeof(buf[1]));
511
512 if (rb->strcmp(buf[0], buf[1]))
513 {
514 rb->splash(HZ, "Password mismatch");
515 return -1;
516 }
517 else
518 {
519 rb->strncpy(pw_buf, buf[0], buflen);
520 hash_pw(&pwhash);
521 return 0;
522 }
523 }
524
525 rb->splash(HZ, "Enter master password");
526 if (rb->kbd_input(pw_buf, buflen))
527 return -1;
528 hash_pw(&pwhash);
529 return 0;
530}
531
532static int keybox(void)
533{
534 int button, fd;
535 bool new_file = !rb->file_exists(KEYBOX_FILE);
536 bool done = false;
537
538 if (enter_pw(master_pw, sizeof (master_pw), new_file))
539 return 0;
540
541 /* Read the existing file */
542 if (!new_file)
543 {
544 fd = rb->open(KEYBOX_FILE, O_RDONLY);
545 if (fd < 0)
546 return FILE_OPEN_ERROR;
547 bytes_read = rb->read(fd, &buffer, sizeof(buffer));
548
549 if (parse_buffer())
550 return 0;
551
552 rb->close(fd);
553 }
554
555 while (!done)
556 {
557 rb->gui_syncstatusbar_draw(rb->statusbars, true);
558 rb->gui_synclist_draw(&kb_list);
559 button = rb->get_action(CONTEXT_LIST, TIMEOUT_BLOCK);
560 if (rb->gui_synclist_do_button(&kb_list, &button, LIST_WRAP_ON))
561 continue;
562
563 switch (button)
564 {
565 case ACTION_STD_OK:
566 splash_pw(rb->gui_synclist_get_sel_pos(&kb_list));
567 break;
568 case ACTION_STD_CONTEXT:
569 context_menu(rb->gui_synclist_get_sel_pos(&kb_list));
570 break;
571 case ACTION_STD_CANCEL:
572 done = true;
573 break;
574 }
575 rb->yield();
576 }
577
578 if (data_changed)
579 {
580 fd = rb->open(KEYBOX_FILE, O_WRONLY | O_CREAT | O_TRUNC);
581 if (fd < 0)
582 return FILE_OPEN_ERROR;
583 write_output(fd);
584 rb->close(fd);
585 }
586
587 return 0;
588}
589
590static void reset(void)
591{
592 static const char *message_lines[]=
593 {"Do you really want", "to reset keybox?"};
594 static const char *yes_lines[]=
595 {"Keybox reset."};
596 static const struct text_message message={message_lines, 2};
597 static const struct text_message yes_message={yes_lines, 1};
598
599 if(rb->gui_syncyesno_run(&message, &yes_message, NULL) == YESNO_YES)
600 {
601 rb->remove(KEYBOX_FILE);
602 rb->memset(&buffer, 0, sizeof(buffer));
603 rb->memset(&pw_list, 0, sizeof(pw_list));
604 init_ll();
605 }
606}
607
608static int main_menu(void)
609{
610 int selection, result, ret;
611 bool exit = false;
612
613 MENUITEM_STRINGLIST(menu,"Keybox", NULL, "Enter Keybox",
614 "Reset Keybox", "Exit");
615
616 do {
617 result = rb->do_menu(&menu, &selection, NULL, false);
618 switch (result) {
619 case 0:
620 ret = keybox();
621 if (ret)
622 return ret;
623 break;
624 case 1:
625 reset();
626 break;
627 case 2:
628 exit = true;
629 break;
630 }
631 rb->yield();
632 } while (!exit);
633
634 return 0;
635}
636
637enum plugin_status plugin_start(const struct plugin_api *api,
638 const void *parameter)
639{
640 (void)parameter;
641 rb = api;
642 int ret;
643
644 rb->gui_synclist_init(&kb_list, &kb_list_cb, NULL, false, 1, NULL);
645
646 rb->gui_synclist_set_title(&kb_list, "Keybox", NOICON);
647 rb->gui_synclist_set_icon_callback(&kb_list, NULL);
648 rb->gui_synclist_set_nb_items(&kb_list, 0);
649 rb->gui_synclist_limit_scroll(&kb_list, false);
650 rb->gui_synclist_select_item(&kb_list, 0);
651
652 md5_init(api);
653
654 init_ll();
655 ret = main_menu();
656
657 switch (ret)
658 {
659 case FILE_OPEN_ERROR:
660 rb->splash(HZ*2, "Error opening file");
661 return PLUGIN_ERROR;
662 }
663
664 return PLUGIN_OK;
665}
666