@@ -164,19 +164,38 @@ private static string DateTimeOffsetToFilenameSafeString(DateTimeOffset dateTime
164164 public IReadOnlyCollection < IKey > GetAllKeys ( )
165165 {
166166 var allElements = KeyRepository . GetAllElements ( ) ;
167+ var processed = ProcessAllElements ( allElements , out _ ) ;
168+ return processed . OfType < IKey > ( ) . ToList ( ) . AsReadOnly ( ) ;
169+ }
170+
171+ /// <summary>
172+ /// Returns an array paralleling <paramref name="allElements"/> but:
173+ /// 1. Key elements become IKeys (with revocation data)
174+ /// 2. KeyId-based revocations become Guids
175+ /// 3. Date-based revocations become DateTimeOffsets
176+ /// 4. Unknown elements become null
177+ /// </summary>
178+ private object ? [ ] ProcessAllElements ( IReadOnlyCollection < XElement > allElements , out DateTimeOffset ? mostRecentMassRevocationDate )
179+ {
180+ var elementCount = allElements . Count ;
181+
182+ var results = new object ? [ elementCount ] ;
167183
168- // We aggregate all the information we read into three buckets
169- Dictionary < Guid , Key > keyIdToKeyMap = new Dictionary < Guid , Key > ( ) ;
184+ Dictionary < Guid , Key > keyIdToKeyMap = [ ] ;
170185 HashSet < Guid > ? revokedKeyIds = null ;
171- DateTimeOffset ? mostRecentMassRevocationDate = null ;
172186
187+ mostRecentMassRevocationDate = null ;
188+
189+ var pos = 0 ;
173190 foreach ( var element in allElements )
174191 {
192+ object ? result ;
175193 if ( element . Name == KeyElementName )
176194 {
177195 // ProcessKeyElement can return null in the case of failure, and if this happens we'll move on.
178196 // Still need to throw if we see duplicate keys with the same id.
179197 var key = ProcessKeyElement ( element ) ;
198+ result = key ;
180199 if ( key != null )
181200 {
182201 if ( keyIdToKeyMap . ContainsKey ( key . KeyId ) )
@@ -189,19 +208,20 @@ public IReadOnlyCollection<IKey> GetAllKeys()
189208 else if ( element . Name == RevocationElementName )
190209 {
191210 var revocationInfo = ProcessRevocationElement ( element ) ;
192- if ( revocationInfo is Guid )
211+ result = revocationInfo ;
212+ if ( revocationInfo is Guid revocationGuid )
193213 {
194214 // a single key was revoked
195- if ( revokedKeyIds == null )
215+ revokedKeyIds ??= [ ] ;
216+ if ( ! revokedKeyIds . Add ( revocationGuid ) )
196217 {
197- revokedKeyIds = new HashSet < Guid > ( ) ;
218+ _logger . KeyRevokedMultipleTimes ( revocationGuid ) ;
198219 }
199- revokedKeyIds . Add ( ( Guid ) revocationInfo ) ;
200220 }
201221 else
202222 {
203223 // all keys as of a certain date were revoked
204- DateTimeOffset thisMassRevocationDate = ( DateTimeOffset ) revocationInfo ;
224+ var thisMassRevocationDate = ( DateTimeOffset ) revocationInfo ;
205225 if ( ! mostRecentMassRevocationDate . HasValue || mostRecentMassRevocationDate < thisMassRevocationDate )
206226 {
207227 mostRecentMassRevocationDate = thisMassRevocationDate ;
@@ -212,16 +232,18 @@ public IReadOnlyCollection<IKey> GetAllKeys()
212232 {
213233 // Skip unknown elements.
214234 _logger . UnknownElementWithNameFoundInKeyringSkipping ( element . Name ) ;
235+ result = null ;
215236 }
237+
238+ results [ pos ++ ] = result ;
216239 }
217240
218241 // Apply individual revocations
219- if ( revokedKeyIds != null )
242+ if ( revokedKeyIds is not null )
220243 {
221- foreach ( Guid revokedKeyId in revokedKeyIds )
244+ foreach ( var revokedKeyId in revokedKeyIds )
222245 {
223- keyIdToKeyMap . TryGetValue ( revokedKeyId , out var key ) ;
224- if ( key != null )
246+ if ( keyIdToKeyMap . TryGetValue ( revokedKeyId , out var key ) )
225247 {
226248 key . SetRevoked ( ) ;
227249 _logger . MarkedKeyAsRevokedInTheKeyring ( revokedKeyId ) ;
@@ -252,7 +274,7 @@ public IReadOnlyCollection<IKey> GetAllKeys()
252274 }
253275
254276 // And we're finished!
255- return keyIdToKeyMap . Values . ToList ( ) . AsReadOnly ( ) ;
277+ return results ;
256278 }
257279
258280 /// <inheritdoc/>
@@ -385,6 +407,76 @@ public void RevokeKey(Guid keyId, string? reason = null)
385407 reason : reason ) ;
386408 }
387409
410+ /// <inheritdoc/>
411+ public bool CanDeleteKeys => KeyRepository is IDeletableXmlRepository ;
412+
413+ /// <inheritdoc/>
414+ public bool DeleteKeys ( Func < IKey , bool > shouldDelete )
415+ {
416+ if ( KeyRepository is not IDeletableXmlRepository xmlRepositoryWithDeletion )
417+ {
418+ throw Error . XmlKeyManager_DoesNotSupportKeyDeletion ( ) ;
419+ }
420+
421+ return xmlRepositoryWithDeletion . DeleteElements ( ( deletableElements ) =>
422+ {
423+ // It is important to delete key elements before the corresponding revocation elements,
424+ // in case the deletion fails part way - we don't want to accidentally unrevoke a key
425+ // and then not delete it.
426+ // Start at a non-zero value just to make it a little clearer in the debugger that it
427+ // was set explicitly.
428+ const int deletionOrderKey = 1 ;
429+ const int deletionOrderRevocation = 2 ;
430+ const int deletionOrderMassRevocation = 3 ;
431+
432+ var deletableElementsArray = deletableElements . ToArray ( ) ;
433+
434+ var allElements = deletableElements . Select ( d => d . Element ) . ToArray ( ) ;
435+ var processed = ProcessAllElements ( allElements , out var mostRecentMassRevocationDate ) ;
436+
437+ var allKeyIds = new HashSet < Guid > ( ) ;
438+ var deletedKeyIds = new HashSet < Guid > ( ) ;
439+
440+ for ( var i = 0 ; i < deletableElementsArray . Length ; i ++ )
441+ {
442+ var obj = processed [ i ] ;
443+ if ( obj is IKey key )
444+ {
445+ var keyId = key . KeyId ;
446+ allKeyIds . Add ( keyId ) ;
447+
448+ if ( shouldDelete ( key ) )
449+ {
450+ _logger . DeletingKey ( keyId ) ;
451+ deletedKeyIds . Add ( keyId ) ;
452+ deletableElementsArray [ i ] . DeletionOrder = deletionOrderKey ;
453+ }
454+ }
455+ else if ( obj is DateTimeOffset massRevocationDate )
456+ {
457+ if ( massRevocationDate < mostRecentMassRevocationDate )
458+ {
459+ // Delete superceded mass revocation elements
460+ deletableElementsArray [ i ] . DeletionOrder = deletionOrderMassRevocation ;
461+ }
462+ }
463+ }
464+
465+ // Separate loop since deletedKeyIds and allKeyIds need to have been populated.
466+ for ( var i = 0 ; i < deletableElementsArray . Length ; i ++ )
467+ {
468+ if ( processed [ i ] is Guid revocationId )
469+ {
470+ // Delete individual revocations of keys that don't (still) exist
471+ if ( deletedKeyIds . Contains ( revocationId ) || ! allKeyIds . Contains ( revocationId ) )
472+ {
473+ deletableElementsArray [ i ] . DeletionOrder = deletionOrderRevocation ;
474+ }
475+ }
476+ }
477+ } ) ;
478+ }
479+
388480 private void TriggerAndResetCacheExpirationToken ( [ CallerMemberName ] string ? opName = null , bool suppressLogging = false )
389481 {
390482 if ( ! suppressLogging )
0 commit comments