diff --git a/.gitignore b/.gitignore
index 50f8f64..57ace6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 build/
 sdkconfig
+sdkconfig.old
 dev_keys.txt
 ttn-esp32
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..248ebc7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+    "files.associations": {
+        "provisioning.h": "c",
+        "config.h": "c",
+        "sdkconfig.h": "c",
+        "oslmic.h": "c"
+    }
+}
\ No newline at end of file
diff --git a/Kconfig b/Kconfig
index c6e7035..f1ef14c 100644
--- a/Kconfig
+++ b/Kconfig
@@ -68,4 +68,75 @@ config TTN_BG_TASK_PRIO
         the LoRaWAN radio chip. It needs a high priority as the timing is crucial.
         Higher numbers indicate higher priority.
 
+
+choice TTN_PROVISION_UART
+    prompt "UART for provisioning"
+    default TTN_PROVISION_UART_DEFAULT
+    help
+        Select whether to use UART for listening for provisioning commands.
+        
+        - Default is to use UART0 on pins GPIO1(TX) and GPIO3(RX).
+        - If "Custom" is selected, UART0 or UART1 can be chosen,
+          and any pins can be selected.
+        - If "None" is selected, the feature is not available.
+
+config TTN_PROVISION_UART_DEFAULT
+    bool "Default: UART0, TX=GPIO1, RX=GPIO3, 115,200 baud"
+config TTN_PROVISION_UART_CUSTOM
+    bool "Custom"
+config TTN_PROVISION_UART_NONE
+    bool "None"
+endchoice
+
+choice TTN_PROVISION_UART_INIT
+    prompt "Initialize UART"
+    default TTN_PROVISION_UART_INIT_NO
+    depends on !TTN_PROVISION_UART_NONE
+    help
+        Select whether to initialize the UART, i.e. set the baud rate, the RX and TX
+        pins. If the UART is shared with other features (e.g. the console), it
+        should not be initialized. 
+
+config TTN_PROVISION_UART_INIT_NO
+    bool "No"
+config TTN_PROVISION_UART_INIT_YES
+    bool "Yes"
+endchoice
+
+choice TTN_PROVISION_UART_NUM
+    prompt "UART peripheral for provisioning (0-1)"
+    depends on TTN_PROVISION_UART_CUSTOM
+    default TTN_PROVISION_UART_CUSTOM_NUM_0
+
+config TTN_PROVISION_UART_CUSTOM_NUM_0
+    bool "UART0"
+config TTN_PROVISION_UART_CUSTOM_NUM_1
+    bool "UART1"
+endchoice
+
+config TTN_PROVISION_UART_NUM
+    int
+    default 0 if TTN_PROVISION_UART_DEFAULT || TTN_PROVISION_UART_NONE
+    default 0 if TTN_PROVISION_UART_CUSTOM_NUM_0
+    default 1 if TTN_PROVISION_UART_CUSTOM_NUM_1
+
+config TTN_PROVISION_UART_TX_GPIO
+    int "Provisioning UART TX on GPIO#"
+    depends on TTN_PROVISION_UART_CUSTOM && TTN_PROVISION_UART_INIT_YES
+    range 0 33
+    default 19
+
+config TTN_PROVISION_UART_RX_GPIO
+    int "Provisioning UART RX on GPIO#"
+    depends on TTN_PROVISION_UART_CUSTOM && TTN_PROVISION_UART_INIT_YES
+    range 0 39
+    default 21
+
+config TTN_PROVISION_UART_BAUDRATE
+    int "Provisioning UART baud rate"
+    depends on TTN_PROVISION_UART_CUSTOM && TTN_PROVISION_UART_INIT_YES
+    default 115200
+    range 1200 4000000
+
+
 endmenu
diff --git a/include/TheThingsNetwork.h b/include/TheThingsNetwork.h
index 2d4f906..99c040e 100644
--- a/include/TheThingsNetwork.h
+++ b/include/TheThingsNetwork.h
@@ -104,6 +104,13 @@ public:
      */
     bool provision(const char *devEui, const char *appEui, const char *appKey);
 
