Skip to content

Commit

Permalink
[nrfconnect] Added support for user data in Factory Data parser (#24088)
Browse files Browse the repository at this point in the history
* [nrfconnect] Added support for user data in Factory Data parser

Factory data parser did not contain methods to obtain user data.

- Added two methods: GetUserData to obtain raw user data
and GetUserKey to obtain a single key.
- Improved the FactoryDataParser to read and manage the user data field.
- Improved documentation.

* Restyled by prettier-markdown

Co-authored-by: Restyled.io <[email protected]>
  • Loading branch information
2 people authored and pull[bot] committed Jan 20, 2023
1 parent 7710e3a commit 1276768
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 2 deletions.
87 changes: 86 additions & 1 deletion docs/guides/nrfconnect_factory_data_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ data secure by applying hardware write protection.
- [Enabling factory data support](#enabling-factory-data-support)
- [Generating factory data](#generating-factory-data)
- [Creating factory data JSON file with the first script](#creating-factory-data-json-file-with-the-first-script)
- [How to set user data](#how-to-set-user-data)
- [How to handle user data](#how-to-handle-user-data)
- [Verifying using the JSON Schema tool](#verifying-using-the-json-schema-tool)
- [Option 1: Using the php-json-schema tool](#option-1-using-the-php-json-schema-tool)
- [Option 2: Using a website validator](#option-2-using-a-website-validator)
Expand Down Expand Up @@ -110,7 +112,7 @@ The following table lists the parameters of a factory data set:
| `spake2_verifier` | SPAKE2+ verifier | 97 B | byte string | mandatory | The SPAKE2+ verifier generated using SPAKE2+ salt, iteration counter, and passcode. |
| `discriminator` | Discriminator | 2 B | uint16 | mandatory | A 12-bit value matching the field of the same name in the setup code. The discriminator is used during the discovery process. |
| `passcode` | SPAKE passcode | 4 B | uint32 | optional | A pairing passcode is a 27-bit unsigned integer which serves as a proof of possession during the commissioning. Its value must be restricted to the values from `0x0000001` to `0x5F5E0FE` (`00000001` to `99999998` in decimal), excluding the following invalid passcode values: `00000000`, `11111111`, `22222222`, `33333333`, `44444444`, `55555555`, `66666666`, `77777777`, `88888888`, `99999999`, `12345678`, `87654321`. |
| `user` | User data | variable | JSON string | max 1024 B | The user data is provided in the JSON format. This parameter is optional and depends on user's or manufacturer's purpose (or both). It is provided as a string from persistent storage and should be parsed in the user application. This data is not used by the Matter stack. |
| `user` | User data | variable | JSON string | max 1024 B | The user data is provided in the JSON format. This parameter is optional and depends on device manufacturer's purpose. It is provided as a CBOR map type from persistent storage and should be parsed in the user application. This data is not used by the Matter stack. To learn how to work with user data, see [How to set user data](#how-to-set-user-data) section. |

### Factory data format

Expand Down Expand Up @@ -345,6 +347,89 @@ If the script finishes successfully, go to the location you provided with the
> location as an existing file. To allow overwriting, add the `--overwrite`
> option to the argument list of the Python script.
### How to set user data
The user data is an optional field provided in the factory data JSON file and
depends on the manufacturer's purpose. The `user` field in a JSON factory data
file is represented by a flat JSON map and it can consist of `string` or `int32`
data types only. On the device side, the `user` data will be available as a CBOR
map containing all defined `string` and `int32` fields.
To add user data as an argument to the
[generate_nrfconnect_chip_factory_data.py](../../scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py)
script, add the following line to the argument list:
```
--user-data {user data JSON}
```
As `user data JSON`, provide a flat JSON map with a value file that consists of
`string` or `int32` types. For example, you can use a JSON file that looks like
follows:
```
{
"name": "product_name",
"version": 123,
"revision": "0x123"
}
```
When added to the argument line, the final result would look like follows:
```
--user-data '{"name": "product_name", "version": 123, "revision": "0x123"}'
```
#### How to handle user data
The user data is not handled anywhere in the Matter stack, so you must handle it
in your application. To do this, you can use the
[Factory Data Provider](../../src/platform/nrfconnect/FactoryDataProvider.h) and
apply one of the following methods:
- `GetUserData` method to obtain raw data in the CBOR format as a
`MutableByteSpan`.
- `GetUserKey` method that lets you search along the user data list using a
specific key, and if the key exists in the user data, the method returns its
value.
If you opt for `GetUserKey`, complete the following steps to set up the search:
1. Add the `GetUserKey` method to your code.
2. Given that all integer fields of the `user` Factory Data field are `int32`,
provide a buffer that has a size of at least `4B` or an `int32_t` variable to
`GetUserKey`. To read a string field from user data, the buffer should have a
size of at least the length of the expected string.
3. Set it up to read all user data fields.
Only after this setup is complete, can you use all variables in your code and
cast the result to your own purpose.
The code example of how to read all fields from the JSON example one by one can
look like follows:
```
chip::DeviceLayer::FactoryDataProvider factoryDataProvider;
factoryDataProvider.Init();
uint8_t user_name[12];
size_t name_len = sizeof(user_name);
factoryDataProvider.GetUserKey("name", user_name, name_len);
int32_t version;
size_t version_len = sizeof(version);
factoryDataProvider.GetUserKey("version", &version, version_len);
uint8_t revision[5];
size_t revision_len = sizeof(revision);
factoryDataProvider.GetUserKey("revision", revision, revision_len);
```
### Verifying using the JSON Schema tool
The JSON file that contains factory data can be verified using the
Expand Down
20 changes: 20 additions & 0 deletions scripts/tools/nrfconnect/tests/test_generate_factory_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def test_generate_factory_data_all_specified(self):
'--discriminator', '0xFED',
'--rd_uid', '91a9c12a7c80700a31ddcfa7fce63e44',
'--enable_key', '00112233445566778899aabbccddeeff',
'--user', '{"name": "product_name", "version": 123, "revision": "0x123"}',
'-o', os.path.join(outdir, 'fd.json')
])

Expand Down Expand Up @@ -199,6 +200,15 @@ def test_generate_factory_data_all_specified(self):
self.assertEqual(factory_data.get('passcode'), 13243546)
self.assertEqual(factory_data.get('rd_uid'), 'hex:91a9c12a7c80700a31ddcfa7fce63e44')
self.assertEqual(factory_data.get('enable_key'), 'hex:00112233445566778899aabbccddeeff')
self.assertEqual(factory_data.get('user'), {'name': 'product_name', 'version': 123, 'revision': '0x123'})

subprocess.check_call(['python3', os.path.join(TOOLS_DIR, 'nrfconnect_generate_partition.py'),
'-i', os.path.join(outdir, 'fd.json'),
'-o', os.path.join(outdir, 'fd'),
'--offset', "0xfb000",
'--size', "0x1000",
'--raw'
])

def test_generate_spake2p_verifier_default(self):
with tempfile.TemporaryDirectory() as outdir:
Expand All @@ -223,6 +233,7 @@ def test_generate_spake2p_verifier_default(self):
'--spake2_salt', 'U1BBS0UyUCBLZXkgU2FsdA==',
'--passcode', '20202021',
'--discriminator', '0xFED',
'--user', '{"name": "product_name", "version": 123, "revision": "0x123"}',
'-o', os.path.join(outdir, 'fd.json')
])

Expand All @@ -234,6 +245,15 @@ def test_generate_spake2p_verifier_default(self):
self.assertEqual(factory_data.get('spake2_it'), 1000)
self.assertEqual(factory_data.get('spake2_verifier'), base64_to_json(
'uWFwqugDNGiEck/po7KHwwMwwqZgN10XuyBajPGuyzUEV/iree4lOrao5GuwnlQ65CJzbeUB49s31EH+NEkg0JVI5MGCQGMMT/SRPFNRODm3wH/MBiehuFc6FJ/NH6Rmzw=='))
self.assertEqual(factory_data.get('user'), {'name': 'product_name', 'version': 123, 'revision': '0x123'})

subprocess.check_call(['python3', os.path.join(TOOLS_DIR, 'nrfconnect_generate_partition.py'),
'-i', os.path.join(outdir, 'fd.json'),
'-o', os.path.join(outdir, 'fd'),
'--offset', "0xfb000",
'--size', "0x1000",
'--raw'
])


if __name__ == '__main__':
Expand Down
75 changes: 74 additions & 1 deletion src/platform/nrfconnect/FactoryDataParser.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,77 @@ static inline bool uint16_decode(zcbor_state_t * states, uint16_t * value)
return false;
}

static bool DecodeEntry(zcbor_state_t * states, void * buffer, size_t bufferSize, size_t * outlen)
{
struct zcbor_string tempString;
int32_t tempInt = 0;

// Try to decode entry as string
bool res = zcbor_tstr_decode(states, &tempString);
if (res)
{
if (bufferSize < tempString.len)
{
return false;
}
memcpy(buffer, tempString.value, tempString.len);
*outlen = tempString.len;
return res;
}

// Try to decode entry as int32
res = zcbor_int32_decode(states, &tempInt);
if (res)
{
if (bufferSize < sizeof(tempInt))
{
return false;
}
memcpy(buffer, &tempInt, sizeof(tempInt));
*outlen = sizeof(tempInt);
return res;
}

return res;
}

bool FindUserDataEntry(struct FactoryData * factoryData, const char * entry, void * buffer, size_t bufferSize, size_t * outlen)
{
if ((!factoryData) || (!factoryData->user.data) || (!buffer) || (!outlen))
{
return false;
}

ZCBOR_STATE_D(states, MAX_FACTORY_DATA_NESTING_LEVEL - 1, factoryData->user.data, factoryData->user.len, 1);

bool res = zcbor_map_start_decode(states);
bool keyFound = false;
struct zcbor_string currentString;

while (res)
{
res = zcbor_tstr_decode(states, &currentString);

if (!res)
{
break;
}

if (strncmp(entry, (const char *) currentString.value, currentString.len) == 0)
{
res = DecodeEntry(states, buffer, bufferSize, outlen);
keyFound = true;
break;
}
else
{
res = res && zcbor_any_skip(states, NULL);
}
}

return res && keyFound && zcbor_list_map_end_force_decode(states);
}

bool ParseFactoryData(uint8_t * buffer, uint16_t bufferSize, struct FactoryData * factoryData)
{
memset(factoryData, 0, sizeof(*factoryData));
Expand Down Expand Up @@ -167,7 +238,9 @@ bool ParseFactoryData(uint8_t * buffer, uint16_t bufferSize, struct FactoryData
}
else if (strncmp("user", (const char *) currentString.value, currentString.len) == 0)
{
res = res && zcbor_bstr_decode(states, (struct zcbor_string *) &factoryData->user);
factoryData->user.data = (void *) states->payload;
res = res && zcbor_any_skip(states, NULL);
factoryData->user.len = (void *) states->payload - factoryData->user.data;
}
else
{
Expand Down
14 changes: 14 additions & 0 deletions src/platform/nrfconnect/FactoryDataParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ struct FactoryData
*/
bool ParseFactoryData(uint8_t * buffer, uint16_t bufferSize, struct FactoryData * factoryData);

/**
* @brief Tries to find an entry within the given factory data user data field.
* The parser parses only the uint32 type of ints. To read int-related objects the buffer size must be aligned to uint32.
* That means, to obtain uint8 or uint16 value users should provide the buffer with size at least sizeof(uint32_t).
*
* @param factoryData An address of object of factory data that contains user field filled.
* @param entry An entry name to be find out.
* @param buffer Output buffer to store found key value.
* @param bufferSize Size of buffer. That size should have size at least equal to expected key value.
* @param outlen Actual size of found user data field.
* @return true on success, false otherwise
*/
bool FindUserDataEntry(struct FactoryData * factoryData, const char * entry, void * buffer, size_t bufferSize, size_t * outlen);

#ifdef __cplusplus
}
#endif
26 changes: 26 additions & 0 deletions src/platform/nrfconnect/FactoryDataProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,32 @@ CHIP_ERROR FactoryDataProvider<FlashFactoryData>::GetEnableKey(MutableByteSpan &
return CHIP_NO_ERROR;
}

template <class FlashFactoryData>
CHIP_ERROR FactoryDataProvider<FlashFactoryData>::GetUserData(MutableByteSpan & userData)
{
ReturnErrorCodeIf(!mFactoryData.user.data, CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND);
ReturnErrorCodeIf(userData.size() < mFactoryData.user.len, CHIP_ERROR_BUFFER_TOO_SMALL);

memcpy(userData.data(), mFactoryData.user.data, mFactoryData.user.len);

userData.reduce_size(mFactoryData.user.len);

return CHIP_NO_ERROR;
}

template <class FlashFactoryData>
CHIP_ERROR FactoryDataProvider<FlashFactoryData>::GetUserKey(const char * userKey, void * buf, size_t & len)
{
ReturnErrorCodeIf(!mFactoryData.user.data, CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND);
ReturnErrorCodeIf(!buf, CHIP_ERROR_BUFFER_TOO_SMALL);

bool success = FindUserDataEntry(&mFactoryData, userKey, buf, len, &len);

ReturnErrorCodeIf(!success, CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND);

return CHIP_NO_ERROR;
}

// Fully instantiate the template class in whatever compilation unit includes this file.
template class FactoryDataProvider<InternalFlashFactoryData>;
template class FactoryDataProvider<ExternalFlashFactoryData>;
Expand Down
22 changes: 22 additions & 0 deletions src/platform/nrfconnect/FactoryDataProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ class FactoryDataProvider : public chip::Credentials::DeviceAttestationCredentia
// ===== Members functions that are platform-specific
CHIP_ERROR GetEnableKey(MutableByteSpan & enableKey);

/**
* @brief Get the user data in CBOR format as MutableByteSpan
*
* @param userData MutableByteSpan object to obtain all user data in CBOR format
* @returns
* CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND if factory data does not contain user field, or the value cannot be read out.
* CHIP_ERROR_BUFFER_TOO_SMALL if provided MutableByteSpan is too small
*/
CHIP_ERROR GetUserData(MutableByteSpan & userData);

/**
* @brief Try to find user data key and return its value
*
* @param userKey A key name to be found
* @param buf Buffer to store value of found key
* @param len Length of the buffer. This value will be updated to the actual value if the key is read.
* @returns
* CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND if factory data does not contain user field, or the value cannot be read out.
* CHIP_ERROR_BUFFER_TOO_SMALL if provided buffer length is too small
*/
CHIP_ERROR GetUserKey(const char * userKey, void * buf, size_t & len);

private:
static constexpr uint16_t kFactoryDataPartitionSize = PM_FACTORY_DATA_SIZE;
static constexpr uint32_t kFactoryDataPartitionAddress = PM_FACTORY_DATA_ADDRESS;
Expand Down

0 comments on commit 1276768

Please sign in to comment.