I investigated option 3 but the result did not really please me.
I considered the method Node decryptData(Document context, EncryptedData ed). This leaves the working document intact a la decryptKey() but leaves the caller with the problem of parenting the result. Reparenting requires more code than replaceChild() which might surprise some application developers and thus encourage bugs.
I considered the method Document decryptData(Element parent, EncryptedData ed). This modifies the working document which breaks symmetry with decryptKey(). This behavior is usually associated with doFinal() which breaks another symmetry.
I considered allowing null as the Element in doFinal(Document, Element). The idea was to use the EncryptedData member of XMLCipher in DECRYPT mode if the Element is null. That member is already set by loadEncryptedData(). This is workable but the feature is hidden by the choice of API.
Since I could not find an API that was clearly better than the others, I decided to implement option 1 (Let the XMLCipher maintain a list of internal key resolvers directly). The resolvers are only used in DECRYPT and UNWRAP mode. In other modes, the KeyInfo is created explicitly by the caller and therefore we let the caller populate it. The KeyResolvers are passed to the EncryptedKeyResolver when the KEK is still unknown. The EncryptedKeyResolver passes the KeyResolvers to the inner XMLCipher to help resolve the KEK.
The API works with one KeyResolver at a time as before. This avoids the problem of list ownership and whether the list is live in the object or just a copy.