Newer
Older
mbed-os / docs / design-documents / features / storage / SecureStore / SecureStore_design.md

SecureStore in Mbed OS

Revision history

Revision Date Authors Mbed OS version Comments
1.0 02 October 2018 David Saada (@davidsaada) 5.11+ Initial revision

Introduction

Overview and background

SecureStore is a KVStore based storage solution, providing security features on the stored data, such as encryption, authentication, rollback protection and write once, over an underlying KVStore class. It references an additional KVStore class for storing the rollback protection keys.

Requirements and assumptions

SecureStore assumes that the underlying KVStore instances are instantiated and initialized.

System architecture and high-level design

Design basics

SecureStore is a storage class, derived from KVStore. It adds security features to the underlying key value store. As such, it offers all KVStore APIs, with additional security options (which can be selected using the creation flags at set). These include:

  • Encryption: Data is encrypted using the AES-CTR encryption method, with a randomly generated 8-byte IV. Key is derived from Device Key, using the NIST SP 800-108 KDF in counter mode spec, where salt is the key trimmed to 32 bytes, with "ENC" as prefix. Flag here is called "require confidentiality flag".
  • Authentication: A 16-byte CMAC is calculated on all stored data (including metadata) and stored at the end of the record. When reading the record, calculated CMAC is compared with the stored one. In the case of encryption, CMAC is calculated on the encrypted data. The key used for generating the CMAC is derived from Device Key, where salt is the key trimmed to 32 bytes, with "AUTH" as prefix. Flag here is called "Require integrity flag".
  • Rollback protection: (Requires authentication) CMAC is stored in a designated rollback protected storage (also of KVStore type) and compared to when reading the data under the same KVStore key. A missing or different key in the rollback protected storage results in an error. The flag here is called "Require replay protection flag".
  • Write once: Key can only be stored once and can't be removed. The flag here is called "Write once flag".

SecureStore Layers

Data layout

When storing the data, it is stored with a preceding metadata header. Metadata includes flags and security related parameters, such as IV. The CMAC, calculated for authentication, is stored at the end of the data as it is calculated on the fly, so it can't be stored with the metadata.

SecureStore Record

Fields are:

  • Metadata size: Size of metadata header.
  • Revision: SecureStore revision (currently 1).
  • Data size: Size of user data.
  • Flags: User flags.
  • IV: Random generated IV.
  • Pad: Pad data to a multiple of 16 bytes (due to encryption).
  • CMAC: CMAC calculated on key, metadata and data.

Basic implementation concepts

Because the code can't construct a single buffer to store all data (including metadata and possibly encrypted data) in one shot, setting the data occurs in chunks, using the incremental set APIs. Get uses the offset argument to extract metadata, data and CMAC separately.

Rollback protection (RBP) keys are stored in the designated rollback protection storage, which is also of KVStore type. RBP keys are the same as the SecureStore keys.

Detailed design

SecureStore Class Hierarchy

Functionality, as defined by KVStore, includes the following:

  • Initialization and reset.
  • Core actions: get, set and remove.
  • Incremental set actions.
  • Iterator actions.

Class header

SecureStore has the following header:

class SecureStore : KVStore {

public:
    SecureStore(KVStore *underlying_kv, KVStore *rbp_kv);
    virtual ~SecureStore();
         
    // Initialization and formatting
    virtual int init();
    virtual int deinit();
    virtual int reset();

    // Core API
    virtual int set(const char *key, const void *buffer, size_t size, uint32_t create_flags);
    virtual int get(const char *key, void *buffer, size_t buffer_size, size_t *actual_size = NULL, size_t offset = 0);
    virtual int get_info(const char *key, info_t *info);
    virtual int remove(const char *key);
 
    // Incremental set API
    virtual int set_start(set_handle_t *handle, const char *key, size_t final_data_size, uint32_t create_flags);
    virtual int set_add_data(set_handle_t handle, const void *value_data, size_t data_size);
    virtual int set_finalize(set_handle_t handle);
 
    // Key iterator
    virtual int iterator_open(iterator_t *it, const char *prefix = NULL);
    virtual int iterator_next(iterator_t it, char *key, size_t key_size);
    virtual int iterator_close(iterator_t it);
    
private:
    Mutex _mutex;
    KVStore *_underlying_kv;
    KVStore *_rbp_kv;
    void *_entropy;
    uint8_t *_scratch_buf;
}

Important data structures

// Record header
typedef struct {
    uint16_t metadata_size;
    uint16_t revision;
    uint32_t data_size;
    uint32_t create_flags;
    uint8_t  iv[8];
} record_metadata_t;

// incremental set handle
typedef struct {
    record_metadata_t metadata;
    bd_size_t offset;
    char *key;
    void *encrypt_handle;
    void *auth_handle;
    KVStore::set_handle_t underlying_handle;
} inc_set_handle_t;

