528 lines
18 KiB
C
528 lines
18 KiB
C
/* main_interactive.c - interactive wolfCrypt PUF demo over UART
|
|
*
|
|
* Captures the real power-on SRAM at reset, reports whether it is a usable
|
|
* PUF source, and then offers an interactive menu over the UART:
|
|
*
|
|
* 1 enroll and show identity / derived key / helper size
|
|
* 2 noise sweep - inject a known number of bit flips per codeword and
|
|
* show where BCH stops correcting (the "correction cliff")
|
|
* 3 derive two unrelated keys from the same silicon (HKDF context)
|
|
* 4 dump the helper data, which is public
|
|
* 5 reconstruct from the stored helper and compare to enrollment
|
|
* r soft reboot
|
|
*
|
|
* The noise sweep needs controllable error counts, which real SRAM cannot
|
|
* provide, so the captured power-on pattern is replayed through
|
|
* wc_PufSetTestData with a known number of flips applied. The bits are real
|
|
* silicon; only the extra noise is synthetic.
|
|
*
|
|
* Lines are terminated with an explicit \r\n: the host behavioral test
|
|
* (host_test/driver.py) matches literal CRLF, and on the hardware UART the
|
|
* extra CR added by _write() is harmless.
|
|
*
|
|
* Copyright (C) 2006-2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfSSL.
|
|
*
|
|
* wolfSSL 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 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfSSL is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <wolfssl/wolfcrypt/settings.h>
|
|
#include <wolfssl/wolfcrypt/puf.h>
|
|
#include <wolfssl/wolfcrypt/error-crypt.h>
|
|
|
|
/* This demo drives PUF APIs added after the v5.9.2 stable release
|
|
* (WC_PUF_RAW_STRIDE_BITS, wc_PufCheckSram, wc_PufGetParams,
|
|
* wc_PufGetProfileId, wc_PufGetHelperData), so INTERACTIVE=1 needs wolfSSL
|
|
* master. The one-shot example still builds against the stable release. */
|
|
#ifndef WC_PUF_RAW_STRIDE_BITS
|
|
#error "INTERACTIVE=1 requires wolfSSL master (post-v5.9.2 PUF API)"
|
|
#endif
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
|
|
extern void hal_init(void);
|
|
extern int uart_getc(void);
|
|
extern void uart_drain(void);
|
|
|
|
static unsigned int helper_sum(const uint8_t* d, uint32_t len);
|
|
static unsigned int helper_sum_cont(unsigned int sum, const uint8_t* d,
|
|
uint32_t len);
|
|
|
|
/* Raw power-on SRAM. NOLOAD section: startup must not zero it. Non-static
|
|
* so the host behavioral test harness can seed it before main() runs. */
|
|
__attribute__((section(".puf_sram")))
|
|
volatile uint8_t puf_sram_region[WC_PUF_RAW_BYTES];
|
|
|
|
/* Snapshot taken before anything else can disturb the region. */
|
|
static uint8_t g_raw[WC_PUF_RAW_BYTES];
|
|
static uint8_t g_work[WC_PUF_RAW_BYTES];
|
|
static uint8_t g_helper[WC_PUF_HELPER_BYTES];
|
|
static uint8_t g_id[WC_PUF_ID_SZ];
|
|
static int g_enrolled = 0;
|
|
/* The noise sweep injects exact flip counts against the readout the helper
|
|
* was enrolled from, so it needs an enrollment taken from THIS boot's g_raw -
|
|
* a blob loaded from a previous boot has an unknown natural flip baseline. */
|
|
static int g_freshEnroll = 0;
|
|
static int g_rawHealthy = 0;
|
|
static int g_onesPct = 0;
|
|
|
|
/* Derived keys are never written to the UART by default: the console is an
|
|
* unauthenticated physical interface. make SHOW_KEYS=1 opts in for lab use. */
|
|
static void print_key(const char* label, const uint8_t* key, uint32_t len)
|
|
{
|
|
#ifdef PUF_DEMO_SHOW_KEYS
|
|
uint32_t i;
|
|
printf("%s", label);
|
|
for (i = 0; i < 16u && i < len; i++)
|
|
printf("%02x", key[i]);
|
|
printf("\r\n");
|
|
#else
|
|
printf("%s%u bytes derived OK (not shown; build SHOW_KEYS=1 to "
|
|
"display)\r\n", label, (unsigned int)len);
|
|
(void)key;
|
|
#endif
|
|
}
|
|
|
|
static void print_hex(const char* label, const uint8_t* d, uint32_t len)
|
|
{
|
|
uint32_t i;
|
|
printf("%s", label);
|
|
for (i = 0; i < len; i++)
|
|
printf("%02x", d[i]);
|
|
printf("\r\n");
|
|
}
|
|
|
|
static int ones_percent(const uint8_t* d, uint32_t len)
|
|
{
|
|
uint32_t i;
|
|
int b, ones = 0;
|
|
for (i = 0; i < len; i++) {
|
|
for (b = 0; b < 8; b++) {
|
|
if (d[i] & (1u << b))
|
|
ones++;
|
|
}
|
|
}
|
|
return (int)((ones * 100u) / (len * 8u));
|
|
}
|
|
|
|
/* Flip 'flips' bits inside each codeword-sized stride of the pattern. */
|
|
static void add_noise(uint8_t* d, int flips)
|
|
{
|
|
int cw, f, bit;
|
|
int stride = WC_PUF_RAW_STRIDE_BITS;
|
|
for (cw = 0; cw < WC_PUF_NUM_CODEWORDS; cw++) {
|
|
for (f = 0; f < flips; f++) {
|
|
bit = cw * stride + (f * 7) + 3;
|
|
if ((bit / 8) < (int)WC_PUF_RAW_BYTES)
|
|
d[bit / 8] ^= (uint8_t)(1u << (bit % 8));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Load a pattern into a fresh context and read it in. */
|
|
static int load_ctx(wc_PufCtx* ctx, const uint8_t* pattern)
|
|
{
|
|
int ret = wc_PufInit(ctx);
|
|
if (ret != 0)
|
|
return ret;
|
|
ret = wc_PufSetTestData(ctx, pattern, WC_PUF_RAW_BYTES);
|
|
if (ret != 0)
|
|
return ret;
|
|
return wc_PufReadSram(ctx, pattern, WC_PUF_RAW_BYTES);
|
|
}
|
|
|
|
static int require_healthy(void)
|
|
{
|
|
if (!g_rawHealthy) {
|
|
printf(" the power-on readout failed the health check, so this is\r\n"
|
|
" disabled - deriving from anything else would produce a\r\n"
|
|
" device-independent key. Power-cycle the board (a warm\r\n"
|
|
" reset leaves old data in SRAM) and try again.\r\n");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void do_enroll(void)
|
|
{
|
|
wc_PufCtx ctx;
|
|
uint8_t key[WC_PUF_KEY_SZ];
|
|
int ret;
|
|
|
|
if (!require_healthy())
|
|
return;
|
|
ret = load_ctx(&ctx, g_raw);
|
|
if (ret != 0) {
|
|
printf(" readout rejected: %d\r\n", ret);
|
|
wc_PufZeroize(&ctx);
|
|
return;
|
|
}
|
|
ret = wc_PufEnroll(&ctx);
|
|
if (ret != 0) {
|
|
printf(" enroll failed: %d\r\n", ret);
|
|
wc_PufZeroize(&ctx);
|
|
return;
|
|
}
|
|
ret = wc_PufGetHelperData(&ctx, g_helper, sizeof(g_helper));
|
|
if (ret == 0)
|
|
ret = wc_PufGetIdentity(&ctx, g_id, sizeof(g_id));
|
|
if (ret == 0)
|
|
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
|
|
key, sizeof(key));
|
|
if (ret != 0) {
|
|
printf(" enroll failed: %d\r\n", ret);
|
|
wc_ForceZero(key, sizeof(key));
|
|
wc_PufZeroize(&ctx);
|
|
return;
|
|
}
|
|
|
|
printf(" enrolled from this boot's power-on SRAM readout\r\n");
|
|
print_hex(" identity : ", g_id, 16);
|
|
print_key(" derived key : ", key, sizeof(key));
|
|
printf(" helper data : %d bytes, stored in the clear\r\n",
|
|
(int)sizeof(g_helper));
|
|
g_enrolled = 1;
|
|
g_freshEnroll = 1;
|
|
|
|
wc_ForceZero(key, sizeof(key));
|
|
wc_PufZeroize(&ctx);
|
|
}
|
|
|
|
static void do_sweep(void)
|
|
{
|
|
wc_PufCtx ctx;
|
|
uint8_t id[WC_PUF_ID_SZ];
|
|
int flips, ret, m, n, k, t, cw;
|
|
|
|
if (!g_freshEnroll) {
|
|
printf(" run [1] enroll first - the sweep needs a helper enrolled\r\n"
|
|
" from this boot's readout so the injected flip counts are\r\n"
|
|
" exact (a loaded blob has an unknown natural flip baseline)\r\n");
|
|
return;
|
|
}
|
|
wc_PufGetParams(&m, &n, &k, &t, &cw);
|
|
printf(" BCH(%d,%d,t=%d), %d codewords - correcting up to %d flips per "
|
|
"%d-bit codeword\r\n", n, k, t, cw, t, n);
|
|
printf(" flips/codeword result\r\n");
|
|
|
|
for (flips = 0; flips <= t + 3; flips++) {
|
|
XMEMCPY(g_work, g_raw, sizeof(g_work));
|
|
add_noise(g_work, flips);
|
|
ret = load_ctx(&ctx, g_work);
|
|
if (ret == 0)
|
|
ret = wc_PufReconstruct(&ctx, g_helper, sizeof(g_helper));
|
|
if (ret == 0)
|
|
ret = wc_PufGetIdentity(&ctx, id, sizeof(id));
|
|
|
|
printf(" %2d ", flips);
|
|
if (ret != 0) {
|
|
printf("rejected (%d) - fails closed", ret);
|
|
}
|
|
else if (XMEMCMP(id, g_id, sizeof(id)) == 0) {
|
|
printf("identity matches");
|
|
}
|
|
else {
|
|
printf("WRONG KEY - would be a bug");
|
|
}
|
|
if (flips == t)
|
|
printf(" <= t, the limit");
|
|
printf("\r\n");
|
|
wc_PufZeroize(&ctx);
|
|
}
|
|
}
|
|
|
|
static void do_two_keys(void)
|
|
{
|
|
wc_PufCtx ctx;
|
|
uint8_t k1[WC_PUF_KEY_SZ], k2[WC_PUF_KEY_SZ];
|
|
int ret;
|
|
|
|
if (!g_enrolled) {
|
|
printf(" run [1] enroll first\r\n");
|
|
return;
|
|
}
|
|
if (!require_healthy())
|
|
return;
|
|
ret = load_ctx(&ctx, g_raw);
|
|
if (ret == 0)
|
|
ret = wc_PufReconstruct(&ctx, g_helper, sizeof(g_helper));
|
|
if (ret != 0) {
|
|
printf(" need an enrollment first ([1]), rc=%d\r\n", ret);
|
|
wc_PufZeroize(&ctx);
|
|
return;
|
|
}
|
|
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
|
|
k1, sizeof(k1));
|
|
if (ret == 0)
|
|
ret = wc_PufDeriveKey(&ctx, (const byte*)"device-identity", 15,
|
|
k2, sizeof(k2));
|
|
if (ret != 0) {
|
|
printf(" key derivation failed: %d\r\n", ret);
|
|
wc_ForceZero(k1, sizeof(k1));
|
|
wc_ForceZero(k2, sizeof(k2));
|
|
wc_PufZeroize(&ctx);
|
|
return;
|
|
}
|
|
printf(" same silicon, same helper data, two HKDF contexts:\r\n");
|
|
print_key(" \"nv-integrity\" : ", k1, sizeof(k1));
|
|
print_key(" \"device-identity\" : ", k2, sizeof(k2));
|
|
printf(" unrelated keys - one PUF backs as many as you need\r\n");
|
|
wc_ForceZero(k1, sizeof(k1));
|
|
wc_ForceZero(k2, sizeof(k2));
|
|
wc_PufZeroize(&ctx);
|
|
}
|
|
|
|
static void do_dump_helper(void)
|
|
{
|
|
uint32_t i;
|
|
unsigned int sum;
|
|
|
|
if (!g_enrolled) {
|
|
printf(" run [1] enroll first\r\n");
|
|
return;
|
|
}
|
|
printf(" Public recovery blob: device identity, %d bytes of helper\r\n"
|
|
" data, then a 2-byte checksum over both. Triple-click the\r\n"
|
|
" single line below and copy it. After a power cycle, [5]\r\n"
|
|
" pastes it back and checks itself, so there is nothing to\r\n"
|
|
" write down.\r\n\r\n",
|
|
(int)WC_PUF_HELPER_BYTES);
|
|
for (i = 0; i < (uint32_t)WC_PUF_ID_SZ; i++) {
|
|
printf("%02x", g_id[i]);
|
|
}
|
|
for (i = 0; i < (uint32_t)WC_PUF_HELPER_BYTES; i++) {
|
|
printf("%02x", g_helper[i]);
|
|
}
|
|
sum = helper_sum(g_id, (uint32_t)WC_PUF_ID_SZ);
|
|
sum = helper_sum_cont(sum, g_helper, (uint32_t)WC_PUF_HELPER_BYTES);
|
|
printf("%04x\r\n\r\n", sum);
|
|
printf(" none of this is secret - it reveals nothing about the key, and\r\n"
|
|
" on another die it reconstructs nothing\r\n");
|
|
}
|
|
|
|
/* Small checksum so a mangled paste is reported as such rather than surfacing
|
|
* as a confusing reconstruct failure. The blob checksum covers the identity
|
|
* and the helper data together. */
|
|
static unsigned int helper_sum_cont(unsigned int sum, const uint8_t* d,
|
|
uint32_t len)
|
|
{
|
|
uint32_t i;
|
|
for (i = 0; i < len; i++) {
|
|
sum = ((sum << 5) ^ (sum >> 11) ^ d[i]) & 0xFFFFu;
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
static unsigned int helper_sum(const uint8_t* d, uint32_t len)
|
|
{
|
|
return helper_sum_cont(0xFFFFu, d, len);
|
|
}
|
|
|
|
static int hexval(int c)
|
|
{
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/* Read helper data back in as pasted hex and reconstruct from it. The helper
|
|
* is public, so it can be carried out of the device and back in over the wire.
|
|
* Pasting it after a power cycle shows the key rebuilt from silicon that has
|
|
* just been re-read, with nothing secret ever leaving the part. */
|
|
static uint8_t g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES + 2];
|
|
|
|
static void do_load_helper(void)
|
|
{
|
|
wc_PufCtx ctx;
|
|
uint8_t id[WC_PUF_ID_SZ];
|
|
uint8_t k1[WC_PUF_KEY_SZ], k2[WC_PUF_KEY_SZ];
|
|
unsigned int sum, expect;
|
|
int c, v, hi = -1, ret, match;
|
|
uint32_t n = 0;
|
|
|
|
if (!require_healthy())
|
|
return;
|
|
printf(" paste the recovery blob from [4]; q aborts.\r\n");
|
|
printf(" nothing is echoed while pasting.\r\n");
|
|
|
|
/* Terminator-free: a triple-click selection carries no trailing newline,
|
|
* so finish as soon as the blob is complete. Whitespace is ignored;
|
|
* anything else non-hex means the selection caught prose, so discard and
|
|
* resynchronise rather than shifting the stream by a nibble.
|
|
*
|
|
* g_blob is only a staging buffer: nothing is committed to the enrolled
|
|
* state (g_id / g_helper / g_enrolled) until the checksum verifies, the
|
|
* reconstruct succeeds, AND the identity matches. Every failure path
|
|
* leaves any previous enrollment untouched. */
|
|
while (n < (uint32_t)sizeof(g_blob)) {
|
|
c = uart_getc();
|
|
if (c == 'q' || c == 'Q' || c == 27) {
|
|
printf(" aborted\r\n");
|
|
return;
|
|
}
|
|
if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
|
|
continue;
|
|
v = hexval(c);
|
|
if (v < 0) {
|
|
n = 0;
|
|
hi = -1;
|
|
continue;
|
|
}
|
|
if (hi < 0) {
|
|
hi = v;
|
|
}
|
|
else {
|
|
g_blob[n++] = (uint8_t)((hi << 4) | v);
|
|
hi = -1;
|
|
}
|
|
}
|
|
|
|
/* Verify the trailing checksum (over identity + helper) before anything
|
|
* else, so a mangled paste is reported as exactly that. */
|
|
sum = helper_sum(g_blob, (uint32_t)WC_PUF_ID_SZ);
|
|
sum = helper_sum_cont(sum, g_blob + WC_PUF_ID_SZ,
|
|
(uint32_t)WC_PUF_HELPER_BYTES);
|
|
expect = ((unsigned int)g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES] << 8) |
|
|
(unsigned int)g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES + 1];
|
|
if (sum != expect) {
|
|
printf(" checksum mismatch (got %04x, blob says %04x) - the paste\r\n"
|
|
" was mangled; nothing was changed, copy the line again\r\n",
|
|
sum, expect);
|
|
return;
|
|
}
|
|
printf(" loaded identity + %d bytes of helper data, checksum %04x OK\r\n",
|
|
(int)WC_PUF_HELPER_BYTES, sum);
|
|
|
|
ret = load_ctx(&ctx, g_raw);
|
|
if (ret == 0)
|
|
ret = wc_PufReconstruct(&ctx, g_blob + WC_PUF_ID_SZ,
|
|
WC_PUF_HELPER_BYTES);
|
|
if (ret == 0)
|
|
ret = wc_PufGetIdentity(&ctx, id, sizeof(id));
|
|
if (ret == 0)
|
|
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
|
|
k1, sizeof(k1));
|
|
if (ret == 0)
|
|
ret = wc_PufDeriveKey(&ctx, (const byte*)"device-identity", 15,
|
|
k2, sizeof(k2));
|
|
if (ret != 0) {
|
|
printf(" reconstruct failed: %d\r\n", ret);
|
|
printf(" either the blob is from a different part, or the readout\r\n"
|
|
" drifted past the correction budget; nothing was changed\r\n");
|
|
}
|
|
else {
|
|
match = (XMEMCMP(id, g_blob, WC_PUF_ID_SZ) == 0);
|
|
print_hex(" identity now : ", id, 16);
|
|
print_hex(" identity enrolled : ", g_blob, 16);
|
|
printf("\r\n >>> %s <<<\r\n\r\n", match ?
|
|
"SAME KEY, REBUILT FROM SILICON AFTER POWER LOSS" :
|
|
"MISMATCH - this blob does not belong to this part");
|
|
if (match) {
|
|
print_key(" \"nv-integrity\" : ", k1, sizeof(k1));
|
|
print_key(" \"device-identity\" : ", k2, sizeof(k2));
|
|
/* Commit only now: verified, reconstructed, and matching. */
|
|
XMEMCPY(g_id, g_blob, WC_PUF_ID_SZ);
|
|
XMEMCPY(g_helper, g_blob + WC_PUF_ID_SZ, WC_PUF_HELPER_BYTES);
|
|
g_enrolled = 1;
|
|
/* Not enrolled from this boot's readout - the sweep stays off. */
|
|
g_freshEnroll = 0;
|
|
}
|
|
else {
|
|
printf(" nothing was changed\r\n");
|
|
}
|
|
}
|
|
wc_ForceZero(k1, sizeof(k1));
|
|
wc_ForceZero(k2, sizeof(k2));
|
|
wc_PufZeroize(&ctx);
|
|
}
|
|
|
|
static void menu(void)
|
|
{
|
|
printf("\r\n [1] enroll and show identity / key / helper\r\n");
|
|
printf(" [2] noise sweep - the correction cliff\r\n");
|
|
printf(" [3] two keys from one PUF\r\n");
|
|
printf(" [4] dump the public recovery blob (identity + helper)\r\n");
|
|
printf(" [5] paste the blob back after a power cycle, and verify\r\n");
|
|
printf(" [r] reboot (soft reset - SRAM is NOT re-randomised)\r\n");
|
|
printf(" [?] this menu\r\n");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int m, n, k, t, cw, c;
|
|
|
|
/* Snapshot the power-on SRAM before anything else can touch it. */
|
|
XMEMCPY(g_raw, (const void*)puf_sram_region, sizeof(g_raw));
|
|
|
|
hal_init();
|
|
c = wolfCrypt_Init();
|
|
if (c != 0) {
|
|
printf("ERROR: wolfCrypt_Init failed: %d\r\n", c);
|
|
for (;;) { }
|
|
}
|
|
|
|
g_onesPct = ones_percent(g_raw, sizeof(g_raw));
|
|
g_rawHealthy = (wc_PufCheckSram(g_raw, sizeof(g_raw), NULL) == 0);
|
|
|
|
wc_PufGetParams(&m, &n, &k, &t, &cw);
|
|
printf("\r\n=== wolfCrypt PUF - interactive demo ===\r\n");
|
|
printf(" profile : BCH(%d,%d,t=%d) over GF(2^%d), %d codewords, "
|
|
"id 0x%08lX\r\n", n, k, t, m, cw,
|
|
(unsigned long)wc_PufGetProfileId());
|
|
printf(" power-on SRAM readout: %d bytes, %d%% ones -> %s\r\n",
|
|
(int)sizeof(g_raw), g_onesPct,
|
|
g_rawHealthy ? "inside the health band" :
|
|
"REJECTED by the health band");
|
|
if (!g_rawHealthy) {
|
|
printf(" this region has no usable power-on entropy on this boot,\r\n"
|
|
" so enrollment and key derivation are disabled - deriving\r\n"
|
|
" from anything else would produce a device-independent\r\n"
|
|
" key. Power-cycle the board (a warm reset leaves old data\r\n"
|
|
" in SRAM) and try again.\r\n");
|
|
}
|
|
else {
|
|
printf(" the readout passed the SRAM health checks; only a genuine\r\n"
|
|
" power cycle establishes that it is fresh power-on entropy\r\n");
|
|
}
|
|
|
|
/* Drop any line noise latched in the receiver before prompting. */
|
|
uart_drain();
|
|
|
|
menu();
|
|
|
|
for (;;) {
|
|
printf("\r\n> ");
|
|
c = uart_getc();
|
|
printf("%c\r\n", (char)c);
|
|
switch (c) {
|
|
case '1': do_enroll(); break;
|
|
case '2': do_sweep(); break;
|
|
case '3': do_two_keys(); break;
|
|
case '4': do_dump_helper(); break;
|
|
case '5': do_load_helper(); break;
|
|
case 'r':
|
|
case 'R':
|
|
printf(" rebooting...\r\n\r\n");
|
|
/* AIRCR: VECTKEY 0x5FA | SYSRESETREQ */
|
|
*(volatile uint32_t*)0xE000ED0Cu = 0x05FA0004u;
|
|
for (;;) { }
|
|
default: menu(); break;
|
|
}
|
|
}
|
|
}
|