|
1 | 1 | -- Copyright 2022 SmartThings, Inc. |
2 | 2 | -- Licensed under the Apache License, Version 2.0 |
3 | 3 |
|
| 4 | +local security = require "st.security" |
| 5 | +local PUB_KEY_PREFIX = "04" |
| 6 | + |
4 | 7 | local lock_utils = { |
5 | 8 | -- Lock device field names |
6 | 9 | LOCK_CODES = "lockCodes", |
@@ -41,7 +44,8 @@ local lock_utils = { |
41 | 44 | DEVICE_KEY_ID = "deviceKeyId", |
42 | 45 | COMMAND_REQUEST_ID = "commandRequestId", |
43 | 46 | MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED", |
44 | | - ALIRO_READER_CONFIG_UPDATED = "aliroReaderConfigUpdated" |
| 47 | + ALIRO_READER_CONFIG_UPDATED = "aliroReaderConfigUpdated", |
| 48 | + LATEST_DOOR_LOCK_FEATURE_MAP = "__latest_door_lock_feature_map" |
45 | 49 | } |
46 | 50 | local capabilities = require "st.capabilities" |
47 | 51 | local json = require "st.json" |
|
104 | 108 | -- keys are the code slots that ST uses |
105 | 109 | -- user_index and credential_index are used in the matter commands |
106 | 110 | -- |
| 111 | +function lock_utils.get_field_for_endpoint(device, field, endpoint) |
| 112 | + return device:get_field(string.format("%s_%d", field, endpoint)) |
| 113 | +end |
| 114 | + |
| 115 | +function lock_utils.set_field_for_endpoint(device, field, endpoint, value, additional_params) |
| 116 | + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) |
| 117 | +end |
107 | 118 |
|
108 | 119 | function lock_utils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list) |
109 | 120 | local previous_capability_map = {} |
@@ -146,4 +157,121 @@ function lock_utils.optional_capabilities_list_changed(new_component_capability_ |
146 | 157 | return false |
147 | 158 | end |
148 | 159 |
|
| 160 | +-- This function check busy_state and if busy_state is false, set it to true(current time) |
| 161 | +function lock_utils.is_busy_state_set(device) |
| 162 | + local c_time = os.time() |
| 163 | + local busy_state = device:get_field(lock_utils.BUSY_STATE) or false |
| 164 | + if busy_state == false or c_time - busy_state > 10 then |
| 165 | + device:set_field(lock_utils.BUSY_STATE, c_time, {persist = true}) |
| 166 | + return false |
| 167 | + else |
| 168 | + return true |
| 169 | + end |
| 170 | +end |
| 171 | + |
| 172 | +function lock_utils.hex_string_to_octet_string(hex_string) |
| 173 | + if hex_string == nil then |
| 174 | + return nil |
| 175 | + end |
| 176 | + local octet_string = "" |
| 177 | + for i = 1, #hex_string, 2 do |
| 178 | + local hex = hex_string:sub(i, i + 1) |
| 179 | + octet_string = octet_string .. string.char(tonumber(hex, 16)) |
| 180 | + end |
| 181 | + return octet_string |
| 182 | +end |
| 183 | + |
| 184 | +function lock_utils.create_group_id_resolving_key() |
| 185 | + math.randomseed(os.time()) |
| 186 | + local result = string.format("%02x", math.random(0, 255)) |
| 187 | + for i = 1, 15 do |
| 188 | + result = result .. string.format("%02x", math.random(0, 255)) |
| 189 | + end |
| 190 | + return result |
| 191 | +end |
| 192 | + |
| 193 | +function lock_utils.generate_keypair(device) |
| 194 | + local request_opts = { |
| 195 | + key_algorithm = { |
| 196 | + type = "ec", |
| 197 | + curve = "prime256v1" |
| 198 | + }, |
| 199 | + signature_algorithm = "sha256", |
| 200 | + return_formats = { |
| 201 | + pem = true, |
| 202 | + der = true |
| 203 | + }, |
| 204 | + subject = { |
| 205 | + common_name = "reader config" |
| 206 | + }, |
| 207 | + validity_days = 36500, |
| 208 | + x509_extensions = { |
| 209 | + key_usage = { |
| 210 | + critical = true, |
| 211 | + digital_signature = true |
| 212 | + }, |
| 213 | + certificate_policies = { |
| 214 | + critical = true, |
| 215 | + policy_2030_5_self_signed_client = true |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + local status = security.generate_self_signed_cert(request_opts) |
| 220 | + if not status or not status.key_der then |
| 221 | + device.log.error("generate_self_signed_cert returned no data") |
| 222 | + return nil, nil |
| 223 | + end |
| 224 | + |
| 225 | + local der = status.key_der |
| 226 | + local privKey, pubKey = nil, nil |
| 227 | + -- Helper: Parse ASN.1 length (handles 1-byte and multi-byte lengths) |
| 228 | + local function get_length(data, start_pos) |
| 229 | + local b = string.byte(data, start_pos) |
| 230 | + if not b then return nil, start_pos end |
| 231 | + |
| 232 | + if b < 0x80 then |
| 233 | + return b, start_pos + 1 |
| 234 | + else |
| 235 | + local num_bytes = b - 0x80 |
| 236 | + local len = 0 |
| 237 | + for i = 1, num_bytes do |
| 238 | + len = (len * 256) + string.byte(data, start_pos + i) |
| 239 | + end |
| 240 | + return len, start_pos + 1 + num_bytes |
| 241 | + end |
| 242 | + end |
| 243 | + -- Start parsing after the initial SEQUENCE tag (0x30) |
| 244 | + -- Most keys start: [0x30][Length]. We find the first length to find the start of content. |
| 245 | + local _, pos = get_length(der, 2) |
| 246 | + |
| 247 | + while pos < #der do |
| 248 | + local tag = string.byte(der, pos) |
| 249 | + local len, content_start = get_length(der, pos + 1) |
| 250 | + if not len then break end |
| 251 | + if tag == 0x04 then |
| 252 | + -- PRIVATE KEY: Octet String |
| 253 | + privKey = utils.bytes_to_hex_string(string.sub(der, content_start, content_start + len - 1)) |
| 254 | + elseif tag == 0xA1 then |
| 255 | + -- PUBLIC KEY Wrapper: Explicit Tag [1] |
| 256 | + -- Inside 0xA1 is a BIT STRING (0x03) |
| 257 | + local inner_tag = string.byte(der, content_start) |
| 258 | + if inner_tag == 0x03 then |
| 259 | + local bit_len, bit_start = get_length(der, content_start + 1) |
| 260 | + -- BIT STRINGS have a "leading null byte" (unused bits indicator) |
| 261 | + -- We skip that byte (bit_start) and the 0x04 EC prefix to get the raw X/Y coordinates |
| 262 | + local actual_key_start = bit_start + 2 |
| 263 | + local actual_key_len = bit_len - 2 |
| 264 | + pubKey = PUB_KEY_PREFIX .. utils.bytes_to_hex_string(string.sub(der, actual_key_start, actual_key_start + actual_key_len - 1)) |
| 265 | + end |
| 266 | + end |
| 267 | + -- Move pointer to the next tag |
| 268 | + pos = content_start + len |
| 269 | + end |
| 270 | + |
| 271 | + if not privKey or not pubKey then |
| 272 | + device.log.error("Failed to extract keys from DER") |
| 273 | + end |
| 274 | + return privKey, pubKey |
| 275 | +end |
| 276 | + |
149 | 277 | return lock_utils |
0 commit comments