import type { PublicMethodsOf } from '@kbn/utility-types' ; import type { ISavedObjectTypeRegistry , KibanaRequest , Logger , SavedObject , SavedObjectsBulkUpdateObject , StartServicesAccessor , } from 'src/core/server' ; import type { AuthenticatedUser , SecurityPluginSetup } from '../../../security/server' ; import { getDescriptorNamespace } from '../saved_objects/get_descriptor_namespace' ; import type { EncryptedSavedObjectsService } from './encrypted_saved_objects_service' ; import { EncryptionError } from './encryption_error' ; interface EncryptionKeyRotationServiceOptions { logger : Logger ; service : PublicMethodsOf < EncryptedSavedObjectsService > ; getStartServices : StartServicesAccessor ; security ? : SecurityPluginSetup ; } interface EncryptionKeyRotationParams { /**
     * The maximum number of the objects we fetch and process in one iteration.
     */ batchSize : number ; /**
     * Optionally allows to limit key rotation to only specified Saved Object type.
     */ type ? : string ; } interface EncryptionKeyRotationResult { /**
     * The total number of the Saved Objects encrypted by the Encrypted Saved Objects plugin.
     */ total : number ; /**
     * The number of the Saved Objects that were still encrypted with one of the secondary encryption
     * keys and were successfully re-encrypted with the primary key.
     */ successful : number ; /**
     * The number of the Saved Objects that were still encrypted with one of the secondary encryption
     * keys that we failed to re-encrypt with the primary key.
     */ failed : number ; } /**
 * Service that deals with encryption key rotation matters.
 */ export class EncryptionKeyRotationService { constructor ( private readonly options ) { } public async rotate ( request , { batchSize , type } ) { const [ { savedObjects } ] = await this . options . getStartServices ( ) ; const typeRegistry = savedObjects . getTypeRegistry ( ) ; this . options . logger . debug ( 'Retrieving Saved Object types that require encryption.' ) ; const registeredSavedObjectTypes = [ ] ; const registeredHiddenSavedObjectTypes = [ ] ; for ( const knownType of typeRegistry . getAllTypes ( ) ) { if ( this . options . service . isRegistered ( knownType . name ) && ( ! type || knownType . name === type ) ) { registeredSavedObjectTypes . push ( knownType . name ) ; if ( knownType . hidden ) { registeredHiddenSavedObjectTypes . push ( knownType . name ) ; } } } const result = { total : 0 , successful : 0 , failed : 0 } ; if ( registeredSavedObjectTypes . length === 0 ) { this . options . logger . info ( type ? `Saved Object type "${ type }" is not registered, encryption key rotation is not needed.` : 'There are no registered Saved Object types that can have encrypted attributes, encryption key rotation is not needed.' ) ; return result ; } this . options . logger . info ( `Saved Objects with the following types [${ registeredSavedObjectTypes }] will be processed.` ) ; const user = this . options . security ?. authc . getCurrentUser ( request ) ?? undefined ; const retrieveClient = savedObjects . getScopedClient ( request , { includedHiddenTypes : registeredHiddenSavedObjectTypes , excludedWrappers : [ 'encryptedSavedObjects' ] , } ) ; const updateClient = savedObjects . getScopedClient ( request , { includedHiddenTypes : registeredHiddenSavedObjectTypes , } ) ; const processedObjectIDs = new Set < string > ( ) ; let batch = 0 ; let maxBatches = 0 ; while ( true ) { this . options . logger . debug ( `Fetching ${ batchSize } objects (batch #${ batch }).` ) ; const savedObjectsToDecrypt = await retrieveClient . find ( { type : registeredSavedObjectTypes , perPage : batchSize , namespaces : [ '*' ] , sortField : 'updated_at' , sortOrder : 'asc' , } ) ; if ( batch === 0 ) { this . options . logger . debug ( `Found ${ savedObjectsToDecrypt . total } objects.` ) ; result . total = savedObjectsToDecrypt . total ; maxBatches = Math . ceil ( ( savedObjectsToDecrypt . total * 2 ) / batchSize ) ; } this . options . logger . debug ( `Decrypting ${ savedObjectsToDecrypt . saved_objects . length } objects (batch #${ batch }).` ) ; const savedObjectsToEncrypt = await this . getSavedObjectsToReEncrypt ( savedObjectsToDecrypt . saved_objects , processedObjectIDs , typeRegistry , user ) ; if ( savedObjectsToEncrypt . length === 0 ) { break ; } this . options . logger . debug ( `Re-encrypting ${ savedObjectsToEncrypt . length } objects (batch #${ batch }).` ) ; try { const succeeded = ( await updateClient . bulkUpdate ( savedObjectsToEncrypt ) ) . saved_objects . filter ( ( savedObject ) => ! savedObject . error ) . length ; this . options . logger . debug ( `Successfully re-encrypted ${ succeeded } out of ${ savedObjectsToEncrypt . length } objects (batch #${ batch }).` ) ; result . successful += succeeded ; result . failed += savedObjectsToEncrypt . length - succeeded ; } catch ( err ) { this . options . logger . error ( `Failed to re-encrypt saved objects (batch #${ batch }): ${ err . message }` ) ; result . failed += savedObjectsToEncrypt . length ; } if ( savedObjectsToDecrypt . total <= batchSize || ++ batch >= maxBatches ) { break ; } } this . options . logger . info ( `Encryption key rotation is completed. ${ result . successful } objects out ouf ${ result . total } were successfully re-encrypted with the primary encryption key and ${ result . failed } objects failed.` ) ; return result ; } param savedObjects param processedObjectIDs param typeRegistry param user private async getSavedObjectsToReEncrypt ( savedObjects , processedObjectIDs , typeRegistry , user ? ) { const decryptedSavedObjects = [ ] ; for ( const savedObject of savedObjects ) { if ( processedObjectIDs . has ( savedObject . id ) ) { continue ; } else { processedObjectIDs . add ( savedObject . id ) ; } let decryptedAttributes ; try { decryptedAttributes = await this . options . service . decryptAttributes ( { type : savedObject . type , id : savedObject . id , namespace : getDescriptorNamespace ( typeRegistry , savedObject . type , savedObject . namespaces ) , } , savedObject . attributes as Record < string , unknown > , { omitPrimaryEncryptionKey : true , user } ) ; } catch ( err ) { if ( ! ( err instanceof EncryptionError ) ) { throw err ; } continue ; } decryptedSavedObjects . push ( { ... savedObject , attributes : decryptedAttributes , namespace : savedObject . namespaces ?. [ 0 ] , } ) ; } return decryptedSavedObjects ; } }