What is the better way to encrypt NSURLCache?
Asked Answered
T

1

6

I want to encrypt/decrypt all cached data from a NSURLSession using AES256. I'm new using Alamofire but I think it is possible to do it without involving the library itself.

I don't know exactly what is the most seamless way to encrypt the data before caching and decrypt it after being retrieved from cache.

I see I can use Alamofire's SessionDelegate and the methods dataTaskWillCacheResponse and dataTaskWillCacheResponseWithCompletion to encrypt but I don't see anything related with the data being extracted from the cache to do the decrypting.

On the other hand I was thinking about a custom NSURLProtocol to override cachedResponse but I don't see anything related with the caching of that response, only with the extracted data.

In summary, I don't know if it is possible to accomplish this, or I have to use a mix between the NSURLSessionDelegate/SessionDelegate and NSURLProtocol, or maybe subclass NSURLCache to do the job and pass it to the Alamofire session, or there is something simpler out there, or I'm terribly wrong :P

Any help will be really appreciated.


EDIT

I'm trying to achieve it with the next implementation. First of all a very simple subclass of the cache:

class EncryptedURLCache: URLCache {

    let encryptionKey: String

    init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String? = nil, encryptionKey: String) {

        guard !encryptionKey.isEmpty else {
            fatalError("No encryption key provided")
        }

        self.encryptionKey = encryptionKey
        super.init(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: path)
    }

    override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        return super.cachedResponse(for: request)?.cloneDecryptingData(withKey: encryptionKey)
    }

    override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        super.storeCachedResponse(cachedResponse.cloneEncryptingData(withKey: encryptionKey), for: request)
    }
}

And an extension of the cached response to return the encrypted/decrypted data

extension CachedURLResponse {

    func cloneEncryptingData(withKey key: String) -> CachedURLResponse {
        return clone(withData: data.aes256Encrypted(withKey: key))
    }

    func cloneDecryptingData(withKey key: String) -> CachedURLResponse {
        return clone(withData: data.aes256Decrypted(withKey: key) ?? data)
    }

    private func clone(withData data: Data) -> CachedURLResponse {
        return CachedURLResponse(
            response: response,
            data: data,
            userInfo: userInfo,
            storagePolicy: storagePolicy
        )
    }
}

This is working but only for a mockable.io that I mounted with the header Cache-Control: max-age=60. I'm also testing against the SWAPI http://swapi.co/api/people/1/ and against Google Books https://www.googleapis.com/books/v1/volumes?q=swift+programming.

In all three cases the responses are correctly encrypted and cached. I'm doing my testing cutting off the Internet connection and setting the session configuration's requestCachePolicy = .returnCacheDataDontLoad.

In this scenario, the request made to mockable.io is correctly decrypted and returned from cache but the others say NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline.". This is VERY strange because, with that policy, it has to say NSURLErrorDomain Code=-1008 "resource unavailable" if there is no possibility to return the cached data. If there is an error decrypting then it says it was an error serializing to a JSON object.

I've also tested with the common shared cache and it works as expected, with that policy the data is returned. I thought it could be something related with the absence of cache headers in the SWAPI and GBooks responses but this test works, it returns the cached data.

Then I made another test: using my cache but without encrypting/decrypting data, simply cloning the returned cached response with the data as is, with no results. Then I tried a final and very stupid test: to avoid cloning the response, just return the cachedResponse and then IT WORKED. How the h*** is that possible? If I clone the cachedResponse to inject my encrypted/decrypted data it does not work! Even in examples from Apple they are creating new cached responses with no fear.

I don't know where is the error but I'm going to jump over the window in a minute or two.

Please, any help? Thank you so much.


EDIT 2

I was changing emails with a DTS engineer from Apple and the conclusion is that this is not possible to achieve this because the backing CF type is doing more logic than the Foundation object, in this case it is doing a validation against the URLRequest that is passed to it when the system caches the response, but I cannot pass it when make the clone with the regular NSCachedURLResponse.

When the system validates against the request, there is none to match with.

Tenant answered 26/2, 2017 at 10:52 Comment(2)
@emengero does it mean you failed do encrypt/decrypt URLCache and there is no way to do it?Alverta
@Alverta correctTenant
N
4

There is no way to intercept cache retrieval calls from the delegate side that I'm aware of, and I don't think that a custom protocol will even be asked to handle the request if it comes out of the cache, but I could be wrong. So probably your options are:

  • Explicitly ask the cache for the data before you make the URL request.
  • Add code in the code that actually handles the response so that it recognizes that the data is encrypted and decrypt it.

    For example, you could insert an additional header into the headers as you store it into the cache to indicate that the cached data is encrypted. Then, when you see that magic header value on the way back out, decrypt it.

  • Write a subclass of NSURLCache and handle the decryption there (and ideally, store the on-disk data in a different file to avoid breaking any requests in your app that use the normal cache).
Naked answered 27/2, 2017 at 2:15 Comment(5)
Thank you @dgatwood. I'm trying to accomplish this subclassing URLCache, overriding the methods related with the store and retrieval to encrypt/decrypt. All seems to work perfectly (the .db file is stored and written, data is encrypted...) but when I disconnect from WiFi to test if the data is retrieved (using .returnCacheDataDontLoad as cache policy) I get a connection error, which is weird, if there is no cached data the error would be "resource not found", no "connection error". I don't know what is happening. Any idea will be really really appreciated.Tenant
It is possible (nay, likely) that you've hit a bug in the framework. You might try making your class wrap NSURLCache instead of subclassing it, just in case there's some undocumented SPI method that's getting called by the session. You would then get a nice crash that would be easier to debug. :-)Naked
Yesterday I opened a TSI with Apple, I hope they can bring light to this. I'll share any response. Thank you :)Tenant
My guess would be that they're calling getCachedResponseForDataTask:completionHandler and that it isn't calling your method because of some class cluster weirdness or something.Naked
I implemented those methods too, the one for storing and the one for retrieving, with the same results.Tenant

© 2022 - 2024 — McMap. All rights reserved.