// iterator handle
typedef struct {
    KVStore::iterator_t underlying_it;
} key_iterator_handle_t;

Initialization and reset

init function

Header:

virtual int init();

Pseudo code:

  • if _is_initialized, return OK.
  • Take _mutex.
  • Initialize _entropy with TLS entropy APIs.
  • Using DeviceKey APIs, get the device key.
  • Allocate _scratch_buf as a 32 byte array.
  • Set _is_initialized to true.
  • Release _mutex.deinit function

Header:

virtual int deinit();

Pseudo code:

  • if not _is_initialized, return OK.
  • Take _mutex.
  • Deinitialize _entropy.
  • Deallocate _scratch_buf.
  • Release _mutex.

reset function

Header:

virtual int reset();

Pseudo code:

  • Take _mutex.
  • Call _underlying_kv reset API.
  • Call _rbp_kv reset API.
  • Release _mutex.

Core APIs

set function

Header:

virtual int set(const char *key, const void *buffer, size_t size, uint32_t create_flags);

Pseudo code:

  • Call set_start with all fields and a local set_handle_t variable.
  • Call set_add_data with buffer and size.
  • Call set_finalize.
  • Return OK.

get function

Header:

virtual int get(const char *key, void *buffer, size_t buffer_size, size_t *actual_size = NULL, size_t offset = 0);

Pseudo code:

  • if not _is_initialized return error.
  • Take _mutex.
  • Call _underlying_kv get API with metadata size into a metadata local structure.
  • If failure:
    • If rollback protection flag set:
      • Call _rbp_kv get API on a local rbp_cmac variable, key is key, size 16.
      • If no error, return "RBP authentication" error.
    • Return "Key not found error".
  • If authentication flag set:
    • Derive a key from device key and key.
    • Allocate and initialize auth_handle CMAC calculation local handle with derived key.
    • Using auth_handle handle, calculate CMAC on key and metadata.
  • If encrypt flag set:
    • Derive a key from device key and key.
    • Allocate and initialize a local enc_handle AES-CTR local handle with derived key and iv field.
  • Set data_size local variable to data size in metadata.
  • Set actual_size to the minimum of buffer_size and data_size.
  • Set current_offset to 0.
  • While data_size > 0:
    • If current_offset between offset and actual_size.
      • Set dest_buf to buffer and chunk_size to actual_size.
    • Else:
      • Set dest_buf to _scratch_buf and chunk_size to actual_size.
    • Call _underlying_kv get API with dest_buf and chunk_size.
    • If authentication flag set, calculate CMAC on dest_buf, using _auth_handle handle.
    • If encrypt flag set, decrypt dest_buf (in place) using _enc_handle handle.
    • Decrement data_size by chunk_size.
  • Call _underlying_kv get API with on a local read_cmac variable, size 16.
  • Generate CMAC on local cmac variable .
  • Using mbedtls_ssl_safer_memcmp function, compare read_cmac with cmac. Return "data corrupt error" if no match.
  • If rollback protection flag set:
    • Call _rbp_kv get API on a local rbp_cmac variable, key is key, size 16.
    • If rbp_cmac doesn't match cmac, clear buffer and return "RBP authentication" error.
  • Deinitialize and free auth_handle and enc_handle.
  • Release _mutex.
  • Return OK.

get_info function

Header:

virtual int get_info(const char *key, info_t *info);

Pseudo code:

  • If not _is_initialized, return error.
  • Call get API with key and 0 in buffer_size parameter.
  • If failed, return error code.
  • Call _underlying_kv get API with metadata size and key.
  • Fill fields in info according to metadata.
  • Return OK.

remove function

Header:

virtual int remove(const char *key);

Pseudo code:

  • If not _is_initialized, return error.
  • Take _mutex.
  • Call _underlying_kv get API with metadata size and key.
  • If not found, return "Not found" error.
  • If write once flag set, return "Already exists" error.
  • Call _underlying_kv remove API with key.
  • If rollback protect flag set, call _rbp_kv remove API with key as key.
  • Return OK.

Incremental set APIs

set_start function

Header:

virtual int set_start(set_handle_t *handle, const char *key, size_t final_data_size, uint32_t create_flags);

Pseudo code:

  • Take _mutex.
  • Allocate an inc_set_handle_t and assign in handle.
  • If flags include write once flag:
    • Call _underlying_kv get_info API.
    • If key exists, return "already exists" error.
    • Call _rbp_kv get API with key as key. If key exists, return "already exists" error.
  • If encrypt flag set:
    • Derive a key from device key and key as salt (trimmed to 32 bytes with "ENC" as prefix).
    • Using TLS entropy function on _entropy handle, randomly generate iv field.
    • Allocate and initialize enc_handle AES-CTR handle field with derived key and iv field.
  • Fill all available fields in metadata.
  • If authentication flag set:
    • Derive a key from device key and key as salt (trimmed to 32 bytes with "AUTH" as prefix).
    • Allocate and initialize auth_handle CMAC calculation handle field with derived key.
    • Using auth_handle handle, calculate CMAC on key and metadata.
  • Call _underlying_kv set_start API.
  • Call _underlying_kv set_add_data API with metadata field.
  • Return OK.

