]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZone.m
Security-58286.41.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSZone.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #include <AssertMacros.h>
25
26 #import <Foundation/Foundation.h>
27
28 #if OCTAGON
29 #import "CloudKitDependencies.h"
30 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
31 #import <CloudKit/CloudKit.h>
32 #import <CloudKit/CloudKit_Private.h>
33
34 #import "CKKSKeychainView.h"
35 #import "CKKSZone.h"
36
37 #include <utilities/debugging.h>
38
39 @interface CKKSZone()
40
41 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
42 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
43 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
44
45 @property NSOperationQueue* operationQueue;
46 @property NSOperation* accountLoggedInDependency;
47
48 @property NSHashTable<NSOperation*>* accountOperations;
49
50 // Make writable
51 @property bool halted;
52 @end
53
54 @implementation CKKSZone
55
56 - (instancetype)initWithContainer: (CKContainer*) container
57 zoneName: (NSString*) zoneName
58 accountTracker:(CKKSCKAccountStateTracker*) tracker
59 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
60 fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
61 queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
62 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
63 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
64 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
65 {
66 if(self = [super init]) {
67 _container = container;
68 _zoneName = zoneName;
69 _accountTracker = tracker;
70
71 _halted = false;
72
73 _database = [_container privateCloudDatabase];
74 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
75
76 _accountStatus = CKKSAccountStatusUnknown;
77
78 __weak __typeof(self) weakSelf = self;
79 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
80 ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
81 }];
82 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
83
84 _accountOperations = [NSHashTable weakObjectsHashTable];
85
86 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
87 _fetchRecordsOperationClass = fetchRecordsOperationClass;
88 _queryOperationClass = queryOperationClass;
89 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
90 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
91 _apsConnectionClass = apsConnectionClass;
92
93 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
94 _operationQueue = [[NSOperationQueue alloc] init];
95 }
96 return self;
97 }
98
99 - (void)initializeZone {
100 [self.accountTracker notifyOnAccountStatusChange:self];
101 }
102
103 - (void)resetSetup {
104 self.zoneCreated = false;
105 self.zoneSubscribed = false;
106 self.zoneCreatedError = nil;
107 self.zoneSubscribedError = nil;
108
109 self.zoneCreationOperation = nil;
110 self.zoneSubscriptionOperation = nil;
111 self.zoneDeletionOperation = nil;
112 }
113
114 - (CKRecordZoneID*)zoneID {
115 return [self.zone zoneID];
116 }
117
118
119 -(void)ckAccountStatusChange:(CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
120 ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
121 self.zoneID.zoneName,
122 [CKKSCKAccountStateTracker stringFromAccountStatus: oldStatus],
123 [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
124
125 __weak __typeof(self) weakSelf = self;
126 switch(currentStatus) {
127 case CKKSAccountStatusAvailable: {
128 ckksnotice("ckkszone", self, "Logged into iCloud.");
129 [self handleCKLogin];
130
131 if(self.accountLoggedInDependency) {
132 [self.operationQueue addOperation:self.accountLoggedInDependency];
133 self.accountLoggedInDependency = nil;
134 };
135 }
136 break;
137
138 case CKKSAccountStatusNoAccount: {
139 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
140
141 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
142 ckksnotice("ckkszone", weakSelf, "CloudKit account logged in again.");
143 }];
144 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
145
146 [self handleCKLogout];
147 }
148 break;
149
150 case CKKSAccountStatusUnknown: {
151 // We really don't expect to receive this as a notification, but, okay!
152 ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
153
154 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
155 ckksnotice("ckkszone", weakSelf, "CloudKit account restored from 'unknown'.");
156 }];
157 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
158
159 [self handleCKLogout];
160 }
161 break;
162 }
163 }
164
165 - (void)restartCurrentAccountStateOperation {
166 __weak __typeof(self) weakSelf = self;
167 dispatch_async(self.queue, ^{
168 __strong __typeof(self) strongSelf = weakSelf;
169 ckksnotice("ckksaccount", strongSelf, "Restarting account in state %@", [CKKSCKAccountStateTracker stringFromAccountStatus:strongSelf.accountStatus]);
170 [strongSelf ckAccountStatusChange:strongSelf.accountStatus to:strongSelf.accountStatus];
171 });
172 }
173
174 - (NSOperation*)handleCKLogin:(bool)zoneCreated zoneSubscribed:(bool)zoneSubscribed {
175 if(!SecCKKSIsEnabled()) {
176 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
177 return nil;
178 }
179
180 // If we've already started set up, skip doing it again.
181 if([self.zoneSetupOperation isPending] || [self.zoneSetupOperation isExecuting]) {
182 ckksnotice("ckkszone", self, "skipping startup: it's already started");
183 return self.zoneSetupOperation;
184 }
185
186 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
187 self.zoneSetupOperation.name = [NSString stringWithFormat:@"zone-setup-operation-%@", self.zoneName];
188
189 self.zoneCreated = zoneCreated;
190 self.zoneSubscribed = zoneSubscribed;
191
192 // Zone setups and teardowns are due to either 1) first CKKS launch or 2) the user logging in to iCloud.
193 // Therefore, they're QoS UserInitiated.
194 self.zoneSetupOperation.queuePriority = NSOperationQueuePriorityNormal;
195 self.zoneSetupOperation.qualityOfService = NSQualityOfServiceUserInitiated;
196
197 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
198
199 __weak __typeof(self) weakSelf = self;
200
201 // First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
202 __weak CKKSGroupOperation* weakZoneSetupOperation = self.zoneSetupOperation;
203 [self.zoneSetupOperation runBeforeGroupFinished:[CKKSResultOperation named:[NSString stringWithFormat:@"zone-setup-%@", self.zoneName] withBlock:^{
204 __strong __typeof(weakSelf) strongSelf = weakSelf;
205 __strong __typeof(self.zoneSetupOperation) zoneSetupOperation = weakZoneSetupOperation;
206 if(!strongSelf || !zoneSetupOperation) {
207 ckkserror("ckkszone", strongSelf, "received callback for released object");
208 return;
209 }
210
211 if(strongSelf.accountStatus != CKKSAccountStatusAvailable) {
212 ckkserror("ckkszone", strongSelf, "Zone doesn't believe it's logged in; quitting setup");
213 return;
214 }
215
216 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
217 __strong __typeof(weakSelf) strongSelf = weakSelf;
218 if(!strongSelf) {
219 secerror("ckkszone: received callback for released object");
220 return;
221 }
222
223 ckksnotice("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
224 }];
225 setupCompleteOperation.name = @"zone-setup-complete-operation";
226
227 // We have an account, so fetch the push environment and bring up APS
228 [strongSelf.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
229 __strong __typeof(weakSelf) strongSelf = weakSelf;
230 if(!strongSelf) {
231 secerror("ckkszone: received callback for released object");
232 return;
233 }
234
235 if(error || (apsPushEnvString == nil)) {
236 ckkserror("ckkszone", strongSelf, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
237 } else {
238 CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
239 namedDelegatePort:SecCKKSAPSNamedPort
240 apsConnectionClass:strongSelf.apsConnectionClass];
241 [aps registerReceiver:strongSelf forZoneID:strongSelf.zoneID];
242 }
243 }];
244
245 NSBlockOperation* modifyRecordZonesCompleteOperation = nil;
246 if(!zoneCreated) {
247 ckksnotice("ckkszone", strongSelf, "Creating CloudKit zone '%@'", strongSelf.zoneName);
248 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation = [[strongSelf.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: @[strongSelf.zone] recordZoneIDsToDelete: nil];
249 zoneCreationOperation.queuePriority = NSOperationQueuePriorityNormal;
250 zoneCreationOperation.qualityOfService = NSQualityOfServiceUserInitiated;
251 zoneCreationOperation.database = strongSelf.database;
252 zoneCreationOperation.name = @"zone-creation-operation";
253
254 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
255 modifyRecordZonesCompleteOperation = [[NSBlockOperation alloc] init];
256 modifyRecordZonesCompleteOperation.name = @"zone-creation-finished";
257
258 zoneCreationOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
259 __strong __typeof(weakSelf) strongSelf = weakSelf;
260 if(!strongSelf) {
261 secerror("ckkszone: received callback for released object");
262 return;
263 }
264
265 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
266
267 if(!operationError) {
268 ckksnotice("ckkszone", strongSubSelf, "Successfully created zone %@", strongSubSelf.zoneName);
269 strongSubSelf.zoneCreated = true;
270 } else {
271 ckkserror("ckkszone", strongSubSelf, "Couldn't create zone %@; %@", strongSubSelf.zoneName, operationError);
272 }
273 strongSubSelf.zoneCreatedError = operationError;
274
275 [strongSubSelf.operationQueue addOperation: modifyRecordZonesCompleteOperation];
276 };
277
278 ckksnotice("ckkszone", strongSelf, "Adding CKKSModifyRecordZonesOperation: %@ %@", zoneCreationOperation, zoneCreationOperation.dependencies);
279 strongSelf.zoneCreationOperation = zoneCreationOperation;
280 [setupCompleteOperation addDependency: modifyRecordZonesCompleteOperation];
281 [zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
282 [zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
283 } else {
284 ckksnotice("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
285 }
286
287 if(!zoneSubscribed) {
288 ckksnotice("ckkszone", strongSelf, "Creating CloudKit record zone subscription for %@", strongSelf.zoneName);
289 CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID: strongSelf.zoneID subscriptionID:[@"zone:" stringByAppendingString: strongSelf.zoneName]];
290 CKNotificationInfo* notificationInfo = [[CKNotificationInfo alloc] init];
291
292 notificationInfo.shouldSendContentAvailable = false;
293 subscription.notificationInfo = notificationInfo;
294
295 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[strongSelf.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave: @[subscription] subscriptionIDsToDelete: nil];
296
297 zoneSubscriptionOperation.queuePriority = NSOperationQueuePriorityNormal;
298 zoneSubscriptionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
299 zoneSubscriptionOperation.database = strongSelf.database;
300 zoneSubscriptionOperation.name = @"zone-subscription-operation";
301
302 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
303 NSBlockOperation* zoneSubscriptionCompleteOperation = [[NSBlockOperation alloc] init];
304 zoneSubscriptionCompleteOperation.name = @"zone-subscription-complete";
305 zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions, NSArray<NSString *> * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError) {
306 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
307 if(!strongSubSelf) {
308 ckkserror("ckkszone", strongSubSelf, "received callback for released object");
309 return;
310 }
311
312 if(!operationError) {
313 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", savedSubscriptions);
314
315 // Success; write that down. TODO: actually ensure that the saved subscription matches what we asked for
316 for(CKSubscription* sub in savedSubscriptions) {
317 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", sub.subscriptionID);
318 strongSubSelf.zoneSubscribed = true;
319 }
320 } else {
321 ckkserror("ckkszone", strongSubSelf, "Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
322 }
323
324 strongSubSelf.zoneSubscribedError = operationError;
325 strongSubSelf.zoneSubscriptionOperation = nil;
326
327 [strongSubSelf.operationQueue addOperation: zoneSubscriptionCompleteOperation];
328 };
329
330 if(modifyRecordZonesCompleteOperation) {
331 [zoneSubscriptionOperation addDependency:modifyRecordZonesCompleteOperation];
332 }
333 strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
334 [setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
335 [zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
336 [zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
337 } else {
338 ckksnotice("ckkszone", strongSelf, "no need to create database subscription");
339 }
340
341 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
342 }]];
343
344 [self scheduleAccountStatusOperation:self.zoneSetupOperation];
345 return self.zoneSetupOperation;
346 }
347
348
349 - (CKKSResultOperation*)beginResetCloudKitZoneOperation {
350 if(!SecCKKSIsEnabled()) {
351 ckksnotice("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
352 return nil;
353 }
354
355 // We want to delete this zone and this subscription from CloudKit.
356
357 // Step 1: cancel setup operations (if they exist)
358 [self.accountLoggedInDependency cancel];
359 [self.zoneSetupOperation cancel];
360 [self.zoneCreationOperation cancel];
361 [self.zoneSubscriptionOperation cancel];
362
363 // Step 2: Try to delete the zone
364 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation = [[self.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: nil recordZoneIDsToDelete: @[self.zoneID]];
365 zoneDeletionOperation.queuePriority = NSOperationQueuePriorityNormal;
366 zoneDeletionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
367 zoneDeletionOperation.database = self.database;
368
369 CKKSResultOperation* doneOp = [CKKSResultOperation named:@"zone-reset-watcher" withBlock:^{}];
370
371 __weak __typeof(self) weakSelf = self;
372
373 zoneDeletionOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
374 __strong __typeof(weakSelf) strongSelf = weakSelf;
375 if(!strongSelf) {
376 ckkserror("ckkszone", strongSelf, "received callback for released object");
377 return;
378 }
379
380 bool fatalError = false;
381 if(operationError) {
382 // Okay, but if this error is either 'ZoneNotFound' or 'UserDeletedZone', that's fine by us: the zone is deleted.
383 NSDictionary* partialErrors = operationError.userInfo[CKPartialErrorsByItemIDKey];
384 if([operationError.domain isEqualToString:CKErrorDomain] && operationError.code == CKErrorPartialFailure && partialErrors) {
385 for(CKRecordZoneID* errorZoneID in partialErrors.allKeys) {
386 NSError* errorZone = partialErrors[errorZoneID];
387
388 if(errorZone && [errorZone.domain isEqualToString:CKErrorDomain] &&
389 (errorZone.code == CKErrorZoneNotFound || errorZone.code == CKErrorUserDeletedZone)) {
390 ckksnotice("ckkszone", strongSelf, "Attempted to delete zone %@, but it's already missing. This is okay: %@", errorZoneID, errorZone);
391 } else {
392 fatalError = true;
393 }
394 }
395
396 } else {
397 fatalError = true;
398 }
399 }
400
401 ckksinfo("ckkszone", strongSelf, "record zones deletion %@ completed with error: %@", deletedRecordZoneIDs, operationError);
402
403 if(operationError && fatalError) {
404 // If the error wasn't actually a problem, don't report it upward.
405 doneOp.error = operationError;
406 }
407 [strongSelf.operationQueue addOperation: doneOp];
408 };
409
410 // If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
411 [zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
412
413 ckksnotice("ckkszone", self, "deleting zone with %@ %@", zoneDeletionOperation, zoneDeletionOperation.dependencies);
414 // Don't use scheduleOperation: zone deletions should be attempted even if we're "logged out"
415 [self.operationQueue addOperation: zoneDeletionOperation];
416 self.zoneDeletionOperation = zoneDeletionOperation;
417 return doneOp;
418 }
419
420 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
421 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
422 }
423
424 - (void)handleCKLogin {
425 ckksinfo("ckkszone", self, "received a notification of CK login");
426 self.accountStatus = CKKSAccountStatusAvailable;
427 }
428
429 - (void)handleCKLogout {
430 ckksinfo("ckkszone", self, "received a notification of CK logout");
431 self.accountStatus = CKKSAccountStatusNoAccount;
432 [self resetSetup];
433 }
434
435 - (bool)scheduleOperation: (NSOperation*) op {
436 if(self.halted) {
437 ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
438 return false;
439 }
440
441 if(self.accountLoggedInDependency) {
442 [op addDependency: self.accountLoggedInDependency];
443 }
444
445 [self.operationQueue addOperation: op];
446 return true;
447 }
448
449 - (void)cancelAllOperations {
450 [self.operationQueue cancelAllOperations];
451 }
452
453 - (void)waitUntilAllOperationsAreFinished {
454 [self.operationQueue waitUntilAllOperationsAreFinished];
455 }
456
457 - (void)waitForOperationsOfClass:(Class) operationClass {
458 NSArray* operations = [self.operationQueue.operations copy];
459 for(NSOperation* op in operations) {
460 if([op isKindOfClass:operationClass]) {
461 [op waitUntilFinished];
462 }
463 }
464 }
465
466 - (bool)scheduleAccountStatusOperation: (NSOperation*) op {
467 if(self.halted) {
468 ckkserror("ckkszone", self, "attempted to schedule an account operation on a halted zone, ignoring");
469 return false;
470 }
471
472 // Always succeed. But, account status operations should always proceed in-order.
473 [op linearDependencies:self.accountOperations];
474 [self.operationQueue addOperation: op];
475 return true;
476 }
477
478 // to be used rarely, if at all
479 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
480 if(self.halted) {
481 ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
482 return false;
483 }
484
485 [self.operationQueue addOperation: op];
486 return true;
487 }
488
489 - (void) dispatchSync: (bool (^)(void)) block {
490 // important enough to block this thread.
491 __block bool ok = false;
492 dispatch_sync(self.queue, ^{
493 if(self.halted) {
494 ckkserror("ckkszone", self, "CKKSZone not dispatchSyncing a block (due to being halted)");
495 return;
496 }
497
498 ok = block();
499 if(!ok) {
500 ckkserror("ckkszone", self, "CKKSZone block returned false");
501 }
502 });
503 }
504
505 - (void)halt {
506 // Synchronously set the 'halted' bit
507 dispatch_sync(self.queue, ^{
508 self.halted = true;
509 });
510
511 // Bring all operations down, too
512 [self cancelAllOperations];
513 }
514
515 @end
516
517 #endif /* OCTAGON */
518