1 /*
2 * Copyright 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <keymaster/android_keymaster.h>
18
19 #include <assert.h>
20 #include <string.h>
21
22 #include <stddef.h>
23
24 #include <keymaster/UniquePtr.h>
25 #include <keymaster/android_keymaster_utils.h>
26 #include <keymaster/key.h>
27 #include <keymaster/key_blob_utils/ae.h>
28 #include <keymaster/key_factory.h>
29 #include <keymaster/keymaster_context.h>
30 #include <keymaster/km_openssl/openssl_err.h>
31 #include <keymaster/operation.h>
32 #include <keymaster/operation_table.h>
33
34 namespace keymaster {
35
36 namespace {
37
38 const uint8_t MAJOR_VER = 2;
39 const uint8_t MINOR_VER = 0;
40 const uint8_t SUBMINOR_VER = 0;
41
CheckVersionInfo(const AuthorizationSet & tee_enforced,const AuthorizationSet & sw_enforced,const KeymasterContext & context)42 keymaster_error_t CheckVersionInfo(const AuthorizationSet& tee_enforced,
43 const AuthorizationSet& sw_enforced,
44 const KeymasterContext& context) {
45 uint32_t os_version;
46 uint32_t os_patchlevel;
47 context.GetSystemVersion(&os_version, &os_patchlevel);
48
49 uint32_t key_os_patchlevel;
50 if (tee_enforced.GetTagValue(TAG_OS_PATCHLEVEL, &key_os_patchlevel) ||
51 sw_enforced.GetTagValue(TAG_OS_PATCHLEVEL, &key_os_patchlevel)) {
52 if (key_os_patchlevel < os_patchlevel)
53 return KM_ERROR_KEY_REQUIRES_UPGRADE;
54 else if (key_os_patchlevel > os_patchlevel)
55 return KM_ERROR_INVALID_KEY_BLOB;
56 }
57
58 return KM_ERROR_OK;
59 }
60
61 } // anonymous namespace
62
AndroidKeymaster(KeymasterContext * context,size_t operation_table_size)63 AndroidKeymaster::AndroidKeymaster(KeymasterContext* context, size_t operation_table_size)
64 : context_(context), operation_table_(new(std::nothrow) OperationTable(operation_table_size)) {}
65
~AndroidKeymaster()66 AndroidKeymaster::~AndroidKeymaster() {}
67
AndroidKeymaster(AndroidKeymaster && other)68 AndroidKeymaster::AndroidKeymaster(AndroidKeymaster&& other)
69 : context_(move(other.context_)), operation_table_(move(other.operation_table_)) {}
70
71 // TODO(swillden): Unify support analysis. Right now, we have per-keytype methods that determine if
72 // specific modes, padding, etc. are supported for that key type, and AndroidKeymaster also has
73 // methods that return the same information. They'll get out of sync. Best to put the knowledge in
74 // the keytypes and provide some mechanism for AndroidKeymaster to query the keytypes for the
75 // information.
76
77 template <typename T>
check_supported(const KeymasterContext & context,keymaster_algorithm_t algorithm,SupportedResponse<T> * response)78 bool check_supported(const KeymasterContext& context, keymaster_algorithm_t algorithm,
79 SupportedResponse<T>* response) {
80 if (context.GetKeyFactory(algorithm) == nullptr) {
81 response->error = KM_ERROR_UNSUPPORTED_ALGORITHM;
82 return false;
83 }
84 return true;
85 }
86
GetVersion(const GetVersionRequest &,GetVersionResponse * rsp)87 void AndroidKeymaster::GetVersion(const GetVersionRequest&, GetVersionResponse* rsp) {
88 if (rsp == nullptr)
89 return;
90
91 rsp->major_ver = MAJOR_VER;
92 rsp->minor_ver = MINOR_VER;
93 rsp->subminor_ver = SUBMINOR_VER;
94 rsp->error = KM_ERROR_OK;
95 }
96
SupportedAlgorithms(const SupportedAlgorithmsRequest &,SupportedAlgorithmsResponse * response)97 void AndroidKeymaster::SupportedAlgorithms(const SupportedAlgorithmsRequest& /* request */,
98 SupportedAlgorithmsResponse* response) {
99 if (response == nullptr)
100 return;
101
102 response->error = KM_ERROR_OK;
103
104 size_t algorithm_count = 0;
105 const keymaster_algorithm_t* algorithms = context_->GetSupportedAlgorithms(&algorithm_count);
106 if (algorithm_count == 0)
107 return;
108 response->results_length = algorithm_count;
109 response->results = dup_array(algorithms, algorithm_count);
110 if (!response->results)
111 response->error = KM_ERROR_MEMORY_ALLOCATION_FAILED;
112 }
113
114 template <typename T>
GetSupported(const KeymasterContext & context,keymaster_algorithm_t algorithm,keymaster_purpose_t purpose,const T * (OperationFactory::* get_supported_method)(size_t * count)const,SupportedResponse<T> * response)115 void GetSupported(const KeymasterContext& context, keymaster_algorithm_t algorithm,
116 keymaster_purpose_t purpose,
117 const T* (OperationFactory::*get_supported_method)(size_t* count) const,
118 SupportedResponse<T>* response) {
119 if (response == nullptr || !check_supported(context, algorithm, response))
120 return;
121
122 const OperationFactory* factory = context.GetOperationFactory(algorithm, purpose);
123 if (!factory) {
124 response->error = KM_ERROR_UNSUPPORTED_PURPOSE;
125 return;
126 }
127
128 size_t count;
129 const T* supported = (factory->*get_supported_method)(&count);
130 response->SetResults(supported, count);
131 }
132
SupportedBlockModes(const SupportedBlockModesRequest & request,SupportedBlockModesResponse * response)133 void AndroidKeymaster::SupportedBlockModes(const SupportedBlockModesRequest& request,
134 SupportedBlockModesResponse* response) {
135 GetSupported(*context_, request.algorithm, request.purpose,
136 &OperationFactory::SupportedBlockModes, response);
137 }
138
SupportedPaddingModes(const SupportedPaddingModesRequest & request,SupportedPaddingModesResponse * response)139 void AndroidKeymaster::SupportedPaddingModes(const SupportedPaddingModesRequest& request,
140 SupportedPaddingModesResponse* response) {
141 GetSupported(*context_, request.algorithm, request.purpose,
142 &OperationFactory::SupportedPaddingModes, response);
143 }
144
SupportedDigests(const SupportedDigestsRequest & request,SupportedDigestsResponse * response)145 void AndroidKeymaster::SupportedDigests(const SupportedDigestsRequest& request,
146 SupportedDigestsResponse* response) {
147 GetSupported(*context_, request.algorithm, request.purpose, &OperationFactory::SupportedDigests,
148 response);
149 }
150
SupportedImportFormats(const SupportedImportFormatsRequest & request,SupportedImportFormatsResponse * response)151 void AndroidKeymaster::SupportedImportFormats(const SupportedImportFormatsRequest& request,
152 SupportedImportFormatsResponse* response) {
153 if (response == nullptr || !check_supported(*context_, request.algorithm, response))
154 return;
155
156 size_t count;
157 const keymaster_key_format_t* formats =
158 context_->GetKeyFactory(request.algorithm)->SupportedImportFormats(&count);
159 response->SetResults(formats, count);
160 }
161
SupportedExportFormats(const SupportedExportFormatsRequest & request,SupportedExportFormatsResponse * response)162 void AndroidKeymaster::SupportedExportFormats(const SupportedExportFormatsRequest& request,
163 SupportedExportFormatsResponse* response) {
164 if (response == nullptr || !check_supported(*context_, request.algorithm, response))
165 return;
166
167 size_t count;
168 const keymaster_key_format_t* formats =
169 context_->GetKeyFactory(request.algorithm)->SupportedExportFormats(&count);
170 response->SetResults(formats, count);
171 }
172
GetHmacSharingParameters()173 GetHmacSharingParametersResponse AndroidKeymaster::GetHmacSharingParameters() {
174 GetHmacSharingParametersResponse response;
175 KeymasterEnforcement* policy = context_->enforcement_policy();
176 if (!policy) {
177 response.error = KM_ERROR_UNIMPLEMENTED;
178 return response;
179 }
180
181 response.error = policy->GetHmacSharingParameters(&response.params);
182 return response;
183 }
184
185 ComputeSharedHmacResponse
ComputeSharedHmac(const ComputeSharedHmacRequest & request)186 AndroidKeymaster::ComputeSharedHmac(const ComputeSharedHmacRequest& request) {
187 ComputeSharedHmacResponse response;
188 KeymasterEnforcement* policy = context_->enforcement_policy();
189 if (!policy) {
190 response.error = KM_ERROR_UNIMPLEMENTED;
191 return response;
192 }
193
194 response.error = policy->ComputeSharedHmac(request.params_array, &response.sharing_check);
195 return response;
196 }
197
198 VerifyAuthorizationResponse
VerifyAuthorization(const VerifyAuthorizationRequest & request)199 AndroidKeymaster::VerifyAuthorization(const VerifyAuthorizationRequest& request) {
200 KeymasterEnforcement* policy = context_->enforcement_policy();
201 if (!policy) {
202 VerifyAuthorizationResponse response;
203 response.error = KM_ERROR_UNIMPLEMENTED;
204 return response;
205 }
206
207 return policy->VerifyAuthorization(request);
208 }
209
AddRngEntropy(const AddEntropyRequest & request,AddEntropyResponse * response)210 void AndroidKeymaster::AddRngEntropy(const AddEntropyRequest& request,
211 AddEntropyResponse* response) {
212 response->error = context_->AddRngEntropy(request.random_data.peek_read(),
213 request.random_data.available_read());
214 }
215
GenerateKey(const GenerateKeyRequest & request,GenerateKeyResponse * response)216 void AndroidKeymaster::GenerateKey(const GenerateKeyRequest& request,
217 GenerateKeyResponse* response) {
218 if (response == nullptr)
219 return;
220
221 keymaster_algorithm_t algorithm;
222 const KeyFactory* factory = nullptr;
223 UniquePtr<Key> key;
224 if (!request.key_description.GetTagValue(TAG_ALGORITHM, &algorithm) ||
225 !(factory = context_->GetKeyFactory(algorithm)))
226 response->error = KM_ERROR_UNSUPPORTED_ALGORITHM;
227 else if (context_->enforcement_policy() &&
228 request.key_description.GetTagValue(TAG_EARLY_BOOT_ONLY) &&
229 !context_->enforcement_policy()->in_early_boot()) {
230 response->error = KM_ERROR_EARLY_BOOT_ENDED;
231 } else {
232 KeymasterKeyBlob key_blob;
233 response->enforced.Clear();
234 response->unenforced.Clear();
235 response->error = factory->GenerateKey(request.key_description, &key_blob,
236 &response->enforced, &response->unenforced);
237 if (response->error == KM_ERROR_OK)
238 response->key_blob = key_blob.release();
239 }
240 }
241
GetKeyCharacteristics(const GetKeyCharacteristicsRequest & request,GetKeyCharacteristicsResponse * response)242 void AndroidKeymaster::GetKeyCharacteristics(const GetKeyCharacteristicsRequest& request,
243 GetKeyCharacteristicsResponse* response) {
244 if (response == nullptr)
245 return;
246
247 UniquePtr<Key> key;
248 response->error =
249 context_->ParseKeyBlob(KeymasterKeyBlob(request.key_blob), request.additional_params,
250 &key);
251 if (response->error != KM_ERROR_OK)
252 return;
253
254 // scavenge the key object for the auth lists
255 response->enforced = move(key->hw_enforced());
256 response->unenforced = move(key->sw_enforced());
257
258 response->error = CheckVersionInfo(response->enforced, response->unenforced, *context_);
259 }
260
BeginOperation(const BeginOperationRequest & request,BeginOperationResponse * response)261 void AndroidKeymaster::BeginOperation(const BeginOperationRequest& request,
262 BeginOperationResponse* response) {
263 if (response == nullptr)
264 return;
265 response->op_handle = 0;
266
267 const KeyFactory* key_factory;
268 UniquePtr<Key> key;
269 response->error = LoadKey(request.key_blob, request.additional_params, &key_factory, &key);
270 if (response->error != KM_ERROR_OK)
271 return;
272
273 response->error = KM_ERROR_UNKNOWN_ERROR;
274 keymaster_algorithm_t key_algorithm;
275 if (!key->authorizations().GetTagValue(TAG_ALGORITHM, &key_algorithm))
276 return;
277
278 response->error = KM_ERROR_UNSUPPORTED_PURPOSE;
279 OperationFactory* factory = key_factory->GetOperationFactory(request.purpose);
280 if (!factory) return;
281
282 OperationPtr operation(
283 factory->CreateOperation(move(*key), request.additional_params, &response->error));
284 if (operation.get() == nullptr) return;
285
286 if (context_->enforcement_policy()) {
287 km_id_t key_id;
288 response->error = KM_ERROR_UNKNOWN_ERROR;
289 if (!context_->enforcement_policy()->CreateKeyId(request.key_blob, &key_id)) return;
290 operation->set_key_id(key_id);
291 response->error = context_->enforcement_policy()->AuthorizeOperation(
292 request.purpose, key_id, operation->authorizations(), request.additional_params,
293 0 /* op_handle */, true /* is_begin_operation */);
294 if (response->error != KM_ERROR_OK) return;
295 }
296
297 response->output_params.Clear();
298 response->error = operation->Begin(request.additional_params, &response->output_params);
299 if (response->error != KM_ERROR_OK)
300 return;
301
302 response->op_handle = operation->operation_handle();
303 response->error = operation_table_->Add(move(operation));
304 }
305
UpdateOperation(const UpdateOperationRequest & request,UpdateOperationResponse * response)306 void AndroidKeymaster::UpdateOperation(const UpdateOperationRequest& request,
307 UpdateOperationResponse* response) {
308 if (response == nullptr)
309 return;
310
311 response->error = KM_ERROR_INVALID_OPERATION_HANDLE;
312 Operation* operation = operation_table_->Find(request.op_handle);
313 if (operation == nullptr)
314 return;
315
316 if (context_->enforcement_policy()) {
317 response->error = context_->enforcement_policy()->AuthorizeOperation(
318 operation->purpose(), operation->key_id(), operation->authorizations(),
319 request.additional_params, request.op_handle, false /* is_begin_operation */);
320 if (response->error != KM_ERROR_OK) {
321 operation_table_->Delete(request.op_handle);
322 return;
323 }
324 }
325
326 response->error =
327 operation->Update(request.additional_params, request.input, &response->output_params,
328 &response->output, &response->input_consumed);
329 if (response->error != KM_ERROR_OK) {
330 // Any error invalidates the operation.
331 operation_table_->Delete(request.op_handle);
332 }
333 }
334
FinishOperation(const FinishOperationRequest & request,FinishOperationResponse * response)335 void AndroidKeymaster::FinishOperation(const FinishOperationRequest& request,
336 FinishOperationResponse* response) {
337 if (response == nullptr)
338 return;
339
340 response->error = KM_ERROR_INVALID_OPERATION_HANDLE;
341 Operation* operation = operation_table_->Find(request.op_handle);
342 if (operation == nullptr)
343 return;
344
345 if (context_->enforcement_policy()) {
346 response->error = context_->enforcement_policy()->AuthorizeOperation(
347 operation->purpose(), operation->key_id(), operation->authorizations(),
348 request.additional_params, request.op_handle, false /* is_begin_operation */);
349 if (response->error != KM_ERROR_OK) {
350 operation_table_->Delete(request.op_handle);
351 return;
352 }
353 }
354
355 response->error = operation->Finish(request.additional_params, request.input, request.signature,
356 &response->output_params, &response->output);
357 operation_table_->Delete(request.op_handle);
358 }
359
AbortOperation(const AbortOperationRequest & request,AbortOperationResponse * response)360 void AndroidKeymaster::AbortOperation(const AbortOperationRequest& request,
361 AbortOperationResponse* response) {
362 if (!response)
363 return;
364
365 Operation* operation = operation_table_->Find(request.op_handle);
366 if (!operation) {
367 response->error = KM_ERROR_INVALID_OPERATION_HANDLE;
368 return;
369 }
370
371 response->error = operation->Abort();
372 operation_table_->Delete(request.op_handle);
373 }
374
ExportKey(const ExportKeyRequest & request,ExportKeyResponse * response)375 void AndroidKeymaster::ExportKey(const ExportKeyRequest& request, ExportKeyResponse* response) {
376 if (response == nullptr)
377 return;
378
379 UniquePtr<Key> key;
380 response->error =
381 context_->ParseKeyBlob(KeymasterKeyBlob(request.key_blob), request.additional_params, &key);
382 if (response->error != KM_ERROR_OK)
383 return;
384
385 UniquePtr<uint8_t[]> out_key;
386 size_t size;
387 response->error = key->formatted_key_material(request.key_format, &out_key, &size);
388 if (response->error == KM_ERROR_OK) {
389 response->key_data = out_key.release();
390 response->key_data_length = size;
391 }
392 }
393
AttestKey(const AttestKeyRequest & request,AttestKeyResponse * response)394 void AndroidKeymaster::AttestKey(const AttestKeyRequest& request, AttestKeyResponse* response) {
395 if (!response)
396 return;
397
398 const KeyFactory* key_factory;
399 UniquePtr<Key> key;
400 response->error = LoadKey(request.key_blob, request.attest_params,
401 &key_factory, &key);
402 if (response->error != KM_ERROR_OK)
403 return;
404
405 keymaster_blob_t attestation_application_id;
406 if (request.attest_params.GetTagValue(TAG_ATTESTATION_APPLICATION_ID,
407 &attestation_application_id)) {
408 key->sw_enforced().push_back(TAG_ATTESTATION_APPLICATION_ID, attestation_application_id);
409 }
410
411 CertChainPtr certchain;
412 response->error = context_->GenerateAttestation(*key, request.attest_params, &certchain);
413 if (response->error == KM_ERROR_OK) {
414 response->certificate_chain = *certchain;
415 // response->certificate_chain took possession of secondary resources. So we shallowly
416 // delete the keymaster_cert_chain_t object, but nothing else.
417 // TODO Can we switch to managed types eventually?
418 delete certchain.release();
419 }
420 }
421
UpgradeKey(const UpgradeKeyRequest & request,UpgradeKeyResponse * response)422 void AndroidKeymaster::UpgradeKey(const UpgradeKeyRequest& request, UpgradeKeyResponse* response) {
423 if (!response)
424 return;
425
426 KeymasterKeyBlob upgraded_key;
427 response->error = context_->UpgradeKeyBlob(KeymasterKeyBlob(request.key_blob),
428 request.upgrade_params, &upgraded_key);
429 if (response->error != KM_ERROR_OK)
430 return;
431 response->upgraded_key = upgraded_key.release();
432 }
433
ImportKey(const ImportKeyRequest & request,ImportKeyResponse * response)434 void AndroidKeymaster::ImportKey(const ImportKeyRequest& request, ImportKeyResponse* response) {
435 if (response == nullptr)
436 return;
437
438 keymaster_algorithm_t algorithm;
439 const KeyFactory* factory = nullptr;
440 UniquePtr<Key> key;
441 if (!request.key_description.GetTagValue(TAG_ALGORITHM, &algorithm) ||
442 !(factory = context_->GetKeyFactory(algorithm)))
443 response->error = KM_ERROR_UNSUPPORTED_ALGORITHM;
444 else {
445 keymaster_key_blob_t key_material = {request.key_data, request.key_data_length};
446 KeymasterKeyBlob key_blob;
447 response->error = factory->ImportKey(request.key_description, request.key_format,
448 KeymasterKeyBlob(key_material), &key_blob,
449 &response->enforced, &response->unenforced);
450 if (response->error == KM_ERROR_OK)
451 response->key_blob = key_blob.release();
452 }
453 }
454
DeleteKey(const DeleteKeyRequest & request,DeleteKeyResponse * response)455 void AndroidKeymaster::DeleteKey(const DeleteKeyRequest& request, DeleteKeyResponse* response) {
456 if (!response)
457 return;
458 response->error = context_->DeleteKey(KeymasterKeyBlob(request.key_blob));
459 }
460
DeleteAllKeys(const DeleteAllKeysRequest &,DeleteAllKeysResponse * response)461 void AndroidKeymaster::DeleteAllKeys(const DeleteAllKeysRequest&, DeleteAllKeysResponse* response) {
462 if (!response)
463 return;
464 response->error = context_->DeleteAllKeys();
465 }
466
Configure(const ConfigureRequest & request,ConfigureResponse * response)467 void AndroidKeymaster::Configure(const ConfigureRequest& request, ConfigureResponse* response) {
468 if (!response)
469 return;
470 response->error = context_->SetSystemVersion(request.os_version, request.os_patchlevel);
471 }
472
has_operation(keymaster_operation_handle_t op_handle) const473 bool AndroidKeymaster::has_operation(keymaster_operation_handle_t op_handle) const {
474 return operation_table_->Find(op_handle) != nullptr;
475 }
476
LoadKey(const keymaster_key_blob_t & key_blob,const AuthorizationSet & additional_params,const KeyFactory ** factory,UniquePtr<Key> * key)477 keymaster_error_t AndroidKeymaster::LoadKey(const keymaster_key_blob_t& key_blob,
478 const AuthorizationSet& additional_params,
479 const KeyFactory** factory, UniquePtr<Key>* key) {
480 KeymasterKeyBlob key_material;
481 keymaster_error_t error = context_->ParseKeyBlob(KeymasterKeyBlob(key_blob), additional_params,
482 key);
483 if (error != KM_ERROR_OK)
484 return error;
485 if (factory) *factory = (*key)->key_factory();
486 return CheckVersionInfo((*key)->hw_enforced(), (*key)->sw_enforced(), *context_);
487 }
488
ImportWrappedKey(const ImportWrappedKeyRequest & request,ImportWrappedKeyResponse * response)489 void AndroidKeymaster::ImportWrappedKey(const ImportWrappedKeyRequest& request,
490 ImportWrappedKeyResponse* response) {
491 if (!response) return;
492
493 KeymasterKeyBlob secret_key;
494 AuthorizationSet key_description;
495 keymaster_key_format_t key_format;
496
497 response->error =
498 context_->UnwrapKey(request.wrapped_key, request.wrapping_key, request.additional_params,
499 request.masking_key, &key_description, &key_format, &secret_key);
500
501 if (response->error != KM_ERROR_OK) {
502 return;
503 }
504
505 int sid_idx = key_description.find(TAG_USER_SECURE_ID);
506 if (sid_idx != -1) {
507 uint8_t sids = key_description[sid_idx].long_integer;
508 if (!key_description.erase(sid_idx)) {
509 response->error = KM_ERROR_UNKNOWN_ERROR;
510 return;
511 }
512 if (sids & HW_AUTH_PASSWORD) {
513 key_description.push_back(TAG_USER_SECURE_ID, request.password_sid);
514 }
515 if (sids & HW_AUTH_FINGERPRINT) {
516 key_description.push_back(TAG_USER_SECURE_ID, request.biometric_sid);
517 }
518 }
519
520 keymaster_algorithm_t algorithm;
521 const KeyFactory* factory = nullptr;
522 if (!key_description.GetTagValue(TAG_ALGORITHM, &algorithm) ||
523 !(factory = context_->GetKeyFactory(algorithm))) {
524 response->error = KM_ERROR_UNSUPPORTED_ALGORITHM;
525 } else {
526 KeymasterKeyBlob key_blob;
527 response->error =
528 factory->ImportKey(key_description, key_format, KeymasterKeyBlob(secret_key), &key_blob,
529 &response->enforced, &response->unenforced);
530 if (response->error == KM_ERROR_OK) {
531 response->key_blob = key_blob;
532 }
533 }
534 }
535
EarlyBootEnded()536 EarlyBootEndedResponse AndroidKeymaster::EarlyBootEnded() {
537 if (context_->enforcement_policy()) {
538 context_->enforcement_policy()->early_boot_ended();
539 }
540 return EarlyBootEndedResponse(KM_ERROR_OK);
541 }
542
DeviceLocked(const DeviceLockedRequest & request)543 DeviceLockedResponse AndroidKeymaster::DeviceLocked(const DeviceLockedRequest& request) {
544 if (context_->enforcement_policy()) {
545 context_->enforcement_policy()->device_locked(request.passwordOnly);
546 }
547 return DeviceLockedResponse(KM_ERROR_OK);
548 }
549
550 } // namespace keymaster
551