]> git.saurik.com Git - apple/security.git/blob - keychain/securityd/Regressions/SOSTransportTestTransports.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / securityd / Regressions / SOSTransportTestTransports.m
1 #include <CoreFoundation/CoreFoundation.h>
2 #include <CoreFoundation/CFRuntime.h>
3
4 #include "keychain/SecureObjectSync/SOSAccount.h"
5 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
6 #include "keychain/SecureObjectSync/SOSTransport.h"
7 #import "keychain/SecureObjectSync/SOSTransportKeyParameter.h"
8 #import "keychain/SecureObjectSync/SOSTransportCircleKVS.h"
9 #import "keychain/SecureObjectSync/SOSTransportMessageKVS.h"
10 #include "keychain/SecureObjectSync/SOSKVSKeys.h"
11 #include "keychain/SecureObjectSync/SOSPeerCoder.h"
12 #include <utilities/SecCFWrappers.h>
13 #include "keychain/SecureObjectSync/SOSPeerInfoV2.h"
14 #import "keychain/SecureObjectSync/SOSAccountTrustClassic+Circle.h"
15 #import "keychain/SecureObjectSync/SOSAccountTrustClassic+Identity.h"
16
17 #include "SOSTransportTestTransports.h"
18 #include "SOSAccountTesting.h"
19
20 CFMutableArrayRef key_transports = NULL;
21 CFMutableArrayRef circle_transports = NULL;
22 CFMutableArrayRef message_transports = NULL;
23
24 ///
25 //Mark Test Key Parameter Transport
26 ///
27
28 @implementation CKKeyParameterTest
29
30 -(id) initWithAccount:(SOSAccount*) acct andName:(CFStringRef) n andCircleName:(CFStringRef) cN
31 {
32 if ((self = [super init])) {
33 self.name = CFRetainSafe(n);
34 self.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
35 self.account = acct;
36 self.circleName = CFRetainSafe(cN);
37
38 if(!key_transports)
39 key_transports = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
40 CFArrayAppendValue(key_transports, (__bridge CFTypeRef)((CKKeyParameter*)self));
41
42 SOSRegisterTransportKeyParameter((CKKeyParameter*)self);
43 }
44 return self;
45 }
46
47 -(void)dealloc {
48 if(self) {
49 CFReleaseNull(self->_changes);
50 CFReleaseNull(self->_circleName);
51 }
52 }
53
54 - (void)setChanges:(CFMutableDictionaryRef)changes
55 {
56 CFRetainAssign(self->_changes, changes);
57 }
58
59 -(bool) SOSTransportKeyParameterHandleKeyParameterChanges:(CKKeyParameterTest*) transport data:(CFDataRef) data err:(CFErrorRef) error
60 {
61 SOSAccount* acct = transport.account;
62 return SOSAccountHandleParametersChange(acct, data, &error);
63 }
64
65
66 -(void) SOSTransportKeyParameterHandleNewAccount:(CKKeyParameterTest*) transport acct:(SOSAccount*) acct
67 {
68
69 if(key_transports){
70 CFArrayRemoveAllValue(key_transports, (__bridge CFTypeRef)(acct.key_transport));
71 }
72 if(message_transports){
73 CFArrayRemoveAllValue(message_transports, (__bridge CFTypeRef)acct.kvs_message_transport);
74 }
75 if(circle_transports)
76 CFArrayRemoveAllValue(circle_transports, (__bridge CFTypeRef)(acct.circle_transport));
77
78 SOSAccountSetToNew(acct);
79 SOSAccountResetToTest(acct, transport.name);
80 }
81
82 CFStringRef SOSTransportKeyParameterTestGetName(CKKeyParameterTest* transport){
83 return transport.name;
84 }
85
86 void SOSTransportKeyParameterTestSetName(CKKeyParameterTest* transport, CFStringRef accountName){
87 transport.name = accountName;
88 }
89
90 SOSAccount* SOSTransportKeyParameterTestGetAccount(CKKeyParameterTest* transport){
91 return ((CKKeyParameter*)transport).account;
92 }
93
94 CFMutableDictionaryRef SOSTransportKeyParameterTestGetChanges(CKKeyParameterTest* transport){
95 return transport.changes;
96 }
97
98 void SOSTransportKeyParameterTestClearChanges(CKKeyParameterTest* transport){
99 transport.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
100 }
101
102 -(bool) SOSTransportKeyParameterPublishCloudParameters:(CKKeyParameterTest*) transport data:(CFDataRef)newParameters err:(CFErrorRef*) error
103 {
104 if(!transport.changes)
105 transport.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
106
107 CFDictionarySetValue(transport.changes, kSOSKVSKeyParametersKey, newParameters);
108
109 return true;
110 }
111 @end
112
113
114 ///
115 //MARK: Test Circle Transport
116 ///
117 @implementation SOSCircleStorageTransportTest
118 @synthesize accountName = accountName;
119
120 -(id)init
121 {
122 return [super init];
123 }
124
125 CFStringRef SOSTransportCircleTestGetName(SOSCircleStorageTransportTest* transport){
126 return (__bridge CFStringRef)(transport.accountName);
127 }
128 void SOSTransportCircleTestSetName(SOSCircleStorageTransportTest* transport, CFStringRef name){
129 transport.accountName = nil;
130 transport.accountName = (__bridge NSString *)(name);
131 }
132
133 -(CFMutableDictionaryRef) SOSTransportCircleTestGetChanges
134 {
135 return (__bridge CFMutableDictionaryRef)(self.pending_changes);
136 }
137
138 void SOSTransportCircleTestClearChanges(SOSCircleStorageTransportTest* transport){
139 transport.pending_changes = [NSMutableDictionary dictionary];
140 }
141
142 -(id) initWithAccount:(SOSAccount *)acct andWithAccountName:(CFStringRef)acctName andCircleName:(CFStringRef)cName
143 {
144 if ((self = [super init])) {
145 self.account = acct;
146 self.accountName = (__bridge NSString *)(acctName);
147 self.circleName = (__bridge NSString*)cName;
148 self.pending_changes = [NSMutableDictionary dictionary];
149 if(!circle_transports)
150 circle_transports = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
151 CFArrayAppendValue(circle_transports, (__bridge CFTypeRef)self);
152
153 SOSRegisterTransportCircle((SOSCircleStorageTransportTest*)self);
154 }
155 return self;
156 }
157
158 -(bool) kvsRingFlushChanges:(CFErrorRef*) error
159 {
160 return true;
161 }
162
163 -(bool) kvsRingPostRing:(CFStringRef) ringName ring:(CFDataRef) ring err:(CFErrorRef *)error
164 {
165 CFStringRef ringKey = SOSRingKeyCreateWithName(ringName, error);
166 CFMutableDictionaryRef changes = [self SOSTransportCircleTestGetChanges];
167 CFDictionaryAddValue(changes, ringKey, ring);
168 CFReleaseNull(ringKey);
169 return true;
170 }
171
172 -(bool) kvssendDebugInfo:(CFStringRef) type debug:(CFTypeRef) debugInfo err:(CFErrorRef *)error
173 {
174 CFMutableDictionaryRef changes = [self SOSTransportCircleTestGetChanges];
175 CFDictionaryAddValue(changes, type, debugInfo);
176 return true;
177 }
178
179 -(bool) postRetirement:(CFStringRef)cName peer:(SOSPeerInfoRef)peer err:(CFErrorRef *)error
180 {
181 CFStringRef retirement_key = SOSRetirementKeyCreateWithCircleNameAndPeer(cName, SOSPeerInfoGetPeerID(peer));
182 CFDataRef retirement_data = SOSPeerInfoCopyEncodedData(peer, kCFAllocatorDefault, error);
183
184 if (retirement_key)
185 [self testAddToChanges:retirement_key data:retirement_data];
186
187 CFReleaseNull(retirement_key);
188 CFReleaseNull(retirement_data);
189 return true;
190 }
191
192 -(bool) flushChanges:(CFErrorRef *)error
193 {
194 return true;
195 }
196
197 -(void) testAddToChanges:(CFStringRef) message_key data:(CFDataRef) message_data
198 {
199 if (self.pending_changes == NULL) {
200 self.pending_changes = [NSMutableDictionary dictionary];
201 }
202 if (message_data == NULL) {
203 [self.pending_changes setObject:[NSNull null] forKey:(__bridge NSString*)(message_key)];
204 } else {
205 [self.pending_changes setObject:(__bridge NSData*)message_data forKey:(__bridge NSString*)message_key];
206 }
207 secnotice("circle-changes", "Adding circle change %@ %@->%@", self.accountName, message_key, message_data);
208 }
209
210 -(void) SOSTransportCircleTestAddBulkToChanges:(CFDictionaryRef) updates
211 {
212 if (self.pending_changes == NULL) {
213 self.pending_changes = [[NSMutableDictionary alloc]initWithDictionary:(__bridge NSDictionary * _Nonnull)(updates)];
214
215 }
216 else{
217 CFDictionaryForEach(updates, ^(const void *key, const void *value) {
218 [self.pending_changes setObject:(__bridge id _Nonnull)value forKey:(__bridge id _Nonnull)key];
219 });
220 }
221 }
222
223
224 -(bool) expireRetirementRecords:(CFDictionaryRef) retirements err:(CFErrorRef *)error
225 {
226 bool success = true;
227 CFMutableDictionaryRef keysToWrite = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
228
229 CFDictionaryForEach(retirements, ^(const void *key, const void *value) {
230 if (isString(key) && isArray(value)) {
231 CFStringRef circle_name = (CFStringRef) key;
232 CFArrayRef retirees = (CFArrayRef) value;
233
234 CFArrayForEach(retirees, ^(const void *value) {
235 if (isString(value)) {
236 CFStringRef retiree_id = (CFStringRef) value;
237
238 CFStringRef kvsKey = SOSRetirementKeyCreateWithCircleNameAndPeer(circle_name, retiree_id);
239
240 CFDictionaryAddValue(keysToWrite, kvsKey, kCFNull);
241
242 CFReleaseSafe(kvsKey);
243 }
244 });
245 }
246 });
247
248 if(CFDictionaryGetCount(keysToWrite)) {
249 [self SOSTransportCircleTestAddBulkToChanges:keysToWrite];
250 }
251 CFReleaseNull(keysToWrite);
252
253 return success;
254 }
255
256 bool SOSTransportCircleTestRemovePendingChange(SOSCircleStorageTransportTest* transport, CFStringRef circleName, CFErrorRef *error){
257 CFStringRef circle_key = SOSCircleKeyCreateWithName(circleName, error);
258 if (circle_key)
259 [transport.pending_changes removeObjectForKey:(__bridge NSString*)circle_key];
260 CFReleaseNull(circle_key);
261 return true;
262 }
263
264 -(bool) postCircle:(CFStringRef)cName circleData:(CFDataRef)circle_data err:(CFErrorRef *)error
265 {
266 CFStringRef circle_key = SOSCircleKeyCreateWithName(cName, error);
267 if (circle_key)
268 [self testAddToChanges:circle_key data:circle_data];
269 CFReleaseNull(circle_key);
270
271 return true;
272 }
273
274 -(CFDictionaryRef)handleRetirementMessages:(CFMutableDictionaryRef) circle_retirement_messages_table err:(CFErrorRef *)error
275 {
276 return SOSAccountHandleRetirementMessages(self.account, circle_retirement_messages_table, error);
277 }
278
279 -(CFArrayRef)CF_RETURNS_RETAINED handleCircleMessagesAndReturnHandledCopy:(CFMutableDictionaryRef) circle_circle_messages_table err:(CFErrorRef *)error
280 {
281 CFMutableArrayRef handledKeys = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault);
282 CFDictionaryForEach(circle_circle_messages_table, ^(const void *key, const void *value) {
283 CFErrorRef circleMessageError = NULL;
284 if (!SOSAccountHandleCircleMessage(self.account, key, value, &circleMessageError)) {
285 secerror("Error handling circle message %@ (%@): %@", key, value, circleMessageError);
286 }
287 else{
288 CFStringRef circle_id = (CFStringRef) key;
289 CFArrayAppendValue(handledKeys, circle_id);
290 }
291 CFReleaseNull(circleMessageError);
292 });
293
294 return handledKeys;
295 }
296
297 SOSAccount* SOSTransportCircleTestGetAccount(SOSCircleStorageTransportTest* transport) {
298 return transport.account;
299 }
300
301 @end
302
303 ///
304 //MARK KVS Message Test Transport
305 ///
306
307
308
309 @implementation SOSMessageKVSTest
310
311
312 -(id) initWithAccount:(SOSAccount*)acct andName:(CFStringRef)n andCircleName:(CFStringRef) cN
313 {
314 if ((self = [super init])) {
315 self.engine = SOSDataSourceFactoryGetEngineForDataSourceName(acct.factory, cN, NULL);
316 self.account = acct;
317 self.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
318 self.name = CFRetainSafe(n);
319 self.circleName = (__bridge NSString*)cN;
320
321 if(!message_transports)
322 message_transports = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
323 CFArrayAppendValue(message_transports, (__bridge const void *)((SOSMessageKVSTest*)self));
324 SOSRegisterTransportMessage((SOSMessageKVSTest*)self);
325 }
326
327 return self;
328 }
329
330 - (void)setChanges:(CFMutableDictionaryRef)changes
331 {
332 CFRetainAssign(self->_changes, changes);
333 }
334
335 -(CFIndex) SOSTransportMessageGetTransportType
336 {
337 return kKVSTest;
338 }
339 -(CFStringRef) SOSTransportMessageGetCircleName
340 {
341 return (__bridge CFStringRef)circleName;
342 }
343 -(CFTypeRef) SOSTransportMessageGetEngine
344 {
345 return engine;
346 }
347 -(SOSAccount*) SOSTransportMessageGetAccount
348 {
349 return self.account;
350 }
351
352 void SOSTransportMessageKVSTestSetName(SOSMessageKVSTest* transport, CFStringRef n)
353 {
354 transport.name = n;
355 }
356
357 CFStringRef SOSTransportMessageKVSTestGetName(SOSMessageKVSTest* transport)
358 {
359 return transport.name;
360 }
361
362 CFMutableDictionaryRef SOSTransportMessageKVSTestGetChanges(SOSMessageKVSTest* transport)
363 {
364 return transport.changes;
365 }
366
367 void SOSTransportMessageTestClearChanges(SOSMessageKVSTest* transport)
368 {
369 transport.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
370 }
371
372 static void SOSTransportMessageTestAddBulkToChanges(SOSMessageKVSTest* transport, CFDictionaryRef updates)
373 {
374 #ifndef __clang_analyzer__ // The analyzer thinks transport.changes is a leak, but I don't know why.
375 if (transport.changes == NULL) {
376 transport.changes = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, CFDictionaryGetCount(updates), updates);
377 }
378 else{
379 CFDictionaryForEach(updates, ^(const void *key, const void *value) {
380 CFDictionarySetValue(transport.changes, key, value);
381 });
382 }
383 #endif // __clang_analyzer__
384 }
385
386 static void SOSTransportMessageTestAddToChanges(SOSMessageKVSTest* transport, CFStringRef message_key, CFDataRef message_data)
387 {
388 if (transport.changes == NULL) {
389 transport.changes = CFDictionaryCreateMutableForCFTypes(kCFAllocatorDefault);
390 }
391 if (message_data == NULL) {
392 CFDictionarySetValue(transport.changes, message_key, kCFNull);
393 } else {
394 CFDictionarySetValue(transport.changes, message_key, message_data);
395 }
396 }
397
398 -(bool) SOSTransportMessageCleanupAfterPeerMessages:(SOSMessageKVSTest*) transport peers:(CFDictionaryRef)circle_to_peer_ids err:(CFErrorRef*) error
399 {
400 if(!transport.engine)
401 return true;
402 CFArrayRef enginePeers = SOSEngineGetPeerIDs((SOSEngineRef)transport.engine);
403
404 CFDictionaryForEach(circle_to_peer_ids, ^(const void *key, const void *value) {
405 if (isString(key) && isArray(value)) {
406 CFStringRef circle_name = (CFStringRef) key;
407 CFArrayRef peers_to_cleanup_after = (CFArrayRef) value;
408
409 CFArrayForEach(peers_to_cleanup_after, ^(const void *value) {
410 if (isString(value)) {
411 CFStringRef cleanup_id = (CFStringRef) value;
412 // TODO: Since the enginePeers list is not authorative (the Account is) this could inadvertently clean up active peers or leave behind stale peers
413 if (enginePeers) CFArrayForEach(enginePeers, ^(const void *value) {
414 if (isString(value)) {
415 CFStringRef in_circle_id = (CFStringRef) value;
416
417 CFStringRef kvsKey = SOSMessageKeyCreateWithCircleNameAndPeerNames(circle_name, cleanup_id, in_circle_id);
418 SOSTransportMessageTestAddToChanges(transport, kvsKey, NULL);
419 CFReleaseSafe(kvsKey);
420
421 kvsKey = SOSMessageKeyCreateWithCircleNameAndPeerNames(circle_name, in_circle_id, cleanup_id);
422 SOSTransportMessageTestAddToChanges(transport, kvsKey, NULL);
423 CFReleaseSafe(kvsKey);
424 }
425 });
426
427 }
428 });
429 }
430 });
431
432 return [transport SOSTransportMessageFlushChanges:transport err:error];
433 return true;
434 }
435
436 static bool sendToPeer(SOSMessageKVSTest* transport, CFStringRef circleName, CFStringRef peerID, CFDataRef message, CFErrorRef *error) {
437 bool result = true;
438 NSString* myID = transport.account.peerID;
439 CFStringRef message_to_peer_key = SOSMessageKeyCreateFromTransportToPeer((SOSMessage*)transport, (__bridge CFStringRef) myID, peerID);
440 CFDictionaryRef a_message_to_a_peer = CFDictionaryCreateForCFTypes(NULL, message_to_peer_key, message, NULL);
441
442 SOSTransportMessageTestAddBulkToChanges(transport, a_message_to_a_peer);
443 CFReleaseNull(a_message_to_a_peer);
444 CFReleaseNull(message_to_peer_key);
445
446 return result;
447 }
448
449 -(bool) SOSTransportMessageSyncWithPeers:(SOSMessageKVSTest*) transport p:(CFSetRef) peers err:(CFErrorRef *)error
450 {
451 // Each entry is keyed by circle name and contains a list of peerIDs
452
453 __block bool result = true;
454
455 CFSetForEach(peers, ^(const void *value) {
456 CFStringRef peerID = asString(value, NULL);
457
458 if (peerID) {
459 SOSEngineWithPeerID((SOSEngineRef)transport.engine, peerID, error, ^(SOSPeerRef peer, SOSCoderRef coder, SOSDataSourceRef dataSource, SOSTransactionRef txn, bool *forceSaveState) {
460 SOSEnginePeerMessageSentCallback* sentCallback = NULL;
461 CFDataRef message_to_send = NULL;
462 bool ok = SOSPeerCoderSendMessageIfNeeded([transport SOSTransportMessageGetAccount], (SOSEngineRef)transport.engine, txn, peer, coder, &message_to_send, peerID, false, &sentCallback, error);
463 if (message_to_send) {
464 CFDictionaryRef peer_dict = CFDictionaryCreateForCFTypes(kCFAllocatorDefault, peerID, message_to_send, NULL);
465 CFDictionarySetValue(SOSTransportMessageKVSTestGetChanges(transport), (__bridge CFStringRef)self->circleName, peer_dict);
466 SOSEngineMessageCallCallback(sentCallback, ok);
467 CFReleaseSafe(peer_dict);
468 }
469
470 SOSEngineFreeMessageCallback(sentCallback);
471 CFReleaseSafe(message_to_send);
472 });
473 }
474 });
475 return result;
476 }
477
478 -(bool) SOSTransportMessageSendMessages:(SOSMessageKVSTest*) transport pm:(CFDictionaryRef) peer_messages err:(CFErrorRef *)error
479 {
480 __block bool result = true;
481
482 CFDictionaryForEach(peer_messages, ^(const void *key, const void *value) {
483 if (isString(key) && isData(value)) {
484 CFStringRef peerID = (CFStringRef) key;
485 CFDataRef message = (CFDataRef) value;
486 bool rx = sendToPeer(transport, (__bridge CFStringRef)transport->circleName, peerID, message, error);
487 result &= rx;
488 }
489 });
490
491 return result;
492 }
493
494 -(bool) SOSTransportMessageFlushChanges:(SOSMessageKVSTest*) transport err:(CFErrorRef *)error
495 {
496 return true;
497 }
498
499 SOSAccount* SOSTransportMessageKVSTestGetAccount(SOSMessageKVSTest* transport) {
500 return transport.account;
501 }
502 @end
503
504
505 void SOSAccountUpdateTestTransports(SOSAccount* account, CFDictionaryRef gestalt){
506 CFStringRef new_name = (CFStringRef)CFDictionaryGetValue(gestalt, kPIUserDefinedDeviceNameKey);
507
508 SOSTransportKeyParameterTestSetName((CKKeyParameterTest*)account.key_transport, new_name);
509 SOSTransportCircleTestSetName((SOSCircleStorageTransportTest*)account.circle_transport, new_name);
510 SOSTransportMessageKVSTestSetName((SOSMessageKVSTest*)account.kvs_message_transport, new_name);
511 }
512
513 static CF_RETURNS_RETAINED SOSCircleRef SOSAccountEnsureCircleTest(SOSAccount* account, CFStringRef name, CFStringRef accountName)
514 {
515 CFErrorRef localError = NULL;
516 SOSAccountTrustClassic *trust = account.trust;
517
518 SOSCircleRef circle = CFRetainSafe([account.trust getCircle:&localError]);
519 if(!circle || isSOSErrorCoded(localError, kSOSErrorIncompatibleCircle)){
520 secnotice("circle", "Error retrieving the circle: %@", localError);
521 CFReleaseNull(localError);
522 CFReleaseNull(circle);
523
524 circle = SOSCircleCreate(kCFAllocatorDefault, name, &localError);
525 if (circle){
526 [trust setTrustedCircle:circle];
527 }
528 else{
529 secnotice("circle", "Could not create circle: %@", localError);
530 }
531 CFReleaseNull(localError);
532 }
533
534 if(![trust ensureFullPeerAvailable:account err:&localError])
535 {
536 secnotice("circle", "had an error building full peer: %@", localError);
537 CFReleaseNull(localError);
538 return circle;
539 }
540
541 CFReleaseNull(localError);
542 return circle;
543 }
544
545 bool SOSAccountEnsureFactoryCirclesTest(SOSAccount* a, CFStringRef accountName)
546 {
547 bool result = false;
548 if (a)
549 {
550 if(!a.factory)
551 return result;
552 CFStringRef circle_name = SOSDataSourceFactoryCopyName(a.factory);
553 if(!circle_name)
554 return result;
555 CFReleaseSafe(SOSAccountEnsureCircleTest(a, (CFStringRef)circle_name, accountName));
556
557 CFReleaseNull(circle_name);
558 result = true;
559 }
560 return result;
561 }
562
563 bool SOSAccountInflateTestTransportsForCircle(SOSAccount* account, CFStringRef circleName, CFStringRef accountName, CFErrorRef *error){
564 bool success = false;
565
566 if(account.key_transport == nil){
567 account.key_transport = (CKKeyParameter*)[[CKKeyParameterTest alloc] initWithAccount:account andName:accountName andCircleName:circleName];
568 require_quiet(account.key_transport, fail);
569 }
570
571 if(account.circle_transport == nil){
572 account.circle_transport = (SOSKVSCircleStorageTransport*)[[SOSCircleStorageTransportTest alloc] initWithAccount:account andWithAccountName:accountName andCircleName:circleName];
573 require_quiet(account.circle_transport, fail);
574 }
575 if(account.kvs_message_transport == nil){
576 account.kvs_message_transport = (SOSMessageKVS*)[[SOSMessageKVSTest alloc] initWithAccount:account andName:accountName andCircleName:circleName];
577 }
578
579 success = true;
580 fail:
581 return success;
582 }
583
584