set_add_data function

Header:

virtual int set_add_data(set_handle_t handle, const void *value_data, size_t data_size);

Pseudo code:

  • If offset + data_size > data size in handle, return error.
  • If flags include encryption:
    • Iterate over value_data field in chunks of _scratch_buf size.
      • Using enc_handle handle field, encrypt chunk into _scratch_buf.
      • If authentication flag set, using auth_handle handle field, update CMAC of _scratch_buf.
      • Call _underlying_kv set_add_data API with _scratch_buf.
  • Else:
    • If authentication flag set, using auth_handle handle field, update CMAC of value_data.
    • Call _underlying_kv set_add_data API with value_data.
  • Update offset field in handle.
  • Return OK.

set_finalize function

Header:

virtual int set_finalize(set_handle_t handle);

Pseudo code:

  • Initialize a local cmac 16-byte array to 0.
  • If authentication flag set, using auth_handle handle field, generate cmac.
  • Call _underlying_kv set_add_data API with cmac.
  • Call _underlying_kv set_finalize.
  • If rollback protect flag set, call _rbp_kv set API with key as key and cmac as data.
  • Deinitialize and free auth_handle and enc_handle.
  • Free handle.
  • Release _mutex.
  • Return OK.

Key iterator APIs

iterator_open function

Header:

virtual int iterator_open(iterator_t *it, const char *prefix = NULL);

Pseudo code:

  • Allocate a key_iterator_handle_t structure into it.
  • Take _mutex.
  • Call _underlying_kv iterator_open with underlying_it field.
  • Release _mutex.
  • Return OK.

iterator_next function

Header:

virtual int iterator_next(iterator_t it, char *key, size_t key_size);

Pseudo code:

  • Take _mutex.
  • Call _underlying_kv iterator_next with underlying_it field.
  • Release _mutex.
  • Return OK.

iterator_close function

Header:

virtual int iterator_close(iterator_t it);

Pseudo code:

  • Take _mutex.
  • Call _underlying_kv iterator_close with underlying_it field.
  • Release _mutex.
  • Deallocate it.
  • Return OK.

Usage scenarios and examples

Standard use of the class

The following example code shows standard use of the SecureStore class:

Standard usage example

// Underlying key value store - here TDBStore (should be instantiated and initialized)
extern TDBStore tdbstore;

// Rollback protect store - also of TDBStore type (should be instantiated and initialized)
extern TDBStore rbp_tdbstore;

// Instantiate SecureStore with tdbstore as underlying key value store and rbp_tdbstore as RBP storage
SecureStore secure_store(&tdbstore, &rbp_tdbstore);

int res;

// Initialize secure_store
res = secure_store.init();

const char *val1 = "Value of key 1";
const char *val2 = "Updated value of key 1";
// Add "Key1" with encryption and authentication flags
res = secure_store.set("Key1", val1, sizeof(val1), KVSTore::REQUIRE_CONFIDENTIALITY_FLAG | KVSTore::REQUIRE_INTEGRITY_FLAG);
// Update value of "Key1" (flags must be the same per key)
res = secure_store.set("Key1", val2, sizeof(val2), KVSTore::REQUIRE_CONFIDENTIALITY_FLAG | KVSTore::REQUIRE_INTEGRITY_FLAG);

uint_8 value[32];
size_t actual_size;
// Get value of "Key1". Value should return the updated value.
res = secure_store.get("Key1", value, sizeof(value), &actual_size);

// Remove "Key1"
res = secure_store.remove("Key1");

// Incremental write, if need to generate large data with a small buffer
const int data_size = 1024;
char buf[8];

KVSTore::set_handle_t handle;
res = secure_store.set_start(&handle, "Key2", data_size, 0);
for (int i = 0; i < data_size / sizeof(buf); i++) {
    memset(buf, i, sizeof(buf));
    res = secure_store.set_add_data(handle, buf, sizeof(buf));
}
res = secure_store.set_finalize(handle);

// Iterate over all keys starting with "Key"
res = 0;
KVSTore::iterator_t it;
secure_store.iterator_open(&it, "Key*");
char key[KVSTore::KV_MAX_KEY_LENGTH];
while (!res) {
    res = secure_store.iterator_next(&it, key, sizeof(key)e);
}
res = secure_store.iterator_close(&it);

// Deinitialize SecureStore
res = secure_store.deinit();

Other information

Open issues

  • Need to figure a way to prevent mutex abuse in incremental set APIs.