Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the better way to encrypt NSURLCache?

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.

like image 907
emenegro Avatar asked Oct 17 '22 16:10

emenegro


1 Answers

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).
like image 185
dgatwood Avatar answered Oct 21 '22 01:10

dgatwood