+    /**
+     * @brief Start task that listens on configured UART for provisioning commands.
+     * 
+     * Run 'make menuconfig' to configure it.
+     */
+    void startProvisioningTask();
+
      /**
      * @brief Activate the device via OTAA.
      * 
@@ -176,12 +183,8 @@ public:
 
 private:
     TTNMessageCallback messageCallback;
-    bool haveKeys;
 
     bool joinCore();
-    bool decodeKeys(const char *devEui, const char *appEui, const char *appKey);
-    bool saveKeys();
-    bool restoreKeys(bool silent);
 };
 
 #endif
diff --git a/src/TheThingsNetwork.cpp b/src/TheThingsNetwork.cpp
index 0ca6d79..21b9a7f 100644
--- a/src/TheThingsNetwork.cpp
+++ b/src/TheThingsNetwork.cpp
@@ -12,13 +12,13 @@
 
 #include "freertos/FreeRTOS.h"
 #include "esp_event.h"
-#include "nvs_flash.h"
-#include "TheThingsNetwork.h"
 #include "esp_log.h"
-#include "oslmic.h"
+#include "TheThingsNetwork.h"
 #include "hal.h"
 #include "hal_esp32.h"
 #include "lmic.h"
+#include "provisioning.h"
+
 
 enum ClientAction
 {
@@ -28,29 +28,14 @@ enum ClientAction
 };
 
 static const char *TAG = "ttn";
-static const char *NVS_FLASH_PARTITION = "ttn";
-static const char *NVS_FLASH_KEY_DEV_EUI = "devEui";
-static const char *NVS_FLASH_KEY_APP_EUI = "appEui";
-static const char *NVS_FLASH_KEY_APP_KEY = "appKey";
 
 static TheThingsNetwork* ttnInstance;
-static uint8_t devEui[8];
-static uint8_t appEui[8];
-static uint8_t appKey[16];
 static QueueHandle_t resultQueue;
 static ClientAction clientAction = eActionUnrelated;
 
 
-static bool readNvsValue(nvs_handle handle, const char* key, uint8_t* data, size_t expectedLength, bool silent);
-static bool writeNvsValue(nvs_handle handle, const char* key, const uint8_t* data, size_t len);
-static bool hexStringToBin(const char *hex, uint8_t *buf, int len);
-static int hexTupleToByte(const char *hex);
-static int hexDigitToVal(char ch);
-static void swapByteOrder(uint8_t* buf, int len);
-
-
 TheThingsNetwork::TheThingsNetwork()
-    : messageCallback(NULL), haveKeys(false)
+    : messageCallback(NULL)
 {
     ASSERT(ttnInstance == NULL);
     ttnInstance = this;
@@ -89,45 +74,23 @@ void TheThingsNetwork::reset()
 
 bool TheThingsNetwork::provision(const char *devEui, const char *appEui, const char *appKey)
 {
-    if (!decodeKeys(devEui, appEui, appKey))
+    if (!provisioning_decode_keys(devEui, appEui, appKey))
         return false;
     
-    return saveKeys();
+    return provisioning_save_keys();
 }
 
-bool TheThingsNetwork::decodeKeys(const char *devEui, const char *appEui, const char *appKey)
+void TheThingsNetwork::startProvisioningTask()
 {
-    haveKeys = false;
-
-    if (strlen(devEui) != 16 || !hexStringToBin(devEui, ::devEui, 8))
-    {
-        ESP_LOGW(TAG, "Invalid device EUI: %s", devEui);
-        return false;
-    }
-
-    swapByteOrder(::devEui, 8);
-
-    if (strlen(appEui) != 16 || !hexStringToBin(appEui, ::appEui, 8))
-    {
-        ESP_LOGW(TAG, "Invalid application EUI: %s", appEui);
-        return false;
-    }
-
-    swapByteOrder(::appEui, 8);
-
-    if (strlen(appKey) != 32 || !hexStringToBin(appKey, ::appKey, 16))
-    {
-        ESP_LOGW(TAG, "Invalid application key: %s", appEui);
-        return false;
-    }
-
-    haveKeys = true;
-    return true;
+#if !defined(CONFIG_TTN_PROVISION_UART_NONE)
+    provisioning_start_task();
+#endif
 }
 
+
 bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char *appKey)
 {
-    if (!decodeKeys(devEui, appEui, appKey))
+    if (!provisioning_decode_keys(devEui, appEui, appKey))
         return false;
     
     return joinCore();
@@ -135,9 +98,9 @@ bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char *
 
 bool TheThingsNetwork::join()
 {
-    if (!haveKeys)
+    if (!provisioning_have_keys())
     {
-        if (!restoreKeys(false))
+        if (!provisioning_restore_keys(false))
             return false;
     }
 
@@ -146,7 +109,7 @@ bool TheThingsNetwork::join()
 
 bool TheThingsNetwork::joinCore()
 {
-    if (!haveKeys)
+    if (!provisioning_have_keys())
     {
         ESP_LOGW(TAG, "Device EUI, App EUI and/or App key have not been provided");
         return false;
@@ -205,118 +168,13 @@ void TheThingsNetwork::onMessage(TTNMessageCallback callback)
     messageCallback = callback;
 }
 
-bool TheThingsNetwork::saveKeys()
-{
-    bool result = false;
-
-    nvs_handle handle = 0;
-    esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READWRITE, &handle);
-    if (res == ESP_ERR_NVS_NOT_INITIALIZED)
-    {
-        ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first.");
-        goto done;
-    }
-    ESP_ERROR_CHECK(res);
-    if (res != ESP_OK)
-        goto done;
-
-    if (!writeNvsValue(handle, NVS_FLASH_KEY_DEV_EUI, ::devEui, sizeof(::devEui)))
-        goto done;
-        
-    if (!writeNvsValue(handle, NVS_FLASH_KEY_APP_EUI, ::appEui, sizeof(::appEui)))
-        goto done;
-        
-    if (!writeNvsValue(handle, NVS_FLASH_KEY_APP_KEY, ::appKey, sizeof(::appKey)))
-        goto done;
-
-    res = nvs_commit(handle);
-    ESP_ERROR_CHECK(res);
-    
-    result = true;
-    ESP_LOGI(TAG, "Dev and app EUI and app key saved in NVS storage");
-
-done:
-    nvs_close(handle);
-    return result;
-}
-
-bool TheThingsNetwork::restoreKeys(bool silent)
-{
-    haveKeys = false;
-    
-    nvs_handle handle = 0;
-    esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READONLY, &handle);
-    if (res == ESP_ERR_NVS_NOT_FOUND)
-        return false; // partition does not exist yet
-    if (res == ESP_ERR_NVS_NOT_INITIALIZED)
-    {
-        ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first.");
-        goto done;
-    }
-    ESP_ERROR_CHECK(res);
-    if (res != ESP_OK)
-        goto done;
-
-    if (!readNvsValue(handle, NVS_FLASH_KEY_DEV_EUI, ::devEui, sizeof(::devEui), silent))
-        goto done;
-
-    if (!readNvsValue(handle, NVS_FLASH_KEY_APP_EUI, ::appEui, sizeof(::appEui), silent))
-        goto done;
-
-    if (!readNvsValue(handle, NVS_FLASH_KEY_APP_KEY, ::appKey, sizeof(::appKey), silent))
-        goto done;
-
-    haveKeys = true;
-    ESP_LOGI(TAG, "Dev and app EUI and app key have been restored from NVS storage");
-
-done:
-    nvs_close(handle);
-    return haveKeys;
-}
 
 bool TheThingsNetwork::isProvisioned()
 {
-    if (haveKeys)
+    if (provisioning_have_keys())
         return true;
     
-    return restoreKeys(true);
-}
-
-bool readNvsValue(nvs_handle handle, const char* key, uint8_t* data, size_t expectedLength, bool silent)
-{
-    size_t size = expectedLength;
-    esp_err_t res = nvs_get_blob(handle, key, data, &size);
-    if (res == ESP_OK && size == expectedLength)
-        return true;
-
-    if (res == ESP_OK && size != expectedLength)
-    {
-        if (!silent)
-            ESP_LOGW(TAG, "Invalid size of NVS data for %s", key);
-        return false;
-    }
-
-    if (res == ESP_ERR_NVS_NOT_FOUND)
-    {
-        if (!silent)
-            ESP_LOGW(TAG, "No NVS data found for %s", key);
-        return false;
-    }
-    
-    ESP_ERROR_CHECK(res);
-    return false;
-}
-
-bool writeNvsValue(nvs_handle handle, const char* key, const uint8_t* data, size_t len)
-{
-    uint8_t buf[16];
-    if (readNvsValue(handle, key, buf, len, true) && memcmp(buf, data, len) == 0)
-        return true; // unchanged
-    
-    esp_err_t res = nvs_set_blob(handle, key, data, len);
-    ESP_ERROR_CHECK(res);
-
-    return res == ESP_OK;
+    return provisioning_restore_keys(true);
 }
 
 
@@ -333,29 +191,6 @@ static const char *eventNames[] = {
 };
 #endif
 
-// This EUI must be in little-endian format, so least-significant-byte first.
-// When copying an EUI from ttnctl output, this means to reverse the bytes.
-// For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
-// The order is swapped in TheThingsNetwork::provision().
-void os_getArtEui (u1_t* buf)
-{
-    memcpy(buf, appEui, 8);
-}
-
-// This should also be in little endian format, see above.
-void os_getDevEui (u1_t* buf)
-{
-    memcpy(buf, devEui, 8);
-}
-
-// This key should be in big endian format (or, since it is not really a number
-// but a block of memory, endianness does not really apply). In practice, a key
-// taken from ttnctl can be copied as-is.
-void os_getDevKey (u1_t* buf)
-{
-    memcpy(buf, appKey, 16);
-}
-
 void onEvent (ev_t ev) {
     #if CONFIG_LOG_DEFAULT_LEVEL >= 3
         ESP_LOGI(TAG, "event %s", eventNames[ev]);
@@ -385,56 +220,3 @@ void onEvent (ev_t ev) {
     clientAction = eActionUnrelated;
     xQueueSend(resultQueue, &result, 100 / portTICK_PERIOD_MS);
 }
-
-
-// --- Helper functions ---
-
-bool hexStringToBin(const char *hex, uint8_t *buf, int len)
-{
-    const char* ptr = hex;
-    for (int i = 0; i < len; i++)
-    {
-        int val = hexTupleToByte(ptr);
-        if (val < 0)
-            return false;
-        buf[i] = val;
-        ptr += 2;
-    }
-    return true;
-}
-
-int hexTupleToByte(const char *hex)
-{
-    int nibble1 = hexDigitToVal(hex[0]);
-    if (nibble1 < 0)
-        return -1;
-    int nibble2 = hexDigitToVal(hex[1]);
-    if (nibble2 < 0)
-        return -1;
-    return (nibble1 << 4) | nibble2;
-}
-
-int hexDigitToVal(char ch)
-{
-    if (ch >= '0' && ch <= '9')
-        return ch - '0';
-    if (ch >= 'A' && ch <= 'F')
-        return ch + 10 - 'A';
-    if (ch >= 'a' && ch <= 'f')
-        return ch + 10 - 'a';
-    return -1;
-}
-
-void swapByteOrder(uint8_t* buf, int len)
-{
-    uint8_t* p1 = buf;
-    uint8_t* p2 = buf + len - 1;
-    while (p1 < p2)
-    {
-        uint8_t t = *p1;
-        *p1 = *p2;
-        *p2 = t;
-        p1++;
-        p2--;
-    }
-}
\ No newline at end of file
diff --git a/src/hal.h b/src/hal.h
index 2acd42b..a1d34c6 100755
--- a/src/hal.h
+++ b/src/hal.h
@@ -28,6 +28,9 @@
 #ifndef _hal_hpp_
 #define _hal_hpp_
 
+#include "oslmic.h"
+
+
 #ifdef __cplusplus
 extern "C" {
 #endif
diff --git a/src/provisioning.c b/src/provisioning.c
new file mode 100644
index 0000000..e0879e2
--- /dev/null
+++ b/src/provisioning.c
@@ -0,0 +1,453 @@
+/*******************************************************************************
+ * 
+ * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x
+ * 
+ * Copyright (c) 2018 Manuel Bleichenbacher
+ * 
+ * Licensed under MIT License
+ * https://opensource.org/licenses/MIT
+ *
+ * Task listening on a UART port for provisioning commands.
+ *******************************************************************************/
+
+#include "freertos/FreeRTOS.h"
+#include "driver/uart.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "nvs_flash.h"
+#include "provisioning.h"
+
+#define UART_NUM CONFIG_TTN_PROVISION_UART_NUM
+#define MAX_LINE_LENGTH 128
+
+static const char *TAG = "ttn_prov";
+static const char *NVS_FLASH_PARTITION = "ttn";
+static const char *NVS_FLASH_KEY_DEV_EUI = "devEui";
+static const char *NVS_FLASH_KEY_APP_EUI = "appEui";
+static const char *NVS_FLASH_KEY_APP_KEY = "appKey";
+
+static void provisioning_task(void* pvParameter);
+static void provisioning_add_line_data(int numBytes);
+static void provisioning_detect_line_end(int start_at);
+static void provisioning_process_line();
+static bool read_nvs_value(nvs_handle handle, const char* key, uint8_t* data, size_t expected_length, bool silent);
+static bool write_nvs_value(nvs_handle handle, const char* key, const uint8_t* data, size_t len);
+static bool hex_str_to_bin(const char *hex, uint8_t *buf, int len);
+static int hex_tuple_to_byte(const char *hex);
+static int hex_digit_to_val(char ch);
+static void bin_to_hex_str(const uint8_t* buf, int len, char* hex);
+static char val_to_hex_digit(int val);
+static void swap_bytes(uint8_t* buf, int len);
+
+
+static QueueHandle_t uart_queue = NULL;
+static char* line_buf;
+static int line_length;
+static uint8_t last_line_end_char = 0;
+static uint8_t global_dev_eui[8];
+static uint8_t global_app_eui[8];
+static uint8_t global_app_key[16];
+static bool have_keys = false;
+
+
+#if defined(CONFIG_TTN_PROVISION_UART_INIT_YES)
+static void provisioning_init_uart();
+#endif
+
+
+// --- LMIC callbacks
+
+// This EUI must be in little-endian format, so least-significant-byte first.
+// When copying an EUI from ttnctl output, this means to reverse the bytes.
+// For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
+// The order is swapped in provisioning_decode_keys().
+void os_getArtEui (u1_t* buf)
+{
+    memcpy(buf, global_app_eui, 8);
+}
+
+// This should also be in little endian format, see above.
+void os_getDevEui (u1_t* buf)
+{
+    memcpy(buf, global_dev_eui, 8);
+}
+
+// This key should be in big endian format (or, since it is not really a number
+// but a block of memory, endianness does not really apply). In practice, a key
+// taken from ttnctl can be copied as-is.
+void os_getDevKey (u1_t* buf)
+{
+    memcpy(buf, global_app_key, 16);
+}
+
+
+// --- Provisioning task
+
+void provisioning_start_task()
+{
+#if defined(CONFIG_TTN_PROVISION_UART_INIT_YES)
+    provisioning_init_uart();
+#endif
+
+    esp_err_t err = uart_driver_install(UART_NUM, 2048, 2048, 20, &uart_queue, 0);
+    ESP_ERROR_CHECK(err);
+
+    xTaskCreate(provisioning_task, "provisioning", 2048, NULL, 1, NULL);
+}
+
+void provisioning_task(void* pvParameter)
+{
+    line_buf = (char*)malloc(MAX_LINE_LENGTH + 1);
+    line_length = 0;
+
+    uart_event_t event;
+
+    ESP_LOGI(TAG, "Provisioning task started");
+
+    while (true)
+    {
+        if (!xQueueReceive(uart_queue, &event, portMAX_DELAY))
+            continue;
+
+        switch (event.type)
+        {
+            case UART_DATA:
+                provisioning_add_line_data(event.size);
+                break;
+
+            case UART_FIFO_OVF:
+            case UART_BUFFER_FULL:
+                uart_flush_input(UART_NUM);
+                xQueueReset(uart_queue);
+                break;
+
+            default:
+                break;
+        }
+    }
+}
+
+void provisioning_add_line_data(int numBytes)
+{
+    int n;
+top:
+    n = numBytes;
+    if (line_length + n > MAX_LINE_LENGTH)
+        n = MAX_LINE_LENGTH - line_length;
+    
+    uart_read_bytes(UART_NUM, (uint8_t*)line_buf + line_length, n, portMAX_DELAY);
+    int start_at = line_length;
+    line_length += n;
+
+    provisioning_detect_line_end(start_at);
+
+    if (n < numBytes)
+    {
+        numBytes -= n;
+        goto top;
+    }
+}
+
+void provisioning_detect_line_end(int start_at)
+{
+top:
+    for (int p = start_at; p < line_length; p++)
+    {
+        char ch = line_buf[p];
+        if (ch == 0x0d || ch == 0x0a)
+        {
+            if (p > 0)
+                uart_write_bytes(UART_NUM, line_buf + start_at, line_length - start_at - 1);
+            if (p > 0 || ch == 0x0d || last_line_end_char == 0x0a)
+                uart_write_bytes(UART_NUM, "\r\n", 2);
+
+            line_buf[p] = 0;
+            last_line_end_char = ch;
+
+            if (p > 0)
+                provisioning_process_line();
+
+            memcpy(line_buf, line_buf + p + 1, line_length - p - 1);
+            line_length -= p + 1;
+            start_at = 0;
+            goto top;
+        }
+    }
+
+    if (line_length > 0)
+        uart_write_bytes(UART_NUM, line_buf + start_at, line_length - start_at);
+
+    if (line_length == MAX_LINE_LENGTH)
+        line_length = 0; // Line too long; flush it
+}
+
+void provisioning_process_line()
+{
+    bool is_ok = true;
+    // Expected format:
+    // AT+PROV?
+    // AT+PROV=hex16-hex16-hex32
+    if (strcmp(line_buf, "AT+PROV?") == 0)
+    {
+        uint8_t binbuf[8];
+        char hexbuf[16];
+
+        memcpy(binbuf, global_dev_eui, 8);
+        swap_bytes(binbuf, 8);
+        bin_to_hex_str(binbuf, 8, hexbuf);
+        uart_write_bytes(UART_NUM, hexbuf, 16);
+        uart_write_bytes(UART_NUM, "-", 1);
+
+        memcpy(binbuf, global_app_eui, 8);
+        swap_bytes(binbuf, 8);
+        bin_to_hex_str(binbuf, 8, hexbuf);
+        uart_write_bytes(UART_NUM, hexbuf, 16);
+
+        uart_write_bytes(UART_NUM, "-00000000000000000000000000000000\r\n", 35);
+    }
+    else if (strncmp(line_buf, "AT+PROV=", 8) == 0)
+    {
+        if (strlen(line_buf) == 74 && line_buf[24] == '-' && line_buf[41] == '-')
+        {
+            line_buf[24] = 0;
+            line_buf[41] = 0;
+            is_ok = provisioning_decode_keys(line_buf + 8, line_buf + 25, line_buf + 42);
+        }
+        else
+        {
+            is_ok = false;
+        }
+    }
+    else
+    {
+        is_ok = false;
+    }
+
+    uart_write_bytes(UART_NUM, is_ok ? "OK\r\n" : "ERROR\r\n", is_ok ? 4 : 7);
+}
+
+#if defined(CONFIG_TTN_PROVISION_UART_INIT_YES)
+
+void provisioning_init_uart()
+{
+
+}
+
+#endif
+
+
+// --- Key handling
+
+bool provisioning_have_keys()
+{
+    return have_keys;
+}
+
+bool provisioning_decode_keys(const char *dev_eui, const char *app_eui, const char *app_key)
+{
+    have_keys = false;
+
+    if (strlen(dev_eui) != 16 || !hex_str_to_bin(dev_eui, global_dev_eui, 8))
+    {
+        ESP_LOGW(TAG, "Invalid device EUI: %s", dev_eui);
+        return false;
+    }
+
+    swap_bytes(global_dev_eui, 8);
+
+    if (strlen(app_eui) != 16 || !hex_str_to_bin(app_eui, global_app_eui, 8))
+    {
+        ESP_LOGW(TAG, "Invalid application EUI: %s", app_eui);
+        return false;
+    }
+
+    swap_bytes(global_app_eui, 8);
+
+    if (strlen(app_key) != 32 || !hex_str_to_bin(app_key, global_app_key, 16))
+    {
+        ESP_LOGW(TAG, "Invalid application key: %s", app_key);
+        return false;
+    }
+
+    have_keys = true;
+    return true;
+}
+
+
+// --- Non-volatile storage
+
+bool provisioning_save_keys()
+{
+    bool result = false;
+
+    nvs_handle handle = 0;
+    esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READWRITE, &handle);
+    if (res == ESP_ERR_NVS_NOT_INITIALIZED)
+    {
+        ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first.");
+        goto done;
+    }
+    ESP_ERROR_CHECK(res);
+    if (res != ESP_OK)
+        goto done;
+
+    if (!write_nvs_value(handle, NVS_FLASH_KEY_DEV_EUI, global_dev_eui, sizeof(global_dev_eui)))
+        goto done;
+        
+    if (!write_nvs_value(handle, NVS_FLASH_KEY_APP_EUI, global_app_eui, sizeof(global_app_eui)))
+        goto done;
+        
+    if (!write_nvs_value(handle, NVS_FLASH_KEY_APP_KEY, global_app_key, sizeof(global_app_key)))
+        goto done;
+
+    res = nvs_commit(handle);
+    ESP_ERROR_CHECK(res);
+    
+    result = true;
+    ESP_LOGI(TAG, "Dev and app EUI and app key saved in NVS storage");
+
+done:
+    nvs_close(handle);
+    return result;
+}
+
+bool provisioning_restore_keys(bool silent)
+{
+    have_keys = false;
+    
+    nvs_handle handle = 0;
+    esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READONLY, &handle);
+    if (res == ESP_ERR_NVS_NOT_FOUND)
+        return false; // partition does not exist yet
+    if (res == ESP_ERR_NVS_NOT_INITIALIZED)
+    {
+        ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first.");
+        goto done;
+    }
+    ESP_ERROR_CHECK(res);
+    if (res != ESP_OK)
+        goto done;
+
+    if (!read_nvs_value(handle, NVS_FLASH_KEY_DEV_EUI, global_dev_eui, sizeof(global_dev_eui), silent))
+        goto done;
+
+    if (!read_nvs_value(handle, NVS_FLASH_KEY_APP_EUI, global_app_eui, sizeof(global_app_eui), silent))
+        goto done;
+
+    if (!read_nvs_value(handle, NVS_FLASH_KEY_APP_KEY, global_app_key, sizeof(global_app_key), silent))
+        goto done;
+
+    have_keys = true;
+    ESP_LOGI(TAG, "Dev and app EUI and app key have been restored from NVS storage");
+
+done:
+    nvs_close(handle);
+    return have_keys;
+}
+
+bool read_nvs_value(nvs_handle handle, const char* key, uint8_t* data, size_t expected_length, bool silent)
+{
+    size_t size = expected_length;
+    esp_err_t res = nvs_get_blob(handle, key, data, &size);
+    if (res == ESP_OK && size == expected_length)
+        return true;
+
+    if (res == ESP_OK && size != expected_length)
+    {
+        if (!silent)
+            ESP_LOGW(TAG, "Invalid size of NVS data for %s", key);
+        return false;
+    }
+
+    if (res == ESP_ERR_NVS_NOT_FOUND)
+    {
+        if (!silent)
+            ESP_LOGW(TAG, "No NVS data found for %s", key);
+        return false;
+    }
+    
+    ESP_ERROR_CHECK(res);
+    return false;
+}
+
+bool write_nvs_value(nvs_handle handle, const char* key, const uint8_t* data, size_t len)
+{
+    uint8_t buf[16];
+    if (read_nvs_value(handle, key, buf, len, true) && memcmp(buf, data, len) == 0)
+        return true; // unchanged
+    
+    esp_err_t res = nvs_set_blob(handle, key, data, len);
+    ESP_ERROR_CHECK(res);
+
+    return res == ESP_OK;
+}
+
+
+// --- Helper functions ---
+
+bool hex_str_to_bin(const char *hex, uint8_t *buf, int len)
+{
+    const char* ptr = hex;
+    for (int i = 0; i < len; i++)
+    {
+        int val = hex_tuple_to_byte(ptr);
+        if (val < 0)
+            return false;
+        buf[i] = val;
+        ptr += 2;
+    }
+    return true;
+}
+
+int hex_tuple_to_byte(const char *hex)
+{
+    int nibble1 = hex_digit_to_val(hex[0]);
+    if (nibble1 < 0)
+        return -1;
+    int nibble2 = hex_digit_to_val(hex[1]);
+    if (nibble2 < 0)
+        return -1;
+    return (nibble1 << 4) | nibble2;
+}
+
+int hex_digit_to_val(char ch)
+{
+    if (ch >= '0' && ch <= '9')
+        return ch - '0';
+    if (ch >= 'A' && ch <= 'F')
+        return ch + 10 - 'A';
+    if (ch >= 'a' && ch <= 'f')
+        return ch + 10 - 'a';
+    return -1;
+}
+
+void bin_to_hex_str(const uint8_t* buf, int len, char* hex)
+{
+    for (int i = 0; i < len; i++)
+    {
+        uint8_t b = buf[i];
+        *hex = val_to_hex_digit((b & 0xf0) >> 4);
+        hex++;
+        *hex = val_to_hex_digit(b & 0x0f);
+        hex++;
+    }
+}
+
+char val_to_hex_digit(int val)
+{
+    return "0123456789ABCDEF"[val];
+}
+
+void swap_bytes(uint8_t* buf, int len)
+{
+    uint8_t* p1 = buf;
+    uint8_t* p2 = buf + len - 1;
+    while (p1 < p2)
+    {
+        uint8_t t = *p1;
+        *p1 = *p2;
+        *p2 = t;
+        p1++;
+        p2--;
+    }
+}
diff --git a/src/provisioning.h b/src/provisioning.h
new file mode 100644
index 0000000..3d1900f
--- /dev/null
+++ b/src/provisioning.h
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * 
+ * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x
+ * 
+ * Copyright (c) 2018 Manuel Bleichenbacher
+ * 
+ * Licensed under MIT License
+ * https://opensource.org/licenses/MIT
+ *
+ * Task listening on a UART port for provisioning commands.
+ *******************************************************************************/
+
+#ifndef _provision_task_h_
+#define _provision_task_h_
+
+#include "oslmic.h"
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+void provisioning_start_task();
+bool provisioning_have_keys();
+bool provisioning_decode_keys(const char *dev_eui, const char *app_eui, const char *app_key);
+bool provisioning_save_keys();
+bool provisioning_restore_keys(bool silent);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif